/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: ConnectionManager.java 20190 2010-08-18 20:35:11Z alitokmen $
 * --------------------------------------------------------------------------
 */


package org.ow2.jonas.dbm.internal.cm;

import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import javax.sql.DataSource;
import javax.sql.XAConnection;
import javax.sql.XADataSource;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.dbm.internal.DBMConstants;
import org.ow2.jonas.dbm.internal.cm.naming.DataSourceFactory;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.service.ServiceException;
import org.ow2.jonas.tm.TransactionManager;
import org.ow2.jonas.tm.TransactionService;

/**
 * DataSource implementation. Manage a pool of connections.
 * @author durieuxp
 */
public class ConnectionManager implements DataSource, XADataSource, Referenceable, Pool, ConnectionEventListener {

    private Logger logger = null;

    /**
     * List of all datasources
     */
    private static HashMap<String, ConnectionManager> cmList = new HashMap<String, ConnectionManager>();

    private TransactionManager tm = null;
    private TransactionService ts = null;


    /**
     * List of JManagedConnection not currently used.
     * This avoids closing and reopening physical connections.
     * We try to keep a minimum of minConPool elements here.
     */
    private TreeSet freeList = new TreeSet();

    /**
     * Total list of JManagedConnection physically opened.
     */
    private LinkedList<JManagedConnection> mcList = new LinkedList<JManagedConnection>();

    /**
     * This HashMap gives the JManagedConnection from its transaction
     * Requests with same tx get always the same connection
     */
    private Map tx2mc = new HashMap();

    /**
     * This is to keep a reference on the Driver Class.
     */
    private Class driverClass = null;

    private int loginTimeout = 60;
    private PrintWriter log = null;

    private PoolMonitor poolKeeper;

    private boolean isClient = false;

    /**
     * Pool closed indicator
     */
    private boolean poolClosed = false;

    /**
     * Is this DataSource observable ?
     */
    private boolean observable = false;

    // -----------------------------------------------------------------
    // Constructors
    // -----------------------------------------------------------------

    /**
     * Constructor for Factory
     * @deprecated
     */
    @Deprecated
    public ConnectionManager() throws Exception {
        /*
           ServiceManager sm = ServiceManager.getInstance();
           ts = (TransactionService) sm.getTransactionService();
           tm = ts.getTransactionManager();
        */
    }

    /**
     * Always called with true
     */
    public ConnectionManager(final boolean isClient) throws Exception {
        this.isClient = isClient;
    }

    public ConnectionManager(final TransactionService txs) throws Exception {
        ts = txs;
        tm = ts.getTransactionManager();
    }

    /**
     * This manager is in the client case or not ?
     * @return boolean true if this is the client case
     * TODO : must be removed. A client can lookup a datasource
     */
    public boolean isClientCase() {
        return isClient;
    }

    /**
     * @return The pool associated to this datasource
     */
    public Pool getPool() {
        return this;
    }

    /**
     * get the ConnectionManager matching the DataSource name
     */
    public static ConnectionManager getConnectionManager(final String dsname) {
        ConnectionManager cm = cmList.get(dsname);
        return cm;
    }

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

    /**
     * @serial for JNDI
     */
    private String dSName = null;

    /**
     * @return Jndi name of the datasource
     */
    public String getDSName() {
        return dSName;
    }

    /**
     * @param s Jndi name for the datasource
     */
    public void setDSName(final String s) {
        dSName = s;
        logger = Log.getLogger(Log.JONAS_DBM_PREFIX + "." + dSName);
        // Start a thread to manage the pool of connections
        poolKeeper = new PoolMonitor(this, logger);
        poolKeeper.start();
        // Add it to the list
        cmList.put(s, this);
    }

    /**
     * @serial datasource name
     */
    private String dataSourceName;

    public String getDatasourceName() {
        return dataSourceName;
    }

    public void setDatasourceName(final String s) {
        dataSourceName = s;
    }

    /**
     * @serial url for database
     */
    private String url = null;

    public String getUrl() {
        return url;
    }

    public void setUrl(final String s) {
        url = s;
    }

    /**
     * @serial JDBC driver Class
     */
    private String className = null;

    public String getClassName() {
        return className;
    }
    public void setClassName(final String s) throws ClassNotFoundException {
        className = s;

        // Loads standard JDBC driver and keeps it loaded (via driverClass)
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Load JDBC driver " + s);
        }
        try {
            driverClass = Class.forName(className);
        } catch (java.lang.ClassNotFoundException e) {
            logger.log(BasicLevel.ERROR, "Cannot load JDBC driver : " + e);
            throw e;
        }
    }

    /**
     * @serial default user
     */
    private String userName = null;

    public String getUserName() {
        return userName;
    }

    public void setUserName(final String s) {
        userName = s;
    }

    /**
     * @serial default passwd
     */
    private String password = null;

    public String getPassword() {
        return password;
    }

    public void setPassword(final String s) {
        password = s;
    }

    /**
     * @serial
     */
    private int isolationLevel = -1;
    private String isolationStr = null;


    public void setTransactionIsolation(final String level) {
        logger.log(BasicLevel.DEBUG, level);
        if (level.equals("serializable")) {
            isolationLevel = Connection.TRANSACTION_SERIALIZABLE;
        } else if (level.equals("none")) {
            isolationLevel = Connection.TRANSACTION_NONE;
        } else if (level.equals("read_committed")) {
            isolationLevel = Connection.TRANSACTION_READ_COMMITTED;
        } else if (level.equals("read_uncommitted")) {
            isolationLevel = Connection.TRANSACTION_READ_UNCOMMITTED;
        } else if (level.equals("repeatable_read")) {
            isolationLevel = Connection.TRANSACTION_REPEATABLE_READ;
        } else {
            isolationStr = "default";
            return;
        }
        isolationStr = level;
    }

    public String getTransactionIsolation() {
        return isolationStr;
    }

    /**
     * @serial
     */
    private String currentMapperName = null;

    public void setMapperName(final String mappername) {
        currentMapperName = mappername;
    }

    public String getMapperName() {
        return currentMapperName;
    }

    /**
     * @serial
     */
    private String dsDescription = null;

    /**
     * @return the desrciption of this datasource
     */
    public String getDataSourceDescription() {
        return dsDescription;
    }

    /**
     * @param dsDesc the desrciption of this datasource
     */
    public void setDataSourceDescription(final String dsDesc) {
        dsDescription = dsDesc;
    }

    // ----------------------------------------------------------------
    // Pool Configuration
    // Each attibute have setter and getter, plus a default value.
    // ----------------------------------------------------------------

    /**
     * count max waiters during current period.
     */
    private int waiterCount = 0;

    /**
     * count max waiting time during current period.
     */
    private long waitingTime = 0;

    /**
     * count max busy connection during current period.
     */
    private int busyMax = 0;

    /**
     * count min busy connection during current period.
     */
    private int busyMin = 0;

    /**
     * High Value for no limit for the connection pool
     */
    private static final int NO_LIMIT = 99999;

    /**
     * Nb of milliseconds in a day
     */
    private static final long ONE_DAY = 1440L * 60L * 1000L;

    /**
     * max number of remove at once in the freelist
     * We avoid removing too much mcs at once for perf reasons.
     */
    private static final int MAX_REMOVE_FREELIST = 10;

    /**
     * minimum size of the connection pool
     */
    private int poolMin = 0;

    /**
     * @return min pool size
     */
    public int getPoolMin() {
        return poolMin;
    }

    /**
     * @param min minimum connection pool size to be set.
     */
    public void setPoolMin(final int min) {
        boolean doadjust = false;
        synchronized(this) {
            if (poolMin != min) {
                poolMin = min;
                doadjust = true;
            }
        }
        if (doadjust) {
            adjust();
        }
    }

    /**
     * maximum size of the connection pool.
     * default value is "NO LIMIT".
     */
    private int poolMax = NO_LIMIT;

    /**
     * @return actual max pool size
     */
    public int getPoolMax() {
        return poolMax;
    }

    /**
     * @param max max pool size. -1 means "no limit".
     */
    public void setPoolMax(int max) {
        boolean doadjust = false;
        synchronized(this) {
            if (poolMax != max) {
                if (max < 0 || max > NO_LIMIT) {
                    if (currentWaiters > 0) {
                        notify();
                    }
                    poolMax = NO_LIMIT;
                } else {
                    if (currentWaiters > 0 && poolMax < max) {
                        notify();
                    }
                    poolMax = max;
                    doadjust = true;
                }
            }
        }
        if (doadjust) {
            adjust();
        }
    }

    /**
     * Max age of a Connection in milliseconds.
     * When the time is elapsed, the connection will be closed.
     * This avoids keeping connections open too long for nothing.
     */
    private long maxAge = ONE_DAY;

    /**
     * Same value in mns
     */
    private int maxAgeMn;

    /**
     * @return max age for connections (in mm)
     */
    public int getMaxAge() {
        return maxAgeMn;
    }

    /**
     * @return max age for connections (in millisec)
     */
    public long getMaxAgeMilli() {
        return maxAge;
    }

    /**
     * @param mn max age of connection in minutes
     */
    public synchronized void setMaxAge(final int mn) {
        maxAgeMn = mn;
        // set times in milliseconds
        maxAge = mn * 60L * 1000L;
    }

    /**
     * max open time for a connection, in millisec
     */
    private long maxOpenTime = ONE_DAY;

    /**
     * Same value in mn
     */
    private int maxOpenTimeMn;

    /**
     * @return max age for connections (in mns)
     */
    public int getMaxOpenTime() {
        return maxOpenTimeMn;
    }

    /**
     * @return max age for connections (in millisecs)
     */
    public long getMaxOpenTimeMilli() {
        return maxOpenTime;
    }

    /**
     * @param mn max time of open connection in minutes
     */
    public void setMaxOpenTime(final int mn) {
        maxOpenTimeMn = mn;
        // set times in milliseconds
        maxOpenTime = mn * 60L * 1000L;
    }

    /**
     * max nb of milliseconds to wait for a connection when pool is empty
     */
    private long waiterTimeout = 10000;

    /**
     * @return waiter timeout in seconds
     */
    public int getMaxWaitTime() {
        return (int) (waiterTimeout / 1000L);
    }

    /**
     * @param sec max time to wait for a connection, in seconds
     */
    public synchronized void setMaxWaitTime(final int sec) {
        waiterTimeout = sec * 1000L;
    }

    /**
     * max nb of waiters allowed to wait for a Connection
     */
    private int maxWaiters = 1000;

    /**
     * @return max nb of waiters
     */
    public int getMaxWaiters() {
        return maxWaiters;
    }

    /**
     * @param nb max nb of waiters
     */
    public synchronized void setMaxWaiters(final int nb) {
        maxWaiters = nb;
    }

    /**
     * sampling period in sec.
     */
    private int samplingPeriod = 60; //default sampling period

    /**
     * @return sampling period in sec.
     */
    public int getSamplingPeriod() {
        return samplingPeriod;
    }

    /**
     * @param sec sampling period in sec.
     */
    public void setSamplingPeriod(final int sec) {
        if (sec > 0) {
            samplingPeriod = sec;
            poolKeeper.setSamplingPeriod(sec);
        }
    }

    /**
     * adjust period in sec.
     */
    private int adjustPeriod = 30; //default adjust period

    /**
     * @return adjust period in sec.
     */
    public int getAdjustPeriod() {
        return adjustPeriod;
    }

    /**
     * @param sec adjust period in sec.
     */
    public void setAdjustPeriod(final int sec) {
        if (sec > 0) {
            adjustPeriod = sec;
            poolKeeper.setAdjustPeriod(sec);
        }
    }

    /**
     * Level of checking on connections when got from the pool.
     * this avoids reusing bad connections because too old, for example
     * when database was restarted...
     * 0 = no checking
     * 1 = check that still physically opened.
     * 2 = try a null statement.
     */
    private int checkLevel = 0; // default = 0

    /**
     * @return connection checking level
     */
    public int getCheckLevel() {
        return checkLevel;
    }

    /**
     * @param level jdbc connection checking level (0, 1, or 2)
     */
    public void setCheckLevel(final int level) {
        checkLevel = level;
    }

    /**
     * PreparedStatement pool size per managed connection
     */
    private int pstmtMax = 12;

    /**
     * @return PreparedStatement cache size
     */
    public int getPstmtMax() {
        return pstmtMax;
    }

    /**
     * @param nb PreparedStatement cache size
     */
    public synchronized void setPstmtMax(final int nb) {
        pstmtMax = nb;
        // Set the value in each connection.
        for (Iterator i = mcList.iterator(); i.hasNext();) {
            JManagedConnection mc = (JManagedConnection) i.next();
            mc.setPstmtMax(pstmtMax);
        }
    }

    /**
     * test statement used when checkLevel = 2.
     */
    private String testStatement;

    /**
     * @return test statement used when checkLevel = 2.
     */
    public String getTestStatement() {
        return testStatement;
    }

    /**
     * @param s test statement
     */
    public void setTestStatement(final String s) {
        testStatement = s;
    }

    /**
	 * @return the observable
	 */
	public boolean isObservable() {
		return observable;
	}

	/**
	 * @param observable the observable to set
	 */
	public void setObservable(final boolean observable) {
		this.observable = observable;
	}

    /**
     * Configure the Connection pool. Called by the Container at init.
     * Configuration can be set in datasource.properties files.
     * @param connchecklevel JDBC connection checking level
     * @param connmaxage JDBC connection maximum age
     * @param maxopentime JDBC connection maximum open time
     * @param connteststmt SQL query for test statement
     * @param pstmtmax prepare statement pool size per managed connection
     * @param minconpool Min size for the connection pool
     * @param maxconpool Max size for the connection pool
     * @param maxwaittime Max time to wait for a connection (in seconds)
     * @param maxwaiters Max nb of waiters for a connection
     * @param samplingperiod sampling period in sec.
     * @param adjustperiod pool adjust period in sec.
     * @throw ServiceException if could not create the initial items in the pool
     */
    public void poolConfigure(final String connchecklevel,
            final String connmaxage,
            final String maxopentime,
            final String connteststmt,
            final String pstmtmax,
            final String minconpool,
            final String maxconpool,
            final String maxwaittime,
            final String maxwaiters,
            final String samplingperiod,
            final String adjustperiod) {

        // Configure pool
        setCheckLevel((new Integer(connchecklevel)).intValue());
        // set con max age BEFORE min/max pool size.
        setMaxAge((new Integer(connmaxage)).intValue());
        setMaxOpenTime((new Integer(maxopentime)).intValue());
        setTestStatement(connteststmt);
        setPstmtMax((new Integer(pstmtmax)).intValue());
        setPoolMin((new Integer(minconpool)).intValue());
        setPoolMax((new Integer(maxconpool)).intValue());
        setMaxWaitTime((new Integer(maxwaittime)).intValue());
        setMaxWaiters((new Integer(maxwaiters)).intValue());
        setSamplingPeriod((new Integer(samplingperiod)).intValue());
        setAdjustPeriod((new Integer(adjustperiod)).intValue());
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "ConnectionManager configured with:");
            logger.log(BasicLevel.DEBUG, "   jdbcConnCheckLevel  = " + connchecklevel);
            logger.log(BasicLevel.DEBUG, "   jdbcConnMaxAge      = " + connmaxage);
            logger.log(BasicLevel.DEBUG, "   jdbcMaxOpenTime     = " + maxopentime);
            logger.log(BasicLevel.DEBUG, "   jdbcTestStmt        = " + connteststmt);
            logger.log(BasicLevel.DEBUG, "   jdbcPstmtMax        = " + pstmtmax);
            logger.log(BasicLevel.DEBUG, "   minConPool          = " + getPoolMin());
            logger.log(BasicLevel.DEBUG, "   maxConPool          = " + getPoolMax());
            logger.log(BasicLevel.DEBUG, "   maxWaitTime         = " + getMaxWaitTime());
            logger.log(BasicLevel.DEBUG, "   maxWaiters          = " + getMaxWaiters());
            logger.log(BasicLevel.DEBUG, "   samplingPeriod      = " + getSamplingPeriod());
            logger.log(BasicLevel.DEBUG, "   adjustPeriod        = " + getAdjustPeriod());
        }
    }

    // ----------------------------------------------------------------
    // Monitoring Attributes
    // Each attribute should have a get accessor.
    // ----------------------------------------------------------------

    /**
     *  maximum nb of busy connections in last sampling period
     */
    private int busyMaxRecent = 0;

    /**
     * @return maximum nb of busy connections in last sampling period
     */
    public int getBusyMaxRecent() {
        return busyMaxRecent;
    }

    /**
     *  minimum nb of busy connections in last sampling period
     */
    private int busyMinRecent = 0;

    /**
     * @return minimum nb of busy connections in last sampling period
     */
    public int getBusyMinRecent() {
        return busyMinRecent;
    }

    /**
     * nb of threads waiting for a Connection
     */
    private int currentWaiters = 0;

    /**
     * @return current number of connection waiters
     */
    public int getCurrentWaiters() {
        return currentWaiters;
    }

    /**
     * total number of opened physical connections since the datasource creation.
     */
    private int openedCount = 0;

    /**
     * @return int number of physical jdbc connection opened
     */
    public int getOpenedCount() {
        return openedCount;
    }

    /**
     * total nb of physical connection failures
     */
    private int connectionFailures = 0;

    /**
     * @return int number of xa connection failures on open
     */
    public int getConnectionFailures() {
        return connectionFailures;
    }

    /**
     * total nb of connection leaks.
     * A connection leak occurs when the caller never issues a close method
     * on the connection.
     */
    private int connectionLeaks = 0;

    /**
     * @return int number of connection leaks
     */
    public int getConnectionLeaks() {
        return connectionLeaks;
    }

    /**
     * total number of opened connections since the datasource creation.
     */
    private int servedOpen = 0;

    /**
     * @return int number of xa connection served
     */
    public int getServedOpen() {
        return servedOpen;
    }

    /**
     * total nb of open connection failures because waiter overflow
     */
    private int rejectedFull = 0;

    /**
     * @return int number of open calls that were rejected due to waiter overflow
     */
    public int getRejectedFull() {
        return rejectedFull;
    }

    /**
     * total nb of open connection failures because timeout
     */
    private int rejectedTimeout = 0;

    /**
     * @return int number of open calls that were rejected by timeout
     */
    public int getRejectedTimeout() {
        return rejectedTimeout;
    }

    /**
     * total nb of open connection failures for any other reason.
     */
    private int rejectedOther = 0;

    /**
     * @return int number of open calls that were rejected
     */
    public int getRejectedOther() {
        return rejectedOther;
    }

    /**
     * @return int number of open calls that were rejected
     */
    public int getRejectedOpen() {
        return rejectedFull + rejectedTimeout + rejectedOther;
    }

    /**
     * maximum nb of waiters since datasource creation
     */
    private int waitersHigh = 0;

    /**
     * @return maximum nb of waiters since the datasource creation
     */
    public int getWaitersHigh() {
        return waitersHigh;
    }

    /**
     * maximum nb of waiters in last sampling period
     */
    private int waitersHighRecent = 0;

    /**
     * @return maximum nb of waiters in last sampling period
     */
    public int getWaitersHighRecent() {
        return waitersHighRecent;
    }

    /**
     * total nb of waiters since datasource creation
     */
    private int totalWaiterCount = 0;

    /**
     * @return total nb of waiters since the datasource creation
     */
    public int getWaiterCount() {
        return totalWaiterCount;
    }

    /**
     * total waiting time in milliseconds
     */
    private long totalWaitingTime = 0;

    /**
     * @return total waiting time since the datasource creation
     */
    public long getWaitingTime() {
        return totalWaitingTime;
    }

    /**
     * max waiting time in milliseconds
     */
    private long waitingHigh = 0;

    /**
     * @return max waiting time since the datasource creation
     */
    public long getWaitingHigh() {
        return waitingHigh;
    }

    /**
     * max waiting time in milliseconds in last sampling period
     */
    private long waitingHighRecent = 0;

    /**
     * @return max waiting time in last sampling period
     */
    public long getWaitingHighRecent() {
        return waitingHighRecent;
    }

    // -----------------------------------------------------------------
    // DataSource + XADataSource Implementation
    // -----------------------------------------------------------------

    public int getLoginTimeout() throws SQLException {
        return loginTimeout;
    }

    public void setLoginTimeout(final int seconds) throws SQLException {
        loginTimeout = seconds;
    }

    public PrintWriter getLogWriter() throws SQLException {
        return log;
    }

    public void setLogWriter(final PrintWriter out) throws SQLException {
        log = out;
    }

    /**
     * Returns true if this either implements the interface argument or is directly or indirectly a wrapper for an object that does.
     * Returns false otherwise. If this implements the interface then return true, else if this is a wrapper then return the result
     * of recursively calling isWrapperFor on the wrapped object. If this does not implement the interface and is not a wrapper,
     * return false. This method should be implemented as a low-cost operation compared to unwrap so that callers can use this method
     * to avoid expensive unwrap calls that may fail. If this method returns true then calling unwrap with the same argument should succeed.
     * @param iface a Class defining an interface. 
     * @returns true if this implements the interface or directly or indirectly wraps an object that does. 
     * @throws SQLException if an error occurs while determining whether this is a wrapper for an object with the given interface.
     */
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }

    /**
     * Returns an object that implements the given interface to allow access to non-standard methods, or standard methods not exposed by the proxy.
     * If the receiver implements the interface then the result is the receiver or a proxy for the receiver. If the receiver is a wrapper
     * and the wrapped object implements the interface then the result is the wrapped object or a proxy for the wrapped object.
     * Otherwise return the the result of calling unwrap recursively on the wrapped object or a proxy for that result. 
     * If the receiver is not a wrapper and does not implement the interface, then an SQLException is thrown.
     * @param iface A Class defining an interface that the result must implement. 
     * @returns an object that implements the interface. May be a proxy for the actual implementing object. 
     * @throws SQLException If no object found that implements the interface
     */
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    public Connection getConnection() throws SQLException {
        return getConnection(userName, password);
    }

    /**
     * Attempts to establish a connection with the data source that this DataSource object represents.
     * - comes from the javax.sql.DataSource interface
     * @param username - the database user on whose behalf the connection is being made
     * @param password - the user's password
     * @return a connection to the data source
     * @throws  SQLException - if a database access error occurs
     */
    public Connection getConnection(final String username, final String password) throws SQLException {
        JManagedConnection mc = null;

        // Get the current Transaction
        Transaction tx = null;
        try {
            tx = tm.getTransaction();
        } catch (NullPointerException n) {
            // current is null: we are not in a JOnAS Server.
            logger.log(BasicLevel.ERROR, "ConnectionManager: should not be used outside a JOnAS Server");
        } catch (SystemException e) {
            logger.log(BasicLevel.ERROR, "ConnectionManager: getTransaction failed", e);
        }
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Tx = " + tx);
        }

        // Get a JManagedConnection in the pool for this user
        mc = openConnection(username, tx);
        Connection ret = mc.getConnection();

        // Enlist XAResource if we are actually in a transaction
        if (tx != null) {
            if (mc.getOpenCount() == 1) { // Only if first/only thread
                try {
                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                        logger.log(BasicLevel.DEBUG, "enlist XAResource on " + tx);
                    }
                    tx.enlistResource(mc.getXAResource());
                } catch (RollbackException e) {
                    // Although tx has been marked to be rolled back,
                    // XAResource has been correctly enlisted.
                    logger.log(BasicLevel.WARN, "XAResource enlisted, but tx is marked rollback", e);
                } catch (IllegalStateException e) {
                    // In case tx is committed, no need to register resource!
                } catch (Exception e) {
                    logger.log(BasicLevel.ERROR, "Cannot enlist XAResource", e);
                    logger.log(BasicLevel.ERROR, "Connection will not be enlisted in a transaction");
                    // should return connection in the pool XXX
                    throw new SQLException("Cannot enlist XAResource");
                }
            }
        }
        // We are not in a transaction yet: we just make a pre-registration.
        // We are in a stateful bean, put it in the list too.
        // TODO: Sometimes, we could avoid this (if no stateful and no tx)
        if (!mc.isRME()) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "register this connection to the TM.");
            }
            mc.setRME(true);
            tm.notifyConnectionOpen(mc);
        }
        // return a Connection object
        return ret;
    }

    public XAConnection getXAConnection() throws SQLException {
        return getXAConnection(userName, password);
    }

    /**
     * Attempts to establish a physical database connection, using the given user name and password.
     * The connection that is returned is one that can be used in a distributed transaction
     * - comes from the javax.sql.XADataSource interface
     * @param user - the database user on whose behalf the connection is being made
     * @param passwd - the user's password
     * @return an XAConnection object, which represents a physical connection to a data source,
     * that can be used in a distributed transaction
     * @throws  SQLException - if a database access error occurs
     */
    public XAConnection getXAConnection(final String user, final String passwd) throws SQLException {
        // Create the actual connection in the std driver
        Connection conn = null;
        try {
            if (user.length() == 0) {
                conn = DriverManager.getConnection(url);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "    * New Connection on " + url);
                }
            } else {
                // Accept password of zero length.
                conn = DriverManager.getConnection(url, user, passwd);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "    * New Connection on " + url + " for " + user);
                }
            }
        } catch (SQLException e) {
            logger.log(BasicLevel.ERROR, "Could not get Connection on " + url + ":", e);
            throw new SQLException("Could not get Connection on url : " + url
                    + " for user : " + user + " inner exception" + e.getMessage());
        }
        if (conn == null) {
            logger.log(BasicLevel.ERROR, "DriverManager returned a null Connection");
            throw new SQLException("Could not get Connection on url : " + url
                    + " for user : " + user);
        }

        // Attempt to set the transaction isolation level
        // Depending on the underlying database, this may not succeed.
        if (isolationLevel != -1) {
            try {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "set transaction isolation to " + isolationLevel);
                }
                conn.setTransactionIsolation(isolationLevel);
            } catch (SQLException e) {
                String ilstr = "?";
                switch (isolationLevel) {
                case Connection.TRANSACTION_SERIALIZABLE:
                    ilstr = "SERIALIZABLE";
                    break;
                case Connection.TRANSACTION_NONE:
                    ilstr = "NONE";
                    break;
                case Connection.TRANSACTION_READ_COMMITTED:
                    ilstr = "READ_COMMITTED";
                    break;
                case Connection.TRANSACTION_READ_UNCOMMITTED:
                    ilstr = "READ_UNCOMMITTED";
                    break;
                case Connection.TRANSACTION_REPEATABLE_READ:
                    ilstr = "REPEATABLE_READ";
                    break;
                }
                logger.log(BasicLevel.ERROR, "Cannot set transaction isolation to " + ilstr + " for this DataSource:" + e);
                logger.log(BasicLevel.ERROR, url);
                isolationLevel = -1;
            }
        }

        // Create the JManagedConnection object
        JManagedConnection mc = new JManagedConnection(conn, this);

        // return the XAConnection
        return mc;
    }

    // -----------------------------------------------------------------
    // Referenceable Implementation
    // -----------------------------------------------------------------

    /**
     * Retrieves the Reference of this object. Used at binding time by JNDI
     * to build a reference on this object.
     *
     * @return  The non-null Reference of this object.
     * @exception  NamingException  If a naming exception was encountered while
     * retrieving the reference.
     */
    public Reference getReference() throws NamingException {

        Reference ref = new Reference (this.getClass().getName(),
                DataSourceFactory.class.getName(),
                null);
        // These values are used by ObjectFactory (see DataSourceFactory.java)
        ref.add(new StringRefAddr(DBMConstants.NAME, getDSName()));
        ref.add(new StringRefAddr(DBMConstants.URL, getUrl()));
        ref.add(new StringRefAddr(DBMConstants.CLASSNAME, getClassName()));
        ref.add(new StringRefAddr(DBMConstants.USERNAME, getUserName()));
        ref.add(new StringRefAddr(DBMConstants.PASSWORD, getPassword()));
        ref.add(new StringRefAddr(DBMConstants.ISOLATIONLEVEL, getTransactionIsolation()));
        ref.add(new StringRefAddr(DBMConstants.MAPPERNAME, getMapperName()));
        Integer checklevel = new Integer(getCheckLevel());
        ref.add(new StringRefAddr("connchecklevel", checklevel.toString()));
        Integer maxage = new Integer(getMaxAge());
        ref.add(new StringRefAddr("connmaxage", maxage.toString()));
        Integer maxopentime = new Integer(getMaxOpenTime());
        ref.add(new StringRefAddr("maxopentime", maxopentime.toString()));
        ref.add(new StringRefAddr("connteststmt", getTestStatement()));
        Integer pstmtmax = new Integer(getPstmtMax());
        ref.add(new StringRefAddr("pstmtmax", pstmtmax.toString()));
        Integer minpool = new Integer(getPoolMin());
        ref.add(new StringRefAddr("minconpool", minpool.toString()));
        Integer maxpool = new Integer(getPoolMax());
        ref.add(new StringRefAddr("maxconpool", maxpool.toString()));
        Integer maxwaittime = new Integer(getMaxWaitTime());
        ref.add(new StringRefAddr("maxwaittime", maxwaittime.toString()));
        Integer maxwaiters = new Integer(getMaxWaiters());
        ref.add(new StringRefAddr("maxwaiters", maxwaiters.toString()));
        Integer samplingperiod = new Integer(getSamplingPeriod());
        ref.add(new StringRefAddr("samplingperiod", samplingperiod.toString()));
        Integer adjustperiod = new Integer(getAdjustPeriod());
        ref.add(new StringRefAddr("adjustperiod", adjustperiod.toString()));
        return ref;
    }

    public int[] getOpenedConnections(final int usedTimeSec) {
        List<Integer> connections = new ArrayList<Integer>();
        long usedTimeMs = usedTimeSec * 1000;

        // Iterate over the managed connections
        for (Iterator<JManagedConnection> i = mcList.iterator(); i.hasNext();) {
            JManagedConnection mc = i.next();

            // Check if the connection is still used
            long duration = System.currentTimeMillis() - mc.getOpeningTime();
            if (mc.isOpen() && (duration >= usedTimeMs)) {
                connections.add(new Integer(mc.getIdent()));
            }
        }

        // Create the array of connection Ids
        int[] ids = new int[connections.size()];
        int idx = 0;
        for (Iterator<Integer> i = connections.iterator(); i.hasNext();) {
            Integer id = i.next();
            ids[idx++] = id.intValue();
        }

        return ids;
    }

    public void forceCloseConnection(final int connectionId) {
        logger.log(BasicLevel.DEBUG, " connectionId=" + connectionId);
        
        // Iterate over the managed connections to find the right connection
        JManagedConnection found = null;
        synchronized(this) {
            for (Iterator<JManagedConnection> i = mcList.iterator(); i.hasNext();) {
                JManagedConnection mc = i.next();

                // Find the right connection
                if (mc.getIdent() == connectionId) {
                    i.remove();
                    found = mc;
                    break;
                }
            }
        }

        // destroy outside the lock
        if (found != null) {
            found.remove();
        }

    }

    public int[] getOpenedConnections() {
        return getOpenedConnections(5);
    }

    public Map getConnectionDetails(final int connectionId) {
        // Iterate over the managed connections
        for (Iterator<JManagedConnection> i = mcList.iterator(); i.hasNext();) {
            JManagedConnection mc = i.next();

            // Find the right connection
            if (mc.getIdent() == connectionId) {
                // Also check if (during the meantime) the connection has been closed
                if (!mc.isClosed()) {
                    return getConnectionDetails(mc);
                }
            }
        }

        // in some case, we may return null
        return null;
    }

    private Map<String, Object> getConnectionDetails(final JManagedConnection connection) {
        Map<String, Object> details = new HashMap<String, Object>();

        // 0: connection-id : Integer
        details.put("id", new Integer(connection.getIdent()));

        // 1: open-count : Integer
        details.put("open-count", new Integer(connection.getOpenCount()));

        // 2: status : Boolean
        details.put("inactive", Boolean.valueOf(connection.inactive()));

        // 3: duration (ms) : Long
        long duration = 0;
        if (connection.getOpeningTime() != -1) {
            duration = System.currentTimeMillis() - connection.getOpeningTime();
        } else {
            // not computed
            duration = -1;
        }
        details.put("duration", new Long(duration));

        // 4: transaction-id (if any) : String
        Transaction tx = connection.getTx();
        String xid = "null";
        if (tx != null) {
            xid = tx.toString();
        }
        details.put("transaction-id", xid);

        // 5: connection-age (ms)
        long age = System.currentTimeMillis() - connection.getCreationTime();
        details.put("age", new Long(age));

        // 6: Openers Thread infos : List<Map<thread-name, time, stack>>
        details.put("openers", connection.getOpenerThreadInfos());

        // 7: Closers Thread infos : List<Map<thread-name, time, stack>>
        details.put("closers", connection.getCloserThreadInfos());

        return details;
    }

    // -----------------------------------------------------------------
    // ConnectionEventListener Implementation
    // -----------------------------------------------------------------

    public void connectionClosed(final ConnectionEvent event) {
        logger.log(BasicLevel.DEBUG, "");

        JManagedConnection mc = (JManagedConnection) event.getSource();

        // remove it from the list of open connections for this thread
        // only if it was opened outside a tx.
        if (closeConnection(mc, XAResource.TMSUCCESS) && mc.isRME()) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "unregister this connection to the TM.");
            }
            mc.setRME(false);
            tm.notifyConnectionClose(mc);
        }
    }

    public void connectionErrorOccurred(final ConnectionEvent event) {

        JManagedConnection mc = (JManagedConnection) event.getSource();
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "mc=" + mc.getIdent());
        }

        // remove it from the list of open connections for this thread
        // only if it was opened outside a tx.
        if (closeConnection(mc, XAResource.TMFAIL) && mc.isRME()) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "unregister this connection to the TM.");
            }
            mc.setRME(false);
            tm.notifyConnectionError(mc);
        }

    }

    // -----------------------------------------------------------------
    // Pool Monitoring
    // -----------------------------------------------------------------

    /**
     * Dump Free List (DEBUG)
     */
    private synchronized void dumpFreeList() {
        for (Iterator i = freeList.iterator(); i.hasNext();) {
            JManagedConnection mc = (JManagedConnection) i.next();
            System.out.println("Id="+mc.getIdent()+" Hit="+mc.psNumber());
        }
        JManagedConnection f = (JManagedConnection) freeList.first();
        JManagedConnection l = (JManagedConnection) freeList.last();
        System.out.println("First="+f.getIdent()+" Last="+l.getIdent());
    }

    /**
     * @return int number of xa connection
     */
    public synchronized int getCurrentOpened() {
        return mcList.size();
    }

    /**
     * @return int number of busy xa connection
     */
    public synchronized int getCurrentBusy() {
        return mcList.size() - freeList.size();
    }

    /**
     * compute current min/max busyConnections
     */
    public synchronized void recomputeBusy() {
        int busy = getCurrentBusy();
        if (busyMax < busy) {
            busyMax = busy;
        }
        if (busyMin > busy) {
            busyMin = busy;
        }
    }

    /**
     * @return int number of xa connection reserved for tx
     */
    public synchronized int getCurrentInTx() {
        return tx2mc.size();
    }

    /**
     * make samples with some monitoring values
     */
    public synchronized void sampling() {
    	if (poolClosed) {
    		return;
    	}
        waitingHighRecent = waitingTime;
        if (waitingHigh < waitingTime) {
            waitingHigh = waitingTime;
        }
        waitingTime = 0;

        waitersHighRecent = waiterCount;
        if (waitersHigh < waiterCount) {
            waitersHigh = waiterCount;
        }
        waiterCount = 0;

        busyMaxRecent = busyMax;
        busyMax = getCurrentBusy();
        busyMinRecent = busyMin;
        busyMin = getCurrentBusy();
    }

    /**
     * Adjust the pool size, according to poolMax and poolMin values.
     * Also remove old connections in the freeList.
     */
    public void adjust() {
    	if (poolClosed) {
    		return;
    	}
        logger.log(BasicLevel.DEBUG, dSName);

        // Remove max aged elements in freelist
        // - Not more than MAX_REMOVE_FREELIST
        // - Don't reduce pool size less than poolMin
        LinkedList removeList = new LinkedList();
        synchronized(this) {
            int count = mcList.size() - poolMin;
            // In case count is null, a new connection will be
            // recreated just after
            if (count >= 0) {
                if (count > MAX_REMOVE_FREELIST) {
                    count = MAX_REMOVE_FREELIST;
                }
                for (Iterator i = freeList.iterator(); i.hasNext();) {
                    JManagedConnection mc = (JManagedConnection) i.next();
                    if (mc.isAged()) {
                        if (logger.isLoggable(BasicLevel.DEBUG)) {
                            logger.log(BasicLevel.DEBUG, "remove a timed out connection");
                        }
                        i.remove();
                        mcList.remove(mc);
                        removeList.add(mc);
                        count--;
                        if (count <= 0) {
                            break;
                        }
                    }
                }
            }
        }
        // Physically close these connections, outside the lock,
        // because in some cases, closing may be very long or even block.
        for (Iterator i = removeList.iterator(); i.hasNext();) {
            JManagedConnection mc = (JManagedConnection) i.next();
            i.remove();
            mc.remove();
        }
        recomputeBusy();

        // Close (physically) connections lost (opened for too long time)
        // removeList should be empty here
        synchronized(this) {
            for (Iterator i = mcList.iterator(); i.hasNext();) {
                JManagedConnection mc = (JManagedConnection) i.next();
                if (mc.inactive()) {
                    logger.log(BasicLevel.WARN, "close a timed out open connection:" + mc.getIdent());
                    i.remove();
                    removeList.add(mc);
                }
            }
        }
        // Physically close these connections, outside the lock,
        // because in some cases, closing may be very long or even block.
        for (Iterator i = removeList.iterator(); i.hasNext();) {
            JManagedConnection mc = (JManagedConnection) i.next();
            i.remove();
            // destroy mc. We want to be sure that the Connection will not be used
            // later, so we close physically this connection.
            mc.remove();
            connectionLeaks++;
            // Notify 1 thread waiting for a Connection.
            if (currentWaiters > 0) {
                logger.log(BasicLevel.DEBUG, "Notify Connection waiters");
                notify();
            }
        }
        recomputeBusy();

        // Shrink the pool in case of max pool size
        // This occurs when max pool size has been reduced by jonas admin.
        if (poolMax != NO_LIMIT) {
            synchronized(this) {
                while (freeList.size() > poolMin && mcList.size() > poolMax) {
                    JManagedConnection mc = (JManagedConnection) freeList.first();
                    freeList.remove(mc);
                    mcList.remove(mc);
                    removeList.add(mc);
                }
            }
            // Physically close these connections, outside the lock,
            // because in some cases, closing may be very long or even block.
            for (Iterator i = removeList.iterator(); i.hasNext();) {
                JManagedConnection mc = (JManagedConnection) i.next();
                i.remove();
                mc.remove();
            }
        }
        recomputeBusy();

        // Recreate more Connections while poolMin is not reached
        synchronized(this) {
            while (mcList.size() < poolMin) {
                JManagedConnection mc = null;
                try {
                    mc = (JManagedConnection) getXAConnection();
                    openedCount++;
                } catch (SQLException e) {
                    throw new ServiceException("Could not create " + poolMin + " mcs in the pool : ", e);
                }
                // tx = null. Assumes maxage already configured.
                freeList.add(mc);
                // Notify 1 thread waiting for a Connection.
                if (currentWaiters > 0) {
                    logger.log(BasicLevel.DEBUG, "Notify Connection waiters");
                    notify();
                }
                mcList.add(mc);
                mc.addConnectionEventListener(this);
            }
        }
    }

    /**
     * lookup connection in the pool for this user/tx
     * @param user user name
     * @param tx Transaction the connection is involved
     * @return a free JManagedConnection (never null)
     * @throws SQLException Cannot open a connection because the pool's max size is reached
     */
    public JManagedConnection openConnection(final String user, final Transaction tx) throws SQLException {
        JManagedConnection mc = null;

        // If a Connection exists already for this tx, just return it.
        // If no transaction, never reuse a connection already used.
        if (tx != null) {
            synchronized(this) {
                mc = (JManagedConnection) tx2mc.get(tx);
                if (mc != null) {
                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                        logger.log(BasicLevel.DEBUG, "Reuse a Connection for same tx");
                    }
                    mc.hold();
                    servedOpen++;
                    return mc;
                }
            }
        }

        // Loop until a valid mc is found
        long timetowait = waiterTimeout;
        long starttime = 0;
        while (mc == null) {
            // If an element exists in the freelist, try to use it.
            synchronized(this) {
                if (!freeList.isEmpty()) {
                    mc = (JManagedConnection) freeList.last();
                    freeList.remove(mc);
                }
            }
            if (mc != null) {
                // Check the connection before reusing it
                if (checkLevel > 0) {
                    try {
                        IConnection conn = (IConnection) mc.getConnection();
                        if (conn == null || conn.isPhysicallyClosed()) {
                            logger.log(BasicLevel.WARN, "The JDBC connection has been closed!");
                            synchronized(this) {
                                mcList.remove(mc);
                            }
                            mc.remove();
                            starttime = 0;
                            mc = null;
                            continue;
                        }
                        if (checkLevel > 1) {
                            conn.setCheckClose(false);
                            java.sql.Statement stmt = conn.createStatement();
                            stmt.execute(testStatement);
                            stmt.close();
                            conn.setCheckClose(true);
                        }
                    } catch (Exception e) {
                        logger.log(BasicLevel.WARN, "DataSource " + getDatasourceName()
                                + " error: removing invalid mc", e);
                        synchronized(this) {
                            mcList.remove(mc);
                        }
                        mc.remove();
                        starttime = 0;
                        mc = null;
                        continue;
                    }
                }
                // We have found a valid mc in the freelist.
                break;
            }
            // No mc available: look if we can create a new one.
            // In case we have reached the maximum limit of the pool,
            // we must wait until a connection is released.
            synchronized(this) {
                if (mcList.size() >= poolMax) {
                    boolean stoplooping = true;
                    // If a timeout has been specified, wait, unless maxWaiters is reached.
                    if (timetowait > 0) {
                        if (currentWaiters < maxWaiters) {
                            currentWaiters++;
                            // Store the maximum concurrent waiters
                            if (waiterCount < currentWaiters) {
                                waiterCount = currentWaiters;
                            }
                            if (starttime == 0) {
                                starttime = System.currentTimeMillis();
                                if (logger.isLoggable(BasicLevel.DEBUG)) {
                                    logger.log(BasicLevel.DEBUG, "Wait for a free Connection" + mcList.size());
                                }
                            }
                            try {
                                wait(timetowait);
                            } catch (InterruptedException ign) {
                                logger.log(BasicLevel.WARN, "Interrupted");
                            } finally {
                                currentWaiters--;
                            }
                            long stoptime = System.currentTimeMillis();
                            long stillwaited = stoptime - starttime;
                            timetowait = waiterTimeout - stillwaited;
                            stoplooping = (timetowait <= 0);
                            if (stoplooping) {
                                // We have been waked up by the timeout.
                                totalWaiterCount++;
                                totalWaitingTime += stillwaited;
                                if (waitingTime < stillwaited) {
                                    waitingTime = stillwaited;
                                }
                            } else {
                                if (!freeList.isEmpty() || mcList.size() < poolMax) {
                                    // We have been notified by a connection released.
                                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                                        logger.log(BasicLevel.DEBUG, "Notified after " + stillwaited);
                                    }
                                    totalWaiterCount++;
                                    totalWaitingTime += stillwaited;
                                    if (waitingTime < stillwaited) {
                                        waitingTime = stillwaited;
                                    }
                                }
                                // loop to find a mc.
                                continue;
                            }
                        }
                    }
                    if (stoplooping && freeList.isEmpty() && mcList.size() >= poolMax) {
                        // Cannot get a mc: throw an exception.
                        if (starttime > 0) {
                            rejectedTimeout++;
                            logger.log(BasicLevel.WARN, "Cannot create a Connection - timeout");
                        } else {
                            rejectedFull++;
                            logger.log(BasicLevel.WARN, "Cannot create a Connection");
                        }
                        throw new SQLException("No more connections in " + getDatasourceName());
                    }
                    // loop to find a mc.
                    continue;
                }
            } // end of synchronized

            // We can create a new mc.
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "empty free list: Create a new Connection");
            }
            try {
                // create a new XA Connection
                mc = (JManagedConnection) getXAConnection();
            } catch (SQLException e) {
                connectionFailures++;
                rejectedOther++;
                logger.log(BasicLevel.WARN, "Cannot create new Connection for tx");
                throw e;
            }
            synchronized(this) {
                // Register the connection manager as a ConnectionEventListener
                openedCount++;
                mc.addConnectionEventListener(this);
                mcList.add(mc);
            }
        }
        // mc found.
        recomputeBusy();
        mc.setTx(tx);
        if (tx == null) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Got a Connection - no TX: ");
            }
        } else {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Got a Connection for TX: ");
            }
            // register synchronization
            try {
                synchronized(this) {
                    tx.registerSynchronization(mc);
                    tx2mc.put(tx, mc);  // only if registerSynchronization was OK.
                }
            } catch (javax.transaction.RollbackException e) {
                /// optimization is probably possible at this point
                if (logger.isLoggable(BasicLevel.WARN)) {
                    logger.log(BasicLevel.WARN, "DataSource " + getDatasourceName()
                            + " error: Pool mc registered, but tx is rollback only", e);
                }
            } catch (javax.transaction.SystemException e) {
                if (logger.isLoggable(BasicLevel.ERROR)) {
                    logger.log(BasicLevel.ERROR, "DataSource " + getDatasourceName()
                            + " error in pool: system exception from transaction manager ", e);
                }
            } catch (IllegalStateException e) {
                // In case transaction has already committed, do as if no tx.
                if (logger.isLoggable(BasicLevel.WARN)) {
                    logger.log(BasicLevel.WARN, "Got a Connection - committed TX: ", e);
                }
                mc.setTx(null);
            }
        }
        mc.hold();
        servedOpen++;
        return mc;
    }

    /**
     * The transaction has committed (or rolled back). We can return its
     * connections to the pool of available connections.
     * @param tx the non null transaction
     */
    public void freeConnections(final Transaction tx) {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "free connection for Tx = " + tx);
        }
        synchronized(this) {
            JManagedConnection mc = (JManagedConnection) tx2mc.remove(tx);
            if (mc == null) {
                logger.log(BasicLevel.ERROR, "pool: no connection found to free for Tx = " + tx);
                return;
            }
            mc.setTx(null);
            if (mc.isOpen()) {
                // Connection may be not closed in case of stateful session bean that
                // keeps connection to be reused in another method of the bean.
                logger.log(BasicLevel.WARN, "WARNING: Connection not closed by caller");
                return;
            }
            freeItem(mc);
        }
    }

    /**
     * Close all connections in the pool, when server is shut down.
     */
    public void closeAllConnection() {
        logger.log(BasicLevel.DEBUG, "");

        // Stop the pool keeper, since all connections will be closed.
        poolKeeper.stopit();

        // Close physically all connections
        synchronized(this) {
            Iterator it = mcList.iterator();
            try {
                while (it.hasNext()) {
                    JManagedConnection mc = (JManagedConnection) it.next();
                    mc.close();
                }
            } catch (java.sql.SQLException e) {
                logger.log(BasicLevel.ERROR, "Error while closing a Connection:", e);
            }
        }
        poolClosed = true;
    }

    // -----------------------------------------------------------------------
    // private methods
    // -----------------------------------------------------------------------

    /**
     * Mark a specific Connection in the pool as closed.
     * If it is no longer associated to a Tx, we can free it.
     * @param mc XAConnection being closed
     * @param flag TMSUCCESS (normal close) or TMFAIL (error)
     * or null if error.
     */
    private boolean closeConnection(final JManagedConnection mc, final int flag) {
        // The connection will be available only if not associated
        // to a transaction. Else, it will be reusable only for the
        // same transaction.
        if (!mc.release()) {
            return false;
        }
        if (mc.getTx() != null) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "keep connection for same tx");
            }
        } else {
            freeItem(mc);
        }

        // delist Resource if in transaction
        Transaction tx = null;
        try {
            tx = tm.getTransaction();
        } catch (NullPointerException n) {
            // current is null: we are not in JOnAS Server.
            logger.log(BasicLevel.ERROR, "Pool: should not be used outside a JOnAS Server", n);
        } catch (SystemException e) {
            logger.log(BasicLevel.ERROR, "Pool: getTransaction failed:", e);
        }
        if (tx != null && mc.isClosed()) {
            try {
                tx.delistResource(mc.getXAResource(), flag);
            } catch (Exception e) {
                logger.log(BasicLevel.ERROR, "Pool: Exception while delisting resource:", e);
            }
        }
        return true;
    }

    /**
     * Free item and return it in the free list.
     * @param item The item to be freed
     */
    private synchronized void freeItem(final JManagedConnection item) {
        // Add it to the free list
        // Even if maxage is reached, because we avoids going under min pool size.
        // PoolKeeper will manage aged connections.
        freeList.add(item);
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "item added to freeList: " + item.getIdent());
        }
        // Notify 1 thread waiting for a Connection.
        if (currentWaiters > 0) {
            notify();
        }
        recomputeBusy();
    }

    /**
     * Check on a connection the test statement
     * @param testStatement the statement to use for test
     * @return the test statement if the test succeeded, an error message otherwise
     * @throws SQLException If an error occured when trying to test (not due to the test itself,
     * but to other preliminary or post operation).
     */
    public String checkConnection(final String testStatement) throws SQLException {
        String noError = testStatement;
        JManagedConnection mc = null;
        boolean jmcCreated = false;
        if (!freeList.isEmpty()) {
            // find a connection to test in the freeList
            Iterator it = freeList.iterator();
            while (it.hasNext()) {
                mc = (JManagedConnection) it.next();
                try {
                    JConnection conn = (JConnection) mc.getConnection();
                    if (!conn.isPhysicallyClosed()) {
                        // ok, we found a connection we can use to test
                        if (logger.isLoggable(BasicLevel.DEBUG)) {
                            logger.log(BasicLevel.DEBUG, "Use a free JManagedConnection to test with " + testStatement);
                        }
                        break;
                    } else {
                        mc = null;
                    }
                } catch (SQLException e) {
                    // Can't use this connection to test
                    mc = null;
                }
            }
        }
        if (mc == null) {
            // Try to create mc Connection
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Create a JManagedConnection to test with " + testStatement);
            }
            Connection conn = null;
            try {
                conn = DriverManager.getConnection(url, userName, password);
            } catch (SQLException e) {
                logger.log(BasicLevel.ERROR, "Could not get Connection on " + url + ":", e);
            }
            if (conn == null) {
                return new String("Could not get a new Connection");
            }
            mc = new JManagedConnection(conn, this);
            jmcCreated = true;
        }
        if (mc != null) {
            // Do the test on the free connection or the created connection
            IConnection conn = (IConnection) mc.getConnection();
            // Don't check here the Connection is close, because it is closed
            // indeed when picked from the pool!
            conn.setCheckClose(jmcCreated);
            java.sql.Statement stmt = conn.createStatement();
            try {
                stmt.execute(testStatement);
            } catch (SQLException e) {
                // The test fails
                return e.getMessage();
            }
            stmt.close();
            if (jmcCreated) {
                mc.close();
            } else {
                conn.setCheckClose(true);
            }
        }
        return noError;
    }

    class PoolMonitor extends Thread {

        /**
         * Pool object to monitor
         */
        private ConnectionManager pool;

        /**
         * Logger object to log events
         */
        private Logger logger = null;

        /**
         * adjustment period
         */
        private long adjustperiod = 5000L;  // milliseconds

        /**
         * Default sampling period
         */
        private long samplingperiod = 60000L;  // milliseconds

        /**
         * time to wait on error
         */
        private final long errorperiod = 120000L;  // milliseconds

        private boolean stopped = false;

        /**
         * Constructor
         * @param pool Pool to monitor
         */
        public PoolMonitor(final ConnectionManager ds, final Logger log) {
            super("PoolMonitor");
            setDaemon(true);
            this.pool = ds;
            logger = log;
        }

        /**
         * Set the sampling period.
         * This cannot be done in the PoolKeeper constructor, because the
         * sampling period has not been set already in the Pool.
         * Moreover, this can be reconfigured later.
         * @param sec sampling period in sec.
         */
        public void setSamplingPeriod(final int sec) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, " to " + sec);
            }
            samplingperiod = sec * 1000L;
        }

        /**
         * Set the adjust period.
         * This cannot be done in the PoolKeeper constructor, because the
         * sampling period has not been set already in the Pool.
         * Moreover, this can be reconfigured later.
         * @param sec adjust period in sec.
         */
        public void setAdjustPeriod(final int sec) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, " to " + sec);
            }
            adjustperiod = sec * 1000L;
        }

        /**
         * Stop this thread.
         */
        public void stopit() {
            stopped  = true;
        }

        /**
         * Start the pool keeper thread
         */
        @Override
        public void run() {
            long timeout;
            long adjusttime = adjustperiod;
            long samplingtime = samplingperiod;
            while (! stopped) {
                timeout = adjusttime;
                if (samplingtime < timeout) {
                    timeout = samplingtime;
                }
                try {
                    sleep(timeout);
                    adjusttime -= timeout;
                    samplingtime -= timeout;
                    if (adjusttime <= 0) {
                        pool.adjust();
                        adjusttime = adjustperiod;
                    }
                    if (samplingtime <= 0) {
                        pool.sampling();
                        samplingtime = samplingperiod;
                    }
                } catch (NullPointerException e) {
                    logger.log(BasicLevel.ERROR, "poolkeeper NPE", e);
                    e.printStackTrace();
                    adjusttime = errorperiod;
                    samplingtime = errorperiod;
                } catch (Exception e) {
                    logger.log(BasicLevel.ERROR, "poolkeeper error", e);
                    e.printStackTrace();
                    adjusttime = errorperiod;
                    samplingtime = errorperiod;
                }
            }
        }
    }

}
