package org.apache.commons.dbcp.jdbc2pool;

/* ====================================================================
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2001 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Apache" and "Apache Software Foundation" and 
 *    "Apache Turbine" must not be used to endorse or promote products 
 *    derived from this software without prior written permission. For 
 *    written permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache",
 *    "Apache Turbine", nor may "Apache" appear in their name, without 
 *    prior written permission of the Apache Software Foundation.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */
 
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import java.util.Hashtable;
import java.io.PrintWriter;
import java.io.Serializable;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import javax.sql.DataSource;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
import javax.naming.Name;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Referenceable;
import javax.naming.Reference;
import javax.naming.RefAddr;
import javax.naming.BinaryRefAddr;
import javax.naming.StringRefAddr;
import javax.naming.NamingException;
import javax.naming.spi.ObjectFactory;

import org.apache.commons.collections.FastHashMap;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.pool.ObjectPool;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.apache.commons.pool.KeyedObjectPool;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;
import org.apache.commons.pool.impl.StackObjectPool;

/**
 * <p>
 * A pooling <code>DataSource</code> appropriate for deployment within
 * J2EE environment.  There are many configuration options.  Multiple users
 * can share a common set of parameters, such as a single maximum number
 * of Connections.  The pool can also support individual pools per user, if the
 * deployment environment can support initialization of mapped properties.
 * So for example, a pool of admin or write-access Connections can be
 * guaranteed a certain number of connections, separate from a maximum
 * set for read-only connections. 
 * </p>
 *
 * <p>
 * A J2EE container will normally provide some method of initializing the
 * <code>DataSource</code> whose attributes are presented
 * as bean getters/setters and then deploying it via JNDI.  It is then
 * available to an application as a source of pooled logical connections to 
 * the database.  The pool needs a source of physical connections.  This
 * source is in the form of a <code>ConnectionPoolDataSource</code> that
 * can be specified via the {@link #setDataSourceName(String)} used to
 * lookup the source via JNDI.
 * </p>
 *
 * <p>
 * Although normally used within a JNDI environment, Jdbc2PoolDataSource
 * can be instantiated and initialized as any bean.  In this case the 
 * <code>ConnectionPoolDataSource</code> will likely be instantiated in
 * a similar manner.  The source can then be attached directly to this
 * pool using the 
 * {@link #setConnectionPoolDataSource(ConnectionPoolDataSource)} method.
 * </p>
 *
 * <p>
 * If this <code>DataSource</code> 
 * is requested via JNDI multiple times, it maintains
 * state between lookups.  Also, multiple instances can be deployed using 
 * different backend <code>ConnectionPoolDataSource</code> sources.  
 * </p>
 *
 * <p>
 * The dbcp package contains an adapter, 
 * {@link org.apache.commons.dbcp.cpdsadapter.DriverAdapterCPDS},
 * that can be used to allow the
 * use of Jdbc2PoolDataSource with jdbc driver implementations that
 * do not supply a <code>ConnectionPoolDataSource</code>, but still
 * provide a {@link java.sql.Driver} implementation.
 * </p>
 *
 * <p>
 * The <a href="package-summary.html">package documentation</a> contains an 
 * example using catalina and JNDI and it also contains a non-JNDI example. 
 * </p>
 *
 * @author <a href="mailto:jmcnally@collab.net">John D. McNally</a>
 * @version $Id: Jdbc2PoolDataSource.java,v 1.1.1.1 2004/02/13 10:02:04 hioki Exp $
 */
public class Jdbc2PoolDataSource
    implements DataSource, Referenceable, Serializable, ObjectFactory
{
    private static final String GET_CONNECTION_CALLED = 
        "A Connection was already requested from this source, " + 
        "further initialization is not allowed.";

    private static Map dsInstanceMap = new HashMap();

    private static ObjectPool userPassKeyPool = 
        new StackObjectPool(new UserPassKey.Factory(), 256);

    private static ObjectPool poolKeyPool = 
        new StackObjectPool(new PoolKey.Factory(), 256);

    private boolean getConnectionCalled = false;

    private ConnectionPoolDataSource cpds;
    /** DataSource Name used to find the ConnectionPoolDataSource */
    private String dataSourceName;
    private boolean defaultAutoCommit;
    private int defaultMaxActive = 0;
    private int defaultMaxIdle = 0;
    private int defaultMaxWait = -1;
    private boolean defaultReadOnly;
    /** Description */
    private String description;
    /** Environment that may be used to set up a jndi initial context. */
    private Properties jndiEnvironment;
    /** Login TimeOut in seconds */
    private int loginTimeout;
    /** Log stream */
    private PrintWriter logWriter;
    private Map perUserDefaultAutoCommit;    
    private Map perUserMaxActive;    
    private Map perUserMaxIdle;    
    private Map perUserMaxWait;
    private Map perUserDefaultReadOnly;    
    private boolean _testOnBorrow;
    private boolean _testOnReturn;
    private int _timeBetweenEvictionRunsMillis;
    private int _numTestsPerEvictionRun;
    private int _minEvictableIdleTimeMillis;
    private boolean _testWhileIdle;
    private String validationQuery = null;
    private boolean testPositionSet;

    private boolean isNew;
    private Integer instanceKey;

    /**
     * Default no-arg constructor for Serialization
     */
    public Jdbc2PoolDataSource() 
    {
        isNew = true;
        defaultAutoCommit = true;
    }

    /**
     * Throws an IllegalStateException, if a PooledConnection has already
     * been requested.
     */
    private void assertInitializationAllowed()
        throws IllegalStateException 
    {
        if (getConnectionCalled) 
        {
            throw new IllegalStateException(GET_CONNECTION_CALLED);
        }
    }

    // -------------------------------------------------------------------
    // Properties

    /**
     * Get the value of connectionPoolDataSource.  This method will return
     * null, if the backing datasource is being accessed via jndi.
     *
     * @return value of connectionPoolDataSource.
     */
    public ConnectionPoolDataSource getConnectionPoolDataSource() 
    {
        return cpds;
    }
    
    /**
     * Set the backend ConnectionPoolDataSource.  This property should not be
     * set if using jndi to access the datasource.
     *
     * @param v  Value to assign to connectionPoolDataSource.
     */
    public void setConnectionPoolDataSource(ConnectionPoolDataSource  v) 
    {
        assertInitializationAllowed();
        if (dataSourceName != null) 
        {
            throw new IllegalStateException(
                "Cannot set the DataSource, if JNDI is used.");
        }
        this.cpds = v;
        if (isNew) 
        {
            registerInstance();
        }
    }

    /**
     * Get the name of the ConnectionPoolDataSource which backs this pool.
     * This name is used to look up the datasource from a jndi service 
     * provider.
     *
     * @return value of dataSourceName.
     */
    public String getDataSourceName() 
    {
        return dataSourceName;
    }
    
    /**
     * Set the name of the ConnectionPoolDataSource which backs this pool.
     * This name is used to look up the datasource from a jndi service 
     * provider.
     *
     * @param v  Value to assign to dataSourceName.
     */
    public void setDataSourceName(String  v) 
    {
        assertInitializationAllowed();
        if (cpds != null) 
        {
            throw new IllegalStateException(
                "Cannot set the JNDI name for the DataSource, if already " +
                "set using setConnectionPoolDataSource.");
        }
        this.dataSourceName = v;
        if (isNew) 
        {
            registerInstance();
        }
    }

    
    /** 
     * Get the value of defaultAutoCommit, which defines the state of 
     * connections handed out from this pool.  The value can be changed
     * on the Connection using Connection.setAutoCommit(boolean).
     * The default is true.
     *
     * @return value of defaultAutoCommit.
     */
    public boolean isDefaultAutoCommit() 
    {
        return defaultAutoCommit;
    }
    
    /**
     * Set the value of defaultAutoCommit, which defines the state of 
     * connections handed out from this pool.  The value can be changed
     * on the Connection using Connection.setAutoCommit(boolean).
     * The default is true.
     *
     * @param v  Value to assign to defaultAutoCommit.
     */
    public void setDefaultAutoCommit(boolean  v) 
    {
        assertInitializationAllowed();
        this.defaultAutoCommit = v;
    }


    /**
     * The maximum number of active connections that can be allocated from
     * this pool at the same time, or zero for no limit.
     * This value is used for any username which is not specified
     * in perUserMaxConnections.  The default is 0.
     */
    public int getDefaultMaxActive() {
        return (this.defaultMaxActive);
    }

    /**
     * The maximum number of active connections that can be allocated from
     * this pool at the same time, or zero for no limit.
     * This value is used for any username which is not specified
     * in perUserMaxConnections.  The default is 0.
     */
    public void setDefaultMaxActive(int maxActive) {
        assertInitializationAllowed();
        this.defaultMaxActive = maxActive;
    }


    /**
     * The maximum number of active connections that can remain idle in the
     * pool, without extra ones being released, or zero for no limit.
     * This value is used for any username which is not specified
     * in perUserMaxIdle.  The default is 0.
     */
    public int getDefaultMaxIdle() {
        return (this.defaultMaxIdle);
    }

    /**
     * The maximum number of active connections that can remain idle in the
     * pool, without extra ones being released, or zero for no limit.
     * This value is used for any username which is not specified
     * in perUserMaxIdle.  The default is 0.
     */
    public void setDefaultMaxIdle(int defaultMaxIdle) {
        assertInitializationAllowed();
        this.defaultMaxIdle = defaultMaxIdle;
    }


    /**
     * The maximum number of milliseconds that the pool will wait (when there
     * are no available connections) for a connection to be returned before
     * throwing an exception, or -1 to wait indefinitely.  Will fail 
     * immediately if value is 0.
     * This value is used for any username which is not specified
     * in perUserMaxWait.  The default is -1.
     */
    public int getDefaultMaxWait() {
        return (this.defaultMaxWait);
    }

    /**
     * The maximum number of milliseconds that the pool will wait (when there
     * are no available connections) for a connection to be returned before
     * throwing an exception, or -1 to wait indefinitely.  Will fail 
     * immediately if value is 0.
     * This value is used for any username which is not specified
     * in perUserMaxWait.  The default is -1.
     */
    public void setDefaultMaxWait(int defaultMaxWait) {
        assertInitializationAllowed();
        this.defaultMaxWait = defaultMaxWait;
    }


    /**
     * Get the value of defaultReadOnly, which defines the state of 
     * connections handed out from this pool.  The value can be changed
     * on the Connection using Connection.setReadOnly(boolean).
     * The default is false.
     *
     * @return value of defaultReadOnly.
     */
    public boolean isDefaultReadOnly() 
    {
        return defaultReadOnly;
    }
    
    /**
     * Set the value of defaultReadOnly, which defines the state of 
     * connections handed out from this pool.  The value can be changed
     * on the Connection using Connection.setReadOnly(boolean).
     * The default is false.
     *
     * @param v  Value to assign to defaultReadOnly.
     */
    public void setDefaultReadOnly(boolean  v) 
    {
        assertInitializationAllowed();
        this.defaultReadOnly = v;
    }

    
    /**
     * Get the description.  This property is defined by jdbc as for use with
     * GUI (or other) tools that might deploy the datasource.  It serves no
     * internal purpose.
     *
     * @return value of description.
     */
    public String getDescription() 
    {
        return description;
    }
    
    /**
     * Set the description.  This property is defined by jdbc as for use with
     * GUI (or other) tools that might deploy the datasource.  It serves no
     * internal purpose.
     * 
     * @param v  Value to assign to description.
     */
    public void setDescription(String  v) 
    {
        this.description = v;
    }
        

    /**
     * Get the value of jndiEnvironment which is used when instantiating
     * a jndi InitialContext.  This InitialContext is used to locate the
     * backend ConnectionPoolDataSource.
     *
     * @return value of jndiEnvironment.
     */
    public String getJndiEnvironment(String key) 
    {
        String value = null;
        if (jndiEnvironment != null) 
        {
            value = jndiEnvironment.getProperty(key);
        }
        return value;
    }
    
    /**
     * Set the value of jndiEnvironment which is used when instantiating
     * a jndi InitialContext.  This InitialContext is used to locate the
     * backend ConnectionPoolDataSource.
     *
     * @param v  Value to assign to jndiEnvironment.
     */
    public void setJndiEnvironment(String key, String value) 
    {
        if (jndiEnvironment == null) 
        {
            jndiEnvironment = new Properties();
        }
        jndiEnvironment.setProperty(key, value);
    }

    
    /**
     * Get the value of loginTimeout.
     * @return value of loginTimeout.
     */
    public int getLoginTimeout() 
    {
        return loginTimeout;
    }
    
    /**
     * Set the value of loginTimeout.
     * @param v  Value to assign to loginTimeout.
     */
    public void setLoginTimeout(int  v) 
    {
        this.loginTimeout = v;
    }
    
    
    /**
     * Get the value of logWriter.
     * @return value of logWriter.
     */
    public PrintWriter getLogWriter() 
    {
        if (logWriter == null) 
        {
            logWriter = new PrintWriter(System.out);
        }        
        return logWriter;
    }
    
    /**
     * Set the value of logWriter.
     * @param v  Value to assign to logWriter.
     */
    public void setLogWriter(PrintWriter  v) 
    {
        this.logWriter = v;
    }
    

    /**
     * The keys are usernames and the value is the --.  Any 
     * username specified here will override the value of defaultAutoCommit.
     */
    public Boolean getPerUserDefaultAutoCommit(String key) 
    {
        Boolean value = null;
        if (perUserDefaultAutoCommit != null) 
        {
            value = (Boolean)perUserDefaultAutoCommit.get(key);
        }
        return value;
    }
    
    /**
     * The keys are usernames and the value is the --.  Any 
     * username specified here will override the value of defaultAutoCommit.
     */
    public void setPerUserDefaultAutoCommit(String username, Boolean value) 
    {
        assertInitializationAllowed();
        if (perUserDefaultAutoCommit == null) 
        {
            perUserDefaultAutoCommit = new HashMap();
        }
        perUserDefaultAutoCommit.put(username, value);
    }

    
    /**
     * The maximum number of active connections that can be allocated from
     * this pool at the same time, or zero for no limit.
     * The keys are usernames and the value is the maximum connections.  Any 
     * username specified here will override the value of defaultMaxActive.
     */
    public Integer getPerUserMaxActive(String username) 
    {
        Integer value = null;
        if (perUserMaxActive != null) 
        {
            value = (Integer)perUserMaxActive.get(username);
        }
        return value;
    }
    
    /**
     * The maximum number of active connections that can be allocated from
     * this pool at the same time, or zero for no limit.
     * The keys are usernames and the value is the maximum connections.  Any 
     * username specified here will override the value of defaultMaxActive.
     */
    public void setPerUserMaxActive(String username, Integer value) 
    {
        assertInitializationAllowed();
        if (perUserMaxActive == null) 
        {
            perUserMaxActive = new HashMap();
        }
        perUserMaxActive.put(username, value);
    }


    /**
     * The maximum number of active connections that can remain idle in the
     * pool, without extra ones being released, or zero for no limit.
     * The keys are usernames and the value is the maximum connections.  Any 
     * username specified here will override the value of defaultMaxIdle.
     */
    public Integer getPerUserMaxIdle(String username) 
    {
        Integer value = null;
        if (perUserMaxIdle != null) 
        {
            value = (Integer)perUserMaxIdle.get(username);
        }
        return value;
    }
    
    /**
     * The maximum number of active connections that can remain idle in the
     * pool, without extra ones being released, or zero for no limit.
     * The keys are usernames and the value is the maximum connections.  Any 
     * username specified here will override the value of defaultMaxIdle.
     */
    public void setPerUserMaxIdle(String username, Integer value) 
    {
        assertInitializationAllowed();
        if (perUserMaxIdle == null) 
        {
            perUserMaxIdle = new HashMap();
        }
        perUserMaxIdle.put(username, value);
    }
    
    /**
     * The maximum number of milliseconds that the pool will wait (when there
     * are no available connections) for a connection to be returned before
     * throwing an exception, or -1 to wait indefinitely.  Will fail 
     * immediately if value is 0.
     * The keys are usernames and the value is the maximum connections.  Any 
     * username specified here will override the value of defaultMaxWait.
     */
    public Integer getPerUserMaxWait(String username) 
    {
        Integer value = null;
        if (perUserMaxWait != null) 
        {
            value = (Integer)perUserMaxWait.get(username);
        }
        return value;
    }
    
    /**
     * The maximum number of milliseconds that the pool will wait (when there
     * are no available connections) for a connection to be returned before
     * throwing an exception, or -1 to wait indefinitely.  Will fail 
     * immediately if value is 0.
     * The keys are usernames and the value is the maximum connections.  Any 
     * username specified here will override the value of defaultMaxWait.
     */
    public void setPerUserMaxWait(String username, Integer value) 
    {
        assertInitializationAllowed();
        if (perUserMaxWait == null) 
        {
            perUserMaxWait = new HashMap();
        }
        perUserMaxWait.put(username, value);
    }


    /**
     * The keys are usernames and the value is the --.  Any 
     * username specified here will override the value of defaultReadOnly.
     */
    public Boolean getPerUserDefaultReadOnly(String username) 
    {
        Boolean value = null;
        if (perUserDefaultReadOnly != null) 
        {
            value = (Boolean)perUserDefaultReadOnly.get(username);
        }
        return value;
    }
    
    /**
     * The keys are usernames and the value is the --.  Any 
     * username specified here will override the value of defaultReadOnly.
     */
    public void setPerUserDefaultReadOnly(String username, Boolean value) 
    {
        assertInitializationAllowed();
        if (perUserDefaultReadOnly == null) 
        {
            perUserDefaultReadOnly = new HashMap();
        }
        perUserDefaultReadOnly.put(username, value);
    }


    /**
     * When <tt>true</tt>, objects will be
     * {*link PoolableObjectFactory#validateObject validated}
     * before being returned by the {*link #borrowObject}
     * method.  If the object fails to validate,
     * it will be dropped from the pool, and we will attempt
     * to borrow another.
     *
     * *see #setTestOnBorrow
     */
    public boolean getTestOnBorrow() {
        return _testOnBorrow;
    }

    /**
     * When <tt>true</tt>, objects will be
     * {*link PoolableObjectFactory#validateObject validated}
     * before being returned by the {*link #borrowObject}
     * method.  If the object fails to validate,
     * it will be dropped from the pool, and we will attempt
     * to borrow another.
     *
     * *see #getTestOnBorrow
     */
    public void setTestOnBorrow(boolean testOnBorrow) {
        assertInitializationAllowed();
        _testOnBorrow = testOnBorrow;
        testPositionSet = true;
    }

    /**
     * When <tt>true</tt>, objects will be
     * {*link PoolableObjectFactory#validateObject validated}
     * before being returned to the pool within the
     * {*link #returnObject}.
     *
     * *see #setTestOnReturn
     */
    public boolean getTestOnReturn() {
        return _testOnReturn;
    }

    /**
     * When <tt>true</tt>, objects will be
     * {*link PoolableObjectFactory#validateObject validated}
     * before being returned to the pool within the
     * {*link #returnObject}.
     *
     * *see #getTestOnReturn
     */
    public void setTestOnReturn(boolean testOnReturn) {
        assertInitializationAllowed();
        _testOnReturn = testOnReturn;
        testPositionSet = true;
    }

    /**
     * Returns the number of milliseconds to sleep between runs of the
     * idle object evictor thread.
     * When non-positive, no idle object evictor thread will be
     * run.
     *
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public int getTimeBetweenEvictionRunsMillis() {
        return _timeBetweenEvictionRunsMillis;
    }

    /**
     * Sets the number of milliseconds to sleep between runs of the
     * idle object evictor thread.
     * When non-positive, no idle object evictor thread will be
     * run.
     *
     * *see #getTimeBetweenEvictionRunsMillis
     */
    public void 
        setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
        assertInitializationAllowed();
            _timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    /**
     * Returns the number of objects to examine during each run of the
     * idle object evictor thread (if any).
     *
     * *see #setNumTestsPerEvictionRun
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public int getNumTestsPerEvictionRun() {
        return _numTestsPerEvictionRun;
    }

    /**
     * Sets the number of objects to examine during each run of the
     * idle object evictor thread (if any).
     * <p>
     * When a negative value is supplied, <tt>ceil({*link #numIdle})/abs({*link #getNumTestsPerEvictionRun})</tt>
     * tests will be run.  I.e., when the value is <i>-n</i>, roughly one <i>n</i>th of the
     * idle objects will be tested per run.
     *
     * *see #getNumTestsPerEvictionRun
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public void setNumTestsPerEvictionRun(int numTestsPerEvictionRun) {
        assertInitializationAllowed();
        _numTestsPerEvictionRun = numTestsPerEvictionRun;
    }

    /**
     * Returns the minimum amount of time an object may sit idle in the pool
     * before it is eligable for eviction by the idle object evictor
     * (if any).
     *
     * *see #setMinEvictableIdleTimeMillis
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public int getMinEvictableIdleTimeMillis() {
        return _minEvictableIdleTimeMillis;
    }

    /**
     * Sets the minimum amount of time an object may sit idle in the pool
     * before it is eligable for eviction by the idle object evictor
     * (if any).
     * When non-positive, no objects will be evicted from the pool
     * due to idle time alone.
     *
     * *see #getMinEvictableIdleTimeMillis
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public void 
        setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
        assertInitializationAllowed();
        _minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    /**
     * When <tt>true</tt>, objects will be
     * {*link PoolableObjectFactory#validateObject validated}
     * by the idle object evictor (if any).  If an object
     * fails to validate, it will be dropped from the pool.
     *
     * *see #setTestWhileIdle
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public boolean getTestWhileIdle() {
        return _testWhileIdle;
    }

    /**
     * When <tt>true</tt>, objects will be
     * {*link PoolableObjectFactory#validateObject validated}
     * by the idle object evictor (if any).  If an object
     * fails to validate, it will be dropped from the pool.
     *
     * *see #getTestWhileIdle
     * *see #setTimeBetweenEvictionRunsMillis
     */
    public void setTestWhileIdle(boolean testWhileIdle) {
        assertInitializationAllowed();
        _testWhileIdle = testWhileIdle;
        testPositionSet = true;
    }


    /**
     * The SQL query that will be used to validate connections from this pool
     * before returning them to the caller.  If specified, this query
     * <strong>MUST</strong> be an SQL SELECT statement that returns at least
     * one row.
     */
    public String getValidationQuery() {
        return (this.validationQuery);
    }

    /**
     * The SQL query that will be used to validate connections from this pool
     * before returning them to the caller.  If specified, this query
     * <strong>MUST</strong> be an SQL SELECT statement that returns at least
     * one row.  Default behavior is to test the connection when it is
     * borrowed.
     */
    public void setValidationQuery(String validationQuery) {
        assertInitializationAllowed();
        this.validationQuery = validationQuery;
        if (!testPositionSet) 
        {
            setTestOnBorrow(true);
        }
    }

    // ----------------------------------------------------------------------
    // DataSource implementation 

    /**
     * Attempt to establish a database connection.
     */
    public Connection getConnection() 
        throws SQLException
    {
        return getConnection(null, null);
    }

    /**
     * Attempt to establish a database connection.
     */
    synchronized public Connection getConnection(String username, String password)
        throws SQLException
    {
        if (isNew) 
        {
            throw new SQLException("Must set the ConnectionPoolDataSource " + 
                "through setDataSourceName or setConnectionPoolDataSource " + 
                "before calling getConnection.");
        }
        getConnectionCalled = true;
        Map pools = (Map)dsInstanceMap.get(instanceKey);
        PoolKey key = getPoolKey(username);
        Object pool = pools.get(key);
        if ( pool == null ) 
        {
            try
            {
                registerPool(username, password);
                pool = pools.get(key);
            }
            catch (Exception e)
            {
                e.printStackTrace();
                throw new SQLException(e.getMessage());
            }
        }
        returnPoolKey(key);

        PooledConnection pc = null;
        if (pool instanceof ObjectPool) 
        {
            try
            {
                pc = (PooledConnection)((ObjectPool)pool).borrowObject();
            }
            catch (Exception e)
            {
                if (e instanceof RuntimeException) 
                {
                    throw (RuntimeException)e;
                }
                else 
                {
                    throw new SQLException(e.getMessage());
                }
            }
        }
        else // assume KeyedObjectPool
        { 
            try
            {
                UserPassKey upkey = getPCKey(username, password);
                pc = (PooledConnection)
                    ((KeyedObjectPool)pool).borrowObject(upkey);
                returnPCKey(upkey);
            }
            catch (Exception e)
            {
                if (e instanceof RuntimeException) 
                {
                    throw (RuntimeException)e;
                }
                else 
                {
                    throw new SQLException(e.getMessage());
                }
            }
        }
        
        boolean defaultAutoCommit = isDefaultAutoCommit();
        if ( username != null ) 
        {
            Boolean userMax = 
                getPerUserDefaultAutoCommit(username);
            if ( userMax != null ) 
            {
                defaultAutoCommit = userMax.booleanValue();
            }
        }    

        boolean defaultReadOnly = isDefaultReadOnly();
        if ( username != null ) 
        {
            Boolean userMax = 
                getPerUserDefaultReadOnly(username);
            if ( userMax != null ) 
            {
                defaultReadOnly = userMax.booleanValue();
            }
        }    

        Connection con = pc.getConnection();        
        con.setAutoCommit(defaultAutoCommit);
        con.setReadOnly(defaultReadOnly);
        return con;
    }

    private UserPassKey getPCKey(String username, String password)
    {
        UserPassKey upk = null;
        try
        {
            upk = (UserPassKey)userPassKeyPool.borrowObject();
        }
        catch (Exception e)
        {
            getLogWriter().println("[WARN] Jdbc2PoolDataSource::getPCKey"
                + " could not get key from pool. Created a new instance. "
                + e.getMessage());
            upk = new UserPassKey();
        }
        upk.init(username, password);
        return upk;
    }

    private void returnPCKey(UserPassKey key)
    {
        if (key.isReusable()) 
        {
            try
            {
                userPassKeyPool.returnObject(key);
            }
            catch (Exception e)
            {
                getLogWriter().println(
                    "[WARN] Jdbc2PoolDataSource::returnPCKey could not return"
                    + " key to pool. " + e.getMessage());
            }
        }
    }

    private PoolKey getPoolKey(String username)
    {
        PoolKey key = null;
        try
        {
            key = (PoolKey)poolKeyPool.borrowObject();
        }
        catch (Exception e)
        {
            getLogWriter().println("[WARN] Jdbc2PoolDataSource::getPoolKey"
                + " could not get key from pool. Created a new instance. "
                + e.getMessage());
            key = new PoolKey();
        }
        if ( username != null && 
             (perUserMaxActive == null 
              || !perUserMaxActive.containsKey(username)) ) 
        {
            username = null;
        }
        key.init(getDataSourceName(), username);
        return key;
    }

    private void returnPoolKey(PoolKey key)
    {
        try
        {
            poolKeyPool.returnObject(key);
        }
        catch (Exception e)
        {
            getLogWriter().println(
                "[WARN] Jdbc2PoolDataSource::returnPoolKey could not return"
                + " key to pool. " + e.getMessage());
        }
    }

    synchronized private void registerInstance()
    {
        if (isNew) 
        {
            int max = 0;
            Iterator i = dsInstanceMap.keySet().iterator();
            while (i.hasNext()) 
            {
                int key = ((Integer)i.next()).intValue();
                max = Math.max(max, key);
            }
            instanceKey = new Integer(max+1);
            FastHashMap fhm = new FastHashMap();
            fhm.setFast(true);
            dsInstanceMap.put(instanceKey, fhm);
            isNew = false;
        }
    }

    synchronized private void registerPool(String username, String password)
         throws javax.naming.NamingException
    {
        Map pools = (Map)dsInstanceMap.get(instanceKey);
        PoolKey key = getPoolKey(username);
        if ( !pools.containsKey(key) ) 
        {
            int maxActive = getDefaultMaxActive();
            int maxIdle = getDefaultMaxIdle();
            int maxWait = getDefaultMaxWait();

            // The source of physical db connections
            ConnectionPoolDataSource cpds = this.cpds;
            if ( cpds == null ) 
            {            
                Context ctx = null;
                if ( jndiEnvironment == null ) 
                {
                    ctx = new InitialContext();                
                }
                else 
                {
                    ctx = new InitialContext(jndiEnvironment);
                }
                cpds = (ConnectionPoolDataSource)ctx.lookup(dataSourceName);
            }

            Object whicheverPool = null;
            if (perUserMaxActive != null 
                && perUserMaxActive.containsKey(username)) 
            {                
                Integer userMax = getPerUserMaxActive(username);
                if ( userMax != null ) 
                {
                    maxActive = userMax.intValue();
                }
                userMax = getPerUserMaxIdle(username);
                if ( userMax != null ) 
                {
                    maxIdle = userMax.intValue();
                }
                userMax = getPerUserMaxWait(username);
                if ( userMax != null ) 
                {
                    maxWait = userMax.intValue();
                }

                // Create an object pool to contain our PooledConnections
                GenericObjectPool pool = new GenericObjectPool(null);
                pool.setMaxActive(maxActive);
                pool.setMaxIdle(maxIdle);
                pool.setMaxWait(maxWait);
                pool.setWhenExhaustedAction(
                    getWhenExhausted(maxActive, maxWait));
                pool.setTestOnBorrow(getTestOnBorrow());
                pool.setTestOnReturn(getTestOnReturn());
                pool.setTimeBetweenEvictionRunsMillis(
                    getTimeBetweenEvictionRunsMillis());
                pool.setNumTestsPerEvictionRun(getNumTestsPerEvictionRun());
                pool.setMinEvictableIdleTimeMillis(
                    getMinEvictableIdleTimeMillis());
                pool.setTestWhileIdle(getTestWhileIdle());
                
                // Set up the factory we will use (passing the pool associates
                // the factory with the pool, so we do not have to do so
                // explicitly)
                new CPDSConnectionFactory(cpds, pool, validationQuery,
                                          username, password);
                whicheverPool = pool;
            }
            else // use default pool
            {
                // Create an object pool to contain our PooledConnections
                GenericKeyedObjectPool pool = new GenericKeyedObjectPool(null);
                pool.setMaxActive(maxActive);
                pool.setMaxIdle(maxIdle);
                pool.setMaxWait(maxWait);
                pool.setWhenExhaustedAction(
                    getWhenExhausted(maxActive, maxWait));
                pool.setTestOnBorrow(getTestOnBorrow());
                pool.setTestOnReturn(getTestOnReturn());
                pool.setTimeBetweenEvictionRunsMillis(
                    getTimeBetweenEvictionRunsMillis());
                pool.setNumTestsPerEvictionRun(getNumTestsPerEvictionRun());
                pool.setMinEvictableIdleTimeMillis(
                    getMinEvictableIdleTimeMillis());
                pool.setTestWhileIdle(getTestWhileIdle());
                
                // Set up the factory we will use (passing the pool associates
                // the factory with the pool, so we do not have to do so
                // explicitly)
                new KeyedCPDSConnectionFactory(cpds, pool, validationQuery);
                whicheverPool = pool;
            }
            
            // pools is a FastHashMap set to put the pool in a thread-safe way
            pools.put(key, whicheverPool);
        }        
    }

    private byte getWhenExhausted(int maxActive, int maxWait)
    {
        byte whenExhausted = GenericObjectPool.WHEN_EXHAUSTED_BLOCK;
        if (maxActive <= 0) 
        {
            whenExhausted = GenericObjectPool.WHEN_EXHAUSTED_GROW;
        }
        else if (maxWait == 0) 
        {
            whenExhausted = GenericObjectPool.WHEN_EXHAUSTED_FAIL;
        }
        return whenExhausted;
    }    

    // ----------------------------------------------------------------------
    // Referenceable implementation 

    /**
     * <CODE>Referenceable</CODE> implementation prepares object for
     * binding in jndi.
     */
    public Reference getReference() 
        throws NamingException
    {
        // this class implements its own factory
        String factory = getClass().getName();
        Reference ref = new Reference(getClass().getName(), factory, null);

        ref.add(new StringRefAddr("isNew", 
                                  String.valueOf(isNew)));
        ref.add(new StringRefAddr("instanceKey", 
            (instanceKey == null ? null : instanceKey.toString()) ));
        ref.add(new StringRefAddr("dataSourceName", getDataSourceName()));
        ref.add(new StringRefAddr("defaultAutoCommit", 
                                  String.valueOf(isDefaultAutoCommit())));
        ref.add(new StringRefAddr("defaultMaxActive", 
                                  String.valueOf(getDefaultMaxActive())));
        ref.add(new StringRefAddr("defaultMaxIdle", 
                                  String.valueOf(getDefaultMaxIdle())));
        ref.add(new StringRefAddr("defaultMaxWait", 
                                  String.valueOf(getDefaultMaxWait())));
        ref.add(new StringRefAddr("defaultReadOnly", 
                                  String.valueOf(isDefaultReadOnly())));
        ref.add(new StringRefAddr("description", getDescription()));

        byte[] ser = null;
        // BinaryRefAddr does not allow null byte[].
        if ( jndiEnvironment != null ) 
        {
            try
            {
                ser = serialize(jndiEnvironment);
                ref.add(new BinaryRefAddr("jndiEnvironment", ser));
            }
            catch (IOException ioe)
            {
                throw new NamingException("An IOException prevented " + 
                   "serializing the jndiEnvironment properties.");
            }
        }

        ref.add(new StringRefAddr("loginTimeout", 
                                  String.valueOf(getLoginTimeout())));

        if ( perUserDefaultAutoCommit != null ) 
        {
            try
            {
                ser = serialize((Serializable)perUserDefaultAutoCommit);
                ref.add(new BinaryRefAddr("perUserDefaultAutoCommit", ser));
            }
            catch (IOException ioe)
            {
                throw new NamingException("An IOException prevented " + 
                   "serializing the perUserDefaultAutoCommit properties.");
            }
        }

        if ( perUserMaxActive != null ) 
        {
            try
            {
                ser = serialize((Serializable)perUserMaxActive);
                ref.add(new BinaryRefAddr("perUserMaxActive", ser));
            }
            catch (IOException ioe)
            {
                throw new NamingException("An IOException prevented " + 
                   "serializing the perUserMaxActive properties.");
            }
        }

        if ( perUserMaxIdle != null ) 
        {
            try
            {
                ser = serialize((Serializable)perUserMaxIdle);
                ref.add(new BinaryRefAddr("perUserMaxIdle", ser));
            }
            catch (IOException ioe)
            {
                throw new NamingException("An IOException prevented " + 
                   "serializing the perUserMaxIdle properties.");
            }
        }

        if ( perUserMaxWait != null ) 
        {
            try
            {
                ser = serialize((Serializable)perUserMaxWait);
                ref.add(new BinaryRefAddr("perUserMaxWait", ser));
            }
            catch (IOException ioe)
            {
                throw new NamingException("An IOException prevented " + 
                   "serializing the perUserMaxWait properties.");
            }
        }

        if ( perUserDefaultReadOnly != null ) 
        {
            try
            {
                ser = serialize((Serializable)perUserDefaultReadOnly);
                ref.add(new BinaryRefAddr("perUserDefaultReadOnly", ser));
            }
            catch (IOException ioe)
            {
                throw new NamingException("An IOException prevented " + 
                   "serializing the perUserDefaultReadOnly properties.");
            }
        }

        ref.add(new StringRefAddr("testOnBorrow", 
                                  String.valueOf(getTestOnBorrow())));
        ref.add(new StringRefAddr("testOnReturn", 
                                  String.valueOf(getTestOnReturn())));
        ref.add(new StringRefAddr("timeBetweenEvictionRunsMillis", 
            String.valueOf(getTimeBetweenEvictionRunsMillis())));
        ref.add(new StringRefAddr("numTestsPerEvictionRun", 
            String.valueOf(getNumTestsPerEvictionRun())));
        ref.add(new StringRefAddr("minEvictableIdleTimeMillis", 
            String.valueOf(getMinEvictableIdleTimeMillis())));
        ref.add(new StringRefAddr("testWhileIdle", 
                                  String.valueOf(getTestWhileIdle())));
        ref.add(new StringRefAddr("validationQuery", getValidationQuery()));
        
        return ref;
    }

    /**
     * Converts a object to a byte array for storage/serialization.
     *
     * @param obj The Serializable to convert.
     * @return A byte[] with the converted Serializable.
     * @exception IOException, if conversion to a byte[] fails.
     */
    private static byte[] serialize(Serializable obj)
        throws IOException
    {
        byte[] byteArray = null;
        ByteArrayOutputStream baos = null;
        ObjectOutputStream out = null;
        try
        {
            // These objects are closed in the finally.
            baos = new ByteArrayOutputStream();
            out = new ObjectOutputStream(baos);

            out.writeObject(obj);
            byteArray = baos.toByteArray();
        }
        finally
        {
            if (out != null) 
            {
                out.close();
            }
        }
        return byteArray;
    }


    // ----------------------------------------------------------------------
    // ObjectFactory implementation 

    /**
     * implements ObjectFactory to create an instance of this class
     */ 
    public Object getObjectInstance(Object refObj, Name name, 
                                    Context context, Hashtable env) 
        throws Exception 
    {
        // The spec says to return null if we can't create an instance 
        // of the reference
        Jdbc2PoolDataSource ds = null;
        if (refObj instanceof Reference) 
        {
            Reference ref = (Reference)refObj;
	
            if (ref.getClassName().equals(getClass().getName())) 
            {   
                RefAddr ra = ref.get("isNew");
                if (ra != null && ra.getContent() != null) 
                {
                    isNew = Boolean.getBoolean(ra.getContent().toString());
                }

                ra = ref.get("instanceKey");
                if (ra != null && ra.getContent() != null) 
                {
                    instanceKey = new Integer(ra.getContent().toString());
                }

                ra = ref.get("dataSourceName");
                if (ra != null && ra.getContent() != null) 
                {
                    setDataSourceName(ra.getContent().toString());
                }

                ra = ref.get("defaultAutoCommit");
                if (ra != null && ra.getContent() != null) 
                {
                    setDefaultAutoCommit
                        (Boolean.getBoolean(ra.getContent().toString()));
                }

                ra = ref.get("defaultMaxActive");
                if (ra != null && ra.getContent() != null) 
                {
                    setDefaultMaxActive(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("defaultMaxIdle");
                if (ra != null && ra.getContent() != null) 
                {
                    setDefaultMaxIdle(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("defaultMaxWait");
                if (ra != null && ra.getContent() != null) 
                {
                    setDefaultMaxWait(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("defaultReadOnly");
                if (ra != null && ra.getContent() != null) 
                {
                    setDefaultReadOnly
                        (Boolean.getBoolean(ra.getContent().toString()));
                }

                ra = ref.get("description");
                if (ra != null && ra.getContent() != null) 
                {
                    setDescription(ra.getContent().toString());
                }

                ra = ref.get("jndiEnvironment");
                if (ra != null  && ra.getContent() != null) 
                {
                    byte[] serialized = (byte[])ra.getContent();
                    jndiEnvironment = 
                        (Properties)SerializationUtils.deserialize(serialized);
                }
                
                ra = ref.get("loginTimeout");
                if (ra != null && ra.getContent() != null) 
                {
                    setLoginTimeout(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("perUserDefaultAutoCommit");
                if (ra != null  && ra.getContent() != null) 
                {
                    byte[] serialized = (byte[])ra.getContent();
                    perUserDefaultAutoCommit = 
                        (Map)SerializationUtils.deserialize(serialized);
                }
                
                ra = ref.get("perUserMaxActive");
                if (ra != null  && ra.getContent() != null) 
                {
                    byte[] serialized = (byte[])ra.getContent();
                    perUserMaxActive = 
                        (Map)SerializationUtils.deserialize(serialized);
                }

                ra = ref.get("perUserMaxIdle");
                if (ra != null  && ra.getContent() != null) 
                {
                    byte[] serialized = (byte[])ra.getContent();
                    perUserMaxIdle = 
                        (Map)SerializationUtils.deserialize(serialized);
                }

                ra = ref.get("perUserMaxWait");
                if (ra != null  && ra.getContent() != null) 
                {
                    byte[] serialized = (byte[])ra.getContent();
                    perUserMaxWait = 
                        (Map)SerializationUtils.deserialize(serialized);
                }
                
                ra = ref.get("perUserDefaultReadOnly");
                if (ra != null  && ra.getContent() != null) 
                {
                    byte[] serialized = (byte[])ra.getContent();
                    perUserDefaultReadOnly = 
                        (Map)SerializationUtils.deserialize(serialized);
                }
                
                ra = ref.get("testOnBorrow");
                if (ra != null && ra.getContent() != null) 
                {
                    setTestOnBorrow
                        (Boolean.getBoolean(ra.getContent().toString()));
                }

                ra = ref.get("testOnReturn");
                if (ra != null && ra.getContent() != null) 
                {
                    setTestOnReturn
                        (Boolean.getBoolean(ra.getContent().toString()));
                }

                ra = ref.get("timeBetweenEvictionRunsMillis");
                if (ra != null && ra.getContent() != null) 
                {
                    setTimeBetweenEvictionRunsMillis(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("numTestsPerEvictionRun");
                if (ra != null && ra.getContent() != null) 
                {
                    setNumTestsPerEvictionRun(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("minEvictableIdleTimeMillis");
                if (ra != null && ra.getContent() != null) 
                {
                    setMinEvictableIdleTimeMillis(
                        Integer.parseInt(ra.getContent().toString()));
                }

                ra = ref.get("testWhileIdle");
                if (ra != null && ra.getContent() != null) 
                {
                    setTestWhileIdle
                        (Boolean.getBoolean(ra.getContent().toString()));
                }
                
                ra = ref.get("validationQuery");
                if (ra != null && ra.getContent() != null) 
                {
                    setValidationQuery(ra.getContent().toString());
                }

                ds = this;
            }            
        }
        
        return ds;
    }
}

