/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2000-2009 Sun Microsystems, Inc. All rights reserved. 
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License ("CDDL") (collectively, the "License").  You may
 * not use this file except in compliance with the License.  You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or mq/legal/LICENSE.txt.  See the License for the specific language
 * governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at mq/legal/LICENSE.txt.  Sun designates
 * this particular file as subject to the "Classpath" exception as provided by
 * Sun in the GPL Version 2 section of the License file that accompanied this
 * code.  If applicable, add the following below the License Header, with the
 * fields enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or  to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright holder. 
 */

/*
 * @(#)DBConnectionPool.java	1.14 06/29/07
 */ 

package com.sun.messaging.jmq.jmsserver.persist.jdbc.comm;

import com.sun.messaging.jmq.util.log.Logger;
import com.sun.messaging.jmq.util.SupportUtil;
import com.sun.messaging.jmq.jmsserver.Globals;
import com.sun.messaging.jmq.jmsserver.config.ConfigListener;
import com.sun.messaging.jmq.jmsserver.config.PropertyUpdateException;
import com.sun.messaging.jmq.jmsserver.config.BrokerConfig;
import com.sun.messaging.jmq.jmsserver.persist.jdbc.Util;
import com.sun.messaging.jmq.jmsserver.resources.*;
import com.sun.messaging.jmq.jmsserver.persist.Store;
import com.sun.messaging.jmq.jmsserver.util.BrokerException;
import com.sun.messaging.jmq.jmsserver.FaultInjection;

import java.sql.*;
import javax.sql.PooledConnection;
import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import java.lang.reflect.Method;
import java.util.*;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The DBConnection class represents a pool of connections to one database.
 */
public class DBConnectionPool {

    private static boolean DEBUG = false;

    private static final String REAP_INTERVAL_PROP_SUFFIX = ".connection.reaptime";
    private static final int DEFAULT_REAP_INTERVAL = 300; // secs

    private static final String TIMEOUT_IDLE_PROP_SUFFIX = ".connection.timeoutIdle";

    public static final String NUM_CONN_PROP_SUFFIX = ".connection.limit";

    private static final String MIN_CONN_PROP_SUFFIX = ".min_connections";
    private static final String MAX_CONN_PROP_SUFFIX = ".max_connections";
    private static final int DEFAULT_NUM_CONN = 5;

    /**
     * a SQL SELECT statement that returns at least one row.
     */
    private static final String VALIDATION_QUERY_PROP_SUFFIX = ".connection.validationQuery";

    public static final String VALIDATE_ON_GET_PROP_SUFFIX =".connection.validateOnGet";

    private int minConnections;
    private int maxConnections;

    private boolean initialized = false;
    private ReentrantLock lock = new ReentrantLock();

    private LinkedBlockingQueue<ConnectionInfo> idleConnections = 
                               new LinkedBlockingQueue<ConnectionInfo>();
    private  ConcurrentHashMap<ConnectionInfo, Thread> activeConnections = 
                              new ConcurrentHashMap<ConnectionInfo, Thread>();

    private Map<Object, ConnectionInfo> connMap = Collections.synchronizedMap(
                                        new HashMap<Object, ConnectionInfo>());

    private ConnectionReaperTask connectionReaper = null;
    private ConnectionEventListener connectionListener = null;
    private long reapInterval;

    private CommDBManager dbmgr = null;
    private Logger logger = Globals.getLogger();
    private BrokerResources br = Globals.getBrokerResources();

    private String validationQuery = null;
    private boolean validateOnGet = false;
    private boolean timeoutIdle = true;
    private boolean isPoolDataSource = false;
    private String name = null;
    private boolean dedicated = false;

    private ConfigListener cfgListener = new ConfigListener() {
        public void validate(String name, String value)
            throws PropertyUpdateException {
            if (name.equals(dbmgr.getJDBCPropPrefix()+MIN_CONN_PROP_SUFFIX)) {
                int min = 0;
                try {
                    min = Integer.parseInt(value);
                } catch (Exception e) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting, br.getString(
                        BrokerResources.X_BAD_PROPERTY_VALUE, name + "=" + value), e);
                }
                if (min < 1) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting,
                        "A minimum value of 1 connection is required");
                } else if (min > maxConnections) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting,
                        "Minimum connections " + min +
                        " is greater than maximum connections " + maxConnections);
                }
            } else if (name.equals(dbmgr.getJDBCPropPrefix()+MAX_CONN_PROP_SUFFIX)) {
                int max = 0;
                try {
                    max = Integer.parseInt(value);
                } catch (Exception e) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting, br.getString(
                        BrokerResources.X_BAD_PROPERTY_VALUE, name + "=" + value), e);
                }
                if (max < minConnections) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting,
                        "Maximum connections " + max +
                        " is less than minimum connections " + minConnections);
                }
            } else if (name.equals(dbmgr.getJDBCPropPrefix()+REAP_INTERVAL_PROP_SUFFIX)) {
                int reaptime = 0;
                try {
                    reaptime = Integer.parseInt(value);
                } catch (Exception e) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting, br.getString(
                        BrokerResources.X_BAD_PROPERTY_VALUE, name + "=" + value), e);
                }
                if (reaptime < 60) {
                    throw new PropertyUpdateException(
                        PropertyUpdateException.InvalidSetting,
                        "A minimum value of 60 seconds is required for reap time interval");
                }
            }
        }

        public boolean update(String name, String value) {
            BrokerConfig cfg = Globals.getConfig();

            lock.lock();
            try {
                if (name.equals(dbmgr.getJDBCPropPrefix()+MAX_CONN_PROP_SUFFIX)) {
                    maxConnections = cfg.getIntProperty(dbmgr.getJDBCPropPrefix()+MAX_CONN_PROP_SUFFIX);
                } else if (name.equals(dbmgr.getJDBCPropPrefix()+MIN_CONN_PROP_SUFFIX)) {
                    minConnections = cfg.getIntProperty(dbmgr.getJDBCPropPrefix()+MIN_CONN_PROP_SUFFIX);
                } else if (name.equals(dbmgr.getJDBCPropPrefix()+REAP_INTERVAL_PROP_SUFFIX)) {
                    reapInterval = cfg.getLongProperty(dbmgr.getJDBCPropPrefix()+REAP_INTERVAL_PROP_SUFFIX)*1000L;
                }
            } finally {
                lock.unlock();
            }

            // Start connection reaper to remove excess connections and idle connections
            if (connectionReaper != null) {
                connectionReaper.cancel(); // Cancel the old reaper task
            }
            connectionReaper = new ConnectionReaperTask();
            Globals.getTimer().schedule(
                connectionReaper, reapInterval, reapInterval);

            return true;
        }
    };

    /**
     * Establish a pool of database connections.
     */
    public DBConnectionPool(CommDBManager mgr, String name)
    throws BrokerException {
       this(mgr, name, false);
    }

    public DBConnectionPool(CommDBManager mgr, String name, boolean dedicated)
    throws BrokerException {

        if ( !initialized ) {
            lock.lock();
            try {
                if ( initialized ) {
                    return;
                }

                dbmgr = mgr;
                this.name = name;
                this.dedicated = dedicated;
                isPoolDataSource = dbmgr.isPoolDataSource();

                String key = dbmgr.getJDBCPropPrefix()+VALIDATION_QUERY_PROP_SUFFIX;
                validationQuery = Globals.getConfig().getProperty(key);

                if (validationQuery != null && validationQuery.trim().length() == 0) {
                    validationQuery = null;
                }
                initValidationQuery();
                if (validationQuery != null) {
                    logger.log(logger.INFO, key+"="+validationQuery);
                }

                key = dbmgr.getJDBCPropPrefix()+VALIDATE_ON_GET_PROP_SUFFIX;
                validateOnGet = Globals.getConfig().getBooleanProperty(key, Globals.getHAEnabled());
                logger.log(logger.INFO, key+"="+validateOnGet);

                key = dbmgr.getJDBCPropPrefix()+TIMEOUT_IDLE_PROP_SUFFIX;
                timeoutIdle = Globals.getConfig().getBooleanProperty(key, true);
                logger.log(logger.INFO, key+"="+timeoutIdle);

                if (!dedicated) {
                    // Check deprecated "imq.persist.jdbc.connection.limit" property
                    key = dbmgr.getJDBCPropPrefix()+NUM_CONN_PROP_SUFFIX;
                    int numConnections = Globals.getConfig().getIntProperty(key, DEFAULT_NUM_CONN);

                    if (numConnections < 1) {
                        numConnections = DEFAULT_NUM_CONN;
                        logger.log(Logger.WARNING,
                            "Invalid number of connections specified, set to default of " +
                             numConnections+toString());
                    }

                    key = dbmgr.getJDBCPropPrefix()+MIN_CONN_PROP_SUFFIX;
                    minConnections = Globals.getConfig().getIntProperty(key, numConnections);

                    if (minConnections < 1) {
                        minConnections = numConnections;
                        logger.log(Logger.WARNING, 
                            "Invalid number of minimum connections specified, set to default of " +
                            minConnections+toString());
                    }

                    key = dbmgr.getJDBCPropPrefix()+MAX_CONN_PROP_SUFFIX;
                    maxConnections = Globals.getConfig().getIntProperty(key, numConnections);

                    if (maxConnections < minConnections) {
                        maxConnections = minConnections;
                        logger.log(Logger.WARNING, 
                            "Invalid number of maximum connections specified, set to default of " +
                            maxConnections+toString());
                    }
                } else {
                    minConnections = 1;
                    maxConnections = 1;
                }

                key = dbmgr.getJDBCPropPrefix()+REAP_INTERVAL_PROP_SUFFIX;
                long reapTime = Globals.getConfig().getLongProperty(key, DEFAULT_REAP_INTERVAL);

                if (reapTime < 60) {
                    reapTime = DEFAULT_REAP_INTERVAL;
                    logger.log(Logger.WARNING,
                        "Invalid reap time interval for pool maintenance thread specified, set to default of " +
                        reapTime+toString());
                }
                logger.log(logger.INFO, key+"="+reapTime);
                
                reapInterval = reapTime * 1000L;

                // With embedded DB, if autocreate store is enabled then we
                // need to create the DB now; otherwise we run into a chicken and
                // egg problem because we will not be able to create a connection
                // to check if the store exists.               
                if (dbmgr.getCreateDBURL() != null &&
                    Globals.getConfig().getBooleanProperty(
                        dbmgr.getCreateStoreProp(),
                        dbmgr.getCreateStorePropDefault())) {
                    try {
                        Connection conn = dbmgr.connectToCreate();
                        conn.close();
                    } catch (Exception e) {
                        String url = dbmgr.getCreateDBURL();
                        String emsg = br.getKString(
                            BrokerResources.E_CREATE_DATABASE_TABLE_FAILED, url);
                        logger.log(Logger.ERROR, emsg+toString(), e);
                        throw new BrokerException(emsg, e);
                    }
                }
                if (connectionListener == null) {
                    connectionListener = new DBConnectionListener();
                }

                if (!dedicated) {
                    logger.log(logger.INFO, dbmgr.getJDBCPropPrefix()+
                               MIN_CONN_PROP_SUFFIX+"="+minConnections);
                    logger.log(logger.INFO, dbmgr.getJDBCPropPrefix()+
                               MAX_CONN_PROP_SUFFIX+"="+maxConnections);
                }

                for (int i = 0; i < minConnections; i++) {
                    ConnectionInfo cinfo = createConnection();
                    idleConnections.offer(cinfo);
                }
                if (!dedicated) {                
                    // Registerd listener so we can dynamically changed pool value
                    Globals.getConfig().addListener(dbmgr.getJDBCPropPrefix()+
                                            MIN_CONN_PROP_SUFFIX, cfgListener);
                    Globals.getConfig().addListener(dbmgr.getJDBCPropPrefix()+
                                            MAX_CONN_PROP_SUFFIX, cfgListener);
                    Globals.getConfig().addListener(dbmgr.getJDBCPropPrefix()+
                                            REAP_INTERVAL_PROP_SUFFIX, cfgListener);
                }

                // Start connection reaper to remove excess connections and idle connections
                if (connectionReaper != null) {
                    connectionReaper.cancel(); // Cancel the old reaper task
                }
                connectionReaper = new ConnectionReaperTask();
                Globals.getTimer().schedule(connectionReaper, reapInterval, reapInterval);

                initialized = true;
            } finally {
                lock.unlock();
            }
        }
    }

    public Hashtable getDebugState() {
        Hashtable ht = new Hashtable();
        ht.put("initialized", String.valueOf(initialized));
        ht.put("minConnections", String.valueOf(minConnections));
        ht.put("maxConnections", String.valueOf(maxConnections));
        ht.put("reapInterval", String.valueOf(reapInterval));
        ht.put("timeoutIdle", Boolean.valueOf(timeoutIdle));
        ht.put("validateQuery", Boolean.valueOf(validationQuery));
        ht.put("validateOnGet", Boolean.valueOf(validateOnGet));
        ht.put("isPoolDataSource", Boolean.valueOf(isPoolDataSource));
        ht.put("idleConnections.size", String.valueOf(idleConnections.size()));
        ht.put("activeConnections.size", String.valueOf(activeConnections.size()));
        return ht;
    }

    /**
     * Closes all available connections. Should be called when the broker is
     * shutting down and all store operations are done so there should not
     * be any connection in the activeConnections list.
     */
    public void close() {

        if (!initialized) {
	    return;
        }

        lock.lock();
        try {
            if (connectionReaper != null) {
                connectionReaper.cancel();
                connectionReaper = null;
            }

            Globals.getConfig().removeListener(
                dbmgr.getJDBCPropPrefix()+MIN_CONN_PROP_SUFFIX, cfgListener);
            Globals.getConfig().removeListener(
                dbmgr.getJDBCPropPrefix()+MAX_CONN_PROP_SUFFIX, cfgListener);
            Globals.getConfig().removeListener(
                dbmgr.getJDBCPropPrefix()+REAP_INTERVAL_PROP_SUFFIX, cfgListener);

            // Close all connections
            Iterator<ConnectionInfo> itr = idleConnections.iterator();
            while (itr.hasNext()) {
                ConnectionInfo cinfo = itr.next();
                destroyConnection(cinfo);
            }

            idleConnections.clear();

            initialized = false;
        } finally {
            lock.unlock();
        }
    }

    public String toString() {
        return "("+name+")";
    }
    /**
     * Recreates all the connections.
     * 
     * Should be used to remove stale connections after the DB is restarted.
     */
    public void reset() throws BrokerException {
        
        if (dbmgr.getDEBUG() || DEBUG) {
	    logger.log(Logger.INFO, toString()+".reset");
        }
                
        if (!initialized) {
	    return;
        }
        
        Collection<ConnectionInfo> oldConnections = new ArrayList<ConnectionInfo>(maxConnections);
        
        lock.lock();
        try {                       
            activeConnections.clear();
            idleConnections.drainTo(oldConnections); 
            
             // Recreates the connections
            for (int i = 0; i < minConnections; i++) {
                ConnectionInfo cinfo = createConnection();
                idleConnections.offer(cinfo);
            }
            
            // Now, close the old connections
            Iterator<ConnectionInfo> itr = oldConnections.iterator();
            while (itr.hasNext()) {
                ConnectionInfo cinfo = itr.next();
                destroyConnection(cinfo);
            }
        } finally {
            lock.unlock();
        }        
    }

    /**
     */
    private ConnectionInfo createConnection() throws BrokerException {
        Object conn = dbmgr.newConnection();
        ConnectionInfo cinfo = new ConnectionInfo(conn, connectionListener);
        connMap.put(conn, cinfo);
        return cinfo;
    }

    /**
     */ 
    private void destroyConnection(ConnectionInfo cinfo) {
        cinfo.destroy();
        connMap.remove(cinfo.getKey());
    }

    /**
     * Checks out a connection from the pool.
     * @throws BrokerException
     */
    public Connection getConnection() throws BrokerException {

        if (dbmgr.getDEBUG() || DEBUG) {
            logger.log(Logger.INFO, "["+Thread.currentThread()+"]"+toString()+".getConnection["+
                                     idleConnections.size()+", "+activeConnections.size()+"]");
        }

        if (DEBUG) {
            FaultInjection fi = FaultInjection.getInjection();
            if (fi.FAULT_INJECTION) {
                fi.checkFaultAndSleep(FaultInjection.FAULT_JDBC_GETCONN_1, null);
            }
        }
        
        Connection conn = null;

        boolean createdNew = false, pollWait = false;
        ConnectionInfo cinfo = (ConnectionInfo)idleConnections.poll();
        if (cinfo == null && (activeConnections.size() < maxConnections)) {
            cinfo = createConnection();
            try {
                conn = cinfo.getConnection();
            } catch (Exception e) {
                destroyConnection(cinfo);
                throw new BrokerException(cinfo+e.getMessage(), e);
            }
            if (dbmgr.getDEBUG() || DEBUG) {
                createdNew = true;
            }
        } else {
            // Wait until a connetion is free up
            while (cinfo == null) {
                try {
                    if (dbmgr.getDEBUG() || DEBUG) {
                        if (!pollWait) pollWait = true;
                    }
                    cinfo = (ConnectionInfo)idleConnections.poll(60, TimeUnit.SECONDS);
                    if (cinfo == null) {
                        // Something is wrong! print the stack trace
                        StringBuffer buff = new StringBuffer(1024);
                        Iterator itr = activeConnections.entrySet().iterator();
                        while (itr.hasNext()) {
                            Map.Entry e = (Map.Entry)itr.next();
                            Thread t = (Thread)e.getValue();
                            buff.append("\n")
                                .append(t.getName())
                                .append(": using connection: ")
                                .append(e.getKey());
                            StackTraceElement[] trace = t.getStackTrace();
                            for (int i=0; i < trace.length; i++) {
                                buff.append("\n\tat " + trace[i]);
                            }
                        }
                        String emsg = br.getKString(br.I_DB_CONN_POLL_TIMEOUT,
                            "("+activeConnections.size()+","+idleConnections.size()+
                            ")["+minConnections+","+maxConnections+"]",
                            String.valueOf(60))+"\n"+buff.toString(); 
                        logger.log(Logger.WARNING, emsg+toString());
                        if (dbmgr.getIsClosing()) {
                            throw new BrokerException(emsg);
                        }
                    }
                } catch (Exception e) {
                    if (e instanceof BrokerException) {
                        throw (BrokerException)e;
                    }
                    if (dbmgr.getDEBUG() || DEBUG) {
                        logger.logStack(Logger.INFO, toString()+
                            ".getConnection: "+e.getMessage(), e);
                    }
                }
            }

            if (!validateConnection(cinfo, validateOnGet, true)) {
                 destroyConnection(cinfo);

                try {
                    cinfo = createConnection();
                    conn = cinfo.getConnection();

                    logger.log(Logger.INFO, br.getKString(
                        BrokerResources.I_RECONNECT_TO_DB, 
                        ""+cinfo, dbmgr.getOpenDBURL())+toString());
                } catch (Exception e) {
                    destroyConnection(cinfo);
                    String emsg = br.getString(
                        BrokerResources.X_RECONNECT_TO_DB_FAILED,
                        dbmgr.getOpenDBURL());
                    logger.logStack(Logger.ERROR, emsg+toString(), e);
                    throw new BrokerException(emsg, e);
                }
            } else {
                try {
                    conn = cinfo.getConnection();
                } catch (Exception e) {
                    destroyConnection(cinfo);
                    throw new BrokerException(cinfo+e.getMessage(), e);
                }
            }
        }

        // move the connection in the activeConnections list
        Thread borrower = Thread.currentThread();
        activeConnections.put(cinfo, borrower);

        if (dbmgr.getDEBUG() || DEBUG) {
            logger.log(Logger.INFO, toString()+".getConnection["+createdNew+","+pollWait+"]: " +
                borrower.getName() + " [" + new Date() +
                "]: check out connection: 0x" + conn.hashCode()+cinfo);
        }

        return conn;
    }

    /**
     * Checks in a connection to the pool.
     */
    public void freeConnection(Connection conn, Throwable ex) {

        if (dbmgr.getDEBUG() || DEBUG) {
	    logger.log(Logger.INFO, toString()+".freeConnection: connection: 0x"+
                    conn.hashCode()+(ex == null ? "":", ex="+ex));
        }

        if (isPoolDataSource) {
            try {
                conn.close();
            } catch (Exception e) {
                logger.log(logger.WARNING,
                    br.getKString(br.W_DB_CONN_CLOSE_EXCEPTION,
                    "0x"+conn.hashCode(), e.toString())+toString());
            }
            return;
        }

        ConnectionInfo cinfo = connMap.get(conn);

        if (cinfo == null) {
            logger.log(logger.WARNING,
                br.getKString(br.W_DB_CONN_RETURN_UNKNOWN,
                "0x"+conn.hashCode())+toString());
            try {
                conn.close();
            } catch (Exception e) {
                logger.log(logger.WARNING, 
                    br.getKString(br.W_DB_CONN_CLOSE_EXCEPTION,
                    "0x"+conn.hashCode(), e.toString())+toString());
            }
            return;
        }
        cinfo.setException(ex);
        returnConnection(cinfo, ex);
    }

    protected void returnConnection(ConnectionInfo cinfo, Throwable ex) {
        returnConnection(cinfo, ex, false);
    }

    /**
     * @param destroy if true, only to be called from 
     *        connectionErrorOccurred for PooledConnection 
     */
    protected void returnConnection(ConnectionInfo cinfo, Throwable ex, boolean destroy) {
        if (dbmgr.getDEBUG() || DEBUG) {
	    logger.log(Logger.INFO, toString()+".returnConnection: connection: "+cinfo+
            (ex == null ? "":", ex="+ex)+(!destroy ? "":", destroy="+destroy));
        }

        Thread thread = activeConnections.remove(cinfo);

        if (thread == null) {
            if (destroy) {
                logger.log(Logger.INFO, br.getKString(
                    br.I_DB_DESTROY_INACTIVE_CONN, 
                    cinfo.toString(), ex.toString())+toString());
                 
                if (!idleConnections.remove(cinfo)) {
                    if (dbmgr.getDEBUG() || DEBUG) {
                        logger.log(Logger.INFO, toString()+".returnConnection: "+
                        "Destroy an inactive/non-idle database connection "+
                         cinfo.toString());
                    }
                }
            } else {
                if (dbmgr.getDEBUG() || DEBUG) {
                    logger.log(Logger.WARNING, toString()+".returnConnection("+
                    cinfo+(ex == null ? "":", ex="+ex)+
                    "): not found in connection pool\n"+ SupportUtil.getStackTrace(""));
                } else {
                    logger.log(Logger.WARNING,
                    br.getKString(br.W_DB_CONN_RETURN_NOT_FOUND_INPOOL,
                    ""+cinfo+"["+(ex == null ? "":", ex="+ex)+ "]")+toString());
                }
            }
            destroyConnection(cinfo);

        } else {

            if (destroy) {
                logger.log(Logger.INFO, br.getKString(
                    br.I_DB_DESTROY_ACTIVE_CONN,
                    cinfo.toString(), ex.toString())+toString());
                destroyConnection(cinfo);
                return;
            }
            if (ex != null) { 
                if (!validateConnection(cinfo, 
                                        (ex instanceof SQLException) ||
                                        (ex.getCause() instanceof SQLException), false)) {
                    destroyConnection(cinfo);
                    return;
                }
            }
            cinfo.idleStart();
            idleConnections.offer(cinfo);
        }
    }

    private void initValidationQuery() throws BrokerException {

        if (dbmgr.isMysql()) {
            validationQuery = "/* ping */";

        } else if (dbmgr.isOracle()) {
            validationQuery = "SELECT 1 FROM DUAL";

        } else if (validationQuery == null && dbmgr.isStoreInited()) {
            try {
                validationQuery = "SELECT 1 FROM "+ 
                    ((BaseDAO)dbmgr.getFirstDAO()).getTableName();
            } catch (Exception e) {}
        } 
    }

    /**
     * @return true if valid connection 
     */
    private boolean validateConnection(ConnectionInfo cinfo,
                                       boolean ping, boolean get) {

        boolean doping = ping;

        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        cinfo.setValidating(true);
        try {
            Object o = cinfo.getKey();
            if (o instanceof Connection) {
                if (((Connection)o).isClosed()) {
                    return false;
                }
                if (Util.isConnectionError(cinfo.getException(), dbmgr)) {
                    return false;
                }
            } else if (cinfo.getException() != null) {
                return false;
            }
            if (get &&
                ((System.currentTimeMillis()-cinfo.getIdleStartTime())
                 >= reapInterval)) {
                if (timeoutIdle) {
                    return false;
                }
                doping = true;  
            }
            if (!doping) {
                return true;
            }

            conn = cinfo.getConnection(); 
            if (conn == null) {
                return false;
            }

            Boolean valid = null;
            try {
                stmt = conn.createStatement();
                int queryTimeout = stmt.getQueryTimeout();
                if (dbmgr.isJDBC4()) {
                    try {
                        Class cc = java.sql.Connection.class;
                        Method m = cc.getMethod("isValid", new Class[]{java.lang.Integer.TYPE});
                        long startime = System.currentTimeMillis();
                        boolean b = ((Boolean)m.invoke(conn,
                            new Object[]{new Integer(queryTimeout)})).booleanValue();
                        if (!b) {
                            if (System.currentTimeMillis() < (startime+queryTimeout*1000L)) {
                                valid = Boolean.valueOf(false);
                            }
                        } else {
                            valid = Boolean.valueOf(b);
                        }
                    } catch (NoSuchMethodException e) {
                        dbmgr.setJDBC4(false);
                    } catch (Throwable t) {
                        if (dbmgr.getDEBUG() || DEBUG) {
                            logger.logStack(logger.INFO, toString()+".validateConnection: "+
                            "Exception in invoking Connection.isValid("+
                             queryTimeout+")", t);
                        }
                    }
                }

                String sql = null;

                if (valid == null) {
				    sql = validationQuery;
                    if (sql == null) {
                        valid = Boolean.valueOf(true);
                    }
                }
                if (valid == null) {
                    try {
                        rs = stmt.executeQuery(sql);
                        if (rs.next()) {
                            valid = Boolean.valueOf(true);
                        } else {
                            valid = Boolean.valueOf(false);
                        }
                    } finally {
                        try {
                            if (!conn.getAutoCommit()) {
                                conn.rollback();
                            }
                        } catch (Exception e) {
                            logger.log(logger.WARNING,
                                br.getKString(br.W_DB_CONN_VALIDATION_EXCEPTION,
                                "["+sql+"]"+cinfo, e.toString())+toString());
                            valid = Boolean.valueOf(false);
                        }
                    }
                }
            } finally {
                if (rs != null) rs.close();
                if (stmt != null) stmt.close();
                if (o instanceof PooledConnection) {
                    try {
                        conn.close();
                    } catch (Exception e) {
                        logger.log(logger.WARNING,
                            br.getKString(br.W_DB_CONN_VALIDATION_EXCEPTION,
                            ""+cinfo+"[0x"+conn.hashCode()+"]", e.toString())+toString());
                        valid = Boolean.valueOf(false);
                    }
                }
            }
            if (valid == null) {
                valid = Boolean.valueOf(false);
            }
            return valid.booleanValue();

        } catch (Exception e) {
            logger.logStack(logger.WARNING,
                br.getKString(br.W_DB_CONN_VALIDATION_EXCEPTION,
                cinfo.toString(), e.getMessage())+toString(), e);
            return false;
        } finally {
            cinfo.setValidating(false);
            cinfo.setException(null);
        }
    }

    private void reapExcessConnection() {

        int reapCnt = 0;
        ConnectionInfo cinfo = null;

        int idleCnt = idleConnections.size();
        int activeCnt = activeConnections.size();

        if (dbmgr.getDEBUG() || DEBUG) {
            logger.log(Logger.INFO, toString()+".reapExcessConnection: "+
                "pool size: min="+minConnections+", max="+maxConnections+
                ", active="+activeCnt+", idle="+idleCnt);
        }
        
        while (idleCnt > 0 && (activeCnt + idleCnt) > minConnections) {
            cinfo = (ConnectionInfo)idleConnections.poll();
            destroyConnection(cinfo);
            reapCnt++;
            idleCnt = idleConnections.size();
            activeCnt = activeConnections.size();
        }

        if (!dedicated || reapCnt != 0) {
            logger.log(logger.INFO, br.getKString(
                       br.I_DB_REAP_EXCESSIVE_CONNS,
                       Integer.valueOf(reapCnt))+toString());
        }

        if (!timeoutIdle) { 
            return;
        }

        ArrayList list = new ArrayList();
        Object[] o = idleConnections.toArray();
        int cnt = (o.length > minConnections ? minConnections:o.length);
        long currtime = System.currentTimeMillis();
        boolean found = false;
        for (int i = 0; i < cnt; i++) {
            list.add(o[i]);
            if ((currtime - ((ConnectionInfo)o[i]).getIdleStartTime()) >= reapInterval) {
                found = true;
            }
        }
        if (!found || list.size() == 0) {
            return;
        }
          
        cinfo = (ConnectionInfo)idleConnections.peek();
        if (cinfo == null) {
            return;
        }
        
        ArrayList seen = new ArrayList();
        Thread borrower = Thread.currentThread();
        int i = 0, idleTimeoutCnt = 0;
        while (i < cnt) {
            if (DEBUG) {
                logger.log(logger.INFO, 
                "DBConnectionPool.reapExcessConnection idleTimeoutCnt="+idleTimeoutCnt+", cnt="+cnt+", i="+i);
            }
            cinfo = (ConnectionInfo)idleConnections.peek();
            if (cinfo == null || !list.contains(cinfo) || seen.contains(cinfo)) {
                break;
            }
            cinfo = idleConnections.poll();
            if (cinfo == null) {
                break;
            }
            seen.add(cinfo);
            i++;
            activeConnections.put(cinfo, borrower);
            if (list.contains(cinfo) && 
                (currtime - cinfo.getIdleStartTime()) >= reapInterval) {
                activeConnections.remove(cinfo);
                destroyConnection(cinfo);
                idleTimeoutCnt++;
                if ((activeConnections.size()+idleConnections.size()) < minConnections) {
                    try {
                        cinfo = createConnection();
                        if (idleConnections.size()+activeConnections.size() < minConnections) {
                            idleConnections.offer(cinfo);
                        } else {
                            destroyConnection(cinfo);
                        }
                    } catch (BrokerException e) {
                        if (dbmgr.getDEBUG() || DEBUG) {
                            logger.logStack(logger.WARNING, 
                            "JDBC connection pool reaper thread failed to create new connection", e);
                        } else {
                            logger.log(logger.WARNING, 
                             br.getKString(br.W_DB_POOL_REAPER_CREATE_NEW_CONN_FAIL, e.getMessage()));
                        }
                        continue;
                    }
                }
            } else {
                activeConnections.remove(cinfo);
                idleConnections.offer(cinfo);
            }
        }
        logger.log(logger.INFO, br.getKString(
                   br.I_DB_REAP_IDLE_CONNS,
                   Integer.valueOf(idleTimeoutCnt))+toString());
    }

    private class ConnectionReaperTask extends TimerTask
    {
        private volatile boolean canceled = false;

        public boolean cancel() {
            canceled = true;
            return super.cancel();
        }

        public void run() {
            if (canceled) {
                return;
            }

            try {
                reapExcessConnection();
            } catch (Exception e) {
                Globals.getLogger().logStack( Logger.ERROR,
                    BrokerResources.E_DB_POOL_REAPER_THREAD_EXCEPTION+
                    toString(), e );
            }
        }
    }

    private class DBConnectionListener implements ConnectionEventListener {

        public DBConnectionListener() {}
   
        /**
         * Notifies this <code>ConnectionEventListener</code> that
         * the application has called the method <code>close</code> on its
         * representation of a pooled connection.
         *
         * @param event an event object describing the source of
         * the event
         */
        public void connectionClosed(ConnectionEvent event) {
            PooledConnection pconn = (PooledConnection) event.getSource();
            ConnectionInfo cinfo = connMap.get(pconn);
            if (cinfo == null) {
                throw new IllegalStateException(
                "No mapping for PooledConnection 0x"+pconn.hashCode()+
                "["+pconn.getClass().getName()+"]");
            }
            if (dbmgr.getDEBUG() || DEBUG) {
                logger.log(logger.INFO, toString()+".connectionClosed event on "+cinfo);
            }
            if (!cinfo.inValidating()) {
                returnConnection(cinfo, cinfo.getException());
            }
        }

        /**
         * Notifies this <code>ConnectionEventListener</code> that
         * a fatal error has occurred and the pooled connection can
         * no longer be used.  The driver makes this notification just
         * before it throws the application the <code>SQLException</code>
         * contained in the given <code>ConnectionEvent</code> object.
         *
         * @param event an event object describing the source of
         * the event and containing the <code>SQLException</code> that the
         * driver is about to throw
         */
        public void connectionErrorOccurred(ConnectionEvent event) {
            PooledConnection pconn = (PooledConnection)event.getSource();
            pconn.removeConnectionEventListener(this);
            ConnectionInfo cinfo = connMap.get(pconn);
            if (cinfo == null) {
                throw new IllegalStateException(
                "connectionErrorOccurred: No mapping for PooledConnection 0x"+
                 pconn.hashCode()+"["+pconn.getClass().getName()+"]");
            }
            SQLException ex = event.getSQLException();
            logger.log(logger.WARNING, 
                       br.getKString(br.W_DB_CONN_ERROR_EVENT,
                       ""+cinfo, ""+ex)+toString());
            if (ex == null) {
                ex = new SQLException();
            }
            cinfo.setException(ex);
            if (!cinfo.inValidating()) {
                returnConnection(cinfo, cinfo.getException(), true);
            }
        }
    }
}

class ConnectionInfo {

    Object conn;  
    Throwable thr = null;
    ConnectionEventListener listener = null;
    boolean validating = false;
    long idleStartTime = System.currentTimeMillis();

    public ConnectionInfo(Object conn, ConnectionEventListener listener) {
        this.conn = conn;
        this.listener = listener;
        if (conn instanceof PooledConnection) {
            ((PooledConnection)conn).addConnectionEventListener(listener);
        }
    }
    
    public Object getKey() {
        return conn;
    }

    public void idleStart() {
        idleStartTime = System.currentTimeMillis();
    }

    public long getIdleStartTime() {
        return idleStartTime;
    }

    public void setValidating(boolean b) {
        validating = b;
    } 

    public boolean inValidating() {
        return validating;
    }

    public void setException(Throwable t) {
        thr = t;
    }

    public Throwable getException() {
        return thr;
    }

    public Connection getConnection() throws SQLException {
        if (conn instanceof PooledConnection) {
            return ((PooledConnection)conn).getConnection();
        } else {
            return (Connection)conn;
        }
    }

    public void destroy() {
        try {
            if (conn instanceof PooledConnection) {
                ((PooledConnection)conn).removeConnectionEventListener(listener);
                ((PooledConnection)conn).close();
            } else { 
                ((Connection)conn).close();
            }
        } catch (Throwable t) {
            Globals.getLogger().log(Globals.getLogger().WARNING, 
                Globals.getBrokerResources().W_DB_CONN_CLOSE_EXCEPTION,
                this.toString(), t.toString());
        }
    }

    public String toString() {
        return (conn instanceof Connection ? "[Connection":"[PooledConnection")+
                ":0x"+conn.hashCode()+(thr == null ? "":", "+thr.toString()) +"]";
    }
} 

