package faqapp;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

/**
 * This class is a Primary Key (PK) Generator based on Oracle
 * database sequence objects. It uses the Singleton Design Pattern.
 * It also uses the Double-Checked Locking idiom to try to prevent
 * race conditions that can occur in a multithreaded application.
 */
public class OracleSequencePKGenerator {
    /**
     * This variable holds the single instance of this class.
     * It is private meaning that it is only available to this
     * class.
     **/
    private static OracleSequencePKGenerator instance = null;
    /**
     * This Hashtable caches all sequences
     **/
    private Hashtable cache = new Hashtable();

    /**
     * Class constructor.  This method is private meaning that
     * it is only accessible from inside this class, thus enforcing
     * the singleton design pattern.
     */
    private OracleSequencePKGenerator() {
    }

    /**
     * This method creates the single instance if it hasn't been
     * created yet and returns it.  Note the use of the Double-Checked
     * Locking idiom.
     * @return The single instance of this class.
     */
    public static OracleSequencePKGenerator getInstance() {
        if (instance == null) {
            synchronized (OracleSequencePKGenerator.class) {
                if (instance == null) instance = new OracleSequencePKGenerator();
            }
        }
        return instance;
    }

    /**
     * This method returns the next sequence value for the requested sequence.
     * Note the use of "synchronized" to try to prevent any concurrency
     * conflicts that can occur in a multithreaded application.
     * @param name: The sequence for which the value should be returned.
     * @return The sequence value.
     */
    public synchronized Integer getNextPKValue(String name) throws NamingException, SQLException {
        Sequence sequence = (Sequence) cache.get(name);
        /* In case the sequence hasn't been generated and cached yet, do so */
        if (sequence == null) {
            sequence = new Sequence(name);
            cache.put(name, sequence);
        }
        /* Return cached sequence value */
        return new Integer(sequence.getValue());
    }

    /**
     * Private inner sequence class used to cache and retrieve sequence values
     **/
    private class Sequence {
        private String name = null;
        private int value = 0;
        private int cache = 0;
        private int increment = 0;

        private Sequence(String name) throws NamingException, SQLException {
            this.name = name;
            /* Retrieve sequence increment value for corresponding Oracle database sequence */
            this.increment = getSequenceIncrementFromDB();
        }

        /**
         * Compute next sequence value. Try to retrieve cached value first.
         * Read next set of values from Oracle database sequence in case
         * no more values are available from cache.
         **/
        private int getValue() throws NamingException, SQLException {
            int retValue = 0;
            if (cache == 0) {
                value = getNextSequenceValueFromDB();
                cache = increment;
            }
            retValue = value;
            value++;
            cache--;
            return retValue;
        }

        /**
         * Private method to retrieve increment value for Oracle database sequence.
         **/
        private int getSequenceIncrementFromDB() throws NamingException, SQLException {
            return executeSQLStatement("SELECT INCREMENT_BY FROM USER_SEQUENCES WHERE SEQUENCE_NAME='" + name.toUpperCase() + "_SEQ'");
        }

        /**
         * Private method to retrieve next value from Oracle database sequence.
         **/
        private int getNextSequenceValueFromDB() throws NamingException, SQLException {
            return executeSQLStatement("SELECT " + name.toUpperCase() + "_SEQ.NEXTVAL FROM DUAL");
        }

        /**
         * Private method to execute a SQL statement against a database using a
         * datasource to retrieve a database connection.
         **/
        private int executeSQLStatement(String sql) throws NamingException, SQLException {
            int retValue = 0;
            Context ic = new InitialContext();
            DataSource ds = (DataSource) ic.lookup("jdbc/OracleDS");
            Connection conn = ds.getConnection();
            Statement stmt = conn.createStatement();
            ResultSet rset = stmt.executeQuery(sql);
            rset.next();
            retValue = rset.getInt(1);
            rset.close();
            stmt.close();
            conn.close();
            return retValue;
        }
    }
}
