/* Copyright (c) Oracle Corporation 2001, All Rights Reserved. */
package oracle.portal.utils.v1;

import java.sql.*;
import java.util.*;
import java.io.*;
import oracle.jdbc.driver.*;
import oracle.portal.utils.xml.v1.*;

// At the moment this class is only used by ConnectionManager, so we have not
// made it public
class DefaultConnectionPool implements InitializableXMLObject
{
    private ConnectionManager mParent;
    // This is actually a hash mapping Connections to single element long
    // arrays. We use these rather than Longs because they are mutable.
    private Hashtable mLastUsedTimes;
    private Vector mFreeConnections;
    private int mCount;
    private Object mLock = new Object();
    private boolean mIsInitialized = false;

    // Connection level attributes
    private String mConnectionName;
    private String mUsername = "";
    private String mPassword = "";
    private String mDburl = "";
    private Class mDriverClass;

    // Pool attributes (defaultable at 'manager' level)
    private int mMinCount = -1;
    private int mMaxCount = -1;
    private int mMaxWaitTime = -1;
    private int mMaxIdleTime = -1;

    public DefaultConnectionPool()
    {
    }

    public void preInitialize(Object parent) throws NodeHandlerException
    {
        if(!(parent instanceof ConnectionManager))
        {
            throw new NodeHandlerException("Parent of a DefaultConnectionPool must be a ConnectionManager");
        }
        mParent = (ConnectionManager)parent;
    }

    public void postInitialize() throws NodeHandlerException
    {
        if(mConnectionName == null)
        {
            throw new NodeHandlerException("A connection without a name has been defined");
        }
    }

    public void setName(String name)
    {
        mConnectionName = name;
    }

    public String getName()
    {
        return mConnectionName;
    }
    
    public void setUsername(String username)
    {
        mUsername = username;
    }

    public void setPassword(String password)
    {
        mPassword = password;
    }

    public void setDburl(String dburl)
    {
        mDburl = dburl;
    }

    public void setDriver(String driver) throws ConnectionManagerException
    {
        try
        {
            mDriverClass = Class.forName(driver);
        }
        catch (ClassNotFoundException e)
        {
            throw new ConnectionManagerException(e);
        }
    }

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

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

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

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

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

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

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

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

    private void init() throws SQLException
    {
        if (mIsInitialized)
        {
            return;
        }
        if(mParent == null)
        {
            throw new SQLException("DefaultConnection Pool not initialized.");
        }
        // Default the attributes that haven't been set yet from the parent
        if(mMinCount == -1)
        {
            mMinCount = mParent.getMinCount();
        }
        if(mMaxCount == -1)
        {
            mMaxCount = mParent.getMaxCount();
        }
        if(mMaxWaitTime == -1)
        {
            mMaxWaitTime = mParent.getMaxWaitTime();
        }
        if(mMaxIdleTime == -1)
        {
            mMaxIdleTime = mParent.getMaxIdleTime();
        }
        mLastUsedTimes = new Hashtable(mMaxCount);
        mFreeConnections = new Vector();
        while(mCount < mMinCount)
        {
            newConnection();
        }
        mIsInitialized = true;
    }

    public Connection getConnection() throws SQLException
    {
        // Calculate the time at which this request should time out if unsuccessful
        int newConnections = 0;
        Connection theConn = null;

        synchronized (mLock)
        {
            if( !mIsInitialized )
            {
                init();
            }

            long stopTime = System.currentTimeMillis() + mMaxWaitTime;

            // If we don't have any connections in the free list, then we
            // should create another one, if we are allowed to, or wait for a
            // connection to be returned (up to the timeout period) otherwise.
            if(mFreeConnections.isEmpty())
            {
                // We haven't reached the maximum number of allowed connections, so
                // go ahead and add another one
                if(mCount < mMaxCount)
                {
                    newConnection();
                }
                // We have reached our maximum number of connections, so we'll just
                // have to wait and hope that one is returned in time
                else
                {
                    long currTime;
                    while(mFreeConnections.isEmpty() && (currTime = System.currentTimeMillis()) < stopTime)
                    {
                        try
                        {
                            mLock.wait(stopTime - currTime);
                        }
                        catch (InterruptedException e)
                        {
                        }
                    }
                    if(mFreeConnections.isEmpty())
                    {
                        throw new SQLException("DefaultConnectionPool timed out waiting for a connection");
                    }
                }
            }

            // Now the pool should contain at least one free connection, loop
            // until we find one of them that is valid, or we have to give up
            do
            {
                // Remove the least recently used connection from the start of
                // the free list
                Connection currCon = (Connection)mFreeConnections.firstElement();
                mFreeConnections.removeElementAt(0);

                // Test the connection is 'alive' by trying a rollback
                try
                {
                    currCon.rollback();
                    theConn = currCon;
                }
                catch (SQLException e)
                {
                    // If we found the connection to be 'dead', close it and
                    // chuck it away
                    closeConnection(currCon);

                    // If we have found one of the connections we have just
                    // created to be dead, then it is unlikely that we'll ever
                    // be able to create a valid connection
                    if(newConnections > mFreeConnections.size())
                    {
                        throw new SQLException("DefaultConnectionPool could not " +
                                               "create a valid connection: " +
                                               e.getMessage());
                    }

                    // Otherwise, if this means we have gone below the minimum
                    // number of connections or our free list is empty,
                    // however, we'll have to add a new connection to the end
                    // of the list
                    if(mCount < mMinCount || mFreeConnections.isEmpty())
                    {
                        newConnection();
                        newConnections++;
                    }
                }

                // We continue looping until we get a valid connection, or we
                // find that one of the connections we just created isn't valid
                // either
            } while (theConn == null);


            // If there is any time left, 'clean up' any open connections that
            // have been hanging around for too long
            long currTime;
            while(mCount > mMinCount && !mFreeConnections.isEmpty() &&
                  (currTime = System.currentTimeMillis()) < stopTime)
            {
                Connection currCon = (Connection)mFreeConnections.firstElement();
                long[] lastUsed = (long[])mLastUsedTimes.get(currCon);

                // If we get to a position in the list where the connection has
                // not yet 'expired' then we have finished cleaning up
                if (currTime - lastUsed[0] <= mMaxIdleTime)
                {
                    break;
                }
                // The connection has been idle for too long. Close it and remove
                // it from the pool.
                closeConnection(currCon);
                mFreeConnections.removeElementAt(0);
            }
        } // End synchronized (mLock)

        return theConn;
    }

    public void releaseConnection(Connection returned)
    {
        long[] lastUsed = (long[])mLastUsedTimes.get(returned);
        // If the connection wasn't in the table, then it wasn't actually in
        // our pool. We won't worry about this, and will just return anyway
        if(lastUsed == null)
        {
            return;
        }

        if (returned instanceof OracleConnection)
        {
            PreparedStatement p = null;
            try
            {
                p = returned.prepareStatement("begin dbms_session.reset_package; end;");
                p.executeUpdate();
            }
            catch (SQLException s)
            {
            }
            finally
            {
                if(p != null)
                {
                    try
                    {
                        p.close();
                    }
                    catch (Exception e)
                    {
                    }
                }
            }
        }
        synchronized (mLock)
        {
            lastUsed[0] = System.currentTimeMillis();
            mFreeConnections.addElement(returned);
            // Notify any other threads currently waiting on a connection to be
            // returned
            mLock.notifyAll();
        }
    }

    private void newConnection() throws SQLException
    {
        Class driverToUse;
        Connection conn = null;
        try
        {
            if(mDriverClass == null)
            {
                driverToUse = Class.forName("oracle.jdbc.driver.OracleDriver");
            }
            else
            {
                driverToUse = mDriverClass;
            }
            Driver d = (Driver) driverToUse.newInstance();
            DriverManager.registerDriver(d);
            conn = (Connection) DriverManager.getConnection (mDburl,
                                                             mUsername,
                                                             mPassword);
        }
        catch (SQLException s)
        {
            throw s;
        }
        catch (Exception e)
        {
            throw new SQLException(e.toString());
        }

        long[] lastUsed = {System.currentTimeMillis()};
        mLastUsedTimes.put(conn, lastUsed);
        mFreeConnections.addElement(conn);
        mCount++;
    }

    private void closeConnection(Connection conn)
    {
        try
        {
            conn.close();
        }
        catch (SQLException se)
        {
        }
        mLastUsedTimes.remove(conn);
        mCount--;
    }
}
