/*
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999 Bull S.A.
 * Contact: jonas-team@objectweb.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$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.dbm;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import javax.sql.XAConnection;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.xa.XAResource;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.jtm.TransactionService;
import org.objectweb.jonas_tm.TransactionImpl;
import org.objectweb.jonas.service.ServiceManager;
import org.objectweb.jonas.service.ServiceException;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.api.BasicLevel;

/**
 * Connection Pool
 * @author Philippe Durieux
 * Contributor(s): 
 *          Markus Fritz
 * 01/11/06 Christophe Ney cney@batisseurs.com for Lutris Technologies
 *          Added ResourceManagerListener mechanism to remove ThreadData
 *          dependency.
 * 
 * 02/01/15 Dean Jennings - Map  instead of Hashtable for tx2item and xac2item
 */

public class Pool {

    static private Logger logger = null;

    private TransactionManager tm = null;

    // List of free available items
    private LinkedList freeList = new LinkedList();
    // requests with no tx are identified by thread
    private int htcount = 0;
    // Requests with same tx get always the same conn
    private Map tx2item = new HashMap();
    // map XAConn => Item (to speedup CloseConn)
    private Map xac2item = new HashMap();

    // ---------------------------------------------------------------------
    // Pool Configuration. 
    // Values are initialized directly from ConnectionManager
    // ---------------------------------------------------------------------
    // Max age of a Connection in milliseconds.
    long jdbcConnMaxAge;
    // Level of checking on connections
    int jdbcConnCheckLevel;
    // test statement
    String jdbcTestStmt;

    /**
     * Pool constructor
     */
    public Pool(ConnectionManager cmgr) {
        logger = Log.getLogger(Log.JONAS_DBM_PREFIX);
	TransactionService ts =
	    (TransactionService) ServiceManager.getInstance().getTransactionService();
	tm = ts.getTransactionManager();
	(new PoolKeeper(this)).start();
    }
    
    /**
     * lookup connection in the pool for this user/tx
     * @args user user name
     * @args tx Transaction the connection is involved
     * @return the free PoolItem or null if no item available
     */
    public synchronized PoolItem openConnection(String user, Transaction tx) {
        PoolItem item = null;
	// Try to find a connection for this tx.
	if (tx != null) {
	    Object i;
	    while ((i = tx2item.get(tx)) != null) {
		if (i instanceof PoolItem) {
		    item = (PoolItem)i;
		    break;
		}
		// Wait for another thread to call newConnection().
		try {
		    wait();
		} catch (InterruptedException ign) {
		}
	    }
	}
	if (item == null) {
	    // No connection found for this tx, or no tx.
	    // try to find an item in the free list
	    while (true) {
		if (freeList.isEmpty()) {
		    if (tx != null) {
			// Put a placeholder before returning null in case of multiple threads
			// Assumes this thread will call newConnection() after.
			tx2item.put(tx, new Object());
		    }
		    return null;
		}
		item = (PoolItem) freeList.removeFirst();		
		// Check the connection before reusing it
		if (jdbcConnCheckLevel > 0) {
		    try {
			Connection conn = item.xaConn.getConnection();
			if (conn.isClosed()) {
			    destroyItem(item);
			    continue;
			}
			if (jdbcConnCheckLevel == 2 || jdbcConnCheckLevel == 4) {
			    logger.log(BasicLevel.DEBUG, "retrying connection: "+jdbcTestStmt);
			    java.sql.Statement stmt = conn.createStatement();
			    stmt.execute(jdbcTestStmt);
			    stmt.close();
			}
		    } catch (Exception e) {
			logger.log(BasicLevel.ERROR, "removing invalid item:"+e);
			destroyItem(item); 
			continue;
		    }
		}
		break;
	    }
	    // We found an item. Associate it with the tx.
	    item.tx = tx;
	    if (tx == null) {
		htcount++;
	    } else {
		logger.log(BasicLevel.DEBUG, "reuse a Connection for Tx = "+tx);
		tx2item.put(tx, item);
		/// register synchronization
		try {
		    tx.registerSynchronization(item);
		} catch (javax.transaction.RollbackException e) {
		    /// optimization is probably possible at this point
		    logger.log(BasicLevel.WARN, "Poolitem registered, but tx is rollback only");
		} catch (javax.transaction.SystemException e) {
		    logger.log(BasicLevel.ERROR, "Pool: system exception from transaction manager "+e.getMessage());
		}
	    }
	}

	// TODO : waiting for exclusive connection ?

	item.open++;
	return item;
    }

    /**
     * Add a new XAConnection in the pool
     */
    public synchronized PoolItem newConnection(XAConnection xac, String user, Transaction tx) {
        logger.log(BasicLevel.DEBUG, "add a Connection for Tx = " + tx);
        PoolItem item = new PoolItem(this, xac, user, jdbcConnMaxAge);
	xac2item.put(xac, item);	
	item.tx = tx;
	item.open++;
	if (tx == null) {
	    htcount++;
	} else {
	    // replace the placeholder by the new item
	    tx2item.remove(tx);
	    tx2item.put(tx, item);
	    /// register synchronization
	    try {
		tx.registerSynchronization(item);
	    } catch (javax.transaction.RollbackException e) {
		/// optimization is probably possible at this point
		logger.log(BasicLevel.WARN, "Poolitem registered, but tx is rollback only");
	    } catch (javax.transaction.SystemException e) {
		logger.log(BasicLevel.ERROR, "Pool: system exception from transaction manager "+e.getMessage());
	    }
	    notifyAll();
	}
	return item;
    }
   
    /**
     * The transaction has committed (or rolled back). We can return its
     * connections to the pool of available connections.
     * @args tx the non null transaction
     */
    public synchronized void freeConnections(Transaction tx) {
	logger.log(BasicLevel.DEBUG, "for Tx = " + tx);
        PoolItem item = (PoolItem) tx2item.remove(tx);
        if (item == null) {
	    logger.log(BasicLevel.ERROR, "Pool: no connection found to free for Tx = " + tx);
	    return;
	}
	item.tx = null;
	if (item.open > 0) {
	    // Forcing a physical close to avoid reusing it in another transaction may
	    // lead to deadlock in database (close on a connection currently in use)
	    //item.open = 0;
	    //destroyItem(item);
	    // XXX don't know what is the best to do here ?
	    logger.log(BasicLevel.ERROR, "WARNING: Connection not closed by caller");
	    return;
	}
	// TODO check max age here ?
	// Add it to the free list
	freeList.addLast(item);
    }

    /**
     * Mark a specific Connection in the pool as closed.
     * If it is no longer associated to a Tx, we can free it.
     *
     * @return the PoolItem the connection is in (=> tx)
     * or null if error.
     */
    public synchronized PoolItem closeConnection(XAConnection xac, int flag) {
        PoolItem item = (PoolItem) xac2item.get(xac);
        if (item == null) {
            logger.log(BasicLevel.ERROR, "no PoolItem for this xac!");
            return null;
        }
        // The connection will be available only if not associated
        // to a transaction. Else, it will be reusable only for the
        // same transaction.
	item.open--;
	if (item.open < 0) {
	    item.open = 0;
	    logger.log(BasicLevel.WARN, "connection was already closed");
	    return null;
	}
	if (item.tx == null && item.open > 0) {
	    logger.log(BasicLevel.ERROR, "connection-open counter overflow");
	    item.open = 0;
	}
	if (item.tx != null) {
	    logger.log(BasicLevel.DEBUG, "keep the Connection for tx = "+item.tx);
	} else {
	    logger.log(BasicLevel.DEBUG, "free the Connection");
	    htcount--;
	    // TODO check max age here ?
	    // Add it to the free list
	    freeList.addLast(item);
	}
	// TODO notify item here ???

	// delist Resource if in transaction
	Transaction tx = null;
	try {
	    tx = tm.getTransaction();
	} catch (NullPointerException n) {
	    // current is null: we are not in EJBServer.
	    logger.log(BasicLevel.ERROR, "Pool: should not be used outside an EJBServer");
	} catch (SystemException e) {
	    logger.log(BasicLevel.ERROR, "Pool: getTransaction failed:"+e);
	}
	if (tx != null && item.open < 1) {
	    try {
		tx.delistResource(xac.getXAResource(), flag);
	    } catch (Exception e) {
		logger.log(BasicLevel.ERROR, "Pool: Exception while delisting resource:"+e);
	    }
	}

	return item;
    }

    /**
     * Check all connections not in a transaction if their maxage
     * is reached and if so kill them.
     */
    public synchronized void check4MaxAge() {
        PoolItem item;
	Iterator i = freeList.iterator();
	while (i.hasNext()) {
	    item = (PoolItem) i.next();
	    if (item.tx != null || item.open > 0) {
		logger.log(BasicLevel.ERROR, "inconsistent PoolItem in FreeList!");
	    }
	    if (item.lMaxAgeMillis < System.currentTimeMillis()) {
		logger.log(BasicLevel.DEBUG, "remove a timed out Connection");
		i.remove();
		destroyItem(item);
	    }
	}
    }

    /**
     * Close all connections in the pool, when server is shut down.
     */
    public synchronized void closeAllConnections() {
	Iterator enum = xac2item.keySet().iterator();
	try {
	    while (enum.hasNext()) {
		XAConnection xac = (XAConnection) enum.next();
		xac.close();
	    }
	} catch (java.sql.SQLException e) {
	}
    }

    /**
     * Destroy an item because connection closed or error occured
     */
    private void destroyItem(PoolItem item) {
       xac2item.remove(item.xaConn);	
       try {
           item.xaConn.close();
       } catch (java.sql.SQLException ign) {
       }
       item.xaConn = null;
    }

    /**
     * Management method:
     * @return int number of xa connection
     */
    public int getCurrentNumberOfXAConnection() {
	return xac2item.size();
    }
}
