/* Copyright (c) Oracle Corporation 2001, All Rights Reserved. */

package oracle.portal.utils.v1;

import java.lang.reflect.Method;
import java.sql.*;
import org.w3c.dom.*;
import org.xml.sax.*;
import java.io.*;
import java.util.*;
import oracle.xml.parser.v2.XMLDocument;
import oracle.xml.parser.v2.XSLStylesheet;
import oracle.xml.parser.v2.XSLException;
import oracle.portal.utils.xml.v1.*;

/**
 * The <code>ConnectionManager</code> is a singleton class that manages a set
 * of 'pooled' JDBC database connections. The <code>ConnectionManager</code>
 * allows multi-threaded applications to overcome the overhead involved in
 * repeatedly initializing and closing database connections by maintaining
 * named 'pools' of pre-initialized connections which can grow and shrink over
 * time as needs suit. Provided the details of each type of connection to be
 * pooled are declared in <code>ConnectionManager</code>'s XML configuration
 * file, threads can gain easy access to a connection of a particular type by
 * calling the static {@link #getConnection(String)} method with the name
 * associated with the corresponding connection pool, and can return the
 * connection back to its pool once finished by calling the static {@link
 * #releaseConnection(String, Connection)} method.<p>
 *
 * <code>ConnectionManager</code> is configured through an XML file, whose
 * location must be specified by the system property
 * <code>ConnectionManager.config</code>. Typically, you would specify a value
 * for this property when you invoke your JVM by using the <code>-D</code>
 * option, e.g.:<p>
 *
 * <code>java -DConnectionManager.config=E:\jpdk\providers\dbPersonalization\connectionManager.xml</code><p>
 *
 * If your JVM runs under JServ, you can specify this option in
 * <code>jserv.properties</code> with the <code>wrapper.bin.properties</code>
 * option, e.g.:<p>
 * <code>wrapper.bin.parameters=-DConnectionManager.config=E:\jpdk\providers\dbPersonalization\connectionManager.xml</code>
 * <p>
 * 
 * The contents of an example XML configuration file for
 * <code>ConnectionManager</code> are included below:<p>
 *
 * <code>
 * <pre>&lt;connectionManager>
 *   &lt;minCount>6&lt;/minCount>
 *   &lt;maxCount>25&lt;/maxCount>
 *   &lt;maxWaitTime>60&lt;/maxWaitTime>
 *   &lt;maxIdleTime>3600&lt;/maxIdleTime>
 * 
 *   &lt;connection>
 *     &lt;name>demo&lt;/name>
 *     &lt;username>scott&lt;/username>
 *     &lt;password>tiger&lt;/password>
 *     &lt;driver>oracle.jdbc.driver.OracleDriver&lt;/driver>
 *     &lt;dburl>jdbc:oracle:thin:@localhost:1521:ORCL&lt;/dburl>
 *   &lt;/connection>
 *   &lt;connection>
 *     &lt;name>provider&lt;/name>
 *     &lt;username>provider&lt;/username>
 *     &lt;password>provider&lt;/password>
 *     &lt;driver>oracle.jdbc.driver.OracleDriver&lt;/driver>
 *     &lt;dburl>jdbc:oracle:oci8:@provider&lt;/dburl>
 *   &lt;/connection>
 *   &lt;connection>
 *     &lt;name>lite&lt;/name>
 *     &lt;username>system&lt;/username>
 *     &lt;password>manager&lt;/password>
 *     &lt;driver>oracle.lite.poljdbc.POLJDBCDriver&lt;/driver>
 *     &lt;dburl>jdbc:Polite:POlite&lt;/dburl>
 *   &lt;/connection>
 * &lt;/connectionManager></pre>
 * </code>
 *
 * As illustrated, the document should contain a
 * <code>&lt;connectionManager></code> element, below which should be a number
 * of <code>&lt;connection></code> elements describing the properties of each
 * type of connection to be pooled. Each element within a
 * <code>&lt;connection></code> element corresponds to a particular property of
 * that connection type. These are described in the table below.<p>
 *
 * <table BORDER CELLSPACING=0 >
 * <tr><th>Property</th><th>Default Value</th><th>Description</th></tr>
 * <tr>
 * <td><code>name</code></td>
 * 
 * <td>none</td>
 * 
 * <td>Name to be associated with the pool for this 'type' of connection</td>
 * </tr>
 * 
 * <tr>
 * <td><code>username</code></td>
 * 
 * <td>none</td>
 * 
 * <td>User name of the database account to be used by this pool</td>
 * </tr>
 * 
 * <tr>
 * <td><code>password</code></td>
 * 
 * <td>none</td>
 * 
 * <td>Password of the database account to be used by this pool</td>
 * </tr>
 * 
 * <tr>
 * <td><code>driver</code></td>
 * 
 * <td><code>"oracle.jdbc.driver.OracleDriver"</code></td>
 * 
 * <td>Full class name of the JDBC driver to be used. Note this class must be
 * present in your <code>CLASSPATH</code></td>
 * </tr>
 * 
 * <tr>
 * <td><code>dburl</code></td>
 * 
 * <td>none</td>
 * 
 * <td>JDBC URL to the database (e.g. <code>jdbc:oracle:oci8:@tiger</code>).
 * Consult the documentation for JDBC driver you intend to use for details on
 * the format of this URL.</td></tr>
 * 
 * <tr>
 * <td><code>minCount</code></td>
 * 
 * <td><code>5</code></td>
 * 
 * <td>Minimum number of connections in this pool. This will be the number
 * of connections initialized when this pool is first accessed, and the minimum
 * number of connections maintained in the pool throughout its lifetime. Connections
 * above this minimum number will be removed from this pool if they remain
 * unused for a period in seconds greater than <code>maxIdleTime</code> (see below).</td>
 * </tr>
 * 
 * <tr>
 * <td><code>maxCount</code></td>
 * 
 * <td><code>25</code></td>
 * 
 * <td>Maximum number of connections in this pool. If the pool ever reaches
 * this size and a request is made for another connection, then the request
 * will be blocked until a connection has been returned to the pool or the
 * <code>maxWaitTime</code>
 * timeout period has expired.</td>
 * </tr>
 * 
 * <tr>
 * <td><code>maxWaitTime</code></td>
 * 
 * <td><code>20</code> seconds</td>
 * 
 * <td>The maximum number of seconds to wait for a connection to be returned
 * to the pool.</td>
 * </tr>
 * 
 * <tr>
 * <td><code>maxIdleTime</code></td>
 * 
 * <td><code>3600</code> seconds (1 hour)</td>
 * 
 * <td>The maximum number of seconds that a connection should be kept in the
 * pool for since it was last used.</td>
 * </tr>
 * </table><p>
 *
 * Within the top-level <code>&lt;connectionManager></code> element, 'defaults'
 * for the <code>minCount</code>, <code>maxCount</code>,
 * <code>maxWaitTime</code>, and <code>maxIdleTime</code> connection properties
 * can also be specified, which are 'inherited' by all
 * <code>&lt;connection></code> elements for which the corresponding property
 * is not specified.
 */
public class ConnectionManager
{
    private static final String CONFIG_PROPERTY  = "ConnectionManager.config";
    private static final String ELEMENT_CONNECTION_MANAGER = "connectionManager";

    private static ConnectionManager mInstance = null;
    // Private lock used for synchronization
    private static Object mLock = new Object();
    private Hashtable mConnectionPools  = new Hashtable();


    // Pool attributes and their default values
    private int mMinCount = 5;
    // By default, we don't allow more than 25 connections in the pool
    private int mMaxCount = 25;
    // Don't wait more that 20 seconds for a connection
    private int mMaxWaitTime = 20000;
    // Kill unused connections after 1 hour
    private int mMaxIdleTime = 3600000;

    private  ConnectionManager()
    {
    }   

    /**
     * Get a JDBC connection from the pool of the given name. Any Connection
     * obtained from this method <i>must</i> later be retured with {@link
     * #releaseConnection(String, Connection)}, even if normal execution is
     * interrupted by an exception. This is typically achieved by puting the
     * <code>releaseConnection</code> call in a <code>finally{}</code> code
     * block.
     *
     * @param pool name of one of the pools managed by this
     * <code>ConnectionManager</code> from which the connection should be
     * retrieved.
     * @return a JDBC connection
     * @exception ConnectionManagerException if an error occurs whilst
     * initializing this <code>ConnectionManager</code>
     * @exception SQLException if an error occurs whilst getting a Connection
     * from the pool.
     */
    public static Connection getConnection(String pool)
        throws ConnectionManagerException, SQLException
    {
        return getInstance().instanceGetConnection(pool);
    }

    /**
     * Return a JDBC connection to the pool of the given name.
     *
     * @param pool name of one of the pools managed by this
     * <code>ConnectionManager</code> from which the connection was obtained.
     * @param conn a Connection obtained from the pool with {@link
     * #getConnection(String)}.
     */
    public static void releaseConnection(String pool, Connection conn)
    {
        try
        {
            getInstance().instanceReleaseConnection(pool, conn);
        }
        catch(ConnectionManagerException e)
        {
            // We don't really care if an exception occurs while releasing
            // a connection
        }
    }

    // We give the accessors protected level protection, so that the
    // ConnectionManagerNodeHandler is able to call the 'setters' and the
    // DefaultConnectionPool is able to call the 'getters'. Since getInstance()
    // is private, it doesn't make sense to expose these elsewhere.
    protected void addConnection(Object o) throws ConnectionManagerException
    {
        if(!(o instanceof DefaultConnectionPool))
        {
            throw new ConnectionManagerException("Only DefaultConnectionPools may be children of a ConnectionManager");
        }
        DefaultConnectionPool dcp = (DefaultConnectionPool)o;
        mConnectionPools.put(dcp.getName(), dcp);
    }

    protected void setMinCount(int count)
    {
        mMinCount = count;
    }

    protected void setMinCount(String count)
    {
        setMinCount(Integer.parseInt(count));
    }

    protected int getMinCount()
    {
        return mMinCount;
    }

    protected void setMaxCount(int count)
    {
        mMaxCount = count;
    }

    protected void setMaxCount(String count)
    {
        setMaxCount(Integer.parseInt(count));
    }

    protected int getMaxCount()
    {
        return mMaxCount;
    }

    protected void setMaxWaitTime(int seconds)
    {
        mMaxWaitTime = seconds * 1000;
    }

    protected void setMaxWaitTime(String seconds)
    {
        setMaxWaitTime(Integer.parseInt(seconds));
    }

    protected int getMaxWaitTime()
    {
        return mMaxWaitTime;
    }

    protected void setMaxIdleTime(int seconds)
    {
        mMaxIdleTime = seconds * 1000;
    }

    protected void setMaxIdleTime(String seconds)
    {
        setMaxIdleTime(Integer.parseInt(seconds));
    }

    protected int getMaxIdleTime()
    {
        return mMaxIdleTime;
    }

    private static ConnectionManager getInstance() throws ConnectionManagerException
    {
        if (mInstance != null)
        {
            return mInstance;
        }
        
        synchronized (mLock)
        {
            if (mInstance == null)
            {
                mInstance = new ConnectionManager();
                mInstance.init();
            }
        }
        return mInstance;
    }

    private Connection instanceGetConnection(String pool)
        throws ConnectionManagerException, SQLException
    {
        DefaultConnectionPool dcp = getPool(pool);
        return dcp.getConnection();
    }

    private void instanceReleaseConnection(String pool, Connection conn)
    {
        DefaultConnectionPool dcp = getPool(pool);
        if (dcp != null)
        {
            dcp.releaseConnection(conn);
        }
    }

    private void init() throws ConnectionManagerException
    {
        XMLDocument registryXML = parseRegistry();

        // Now Process the Registry
        NodeHandler nodeHandler = new ConnectionManagerNodeHandler();
        try {
            nodeHandler.processNode(registryXML.getDocumentElement(),this);
        } catch (NodeHandlerException e) {
            throw new ConnectionManagerException("Error processing ConnectionManager " +
                                                 "configuration: " + e.getMessage());
        }
    }

    private DefaultConnectionPool getPool( String poolName)
    {
        return (DefaultConnectionPool)mConnectionPools.get(poolName);
    }

    private static XMLDocument parseRegistry() throws ConnectionManagerException
    {
        XMLDocument xmldoc = null;

        // Ensure the path to the configuration file has been defined
        String configFile = System.getProperty(CONFIG_PROPERTY);
        if(configFile == null)
        {
            throw new ConnectionManagerException("Unable to create ConnectionManager instance: " +
                                                 CONFIG_PROPERTY +  " property not defined.");
        }

        //Capture any XML / XSL parsing errors in the following block to this
        //output stream. A finally{} block at the end ensures it gets closed
        //afterwards
        ByteArrayOutputStream errOut = new ByteArrayOutputStream();
        try
        {
            // Make sure we release this InputStream when finished with it
            InputStream is = null;
            try
            {
                is = new FileInputStream(new File(configFile));
                // Obtain the document.
                xmldoc = XMLUtil.parseDocument(is, errOut);
            }
            finally
            {
                if(is != null)
                {
                    try
                    {
                        is.close();
                    }
                    catch(Exception e1)
                    {
                    }
                    is = null;
                }
            }
            Enumeration xsldocs = XMLUtil.getXslDocs(xmldoc);

            // If the document explicitly names a list of stylesheets, then
            // apply them
            while(xsldocs.hasMoreElements())
            {
                xmldoc = XMLUtil.applyXSL(xmldoc, (XSLStylesheet)xsldocs.nextElement(), errOut);
            } 
        } // End try
        catch (SAXException se)
        {
            String msg = se.getMessage() + Character.LINE_SEPARATOR + errOut.toString();
            throw new ConnectionManagerException("Error in " + configFile + ": " + msg);
        }
        catch (XSLException xsle)
        {
            String msg = xsle.getMessage() + Character.LINE_SEPARATOR + errOut.toString();
            throw new ConnectionManagerException("XSL error processing " + configFile +
                                                 ": " + msg);
        }
        catch (IOException ioe)
        {
            throw new ConnectionManagerException("IO Error reading " + configFile + ": " +
                                                 ioe.getMessage());
        }
        finally
        {
            if(errOut!= null)
            {
                try
                {
                    errOut.close();
                }
                catch(Exception e1)
                {
                }
            }
        }

        // Make sure that the XML we end up with actually has <connectionManager> at the root!
        Element docEl = xmldoc.getDocumentElement();
        if(docEl == null || !docEl.getTagName().equalsIgnoreCase(ELEMENT_CONNECTION_MANAGER))
        {
            throw new ConnectionManagerException("No connectionManager element found in " +
                                                 configFile);
        }
        return xmldoc;
    }
}

