
/*
 * 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.rmi.RemoteException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.transaction.*;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.objectweb.jonas_timer.TimerEvent;
import org.objectweb.jonas_timer.TimerEventListener;
//import org.objectweb.common.Env;

/**
 * TransactionImpl is the implementation of the Transaction interface,
 * defined in JTA specifications. This object is intended to be used
 * by the EJBServer for transaction management. It is used indirectly
 * by the UserTransaction implementation too, i.e. the Current object.
 * The goal is to use the JTA interface to hide the JTM interface to
 * the caller (EJBServer, Bean or Client).
 * @author Philippe Durieux
 * Contributor(s):
 * 01/11/06 Christophe Ney cney@batisseurs.com for Lutris Technologies
 *          Added ResourceManagerListener mechanism to remove ThreadData
 *          dependency.
 * 02/01/15 Dean Jennings - List  instead of Vector for enlistedXARes delistedXARes
 */
public class TransactionImpl implements Transaction, TimerEventListener {

    // ------------------------------------------------------------------
    // Private data
    // ------------------------------------------------------------------
    private SubCoordinator subcoord = null;
    private TransactionContext myCtx = null;
    private Xid myXid = null;
    private boolean interpose = false;
    private int ucount = 0;	// count of users (0 => can be freed)
    private TimerEvent timer = null; // keep this to unvalidate timer
    private RecoveryCoordinator recoveryCoord = null;
    /// store enlisted resources
    private List enlistedXARes = Collections.synchronizedList(new ArrayList());
    /// store suspended resources
    private List delistedXARes = null;

    // ------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------

    /**
     * New transaction (begin).
     *
     * @param timeout The value of the timeout in seconds.
     *
     */
    public TransactionImpl(Xid xid, int timeout, boolean corba) throws SystemException {
	TraceTm.debugJta("TransactionImpl.TransactionImpl(Xid xid, int timeout)");

	// Build a propagation context local (no ref to JTM yet)
	myXid = xid;
	if (corba) {
	    myCtx = new TransactionContextCORBA(timeout, null, xid);
	} else {
	    myCtx = new TransactionContextRMI(timeout, null, xid);
	}
    }

    /**
     * New Transaction for this thread (setPropagationContext)
     *
     * @param pctx propagation context
     *
     */
    public TransactionImpl(TransactionContext pctx) {
	if (pctx == null) {
	    TraceTm.error("TransactionImpl: null PropagationContext");
	}
	myCtx = pctx;
	myXid = pctx.getXid();
	// interposition will be done later, only if necessary.
	interpose = true;
    }

    // ------------------------------------------------------------------
    // Transaction implementation
    // ------------------------------------------------------------------

    /**
     * Complete the transaction represented by this Transaction object
     * The calling thread is not required to have the same transaction
     * associated with the thread. (JTA 3.3.3)
     *
     * @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, SystemException {

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

	// *** Distributed transaction.
	Terminator term = myCtx.getTerminator();
	if (term != null) {
	    // Commits the Transaction, with heuristic report
	    try {
		term.commit(true);
	    } catch (java.rmi.ServerException e) {
		if (e.detail instanceof TransactionRolledbackException) {
		    Current.getCurrent().forgetTx(getXid());
		    TraceTm.debugJta("Commit distributed transaction -> rolled back!");
		    throw new RollbackException();
		}
		if (e.detail instanceof HeuristicMixed) {
		    TraceTm.verbose("Commit distributed transaction -> Heuristic mixed!");
		    throw new HeuristicMixedException();
		}
	    } catch (TransactionRolledbackException e) {
		Current.getCurrent().forgetTx(getXid());
		TraceTm.debugJta("Commit distributed transaction -> rolled back!");
		throw new RollbackException();
	    } catch (HeuristicMixed e) {
		TraceTm.verbose("Commit distributed transaction -> Heuristic mixed!");
		throw new HeuristicMixedException();
	    } catch (Exception e) {
		TraceTm.error("Unexpected Exception on commit:", e);
		throw new SystemException("Unexpected Exception on commit");
	    }
	    if (subcoord == null) {
		// if no coordinator, timer will not be unset by JTM.
		unsetTimer();
	    }
	    Current.getCurrent().forgetTx(getXid());
	    return;
	}

	// *** Local transaction
	// commit_one_phase may raise remote exceptions. We must rethrow local exceptions.
	if (subcoord != null) {
	    try {
		subcoord.commit_one_phase();
	    } catch (TransactionRolledbackException e) {
		TraceTm.debugJta("Commit local transaction -> rolled back!");
		throw new RollbackException();
	    } catch (RemoteException e) {
		TraceTm.error("Unexpected Exception on commit_one_phase:", e);
		throw new SystemException("Unexpected Exception on commit_one_phase");
	    }
	} else {
	    // if no coordinator, just unset the timer and release this object.
	    unsetTimer();
	    Current.getCurrent().forgetTx(getXid());
	}
    }
    
    /**
     * Delist the resource specified from the current transaction 
     * associated with the calling thread.
     *
     * @param xaRes The XAResource object representing the resource to delist
     *
     * @param flag One of the values of TMSUCCESS, TMSUSPEND, or TMFAIL.
     *
     * @exception IllegalStateException Thrown if the transaction in the 
     *    target object is inactive.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     * @return true if the dissociation of the Resource is successful.
     */
    public boolean delistResource(XAResource xares, int flag)
        throws IllegalStateException, SystemException {

	TraceTm.debugJta("TransactionImpl.delistResource()");

	// Send the XA end to the XAResource
	try {
	    xares.end(getXid(), flag);
	} catch (XAException e) {
		throw new SystemException("Cannot send XA end:"+e);
	}

	/// remove from enlisted list
	enlistedXARes.remove(xares);
	return true;
    }
    
    /**
     * Enlist the resource specified with the current transaction 
     * context of the calling thread
     *
     * @param xaRes The XAResource object representing the resource to delist
     *
     * @return <i>true</i> if the resource was enlisted successfully; otherwise 
     *    false.
     *
     * @exception RollbackException Thrown to indicate that
     *    the transaction has been marked for rollback only.
     *
     * @exception IllegalStateException Thrown if the transaction in the 
     *    target object is in prepared state or the transaction is inactive.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public boolean enlistResource(XAResource xares)
	throws RollbackException, IllegalStateException, SystemException {

	TraceTm.debugJta("TransactionImpl.enlistResource(XAResource xares)");

	// Check trivial cases
	if (xares == null) {
	    TraceTm.error("enlistResource: null argument");
	    throw new SystemException("enlistResource: null argument");
	}
	if (myCtx == null) {
	    throw new SystemException("enlistResource: no Transactional Context");
	}

	// make a subCoordinator object if not existing yet
	if (subcoord == null) {
	    makeSubCoord();
	    if (subcoord == null) {
		TraceTm.error("enlistResource: could not create subcoordinator");
		throw new SystemException("enlistResource: could not create subcoordinator");
	    }
	}

	// Add the Resource to the list
	// may raise exceptions
	boolean found = false;
	boolean rollbackOnly = false;
	try {
	    found = subcoord.addResource(xares);
	} catch (RollbackException e) {
	    rollbackOnly = true;
	}

	// Send the XA start to the XAResource
	// A new Xid branch should be generated in case of new RM (if !found)
	//    See JTA Specifications, page 12/13.
	int flag = found ? XAResource.TMJOIN : XAResource.TMNOFLAGS;
	if ((delistedXARes != null) && delistedXARes.contains(xares)) {
	    flag =  XAResource.TMRESUME;
	}
	try {
	    xares.start(getXid(), flag);
	} catch (XAException e) {
		throw new SystemException("Cannot send XA start:"+e);
	}
	if (rollbackOnly) {
	    throw new RollbackException();
	}

	/// add to enlisted list
	enlistedXARes.add(xares);
	return true;
    }

    /// delist  all enlisted resources and move to suspended
    public void doDetach(int status) throws SystemException {
	TraceTm.debugJta("doDetach - enlisted="+enlistedXARes.size());
        // always copy enlisted to suspended resource list
        // since jonas may resume the transaction in beforecompletion
        delistedXARes = new ArrayList(enlistedXARes);
        for (int i = 0; i < delistedXARes.size() ;i++) {
            delistResource((XAResource)delistedXARes.get(i), status); 
        }
    }
    
    /// enlist/clear all suspended resource
    public void doAttach(int status) throws SystemException, RollbackException {
	TraceTm.debugJta("doAttach - enlisted="+enlistedXARes.size());
        // we attach suspended transactions
        if (status == XAResource.TMRESUME) {
            // we may be calling resume from beforecompletion on transaction that are not suspended!
            for (int i = 0; (delistedXARes != null) && (i < delistedXARes.size()) ; i++) {
                enlistResource((XAResource)delistedXARes.get(i)); 
            }
        }
        delistedXARes = null;
    }
    
    /// get a copy of the list of currently enlisted resource
    public List getEnlistedXAResource() {
	TraceTm.debugJta("TransactionImpl.getEnlistedXAResource - size="+enlistedXARes.size());
        return new ArrayList(enlistedXARes);
    }
    
    /**
     * 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("TransactionImpl.getStatus()");

	// *** Distributed transaction
	Coordinator coord = myCtx.getCoordinator();
	if (coord != null) {
	    // Ask the transaction status to JTM 
	    int ret;
	    try {
		ret = coord.get_status();
	    } catch (Exception e) {
		TraceTm.error("TransactionImpl.getStatus: cannot reach JTM:", e);
		return Status.STATUS_NO_TRANSACTION;
	    }
	    return ret;
	}

	// *** Local transaction
	// The status is kept in the subcoordinator
	if (subcoord == null) {
	    // The transaction has just been started
	    // No resource or synchro are registered yet.
	    return Status.STATUS_ACTIVE;
	}
	return subcoord.getStatus();
    }

    /** 
     * Register a synchronization object for the transaction currently
     * associated with the calling thread. The transction manager invokes
     * the beforeCompletion method prior to starting the transaction 
     * commit process. After the transaction is completed, the transaction
     * manager invokes the afterCompletion method.
     *
     * @param sync The javax.transaction.Synchronization object for the 
     *    transaction associated with the target object
     *
     * @exception RollbackException Thrown to indicate that
     *    the transaction has been marked for rollback only.
     *
     * @exception IllegalStateException Thrown if the transaction in the 
     *    target object is in prepared state or the transaction is inactive.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     */
    public void registerSynchronization(Synchronization sync)
	throws RollbackException, IllegalStateException, SystemException {

	TraceTm.debugJta("TransactionImpl.registerSynchronization(Synchronization sync)");
	    
	// It's time to make the subcoordinator, if not existing yet.
	if (subcoord == null) {
	    makeSubCoord();
	}

	// Add Synchronization to the list.
	// may raise exceptions
	subcoord.addSynchronization(sync);
    }

    /**
     * Rollback the transaction represented by this Transaction object.
     *
     * @exception IllegalStateException Thrown if the transaction in the 
     *    target object is in prepared state or the transaction is inactive.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public void rollback() throws IllegalStateException, SystemException {

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

	// *** Distributed transaction.
	Terminator term = myCtx.getTerminator();
	if (term != null) {
	    // Rollback the Transaction
	    try {
		term.rollback();
	    } catch (java.rmi.ServerException e) {
		// HeuristicCommit ????
		throw new IllegalStateException("Exception on rollback:"+e.detail);
	    } catch (Exception e) {
		throw new SystemException("Unexpected Exception on rollback");
	    } 
	    if (subcoord == null) {
		// if no coordinator, timer will not be unset by JTM.
		unsetTimer();
	    }
	    Current.getCurrent().forgetTx(getXid());
	    return;
	}

	// *** Local transaction.
	// if no coordinator, nothing to do.
	if (subcoord != null) {
	    try {
		subcoord.rollback();
	    } catch (RemoteException e) {
		throw new IllegalStateException("Exception on rollback:"+e);
	    }
	} else {
	    // if no coordinator, just unset the timer.
	    unsetTimer();
	}
    }
	
    /**
     * 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 any transaction.
     *
     * @exception SystemException Thrown if the transaction manager 
     *    encounters an unexpected error condition
     *
     */
    public void setRollbackOnly() throws IllegalStateException, SystemException {

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

	// make a subCoordinator object if not existing yet
	if (subcoord == null) {
	    makeSubCoord();
	}

	// status is kept in subcoordinator
	subcoord.setRollbackOnly();
    }

    // ------------------------------------------------------------------
    // TimerEventListener implementation
    // ------------------------------------------------------------------

    /**
     * timeout for that transaction has expired
     */
    public void timeoutExpired(Object arg) {

	// increment counter for management
	Current.getCurrent().incrementTimeOutExpCounter();

	// make the subcoordinator object, if not existing yet.
	if (subcoord == null) {
	    // if this is a proxy, just forget this object. The JTM will
	    // rollback transaction with its own timer.
	    Terminator term = myCtx.getTerminator();
	    if (term != null) {
		TraceTm.verbose("TransactionImpl.timeoutExpired: forget tx");
		Current.getCurrent().forgetTx(getXid());
		return;
	    }
	    makeSubCoord();
	}

	// Try first to set it "rollback only"
	// avoids a rollback while SQL requests are in progress
	TraceTm.verbose("TransactionImpl.timeoutExpired: set rollback only "+this);
	try {
	    subcoord.setRollbackOnly();
	} catch (Exception e) {
	    TraceTm.error("TransactionImpl.timeoutExpired: cannot rollbackonly:"+e);
	    return;
	}

	// In jonas2.4 = wait 2 sec and if transaction still alive, send a rollback.
	// In Jonas 2.5, this has been removed.

	/*
	// Subtransaction: never rollback directly (done by main coordinator)
	if (interpose) {
	    return;
	}

	// Sometimes, there is a deadlock. In that cases, we must issue a "rollback"
	// to unblock threads (at afterCompletion())
	try {
	    Thread.currentThread().sleep(2000);	// sleep 2 sec.
	    switch (subcoord.getStatus()) {
	    case Status.STATUS_MARKED_ROLLBACK:
		TraceTm.verbose("TransactionImpl.timeoutExpired: Probable deadlock -> rollback");
		subcoord.rollback();
		break;
	    case Status.STATUS_ROLLEDBACK:
		TraceTm.verbose("TransactionImpl.timeoutExpired: TX normally rolled back");
		break;
	    default:
		TraceTm.verbose("TransactionImpl.timeoutExpired: Bad status:"+subcoord.stringStatus());
		break;
	    }
	} catch (Exception e) {
	    TraceTm.error("TransactionImpl.timeoutExpired: "+e);
	}
	*/
    }

    // ------------------------------------------------------------------
    // This object is used as an HashTable index
    // ------------------------------------------------------------------

    /**
     * return true if objects are identical
     */
    public boolean equals(Object obj2) {
	TransactionImpl tx2 = (TransactionImpl) obj2;

	// trivial cases
	if (tx2 == this) {
	    return true;
	}
	if (tx2 == null) {
	    TraceTm.verbose("Null pointer in Transaction.equals");
	    return false;
	}

	// compare otids
	return getXid().equals(((TransactionImpl)tx2).getXid());
    }

    /**
     * return a hashcode value for this object
     */
    public int hashCode() {
	return getXid().hashCode();
    }

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

    /**
     * string form
     */
    public String toString() {
	return getXid().toString();
    }
	
    /**
     * Return associated PropagationContext
     * Used for implicit Context propagation.
     *
     * @return PropagationContext associated with the transaction.
     */
    public synchronized TransactionContext getPropagationContext(boolean hold) {
	if (hold) {
	    ucount++;
	}
	return myCtx;
    }

    /**
     * set a timer for the transaction
     */
    public void setTimer(TimerEvent timer) {
	TraceTm.debugJta("TransactionImpl.setTimer(TimerEvent timer)");
	this.timer = timer;
    }

    /**
     * unset the timer
     */
    public void unsetTimer() {
	TraceTm.debugJta("TransactionImpl.unsetTimer()");
	if (timer != null) {
	    timer.unset();
	    timer = null;
	}
    }

    /**
     * update the propagation context
     * We should be inside the reply of a request involved in a tx here!
     */
    public synchronized void updatePropagationContext(TransactionContext pctx) {

	TraceTm.debugJta("TransactionImpl.updatePropagationContext");

	Coordinator remoteCoord = pctx.getCoordinator();
	if (remoteCoord == null && myCtx.getCoordinator() != null) {
	    TraceTm.error("setPropagationContext: Bad Coordinator");
	    return;
	}

	// release count of users
	ucount--;

	// Interpose subCoordinator if newly distributed Tx
	if (remoteCoord != null && myCtx.getCoordinator() == null) {
	    //PropagationContextUtil.copyCoordinator(pctx, myCtx);
	    myCtx.setCoordinator(pctx.getCoordinator());
	    if (subcoord != null) {
		// register the subCoordinator as a Resource.
		try {
		    recoveryCoord = remoteCoord.register_resource(subcoord);
		} catch (RemoteException e) {
		    TraceTm.error("Cannot make interposition:", e);
		    return;
		}
	    }
	}
    }

    /**
     * Get the Xid of the transaction
     */
    public Xid getXid() {
	return myXid;
    }

    /**
     * make a SubCoordinator for this Transaction object
     */
    private void makeSubCoord() {

	TraceTm.debugJta("TransactionImpl.makeSubCoord()");

	// Build the SubCoordinator object
	try {
	    subcoord = new SubCoordinator(this, getXid());
	} catch (RemoteException e) {
	    // should never go here.
	    TraceTm.error("new SubCoordinator raised exception: ", e);
	    return;
	}

	// If interposition must be done: do it now!
	// Each time we have a remoteCoord + a subCoord, we must interpose.
	Coordinator remoteCoord = myCtx.getCoordinator();

	// First of all, create the Control object on JTM
	// if it was not created before, if interpose flag is set.
	if (interpose && remoteCoord == null) {
	    try {
		// Note that the JTM will build another Xid
		// This must be clarified XXX
		TraceTm.debugJta("Creating a remote Control on JTM for a distributed transaction");
		
		/* This code dos not work today. Since it introduce a dependency on common,
		 * we don't keep it.
		if (Env.isDavid())
		    // import the CORBA client initiated transaction if it exists
		    remoteCoord = (Coordinator) javax.rmi.PortableRemoteObject.narrow(Current.getJTM().recreate(myCtx), Coordinator.class);
		else
		*/
		    remoteCoord = (Coordinator) javax.rmi.PortableRemoteObject.narrow(Current.getJTM().create(myCtx.getTimeout()), Coordinator.class);
	    
	    } catch (RemoteException e) {
		TraceTm.error("Cannot create distributed transaction:", e);
		return;
	    }
		
	    myCtx.setCoordinator(remoteCoord);
	}
	
	// Achieve interposition if not already done:
	// - register the subCoordinator as a Resource.
	if (remoteCoord != null && recoveryCoord == null) {
	    try {
		recoveryCoord = remoteCoord.register_resource(subcoord);
	    } catch (RemoteException e) {
		TraceTm.error("Cannot make interposition:", e);
		return;
	    }
	}
    }

    /**
     * return true if object is no more used (= removable)
     */
    public boolean isRemovable() {
	return (ucount == 0 && subcoord == null);
    }
	    
}
