/*
 * $Source: /usr/local/cvsroot/erserver/erserver/java/src/com/postgres/util/jdbc/ConnectionPool.java,v $
 * $Author: asullivan $ $Revision: 1.1.1.1 $ $Date: 2003/08/26 19:02:34 $
 *
 */

package com.postgres.util.jdbc;

import com.postgres.util.timer.*;
import com.postgres.util.Logger.Logger;
import java.sql.*;

/**
 * <p>This class serves as a JDBC connection repository. Since
 * creating database connections is one of the most time
 * intensive aspects of JDBC, we'll create a pool of connections.
 * Each connection can be used and then replaced back into the pool.
 * <p>A properties file 'ConnectionPool.cfg' will be used to
 * specify the types of JDBC connections to create, as well as
 * the minimum and maximum size of the pool. The format of
 * the configuration file is: #(comment) JDBCDriver=<JDBC driver name>
 * JDBCConnectionURL=<JDBC Connection URL>
 * ConnectionPoolSize=<minimum size of the pool>
 * ConnectionPoolMax=<maximum size of the pool, or -1 for none>
 * ConnectionUseCount=<maximum usage count for one connection>
 * ConnectionTimeout=<maximum idle lifetime (in minutes) of a connection>
 * <other property for JDBC connection>=<value>
 * <p>Any number of additional properties may be given (such
 * as username and password) as required by the JDBC driver.
 */

public class ConnectionPool
implements TimerListener {


    public static final String JDBC_DRIVER = "JDBCDriver";
    public static final String JDBC_USER = "user";
    public static final String JDBC_PASS = "password";
    public static final String JDBC_URL = "JDBCConnectionURL";
    public static final String SIZE = "ConnectionPoolMax";
    public static final String SIZE_MAX = "JDBCDriver";
    public static final String CONN_TIMEOUT = "ConnectionTimeout";
    public static final String CONN_USE_COUNT = "ConnectionUseCount";

    private boolean debug = false;
    private Logger logger;

    // JDBC Driver name
    String m_JDBCDriver;

    // JDBC Connection URL
    String m_JDBCConnectionURL;

    // Minimum size of the pool
    int m_ConnectionPoolSize;

    // Maximum size of the pool
    int m_ConnectionPoolMax;

    // Maximum number of uses for a single connection, or -1 for
    // none
    int m_ConnectionUseCount;

    // Maximum connection idle time (in minutes)
    int m_ConnectionTimeout;
    // user
    private String m_JDBC_user;
    // password
    private String m_JDBC_pass;

    // Additional JDBC properties
    java.util.Properties m_JDBCProperties;

    // The Connection pool. This is a vector of ConnectionObject
    // objects
    java.util.Vector m_pool;

    // The maximum number of simultaneous connections as reported
    // by the JDBC driver
    int m_MaxConnections = -1;

    // Our Timer object
    Timer m_timer;

    // Some default constants:
    public static final int DEF_SIZE = 1;
    public static final int DEF_SIZE_MAX = 1;
    public static final int DEF_CONN_TIMEOUT = 2; // minutes
    public static final int DEF_CONN_USE_COUNT = 100;

    public String getConnectionURL() {
        return m_JDBCConnectionURL;
    }

    /**
     * <p>Initializes the ConnectionPool object using
     * 'ConnectionPool.cfg' as the configuration file
     * @return true if the ConnectionPool was initialized properly
     */
    public boolean initialize() throws Exception {
        return initialize("ConnectionPool.cfg");
    }

    public void setLogger(Logger logger) {
        this.logger = logger;
    }

    /**
     * <p>Initializes the ConnectionPool object with the specified
     * configuration file
     * @param config Configuration file name
     * @return true if the ConnectionPool was initialized properly
     */
    public boolean initialize(String config) throws Exception {
        // Load the configuration parameters. Any leftover parameters
        // from the configuration file will be considered JDBC
        // connection attributes to be used to establish the
        // JDBC connections
        boolean rc = loadConfig(config);

        if (rc) {
            // Properties were loaded; attempt to create our pool
            // of connections
            createPool();

            // Start our timer so we can timeout connections. The
            // clock cycle will be 20 seconds
            m_timer = new Timer(this, 20);
            m_timer.start();
        }
        return rc;
    }

    /**
     * <p>Initializes the ConnectionPool object with the specified
     * configuration file
     * @param config Configuration file name
     * @return true if the ConnectionPool was initialized properly
     */
    public void initialize(java.util.Properties props) throws Exception {
        // Load the configuration parameters. Any leftover parameters
        // from the configuration file will be considered JDBC
        // connection attributes to be used to establish the
        // JDBC connections
        loadConfig(props);

        // Properties were loaded; attempt to create our pool
        // of connections
        createPool();

        // Start our timer so we can timeout connections. The
        // clock cycle will be 20 seconds
        m_timer = new Timer(this, 20);
        m_timer.start();

    }

    /**
     * <p>Destroys the pool and it's contents. Closes any open
     * JDBC connections and frees all resources
     */
    public void destroy() {
        try {

            // Stop our timer thread
            if (m_timer != null) {
                try {
                     m_timer.stop();
                } catch (Exception e) {}

                m_timer = null;
            }

            // Clear our pool
            if (m_pool != null) {

                // Loop throught the pool and close each connection
                for (int i = 0; i < m_pool.size(); i++) {
                    ConnectionObject connObj = (ConnectionObject) m_pool.elementAt(i);
                    try {
                         if (connObj != null && connObj.con != null) {
                             connObj.con.rollback();
                         }
                    } catch (Exception e) {}
                    close(connObj);
                }
            }
            m_pool = null;
        }
        catch (Exception ex) {
            //ex.printStackTrace();
            traceError("destroy", ex);
        }
    }

    /**
     * <p>Gets an available JDBC Connection. Connections will be
     * created if necessary, up to the maximum number of connections
     * as specified in the configuration file.
     * @return JDBC Connection, or null if the maximum
     * number of connections has been exceeded
     */
    public synchronized java.sql.Connection getConnection() {
        // If there is no pool it must have been destroyed
        if (m_pool == null) {
            return null;
        }

        java.sql.Connection con = null;
        ConnectionObject connectionObject = null;
        int poolSize = m_pool.size();

        // Get the next available connection
        for (int i = 0; i < poolSize; i++) {

            // Get the ConnectionObject from the pool
            ConnectionObject co = (ConnectionObject)
            m_pool.elementAt(i);

            // If this is a valid connection and it is not in use,
            // grab it
            if (co.isAvailable()) {
                connectionObject = co;
                break;
            }
        }

        // No more available connections. If we aren't at the
        // maximum number of connections, create a new entry
        // in the pool
        if (connectionObject == null) {
            if ((m_ConnectionPoolMax < 0) ||
            ((m_ConnectionPoolMax > 0) &&
            (poolSize < m_ConnectionPoolMax))) {

                // Add a new connection.
                int i = addConnection();

                // If a new connection was created, use it
                if (i >= 0) {
                    connectionObject = (ConnectionObject)
                    m_pool.elementAt(i);
                }
            }
            else {
                trace("Maximum number of connections " + m_ConnectionPoolMax
                + " exceeded for pool " + m_JDBCConnectionURL);
            }
        }

        // If we have a connection, set the last time accessed,
        // the use count, and the in use flag
        if (connectionObject != null) {
            connectionObject.inUse = true;
            connectionObject.useCount++;
            touch(connectionObject);
            con = connectionObject.con;
        }

        return con;
    }

    /**
     * <p>Places the connection back into the connection pool,
     * or closes the connection if the maximum use count has been reached
     * @param Connection object to close
     */
    public synchronized void close(java.sql.Connection con) {
        // Find the connection in the pool
        int index = find(con);

        if (index != -1) {
            ConnectionObject co = (ConnectionObject)
            m_pool.elementAt(index);

            // If the use count exceeds the max, remove it from
            // the pool.
            if ((m_ConnectionUseCount > 0) &&
            (co.useCount >= m_ConnectionUseCount)) {
                trace("Connection use count exceeded");
                removeFromPool(index);
            }
            else {
                // Clear the use count and reset the time last used
                touch(co);
                co.inUse = false;
            }
        }
    }

    /**
     * <p>Prints the contents of the connection pool to the
     * standard output device
     */
    public void printPool() {
        trace("--ConnectionPool--");
        if (m_pool != null) {
            for (int i = 0; i < m_pool.size(); i++) {
                ConnectionObject co = (ConnectionObject)
                m_pool.elementAt(i);
                trace("" + i + "=" + co);
            }
        }
    }

    /**
     * <p>Removes the ConnectionObject from the pool at the given index
     * @param index Index into the pool vector
     */
    private synchronized void removeFromPool(int index) {
        // Make sure the pool and index are valid
        if (m_pool != null) {

            if (index < m_pool.size()) {

                // Get the ConnectionObject and close the connection
                ConnectionObject co = (ConnectionObject)
                m_pool.elementAt(index);
                close(co);

                // Remove the element from the pool
                m_pool.removeElementAt(index);
            }
        }
    }

    /**
     * <p>Closes the connection in the given ConnectionObject
     * @param connectObject ConnectionObject
     */
    private void close(ConnectionObject connectionObject) {
        if (connectionObject != null) {
            if (connectionObject.con != null) {
                try {

                    // Close the connection
                    connectionObject.con.close();
                }
                catch (Exception ex) {
                    // Ignore any exceptions during close
                }

                // Clear the connection object reference
                connectionObject.con = null;
            }
        }
    }



    /**
     * <p>Loads the given configuration file.  All global
     * properties (such as JDBCDriver) will be
     * read and removed from the properties list. Any leftover
     * properties will be returned. Returns null if the
     * properties could not be loaded
     * @param name Configuration file name
     * @return true if the configuration file was loaded
     */
    private boolean loadConfig(String name)
    throws Exception {
        boolean rc = false;

        // Get our class loader
        ClassLoader cl = getClass().getClassLoader();

        // Attempt to open an input stream to the configuration file.
        // The configuration file is considered to be a system
        // resource.
        java.io.InputStream in;

        if (cl != null) {
            in = cl.getResourceAsStream(name);
        }
        else {
            in = ClassLoader.getSystemResourceAsStream(name);
        }

        // If the input stream is null, then the configuration file
        // was not found
        if (in == null) {
            throw new Exception("ConnectionPool configuration file '" +
            name + "' not found");
        }
        else {
            try {
                m_JDBCProperties = new java.util.Properties();

                // Load the configuration file into the properties table
                m_JDBCProperties.load(in);

                // Got the properties. Pull out the properties that we
                // are interested in
                setProperties();
                rc = true;
            }
            finally {
                // Always close the input stream
                if (in != null) {
                    try {
                        in.close();
                    }
                    catch (Exception ex) {
                    }
                }
            }
        }
        return rc;
    }

    /**
     * <p>Loads the given configuration file.  All global
     * properties (such as JDBCDriver) will be
     * read and removed from the properties list. Any leftover
     * properties will be returned. Returns null if the
     * properties could not be loaded
     * @param props Configuration Properties
     * @return true if the configuration file was loaded
     */
    private void loadConfig(java.util.Properties props)
    throws Exception {
        boolean rc = false;

        try {
            m_JDBCProperties = props;

            // Got the properties. Pull out the properties that we
            // are interested in
            setProperties();
            rc = true;
        } catch (Exception e) {
            throw new Exception("ConnectionPool::loadConfig: " + e.toString());
        }
    }

    private void setProperties() {
        // Got the properties. Pull out the properties that we
        // are interested in
        m_JDBCDriver = consume(m_JDBCProperties, "JDBCDriver");
        //trace("JDBCDriver:"+m_JDBCDriver);
        m_JDBC_user = consume(m_JDBCProperties, "user");
        //trace("user:"+m_JDBC_user);
        m_JDBC_pass = consume(m_JDBCProperties, "password");
        m_JDBCConnectionURL = consume(m_JDBCProperties,
        "JDBCConnectionURL");
        m_ConnectionPoolSize = consumeInt(m_JDBCProperties,
        "ConnectionPoolSize");
        m_ConnectionPoolSize = Math.max(DEF_SIZE, m_ConnectionPoolSize);

        m_ConnectionPoolMax = consumeInt(m_JDBCProperties,
        "ConnectionPoolMax");
        m_ConnectionPoolMax = Math.max(DEF_SIZE_MAX, m_ConnectionPoolMax);

        m_ConnectionTimeout = consumeInt(m_JDBCProperties,
        "ConnectionTimeout");
        m_ConnectionTimeout = m_ConnectionTimeout < 0 ? DEF_CONN_TIMEOUT :
            m_ConnectionTimeout;

        m_ConnectionUseCount = consumeInt(m_JDBCProperties,
        "ConnectionUseCount");
        m_ConnectionUseCount = m_ConnectionUseCount < 0 ? DEF_CONN_USE_COUNT :
            m_ConnectionUseCount;
    }

    /**
     * <p>Consumes the given property and returns the value.
     * @param properties Properties table
     * @param key Key of the property to retrieve and remove from
     * the properties table
     * @return Value of the property, or null if not found
     */
    private String consume(java.util.Properties p, String key) {
        String s = null;

        if ((p != null) &&
        (key != null)) {

            // Get the value of the key
            s = p.getProperty(key);

            // If found, remove it from the properties table
            //if (s != null) {
            //    p.remove(key);
            //}
        }
        return s;
    }

    /**
     * <p>Consumes the given property and returns the integer value.
     * @param properties Properties table
     * @param key Key of the property to retrieve and remove from
     * the properties table
     * @return Value of the property, or -1 if not found
     */
    private int consumeInt(java.util.Properties p, String key) {
        int n = -1;

        // Get the String value
        String value = consume(p, key);

        // Got a value; convert to an integer
        if (value != null) {
            try {
                n = Integer.parseInt(value);
            }
            catch (Exception ex) {
            }
        }
        return n;
    }

    /**
     * <p>Creates the initial connection pool. A timer thread
     * is also created so that connection timeouts can be handled.
     * @return true if the pool was created
     */
    private void createPool() throws Exception {
        // Sanity check our properties
        if (m_JDBCDriver == null) {
            throw new Exception("JDBCDriver property not found");
        }
        if (m_JDBCConnectionURL == null) {
            throw new Exception("JDBCConnectionURL property not found");
        }

        if (m_JDBC_user == null) {
            throw new Exception("JDBC user property not found");
        }
        if (m_JDBC_pass == null) {
            throw new Exception("JDBC password property not found");
        }

        if (m_ConnectionPoolSize < 0) {
            throw new Exception("ConnectionPoolSize property not found");
        }
        if (m_ConnectionPoolSize == 0) {
            throw new Exception("ConnectionPoolSize invalid");
        }
        if (m_ConnectionPoolMax < m_ConnectionPoolSize) {
            trace("WARNING - ConnectionPoolMax is invalid and will " +
            "be ignored");
            m_ConnectionPoolMax = -1;
        }
        if (m_ConnectionTimeout < 0) {
            // Set the default to 30 minutes
            m_ConnectionTimeout = 30;
        }

        if ( debug) {
            // Dump the parameters we are going to use for the pool.
            // We don't know what type of servlet environment we will
            // be running in - this may go to the console or it
            // may be redirected to a log file
            trace("JDBCDriver = " + m_JDBCDriver);
            trace("JDBCConnectionURL = " + m_JDBCConnectionURL);
            trace("ConnectionPoolSize = " + m_ConnectionPoolSize);
            trace("ConnectionPoolMax = " + m_ConnectionPoolMax);
            trace("ConnectionUseCount = " + m_ConnectionUseCount);
            trace("ConnectionTimeout = " + m_ConnectionTimeout +
            " seconds");

            // Also dump any additional JDBC properties
            java.util.Enumeration enum = m_JDBCProperties.keys();
            while (enum.hasMoreElements()) {
                String key = (String) enum.nextElement();
                String value = m_JDBCProperties.getProperty(key);
                trace("(JDBC Property) " + key + " = " + value);
            }

        }

        // Attempt to create a new instance of the specified
        // JDBC driver. Well behaved drivers will register
        // themselves with the JDBC DriverManager when they
        // are instantiated
        if (debug) trace("Registering " + m_JDBCDriver);
        java.sql.Driver d = (java.sql.Driver)
        Class.forName(m_JDBCDriver).newInstance();

        // Create the vector for the pool
        m_pool = new java.util.Vector();

        // Bring the pool to the minimum size
        fillPool(m_ConnectionPoolSize);
    }

    /**
     * <p>Adds a new connection to the pool
     * @return Index of the new pool entry, or -1 if an error has occurred
     */
    private int addConnection() {
        int index = -1;

        try {
            // Calculate the new size of the pool
            int size = m_pool.size() + 1;

            // Create a new entry
            fillPool(size);

            // Set the index pointer to the new connection if one
            // was created
            if (size == m_pool.size()) {
                index = size - 1;
            }
        }
        catch (Exception ex) {
            //ex.printStackTrace();
            traceError("addConnection", ex);
        }
        return index;
    }

    /** <p>Brings the pool to the given size */
    private synchronized void fillPool(int size) throws Exception {
        // Loop while we need to create more connections
        while (m_pool.size() < size) {

            ConnectionObject co = new ConnectionObject();

            if (debug) trace("ConnectionPool::fillPool: m_JDBCConnectionURL=" + m_JDBCConnectionURL
                + "; m_JDBC_user=" + m_JDBC_user + "; m_JDBC_pass=" + m_JDBC_pass);

            // Create the connection
            co.con = java.sql.DriverManager.getConnection(m_JDBCConnectionURL,
            m_JDBC_user, m_JDBC_pass);
            if (debug)
            trace("ConnectionPool::fillPool: CREATED connection to " + m_JDBCConnectionURL);
            // Do some sanity checking on the first connection in
            // the pool
            if (m_pool.size() == 0) {

                // Get the maximum number of simultaneous connections
                // as reported by the JDBC driver
                java.sql.DatabaseMetaData md = co.con.getMetaData();
                m_MaxConnections = md.getMaxConnections();
            }

            // Give a warning if the size of the pool will exceed
            // the maximum number of connections allowed by the
            // JDBC driver
            if ((m_MaxConnections > 0) &&
            (size > m_MaxConnections)) {
                trace("WARNING: Size of pool will exceed safe maximum of " +
                m_MaxConnections);
            }

            // Clear the in use flag
            co.inUse = false;

            // Set the last access time
            touch(co);

            m_pool.addElement(co);
        }
        if (debug)
        trace("ConnectionPool::fillPool: FILLED connection pool to " + m_JDBCConnectionURL);
    }

    /**
     * <p>Find the given connection in the pool
     * @return Index into the pool, or -1 if not found
     */
    private int find(java.sql.Connection con) {
        int index = -1;

        // Find the matching Connection in the pool
        if ((con != null) &&
        (m_pool != null)) {
            for (int i = 0; i < m_pool.size(); i++) {
                ConnectionObject co = (ConnectionObject)
                m_pool.elementAt(i);
                if (co.con == con) {
                    index = i;
                    break;
                }
            }
        }
        return index;
    }

    /**
     * <p>Called by the timer each time a clock cycle expires.
     * This gives us the opportunity to timeout connections
     */
    public synchronized void TimerEvent(Object object) {
        // No pool means no work
        if (m_pool == null) {
            return;
        }

        // Get the current time in milliseconds
        long now = System.currentTimeMillis();

        // Check for any expired connections and remove them
        for (int i = m_pool.size() - 1; i >= 0; i--) {
            ConnectionObject co = (ConnectionObject)
            m_pool.elementAt(i);

            // If the connection is not in use and it has not been
            // used recently, remove it
            if (!co.inUse) {
                if ((m_ConnectionTimeout > 0) &&
                (co.lastAccess +
                (m_ConnectionTimeout * 1000 * 60) < now)) {
                    removeFromPool(i);
                }
            }
        }

        // Remove any connections that are no longer open
        for (int i = m_pool.size() - 1; i >= 0; i--) {
            ConnectionObject co = (ConnectionObject)
            m_pool.elementAt(i);
            try {
                // If the connection is closed, remove it from the pool
                if (co.con.isClosed()) {
                    trace("Connection closed unexpectedly");
                    removeFromPool(i);
                }
            }
            catch (Exception ex) {
            }
        }

        // Now ensure that the pool is still at it's minimum size
        try {
            if (m_pool != null) {
                if (m_pool.size() < m_ConnectionPoolSize) {
                    fillPool(m_ConnectionPoolSize);
                }
            }
        }
        catch (Exception ex) {
            //ex.printStackTrace();
            traceError("TimerEvent", ex) ;
        }
    }

    /** <p>Sets the last access time for the given ConnectionObject */
    private void touch(ConnectionObject co) {
        if (co != null) {
            co.lastAccess = System.currentTimeMillis();
        }
    }

    /** <p>Trace the given string */
    private void trace(String s) {
        if (logger == null) {
            System.out.println("ConnectionPool: " + s);
        } else {
            logger.info("ConnectionPool: " + s);
        }
    }

    /** <p>Trace the given string */
    private void traceError(String s, Exception e) {
        if (logger == null) {
            System.out.println("ConnectionPool: " + s + ": " + e.toString());
        } else {
            logger.error("ConnectionPool: " + s, e);
        }
    }

    public void printPoolProps() {
        // Also dump any additional JDBC properties
        java.util.Enumeration enum = m_JDBCProperties.keys();
        trace("-- ConnectionPool Properties --");
        while (enum.hasMoreElements()) {
            String key = (String) enum.nextElement();
            String value = m_JDBCProperties.getProperty(key);
            trace(" (JDBC Property) " + key + " = " + value);
        }
    }
}


// This package-private class is used to represent a single
// connection object
class ConnectionObject {
    // The JDBC Connection
    public java.sql.Connection con;

    // true if this connection is currently in use
    public boolean inUse;

    // The last time (in milliseconds) that this connection was used
    public long lastAccess;

    // The number of times this connection has been used
    public int useCount;

    /**
     * <p>Determine if the connection is available
     * @return true if the connection can be used
     */
    public boolean isAvailable() {
        boolean available = false;

        try {

            // To be available, the connection cannot be in use
            // and must be open
            if (con != null) {
                if (!inUse &&
                !con.isClosed()) {
                    available = true;
                }
            }
        }
        catch (Exception ex) {
        }

        return available;
    }

    /** <p>Convert the object contents to a String */
    public String toString() {
        return "Connection=" + con + ",inUse=" + inUse +
        ",lastAccess=" + lastAccess + ",useCount=" + useCount;
    }
}
