/*
 * 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_tm;

import java.util.*;
import java.rmi.RemoteException;
import java.sql.SQLException;
import javax.naming.Referenceable;
import javax.naming.Reference;
import javax.naming.NamingException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.InvalidTransactionException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.objectweb.jonas_timer.TimerManager;
// Current should not import ThreadData since it is needed in UserTransaction only
import org.objectweb.jonas.thds.ThreadData;
import org.objectweb.transaction.api.ResourceManagerEvent;
import org.objectweb.transaction.api.ResourceManagerEventListener;

/**
 * Current is the common Implementation for UserTransaction and TransactionManager.
 * UserTransaction is used by clients that want manage transactions themselves.
 * It is referenceable via JNDI
 * TransactionManager is used by an EJBServer.
 * This object is unique in a VM, i.e. each EJBServer has ONE Current object
 * and each client program should normally issue only ONE lookup on JNDI.
 * Current also implements Referenceable, because of JNDI.
 * @author Philippe Durieux
 * Contributor(s):
 * 01/11/06 Christophe Ney cney@batisseurs.com for Lutris Technologies
 *          Added ResourceManagerListener mechanism to remove ThreadData
 *          dependency.
 * 01/12/03 Dean Jennings - synchronizedMap for txXids
 */
public class Current implements UserTransaction, TransactionManager, Referenceable, ResourceManagerEventListener {

    // transaction associated to the thread
    // Thread specific data
    private static ThreadLocal threadTx = new ThreadLocal();

    // ------------------------------------------------------------------
    // Private data
    // ------------------------------------------------------------------

    // Static hashtable: Xid ---> transaction
    private static Map txXids = Collections.synchronizedMap(new HashMap());

    // Must be init at null, for clients that do not get UserTransaction
    private static Current unique = null;

    // true if CORBA environment
    private static boolean corba = false;

    // private info	
    private static TimerManager timermgr = null;	// local
    private static TransactionFactory tm = null; 	// local or remote

    private static final int DEFAULT_TIMEOUT = 60;
    private int transactionTimeout = DEFAULT_TIMEOUT;

    // management counter
    private int nb_bg_tx = 0;
    private int nb_rb_tx = 0;
    private int nb_cm_tx = 0;
    private int nb_to = 0;
    
    // ------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------

    /**
     * Default constructor.
     * A client does not need the TMFactory.
     */
    public Current() {

	TraceTm.debugJta("Current.Current()");
	unique = this;
    }
	
    /**
     * Constructor for EJBServer. The TM factory is passed as argument.
     * Note that the TM factory can be either local or remote.
     */
    public Current(TransactionFactory tmfact, boolean iscorba) {

	TraceTm.debugJta("Current.Current(TransactionFactory tmfact)");
	unique = this;
	tm = tmfact;
	corba = iscorba;
	timermgr = TimerManager.getInstance();
    }

    public static TransactionManager getTransactionManager() {
	return (TransactionManager)unique;
    }

    // ------------------------------------------------------------------
    // UserTransaction implementation
    // ------------------------------------------------------------------

    /**
     * Create a new transaction and associate it with the current thread.
     *
     * @exception NotSupportedException Thrown if the thread is already
     *    associated with a transaction. (nested transaction are not
     *    supported)
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public void begin() throws NotSupportedException, SystemException {

	TraceTm.debugJta("Current.begin()");

	// Checks that no transaction is already associated with this thread.
	TransactionImpl tx = (TransactionImpl) threadTx.get();
	if (tx != null) {
	    throw new NotSupportedException("Nested transactions not supported");
	}

	// Build a new Xid
	// - should pass servername + ip addr. (LATER)
	XidImpl otid = new XidImpl("", 0);

	// Creates a new TransactionImpl object
	// - May raise SystemException
	tx = new TransactionImpl(otid, transactionTimeout, corba);

	/// clear suspended transaction. 
	try {
	    tx.doAttach(XAResource.TMJOIN);
	} catch (RollbackException e) {
	    // never.
	    TraceTm.error("doAttach: RollbackException");
	    throw new SystemException("RollbackException in occured in begin() "+e.getMessage());
	} 
	
	/// associate transaction with current thread
	threadTx.set(tx);

	// Associates this Tx with the Xid
	putTxXid(otid, tx);

	// Set a timer for the transaction
	if (timermgr != null) {
	    tx.setTimer(timermgr.addTimer(tx, transactionTimeout, null, false));
	}

	/// enlist all connections using the event callback this should be done in UserTransaction
	// was done in doAttach
	List connectionList = ThreadData.getInstance().getConnectionList();
	for (int i = 0; (connectionList != null) && (i < connectionList.size()) ; i++) {
	    TraceTm.debugJta("Current.begin: enlist ResourceManagerEvent");
	    ((ResourceManagerEvent)connectionList.get(i)).enlistConnection(tx); 
	}
    }

    /**
     * Complete the transaction associated with the current thread. When this
     * method completes, the thread becomes associated with no transaction.
     *
     * @exception RollbackException Thrown to indicate that
     *    the transaction has been rolled back rather than committed.
     *
     * @exception HeuristicMixedException Thrown to indicate that a heuristic 
     *    decision was made and that some relevant updates have been committed
     *    while others have been rolled back.
     *
     * @exception HeuristicRollbackException Thrown to indicate that a 
     *    heuristic decision was made and that some relevant updates have been
     *    rolled back.
     *
     * @exception SecurityException Thrown to indicate that the thread is
     *    not allowed to commit the transaction.
     *
     * @exception IllegalStateException Thrown if the current thread is
     *    not associated with a transaction.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     */
    public void commit() throws RollbackException, 
	HeuristicMixedException, HeuristicRollbackException, SecurityException,
	IllegalStateException, SystemException {

	TraceTm.debugJta("Current.commit()");

	// Get Transaction
	TransactionImpl tx = (TransactionImpl) getTransaction();
	if (tx == null) {
	    throw new IllegalStateException("Cannot get Transaction for commit");
	}

	// Detach Transaction from current thread
	// Must be done now (XA protocol: XA-END before XA-COMMIT)
	// TMSUCCESS should be defined in Transaction.
	tx.doDetach(XAResource.TMSUCCESS);

	// Commit Transaction. Exceptions may be raised!
	try {
	    tx.commit();
	} finally {
	    // Dissociates the transaction from current thread
	    // Has not been done in doDetach because we need to
	    // be in the transactional context for beforeCompletion
	    threadTx.set(null);
	}
    }

    /**
     * Roll back the transaction associated with the current thread. When this
     * method completes, the thread becomes associated with no transaction.
     *
     * @exception SecurityException Thrown to indicate that the thread is
     *    not allowed to roll back the transaction.
     *
     * @exception IllegalStateException Thrown if the current thread is
     *    not associated with a transaction.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public void rollback() throws IllegalStateException, SecurityException, SystemException {

	TraceTm.debugJta("Current.rollback()");

	// Get Transaction
	TransactionImpl tx = (TransactionImpl) getTransaction();
	if (tx == null) {
	    throw new IllegalStateException("Cannot get Transaction for rollback");
	}

	// Dissociates this Transaction from current thread
	// Must be done at this level (see JTA spec. 3.3.3)
	// TMFAIL should be defined in Transaction.
	tx.doDetach(XAResource.TMFAIL);
	threadTx.set(null);

	// Roll back the transaction. Exceptions may be raised!
	tx.rollback();
    }

    /**
     * Modify the transaction associated with the current thread such that
     * the only possible outcome of the transaction is to roll back the
     * transaction.
     *
     * @exception IllegalStateException Thrown if the current thread is
     *    not associated with a transaction.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public void setRollbackOnly() throws IllegalStateException, SystemException {

	TraceTm.debugJta("Current.setRollbackOnly()");

	// Get Transaction
	TransactionImpl tx = (TransactionImpl) getTransaction();
	if (tx == null) {
	    throw new IllegalStateException("Cannot get Transaction for setRollbackOnly");
	}

	// Set transaction rollback only. Exceptions may be raised!
	tx.setRollbackOnly();
    }

    /**
     * Obtain the status of the transaction associated with the current thread.
     * 
     * @return The transaction status. If no transaction is associated with 
     *    the current thread, this method returns the Status.NoTransaction 
     *    value.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public int getStatus() throws SystemException {

	TraceTm.debugJta("Current.getStatus()");

	// Get Transaction
	TransactionImpl tx = (TransactionImpl) getTransaction();
	if (tx == null) {
	    return Status.STATUS_NO_TRANSACTION;
	}

	// Get TX status. Exceptions may be raised!
	return tx.getStatus();
    }

    /**
     * Modify the value of the timeout value that is associated with the
     * transactions started by the current thread with the begin method.
     *
     * <p> If an application has not called this method, the transaction
     * service uses some default value for the transaction timeout.
     *
     * @param seconds The value of the timeout in seconds. If the value
     *    is zero, the transaction service restores the default value.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public void setTransactionTimeout(int seconds) throws SystemException {

	TraceTm.debugJta("Current.setTransactionTimeout(int "+seconds+")");

	if (seconds > 0) {
	    transactionTimeout = seconds;
	} else {
	    transactionTimeout = DEFAULT_TIMEOUT;
	}
    }

    // ------------------------------------------------------------------
    // TransactionManager implementation
    // (only the methods that are not already in UserTransaction)
    // ------------------------------------------------------------------

    /**
     * Get the transaction object that represents the transaction 
     * context of the calling thread.
     *
     * @return the Transaction object representing the transaction 
     * associated with the calling thread. If the calling thread is
     * not associated with a transaction, a null object reference
     * is returned.
     *
     * @exception SystemException Thrown if the transaction manager 
     * encounters an unexpected error condition
     *
     */
    public Transaction getTransaction() throws SystemException {
	Transaction ret = (Transaction) threadTx.get();
	return ret;
    }

    /** 
     * Resume the transaction context association of the calling thread
     * with the transaction represented by the supplied Transaction object. 
     * When this method returns, the calling thread is associated with the 
     * transaction context specified.
     * Warning: No XA start is done here. We suppose it is already done
     * after a getConnection. The supposed programming model is: getConnection,
     * SQL, close.
     *
     * @param tobj The <code>Transaction</code> object that represents the 
     *    transaction to be resumed.
     *
     * @exception InvalidTransactionException Thrown if the parameter 
     *    transaction object contains an invalid transaction
     *
     * @exception IllegalStateException Thrown if the thread is already
     *    associated with another transaction.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     */
    public void resume(Transaction tobj) 
	throws InvalidTransactionException, IllegalStateException, SystemException {

	TraceTm.debugJta("Current.resume()");

	// invalid Transaction
	if (tobj == null) {
	    TraceTm.error("resume: null arg.");
	    throw new InvalidTransactionException("resume(null) is not valid");
	}

	// Check that the thread is not already associated to ANOTHER transaction
	Transaction mytx = (Transaction) threadTx.get();
	if (mytx != null) {
	    if (mytx.equals(tobj)) {
		TraceTm.debugJta("resume: nothing to do");
		return;
	    }
	    TraceTm.error("resume: already associated with another transaction.");
	    throw new IllegalStateException("the thread is already associated with another transaction.");
	}

	/// test for type before cast
	if (! (tobj instanceof TransactionImpl)) {
	    TraceTm.error("resume: non TransactionImpl arg.");
	    throw new InvalidTransactionException("resume("+tobj.getClass().getName()+") is not valid");
	}
	
	/// Associates this Tx with the current thread
	threadTx.set(tobj);
	/// attach suspended resources
	try {
	    ((TransactionImpl)tobj).doAttach(XAResource.TMRESUME);
	} catch (RollbackException e) {
	    // never.
	    TraceTm.error("doAttach: RollbackException");
	    throw new SystemException("RollbackException in occured in begin() "+e.getMessage());
	}     
    }
	
    /** 
     * Suspend the transaction currently associated with the calling 
     * thread and return a Transaction object that represents the 
     * transaction context being suspended. If the calling thread is 
     * not associated with a transaction, the method returns a null 
     * object reference. When this method returns, the calling thread 
     * is associated with no transaction. 
     * Warning: No XA end is done here. We suppose it is already done
     * after a close. The supposed programming model is: getConnection,
     * SQL, close.
     *
     * @return Transaction object representing the suspended transaction.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public Transaction suspend() throws SystemException {

	TraceTm.debugJta("Current.suspend()");
	
	/// TMSUSPEND should be defined in Transaction.
	TransactionImpl tx = (TransactionImpl) threadTx.get();
	if (tx != null) {
	    tx.doDetach(XAResource.TMSUSPEND);
	    threadTx.set(null);
	}
	return tx;
    }


    // ------------------------------------------------------------------
    // ResourceManagerEventListener implementation
    // Should be in UserTransaction only.
    // ------------------------------------------------------------------

    /**
     */
    public void connectionOpened(ResourceManagerEvent event) {
	TraceTm.debugJta("Current.connectionOpened");
	ThreadData.getInstance().connectionOpened(event);
    }

    /**
     */
    public void connectionClosed(ResourceManagerEvent event) {
	TraceTm.debugJta("Current.connectionClosed");
	ThreadData.getInstance().connectionClosed(event);
    }

    /**
     */
    public void connectionErrorOccured(ResourceManagerEvent event) {
	TraceTm.debugJta("Current.connectionErrorOccured");
	ThreadData.getInstance().connectionErrorOccured(event);
    }

    // ------------------------------------------------------------------
    // Referenceable implementation
    // ------------------------------------------------------------------

    /**
     * Retrieves the Reference of this object. 
     * 
     * @return  The non-null Reference of this object. 
     * @exception  NamingException  If a naming exception was encountered while retrieving the reference.  
     */
    public Reference getReference() throws NamingException {

	TraceTm.debugJta("Current.getReference()");

	// create the reference
	Reference ref = new Reference(this.getClass().getName(), 
				      "org.objectweb.jonas_tm.UserTransactionFactory", 
				      null);
	Integer i = new Integer(transactionTimeout);
	ref.add(new StringRefAddr("jonas.tm.timeout", i.toString()));
	ref.add(new StringRefAddr("jonas.orb.corba", corba ? "true" : "false"));
	return ref;
    }

    // ------------------------------------------------------------------
    // Other public methods
    // ------------------------------------------------------------------

    /**
     * Returns the unique instance of the class or null if not initialized
     * in case of plain client.
     *
     * @return The Current object created 
     */
    public static Current getCurrent() {
	return unique;
    }

    /**
     * Returns the TMFactory (in JTM)
     */
    public static TransactionFactory getJTM() {
	if (tm == null) {
	    TraceTm.error("Current: TMFactory is null!");
	}
	return tm;
    }

    /**
     * Sets the default timeout value
     */
    public void setDefaultTimeout(int timeout) {
	if (timeout != 0) {
	    transactionTimeout = timeout;
	}
    }

    /**
     * Gets the default timeout value
     */
    public int getDefaultTimeout() {
	    return transactionTimeout;
    }

    /**
     * Sets the corba value
     */
    public void setCorba(boolean iscorba) {
	corba = iscorba;
    }

    /**
     * Associate to the current thread a transaction represented by its
     * propagation context. This is used internally by the implicit
     * propagation of the transactionnal context:
     * - in the skeleton, before calling the request (isReply = false)
     * - in the stub, after receiving the reply (isReply = true)
     */
    public void setPropagationContext(TransactionContext pctx, boolean isReply) {

	if (pctx == null) {	// never isReply when pctx=null
	    TraceTm.debugJta("Current.setPropagationContext(null) -> detach");
	    
	    TransactionImpl tx = (TransactionImpl) threadTx.get();
	    if (tx != null) {
		// Free TransactionImpl, if not used only!!!
		// This is to avoid memory leaks.
		if (tx.isRemovable()) {
		    forgetTx(tx.getXid());
		}
		// Detaches thread from any transaction
		threadTx.set(null);
	    }
	    return;
	}

	// Get the Transaction matching this Xid if it exists
	Xid xid = pctx.getXid();
	TransactionImpl tx = getTxXid(xid);
	if (tx == null) {
	    if (!isReply) {
		// create a Transaction object
		TraceTm.debugJta("Current: setPropagationContext -> new Tx");
		tx = new TransactionImpl(pctx);
		putTxXid(xid, tx);
	    } else {
		// This may happen with Jeremie and distributed transactions with
		// no dammage.
		TraceTm.debugJta("Current.setPropagationContext: unknown tx:"+xid);
	    }
	} else {
	    if (isReply) {
		// This thread is already associated to a transaction.
		// A proper PropagationContext exists yet, but it may have been changed
		// in case of distributed transaction.
		TraceTm.debugJta("Current.setPropagationContext: updating Xid="+xid);
		tx.updatePropagationContext(pctx);
	    } else {
		TraceTm.debugJta("Current.setPropagationContext: transaction already known:"+xid);
	    }
	}

	// Associates this Tx with the current thread (= resume)
	// In case of Jeremy, if isReply, we are not in the correct thread, so
	// never touch threadTx in that case!!!
	if (!isReply) {
	    threadTx.set(tx);
	}
    }

    /**
     * Get the propagation context associated with the current thread
     * or null if the thread is not involved in a transaction.
     */
    public TransactionContext getPropagationContext(boolean hold) {
	try {
	    TransactionImpl tx = (TransactionImpl) getTransaction();
	    if (tx != null) {
		// will return null if Tx not valid.
		return tx.getPropagationContext(hold);
	    }
	} catch (SystemException e) {
	    TraceTm.error("getPropagationContext system exception:", e);
	}
	return null;
    }
	
    /**
     * Forget all about this transaction. We must destroy references to
     * TransactionImpl object to allow the garbage collector to free
     * memory allocated to this transaction.
     */
    public void forgetTx(Xid xid) {
	removeTxXid(xid);
    }

    // ------------------------------------------------------------------
    // private methods
    // ------------------------------------------------------------------
	
    /*
     * putTxXid
     */
    private void putTxXid(Xid xid, TransactionImpl tx) {
	TraceTm.debugJta("Associate Tx to Xid:"+xid);
	txXids.put(xid, tx);
    }

    /*
     * getTxXid
     */
    private TransactionImpl getTxXid(Xid xid) {
	TransactionImpl tx = (TransactionImpl) txXids.get(xid);
	return tx;
    }

    /*
     * removeTxXid
     */
    private void removeTxXid(Xid xid) {
	TraceTm.debugJta("Remove Tx from Xid:"+xid);
	txXids.remove(xid);
    }

    /**
     * MBean method:
     * @return the current number of transaction
     */
    public int getCurrentNumberOfTx() { 
	return 	txXids.size();
    }


    /**
     * management method:
     * increment the number of beginned tx
     */
    public synchronized void incrementBeginCounter() {
	nb_bg_tx++;
    }

    /**
     * MBean method:
     * @return the Total number of transaction
     */
    public int getTotalNumberOfBeginTx() {
	return nb_bg_tx;
    }
    
    /**
     * management method:
     * increment the number of rollbacked tx
     */
    public synchronized void incrementRollbackCounter() {
	nb_rb_tx++;
    }

    /**
     * MBean method:
     * @return the current number of rollbacked transaction
     */
    public int getTotalNumberOfRollbackTx() {
	return nb_rb_tx;
    }

    /**
     * management method:
     * increment the number of beginned tx
     */
    public synchronized void incrementCommitCounter() {
	nb_cm_tx++;
    }

    /**
     * MBean method:
     * @return the current number of commited transaction
     */
    public int getTotalNumberOfCommitTx() {
	return nb_cm_tx;
    }

    /**
     * MBean method:
     * reset total number of tx
     */
    public synchronized void resetAllTxTotalCounters() {
	nb_bg_tx = 0;
	nb_cm_tx = 0;
	nb_rb_tx = 0;
	nb_to = 0;	
    }


    /**
     * management method:
     * increment the number of rollbacked by time out
     */
    public synchronized void incrementTimeOutExpCounter() {
	nb_to++;
    }

    /**
     * management method:
     * @return the number of rollbacked tx by time out
     */
    public int getTotalNumberOfTimeOutExpired() {
	return nb_to;
    }


    /**
     * management method:
     * @return all counter
     */
    public synchronized Integer [] getTxCounters() {
	Integer[] result = new Integer[5];
	result[0] = new Integer(txXids.size());   
	result[1] = new Integer(nb_bg_tx); 
	result[2] = new Integer(nb_cm_tx);
	result[3] = new Integer(nb_rb_tx);
	result[4] = new Integer(nb_to);
	return result;
    }

}

