/*-----------------------------------------------------------------------------
 *
 * Enhydra JdbcS
 * The Initial Developer of the Original Code is Lutris Technologies Inc.
 * Portions created by Lutris are Copyright 1997-2000 Lutris Technologies Inc.
 * All Rights Reserved.
 *
 * The contents of this file are subject to the Enhydra Public License
 * Version 1.1 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * www.enhydra.org/license/epl.html
 *
 * Software distributed under the License is distributed on an "ASIS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
 * the specific language governing rights and limitations under the License.
 *
 * -----------------------------------------------------------------------------
 * $Id$
 * -----------------------------------------------------------------------------
 */
package org.enhydra.jdbc.standard;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.sql.XAConnection;
import javax.sql.XADataSource;
import javax.transaction.Status;
import javax.transaction.xa.XAException;
import javax.transaction.xa.Xid;
import javax.transaction.TransactionManager;


/**
 * Data source for creating StandardXAConnections.
 */
public class StandardXADataSource extends StandardConnectionPoolDataSource implements XADataSource  {

    public int 			minCon;		// minimum number of connections
    public int 			maxCon;		// maximum number of connections
    public long			deadLockMaxWait;	// time (in ms) to wait before return an exception
    Vector			freeConnections;// connections not currently associated with an XID
    Hashtable			xidConnections;	// connections currently associated with an XID
    Hashtable                   deadConnections; // connections which should be discarded when the transaction finishes
    public int			connectionCount = 0;	// total number of connections created
    public long        		deadLockRetryWait;	// time to wait before 2 try of loop
    transient public TransactionManager 	transactionManager;

    public static final int 	DEFAULT_MIN_CON = 50;	// minimum number of connections
    public static final int 	DEFAULT_MAX_CON = 100;	// minimum number of connections
    public static final long 	DEFAULT_DEADLOCKMAXWAIT 	= 300000;	// 5 minutes
    public static final int     DEFAULT_DEADLOCKRETRYWAIT 	= 10000;	// 10 seconds

    /**
     * Constructor
     */
    public StandardXADataSource () {
	super();
	minCon = DEFAULT_MIN_CON;
	maxCon = DEFAULT_MAX_CON;
	deadLockMaxWait = DEFAULT_DEADLOCKMAXWAIT;
	deadLockRetryWait = DEFAULT_DEADLOCKRETRYWAIT;
	freeConnections = new Vector (minCon, 1);	// allow a reasonable size for free connections
	xidConnections = new Hashtable (minCon*2, 0.5f);// ...and same for used connections
	log("StandardXADataSource is created");
    } // constructor

    public int getConnectionCount() {
	return connectionCount;
    } // method getConnectionCount

    public Hashtable getXidConnections() {
	return xidConnections;
    } // method getXidConnections

    /**
     * Creates an XA connection using the default username and password.
     */
    public XAConnection getXAConnection () throws SQLException {
	log("StandardXADataSource:getXAConnection(0) XA connection returned");
	return getXAConnection (user, password);
    } // method getXAConnection

    /**
     * Creates an XA connection using the supplied username and password.
     */
    public synchronized XAConnection getXAConnection (String user, String password) throws SQLException {
	log("StandardXADataSource:getXAConnection(user, password)");
	StandardXAConnection xac = new StandardXAConnection (this, user, password);
	xac.setTransactionManager(transactionManager);
	connectionCount++;
	return xac;
    } // method getXAConnection

    public void setTransactionManager(TransactionManager tm) {
	log("StandardXADataSource:setTransactionManager");
	this.transactionManager = tm;
    } // method setTransactionManager

    public void setUser(String user) {
    if (((user==null) || (getUser()==null)) ? user!=getUser() : !user.equals(getUser()) ) {
	    super.setUser(user);
	    resetCache();
	}
    }

    public void setPassword(String password) {
    if (((password==null) || (getPassword()==null)) ? password!=getPassword() : !password.equals(getPassword()) ) {
	    super.setPassword(password);
	    resetCache();
	}
    }

    public void setUrl(String url) {
    if (((url==null) || (getUrl()==null)) ? url!=getUrl() : !url.equals(getUrl()) ) {
	    super.setUrl(url);
	    resetCache();
	}
    }

    public void setDriverName(String driverName) throws SQLException {
	if ((driverName == null && getDriverName() != null)
		|| (!driverName.equals(getDriverName()))) {
	    super.setDriverName(driverName);
	    resetCache();
	}
    }

    private synchronized void resetCache() {
	log("StandardXADataSource:resetCache");
	// deadConnections will temporarily hold pointers to the
	// current ongoing transactions.  These will be discarded when
	// freed
	deadConnections = (Hashtable)xidConnections.clone();
	deadConnections.putAll(xidConnections);

	// now we'll just clear out the freeConnections
	Enumeration enum = freeConnections.elements();
	while (enum.hasMoreElements()) {
	    StandardXAStatefulConnection xasc = (StandardXAStatefulConnection)
		enum.nextElement();
	    try {
		log("StandardXADataSource:resetCache:closing Connection:" +
			xasc.con);
		xasc.con.close();
	    } catch (SQLException e) {
		log("StandardXADataSource:resetCache:Error closing connection:"
			+ xasc.con);
	    }
	    freeConnections.removeElement(xasc);
	}
    }

    /**
     * Called when an XA connection gets closed. When they have all
     * been closed then any remaining physical connections are also
     * closed.
     */
    synchronized void connectionClosed () throws SQLException {
	log("StandardXADataSource:connectionClosed");
	connectionCount--;				// one more connection closed
	if (connectionCount==0) {			// if no connections left

	    // Close any connections still associated with XIDs.
	    Enumeration cons = xidConnections.elements();// used to iterate through the used connections
	    while (cons.hasMoreElements()) {		// while there are more connections
		Object o = cons.nextElement();		// get the next connection
		StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
		cur.con.close();			// close the physical connection
		log("StandardXADataSource:connectionClosed close physical connection");
	    }

	    // Now close any free connections.
	    for (int i=0; i<freeConnections.size(); i++) {		// check each free connection
		Object o = freeConnections.elementAt(i);		// get next connection
		StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
		cur.con.close();					// close the physical connection
		log("StandardXADataSource:connectionClosed close any free connections");
	    }
	}
    } // method connectionClosed

    /**
     * Returns the number of connections that are either
     * prepared or heuristically completed.
     */
     public int getXidCount () {
	int count = 0;					// the return value
	Enumeration cons = xidConnections.elements();	// used to iterate through the used connections
	while (cons.hasMoreElements()) {		// while there are more connections
	    Object o = cons.nextElement();		// get the next connection
	    StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
	    if ((cur.getState() == Status.STATUS_PREPARED) ||	// if prepared
		(cur.getState() == Status.STATUS_PREPARING) ) {	// ...or heuristically committed
		count++;									// one more connection with a valid xid
	    }
	}
	log("StandardXADataSource:getXidCount return XidCount=<"+count+">");
	return count;
    } // method getXidCount

    /**
     * Constructs a list of all prepared connections' xids.
     */
    Xid[] recover () {
	int nodeCount = getXidCount();		// get number of connections in transactions
	Xid[] xids = new Xid[nodeCount];	// create the return array
	int i=0;				// used as xids index
	Enumeration cons = xidConnections.elements();	// used to iterate through the used connections
	while (cons.hasMoreElements()) {		// while there are more connections
	    Object o = cons.nextElement();		// get the next connection
	    StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
	    if ((cur.getState() == Status.STATUS_PREPARED) ||// if prepared
		(cur.getState() == Status.STATUS_PREPARING) ) {// ...or heuristically committed
		xids [i++] = cur.xid;			// save in list
	    }
	}
	return xids;
    } // method recover

    /**
     * Frees a connection to make it eligible for reuse. The free list
     * is normally a last in, first out list (LIFO). This is efficient.
     * However, timed out connections are nice to hang onto for error
     * reporting, so they can be placed at the start. This is less
     * efficient, but hopefully is a rare occurence.
     *
     * Here, no need to verify the number of connections, we remove an
     * object from the xidConnections to put it in th freeConnections
     *
     */
    public synchronized void freeConnection (Xid id, boolean placeAtStart) {
	log("StandardXADataSource:freeConnection");
	Object o = xidConnections.get (id);		// lookup the connection by XID
	StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
	xidConnections.remove (id);			// remove connection from in use list
	log("StandardXADataSource:freeConnection remove id from xidConnections");

	if (!deadConnections.containsKey(id)) { // if this isn't to be discarded
	    try {
		log("StandardXADataSource:freeConnection setAutoCommit(true):" + cur.id);
		cur.con.setAutoCommit(true);
	    } catch(SQLException e) {
		log("ERROR: Failed while autocommiting a connection: "+e);
	    }
	    cur.setState (Status.STATUS_NO_TRANSACTION);	// set its new internal state
	    if (placeAtStart) {				// if we want to keep for as long as possible
		freeConnections.insertElementAt (cur, 0);	// then place it at the start of the list
	    } else {
		freeConnections.addElement (cur);		// otherwise it's a LIFO list
	    }
	} else {
	    deadConnections.remove(id);
	    try {
		cur.con.close();
	    } catch (SQLException e) {
		//ignore
	    }
	}
	notify();

    } // method freeConnection

    /**
     * Invoked by the timer thread to check all transactions
     * for timeouts. Returns the time of the next timeout event
     * after current timeouts have expired.
     */
    synchronized long checkTimeouts (long curTime) throws SQLException {
	//log("StandardXADataSource:checkTimeouts");
	long nextTimeout=0;				// the earliest non-expired timeout in the list
	Enumeration cons = xidConnections.elements();	// used to iterate through the used connections
	while (cons.hasMoreElements()) {		// while there are more connections
	    Object o = cons.nextElement();		// get the next connection
	    StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
	    if ((cur.timeout != 0) &&			// if connection has a timeout
		(curTime > cur.timeout)) {		// ...and transaction has timed out
		//log("StandardXADataSource:checkTimeouts connection timeout");
		cur.con.rollback();			// undo everything to do with this transaction
		cur.timedOut = true;			// flag that it has timed out
		//log(cur.toString()+" timed out");
		freeConnection (cur.xid, true);		// make the connection eligible for reuse
				// The timed out connection is eligible for reuse. The Xid and timedOut
				// flag will nevertheless remain valid until it is reallocated to another
				// global transaction. This gives the TM a *chance* to get a timeout
				// exception, but we won't hang on to it forever.
	    } else {					// transaction has not timed out
		if (cur.timeout != 0) {			// but it has a timeout scheduled
		    if ((cur.timeout < nextTimeout) ||	// and it's the next timeout to expire
			(nextTimeout == 0)) {		// ...or first timeout we've found
			nextTimeout = cur.timeout;	// set up next timeout
		    }
		}
	    }
	}
	return nextTimeout;
    } // method checkTimeouts


    /**
     * Checks the start of the free list to see if the connection
     * previously associated with the supplied Xid has timed out.
     * <P>
     * Note that this can be an expensive operation as it has to
     * scan all free connections. so it should only be called in
     * the event of an error.
     */
    synchronized private void checkTimeouts (Xid xid) throws XAException {
	log("StandardXADataSource:checkTimeouts");
	for (int i=0; i<freeConnections.size(); i++) {		// check each free connection
	    Object o = freeConnections.elementAt(i);		// get next connection
	    StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
	    if (!cur.timedOut) {				// if it hasn't timed out
		continue;					// skip it
	    }
	    log("StandardXADataSource:checkTimeouts ("+i+"/"+freeConnections.size()+") xid     = "+xid);
	    log("StandardXADataSource:checkTimeouts cur.xid = "+cur.xid);
	    if (xid.equals (cur.xid)) {				// if we've found our xid
		cur.timedOut = false;				// cancel time out
		throw new XAException (XAException.XA_RBTIMEOUT);
	    }
	}
    } // method checkTimeouts

    /**
     * Returns the connection associated with a given XID.
     * is reached, the Xid is found or an exception is thrown.
     */
    synchronized StandardXAStatefulConnection getConnection (Xid xid, boolean mustFind) throws XAException {
	log("StandardXADataSource:getConnection");
	Object o = xidConnections.get (xid);				// lookup the connection by XID
	StandardXAStatefulConnection cur = (StandardXAStatefulConnection)o;// cast to something more convenient
	if (mustFind) {					// if we expected to find the connection
	    if (cur==null) {				// and we didn't
		checkTimeouts(xid);			// see if it's been freed during a timeout
		throw new XAException (XAException.XAER_NOTA);// not a valid XID
	    }
	} else {					// didn't expect to find the connection
	    if (cur != null) {				// but we found it anyway
		throw new XAException (XAException.XAER_DUPID);	// duplicate XID
	    }
	}
	log("StandardXADataSource:getConnection return connection associated with a given XID");
	return cur;
    } // method getConnection

    /**
     * Returns a connection from the free list, removing it
     * in the process. If none area available then a new
     * connection is created.
     */
    synchronized StandardXAStatefulConnection getFreeConnection () throws SQLException {
	log("StandardXADataSource:getFreeConnection");
	StandardXAStatefulConnection cur = null;		// this will be the return value
	int freeCount = freeConnections.size();			// get number of free connections
	if (freeCount == 0) {					// if there are no free connections
	    log("StandardXADataSource:getFreeConnection  there are no free connections, get a new database connection");
	    Connection con = super.getConnection (user, password);// get a new database connection
	    cur = new StandardXAStatefulConnection (this, con);	// make the connection stateful
	} else {
	    Object o = freeConnections.lastElement();		// get the last element
	    cur = (StandardXAStatefulConnection)o;		// cast to something more convenient
	    freeConnections.removeElementAt (freeCount-1);	// remove from free list
	    cur.timeout = 0;					// no timeout until start() called
	    cur.timedOut=false;					// cancel any time old out
	}
	log("StandardXADataSource:getFreeConnection return a connection from the free list");

	try {
	    log("StandardXADataSource:getFreeConnection setAutoCommit(false)");
        // changed by cney - was false
	    cur.con.setAutoCommit(true);
 	} catch(SQLException e) {
 	    log("ERROR: Failed while autocommiting a connection: "+e);
 	}

	return cur;
    } // method getFreeConnection

    public void closeFreeConnection () {
	log("StandardXADataSource:closeFreeConnection empty method TBD");
    } // method closeFreeConnection

    public void setMinCon(int min) {
	this.minCon = min;
    } // method setMinCon

    public void setMaxCon(int max) {
	this.maxCon = max;
    } // method setMaxCon

    public void setDeadLockMaxWait(long deadLock) {
	this.deadLockMaxWait = deadLock;
    } // method seDeadlockMaxWait

    public int getMinCon() {
	return this.minCon;
    } // method getMinCon

    public int getMaxCon() {
	return this.maxCon;
    } // method getMaxCon

    public long getDeadLockMaxWait() {
	return this.deadLockMaxWait;
    } // method getDeadLockMaxWait

    public int getAllConnections() {
	return xidConnections.size() + freeConnections.size();
    } // method getAllConnections

    public synchronized void processToWait () throws Exception {
	log("StandardXADataSource:processToWait");
	int currentWait = 0;

	while ((getAllConnections() >= maxCon) && (currentWait < getDeadLockMaxWait())) {
	    try {
		synchronized(this) {
		    wait(getDeadLockRetryWait());
		}
	    } catch(InterruptedException e) {
		log("ERROR: Failed while waiting for an object: "+e);
	    }
	    currentWait+=getDeadLockRetryWait();
	}
	if (getAllConnections() >= getMaxCon())
	    throw new Exception ("ERROR : impossible to obtain a new xa connection");
    } // method processToWait

    public void setDeadLockRetryWait(long deadLockRetryWait) {
        this.deadLockRetryWait = deadLockRetryWait;
    } // method setDeadLockRetryWait

    public long getDeadLockRetryWait() {
        return this.deadLockRetryWait;
    } // method getDeadLockRetryWait

} // class StandardXADataSource
