/*-----------------------------------------------------------------------------
 *
 * 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 javax.sql.XAConnection;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.Referenceable;
import javax.naming.StringRefAddr;
import javax.transaction.Status;
import javax.transaction.TransactionManager;

/**
 * Provides a generic wrapper for JDBC 1 drivers. JDBC 1 drivers always
 * associate a single transaction at every point in time with a physical
 * connection. J2EE drivers, on the other hand, allow an XAResource (and
 * therefore an XAConnection with which it has a one to one mapping) to
 * switch between global transactions.
 * <P>
 * To accomodate this, the StandardXADataSource class maintains a list of
 * Connection objects. When the Transaction Manager associates an XID 
 * with a StandardXAConnection, it looks for a physical connection which 
 * is associated with that transaction.
 * <P>
 * The "current" connection (super.con and curCon) is the connection
 * currently being used by the application (i.e. getConnection has
 * been called, but not Connection.close()). The current connection
 * is removed and handed to the data source if it becomes associated
 * with a global transaction.
 */
public class StandardXAConnection extends StandardPooledConnection 
    implements XAConnection, XAResource, Referenceable, Runnable {

    protected StandardXAStatefulConnection 	curCon;	// the "current" stateful connection, null if none
    private boolean		commitOnPrepare;	// true if commit takes place during prepare call
    boolean			isClosed;		// true if this connection has been closed
    private int			timeoutSecs;		// timeout in seconds
    private long		timeoutPeriod=60000;	// interval in ms to check for timeouts
    private long		nextTimeout;		// time when next timeout occurs
    public Thread		timerThread;		// the thread that checks for timeouts
    public TransactionManager 	transactionManager;
    public StandardXAConnectionHandle	connectionHandle;
    protected StandardXADataSource xaDataSource;

    /**
     * Creates the first free connection.
     */
    public StandardXAConnection (StandardXADataSource dataSource, String user, String password) throws SQLException {
	super (dataSource, user, password);	// creates the first Connection object
	
	// Save the constructor parameters.
	this.xaDataSource = dataSource;
	curCon = new StandardXAStatefulConnection (dataSource, con);// wrap connection as a stateful connection
	
	// NOTE - the current connection is not made known to the data source
	// so it is not eligible for re-use. It only goes on the data source list
	// if it ever becomes associated with a global transaction.
	
	/*
	// get the timer thread
	if (xaDataSource.getThreadFactory() != null) {
	    dataSource.log("StandardXAConnection: Getting thread from factory");
	    timerThread = xaDataSource.getThreadFactory().getThread(this);
	    dataSource.log("StandardXAConnection: Got thread from factory");
	} else {
	    dataSource.log("StandardXAConnection: Getting thread from new Thread()");
	    timerThread = new Thread (this);	// create the backgroup thread to check for timeouts
	}

	timerThread.start();			// start the timer thread
	//timerThread.suspend();			// and suspend until some timeouts get set up
	*/
	dataSource.log("StandardXAConnection created");
    } // constructor
    
    /**
     * We are required to maintain a 1-1 mapping between an XAConnection
     * and its corresponding XAResource. We achieve this by implementing
     * both interfaces in the same class.
     */
    public XAResource getXAResource () {
	return this;
    } // method getXAResource
    
    /**
     * Creates a new StandardXAConnectionHandle for use by an application.
     * If there is already an StandardXAConnectionHandle in use then it is
     * closed (i.e. the application has the connection withdrawn).
     * <P>
     * This method always returns a Connection in the free state (i.e.
     * (not associated with an Xid). This is necessary since, unless
     * Start (Xid, flags) gets called, the Connection must do local
     * transaction processing.
     */
    public synchronized Connection getConnection() throws SQLException {
	dataSource.log("StandardXAConnection:getConnection");
	if (connectionHandle != null) {			// if there's already a delegated connection
	    if (!connectionHandle.isClosed()) 		// and it hasn't been closed
		connectionHandle.close();		// close it now
	}
	if (curCon == null) {					// if there's no current connection
	    curCon = xaDataSource.getFreeConnection();		// find or create a free connection
	    con = curCon.con;					// save it's Connection
	}	

	// Note that we share the PreparedStatement cache across many physical
	// connections. This is OK since the connection is used in the lookup key.

        connectionHandle = new StandardXAConnectionHandle (this, dataSource.getMasterPrepStmtCache(), dataSource.getPreparedStmtCacheSize(), transactionManager);
        dataSource.log("StandardXAConnection:getConnection return a connection");
        return connectionHandle;
    } // method getConnection

    protected void newConnectionHandle() {
	connectionHandle = new StandardXAConnectionHandle (this, dataSource.getMasterPrepStmtCache(), dataSource.getPreparedStmtCacheSize(), transactionManager);
    }  // method newConnectionHandle

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

    /**
     * Close this XA connection.
     */
    public synchronized void close() throws java.sql.SQLException {
	if (con != null) {				// if we have a current connection
	    con.close();				// then close it
	    dataSource.getMasterPrepStmtCache().remove(con.toString());
	} else {					// no "current" connection
	    if (xaDataSource.freeConnections.size() > 1) {	// if there are some free connections
		curCon = xaDataSource.getFreeConnection();	// get one of the free connections
		curCon.con.close();				// close it
		dataSource.getMasterPrepStmtCache().remove(curCon.con.toString());
	    }
	}
	curCon = null;						// remove stateful connection
	con = null;						// and physical connection
	dataSource.log("StandardXAConnection:close close a connection");
	xaDataSource.connectionClosed();				// tell data source that connection's gone
	isClosed = true;					// connection is now closed
	connectionHandle = null;
	//timerThread.resume();					// stop the timeout checking
	nextTimeout = 0;               // I don't know how the above line was
	                               // supposed to stop the thread but we
				       // should really just set the condition
				       // and let the thread stop itself
    } // method close 

    /**
     * Does most of the work of the start() call (below). Kept as
     * a separate method so that subclasses can call it and retain
     * the curCon property.
     */
    public synchronized void doStart (Xid xid, int flags) throws XAException{
	dataSource.log("StandardXAConnection:doStart");
	dataSource.log("StandardXAConnection:doStart xid = "+xid);
	dataSource.log("StandardXAConnection:doStart flags = "+flags);

	if (xid == null) throw new XAException (XAException.XAER_INVAL);
	// should only get called after a new/free connection has been made current
	if (curCon == null) throw new XAException (XAException.XAER_PROTO);
	if (curCon.getState() != Status.STATUS_NO_TRANSACTION) {
	    dataSource.log("StandardXAConnection:doStart:Invalid state:status=" 
		    + curCon.getState() + ":id=" + curCon.id);
	    throw new XAException (XAException.XAER_PROTO);
	}
	if (flags == TMRESUME || flags == TMJOIN) {		// if resuming or joining an existing transaction	
	    try {
		xaDataSource.processToWait();
	    } catch (Exception e) {
		throw new XAException ("Exception : "+e.toString());
	    }
	    synchronized (xaDataSource) {
		xaDataSource.freeConnections.addElement (curCon);	// save the current connection
	    }
	    curCon = xaDataSource.getConnection (xid, true);	// must find connection handling xid
	    con = curCon.con;					// must use correct physical connection
	} else {
	    xaDataSource.getConnection (xid, false);		// must NOT find connection handling xid
	}

	StandardXAConnectionHandle xad = (StandardXAConnectionHandle)connectionHandle;
	try {
	    xad.setGlobalTransaction(true);			// delegate must use current physical connection
	} catch (SQLException e) {
	    throw new XAException (e.toString());
	}
	if (timeoutSecs != 0) {					// if a timeout has been defined
	    curCon.timeout = System.currentTimeMillis()		// set the timeout
		+ timeoutSecs * 1000;
	    if (nextTimeout==0) {				// if there are currently no timeouts set up
		nextTimeout = curCon.timeout;			// set new timeout
		notify();
		//timerThread.resume();				// start checking for timeouts
	    } else {						// some timeouts already exist
		if (curCon.timeout < nextTimeout) {		// if this expires sooner than next timeout
		    nextTimeout = curCon.timeout;		// set new timeout
		}
	    }
	}

	curCon.xid = xid;					// connection now associated with this XID
	curCon.timedOut = false;				// forget about any old timeouts
	curCon.commitOnPrepare = commitOnPrepare;		// tell it when to do a commit
	if (!xaDataSource.xidConnections.containsKey(xid)) {
	    try {
		xaDataSource.processToWait();
	    } catch (Exception e) {
		throw new XAException ("Exception : "+e.toString());
	    }
	    synchronized (xaDataSource) {
		xaDataSource.xidConnections.put (xid, curCon);		// place on allocated list
	    }
	}
	curCon.setState (Status.STATUS_ACTIVE);			// set new connection state

    } // method doStart
    
    /**
     * Associates this XAConnection with a global transaction. This
     * is the only method which can associate the current connection
     * with a global transaction. It acts only on the current
     * connection which must have been previously established using
     * getConnection.
     */
    public synchronized void start(Xid xid, int flags) throws XAException {
	dataSource.log("StandardXAConnection:start associate the current connection with a global transaction");
	doStart(xid, flags);					// do most of the work
	curCon = null;						// no longer owned by this object
	con = null;						// ditto
    } // method start
    
    
    /**
     * Ends a connection's association with a global transaction. 
     * <P>
     * It need not act on the current transaction. There is an
     * interval between being returned to the pool manager and
     * being invoked by the transaction manager during which the
     * current connection can change.
     * <P>
     * Note that the only effect is to change the connection state.
     */
    public synchronized void end(Xid xid, int flags) throws XAException { //not tested XS
	dataSource.log("StandardXAConnection:end");
	dataSource.log("StandardXAConnection:end xid = " +xid);
	dataSource.log("StandardXAConnection:end flags = "+flags);

	if (xid == null) throw new XAException (XAException.XAER_INVAL);
	StandardXAStatefulConnection statecon = 
	    xaDataSource.getConnection (xid, true);	// must find connection for this transaction
	int state = statecon.getState();		// get current state of connection
	if (state != Status.STATUS_ACTIVE) 		// must have had start() called
	    throw new XAException (XAException.XAER_PROTO);
    } // method end

    /**
     * Does most of the work of a generic prepare. Kept as a
     * separate method so that sub-classes can call it and get
     * the StandardXAStatefulConnection back.
     */
    public StandardXAStatefulConnection checkPreparedState (Xid xid) throws XAException {
	dataSource.log("StandardXAConnection:checkPreparedState");
	if (xid == null) throw new XAException (XAException.XAER_INVAL);
	StandardXAStatefulConnection statecon = xaDataSource.getConnection (xid, true);// must find connection for this transaction

	try {
	    if (statecon.commitOnPrepare) {			// if early commit is required
		statecon.con.commit();				// perform the commit operation now
		statecon.setState(Status.STATUS_PREPARING);// heuristaclly committed
	    } else {
		statecon.setState(Status.STATUS_PREPARED);	// prepared
	    }
	} catch (SQLException e) {
	    dataSource.log("StandardXAConnection: Exception on prepare, rolling back");
	    statecon.setState(Status.STATUS_NO_TRANSACTION);	// release connection
	    throw new XAException (XAException.XA_RBROLLBACK);	// rollback will have been performed
	} // try-catch
	
	return statecon;
    } // method checkPreparedState
    
    /**
     * Prepares to perform a commit. May actually perform a commit
     * in the flag commitOnPrepare is set to true.
     */
    public int prepare(Xid xid) throws XAException {
	dataSource.log("StandardXAConnection:prepare prepare to perform a commit");
	checkPreparedState (xid);
	return XA_OK;
    } // method prepare
    
    /**
     * Performs a commit on this resource manager's branch of
     * the global transaction.
     */
    public synchronized void commit(Xid xid, boolean onePhase) throws XAException {
	dataSource.log("StandardXAConnection:commit perform a commit");
	if (xid == null) throw new XAException (XAException.XAER_INVAL);
	StandardXAStatefulConnection statecon = xaDataSource.getConnection (xid, true);// must find connection for this transaction
	dataSource.log("StandardXAConnection:commit case(state)");
	
	try {
	    switch (statecon.getState()) {				// action depends on current state
	    case Status.STATUS_PREPARING:				// already commited
		break;						// ...so do nothing
	    case Status.STATUS_PREPARED:				// ready to do commit
		try {
		    dataSource.log("StandardXAConnection:commit try to commit a connection (STATUS_PREPARED)");
		    statecon.con.commit();				// perform the commit operation now
		    dataSource.log("StandardXAConnection:commit commit is ok");
		} catch (SQLException e) {
		    throw new XAException (XAException.XA_RBROLLBACK);// rollback will have been performed
		} // try-catch
		break;
	    case Status.STATUS_COMMITTED:		// could be a 1-phase commit
	    case Status.STATUS_ACTIVE:
		if (!onePhase) {					// if not a one-phase commit
		    throw new XAException (XAException.XAER_PROTO);
		} // if
		try {
		    dataSource.log("StandardXAConnection:commit try to commit a connection (STATUS_ACTIVE)");
		    statecon.con.commit();				// perform the commit operation now
		    dataSource.log("StandardXAConnection:commit commit is ok");
		} catch (SQLException e) {
		    throw new XAException (XAException.XA_RBROLLBACK);// rollback will have been performed
		} // try-catch
		break;
	    default:
		{ 
		    dataSource.log("StandardXAConnection:UNKNOWN STATUS!:" + statecon.getState());
		    throw new XAException (XAException.XAER_PROTO);
		}
	    } // switch
	} catch (XAException e) {
	    throw e;
	} finally {
	    xaDataSource.freeConnection (xid, false);	
	}
    } // method commit
    
    /**
     * Performs a rollback on this resource manager's branch of
     * the global transaction.
     */
    public synchronized void rollback(Xid xid) throws XAException {
	dataSource.log("StandardXAConnection:rollback");
	if (xid == null) throw new XAException (XAException.XAER_INVAL);
	
	StandardXAStatefulConnection statecon = xaDataSource.getConnection (xid, true);// must find connection for this transaction
	
	try {
	    switch (statecon.getState()) {				// action depends on current state
	    case Status.STATUS_PREPARING:				// already commited
		throw new XAException (XAException.XA_HEURCOM);
	    case Status.STATUS_PREPARED:				// ready to do rollback
	    case Status.STATUS_ROLLING_BACK:
	    case Status.STATUS_ACTIVE:
		try {
		    dataSource.log("StandardXAConnection:rollback try to perform the rollback operation");
		    statecon.con.rollback();			// perform the rollback operation 
		    dataSource.log("StandardXAConnection:rollback performed the rollback");
		} catch (SQLException e) {
		    throw new XAException (XAException.XA_RBROLLBACK);// rollback will have been performed
		} // try-catch
		break;
	    default:
		throw new XAException (XAException.XAER_PROTO);
	    } // switch
	} catch (XAException e) {
	    throw e;
	} finally {
	    xaDataSource.freeConnection (xid, false);
	}
    } // method rollback
    
    
    public boolean isSameRM(XAResource xares) throws XAException {
	dataSource.log("StandardXAConnection:isSameRM isSameRM with XAResource");
	if (equals(xares)) {					// if the same object
	    return true;					// then definitely the same RM
	}
	if (!(xares instanceof StandardXAConnection)) {		// if it's not one of our wrappers
	    return false;					// then it's definitely not the same RM
	}
	StandardXAConnection xac = (StandardXAConnection)xares;	// cast to something more convenient
	if (dataSource.equals(xac.dataSource)) {		// if they originate from same data source
	    return true;				// then they're the same RM
	} else {
	    return false;
	}
    } // method isSameRM
    
    /**
     * This is called by a TM when the RM has reported a heuristic
     * completion. It must retain the transaction context until told
     * to forget about it.
     */
    public void forget(Xid xid) throws XAException {
	dataSource.log("StandardXAConnection:forget forget with Xid");
	if (xid == null) throw new XAException (XAException.XAER_INVAL);
	
	StandardXAStatefulConnection statecon = xaDataSource.getConnection (xid, true);// must find connection for this transaction
	xaDataSource.freeConnection (xid, false);			// finished with this transaction
    } // method forget
    
    /**
     * Called by the transaction manager during recovery. If it was the
     * transaction manager or another compoenent which failed then we
     * can supply our known Xids. However if we failed then this method
     * does nothing - we need to know about database internals to do that.
     */
    public Xid[] recover(int flag) throws XAException {
	dataSource.log("StandardXAConnection:recover recover flag="+flag);
	if (flag != TMSTARTRSCAN && flag != TMENDRSCAN && flag != TMNOFLAGS) {
	    throw new XAException (XAException.XAER_INVAL);
	}
	
	Xid[] retval=null;
	retval = xaDataSource.recover ();						// get all valid Xids
	return retval;
    } // method recover
    
    /**
     * Accessor methods for timeout.
     */
    public boolean setTransactionTimeout(int seconds) {
	timeoutSecs = seconds;
	return false;
    } // method setTransactionTimeout
    
    public int getTransactionTimeout() {
	return timeoutSecs;
    } // method getTransactionTimeout
    

    public void setCommitOnPrepare(boolean commitOnPrepare) {
	this.commitOnPrepare = commitOnPrepare;
    } // method setCommitOnPrepare

    public boolean getCommitOnPrepare() {
	return commitOnPrepare;
    } // method getCommitOnPrepare

    /**
     * Periodically checks for timed out connections.
     */
    public void run () {
	//dataSource.log("StandardXAConnection:run check for timed out connections");
	while (true) {					// loop forever
	    /*
	    if (nextTimeout == 0) {			// if there are no more timeouts scheduled
		timerThread.suspend();			// then go to sleep
		if (isClosed) return;						// exit if connection is closed
	    }
	    */
	    try {
		synchronized (this) {
		    while (nextTimeout == 0) {
			wait();
		    }
		}
	    } catch (InterruptedException e) {
	    }

	    if (isClosed) {
		return;
	    }
		
	    try {
		Thread.sleep (timeoutPeriod);		// sleep for a few seconds
		if (isClosed) return;						// exit if connection is closed
	    } catch (InterruptedException  e) {
		e.printStackTrace();			// we don't expect any of these
	    }
	    
	    long curTime = System.currentTimeMillis();	// get system time
	    if (curTime < nextTimeout) continue;	// check for time still to go
	    
	    // One or more transactions have timeout out.
	    try {
		nextTimeout = 
		    xaDataSource.checkTimeouts(curTime);	// check to see if connections have timeout out
	    } catch (Exception e) {
		e.printStackTrace();				// we don't expect any of these
	    }
	} // while
    } // method run
    

    public Reference getReference() throws NamingException {
	// Note that we use getClass().getName() to provide the factory
	// class name. It is assumed that this class, and all of its
	// descendants are their own factories.
	dataSource.log("StandardXAConnection:getReference return a reference of the object");
	Reference ref = new Reference(getClass().getName(), getClass().getName(), null);
	ref.add(new StringRefAddr("commitOnPrepare", String.valueOf(getCommitOnPrepare())));
	ref.add(new StringRefAddr("timeoutSecs", Integer.toString(getTransactionTimeout())));
	return ref;
    } // method getReference

} // class StandardXAConnection
