/*
 * 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.util.Vector;
import javax.transaction.*;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.objectweb.common.RemoteObject;

/**
 * Log associated to this transaction coordinator
 * @author Philippe Durieux
 * Contributor(s):
 * 01/11/06 Christophe Ney cney@batisseurs.com for Lutris Technologies
 *          Added ResourceManagerListener mechanism to remove ThreadData
 *          dependency.
 */
class SLog {

    Vector resourceLogged = new Vector();
    int decision;
    static final int DECISION_TO_COMMIT = 1;
    static final int DECISION_TO_ROLLBACK = 2;

    public void addResource(XAResource res) {
	resourceLogged.addElement(res);
    }

    public void flushLog(int decide) {
	decision = decide;

	// serialize log on disk
	// XXX
    }    

    public void forgetLog() {

	// remove file on disk
	// XXX
    }
}

/**
 * This object is the local coordinator. It may be registered as
 * sub-coordinator in case of distributed transaction, so it must
 * be callable remotely and implement Resource
 */
public class SubCoordinator extends RemoteObject implements Resource {

    // ------------------------------------------------------------------
    // Object state
    // ------------------------------------------------------------------

    /**
     * @serial
     */
    private TransactionImpl tx = null;

    /**
     * List of Synchronization objects
     * @serial
     */
    private Vector synchroList = new Vector();

    /**
     * List of XAResource objects
     * @serial
     */
    private Vector resourceList = new Vector();

    /**
     * Keep a reference on TransactionManager
     * @serial
     */
    private TransactionManager tm;

    /**
     * @serial
     */
    private Xid xid = null;

    /**
     * @serial
     */
    private SLog log = null;

    /**
     * javax.transaction.Status
     * possible values for status:
     * ---------------- : ----------------------------------------------
     * ACTIVE           : transaction started, commit phase not started.
     * PREPARING        : Prepare is being sent to resources.
     * PREPARED         : Prepare is done. Must commit now.
     * COMMITTING       : Commit is being sent to resources.
     * COMMITTED        : Commit was successful.
     * ROLLING_BACK     : Not used.
     * MARKED_ROLLBACK  : setRollbackOnly has been called.
     * ROLLEDBACK       : Transaction has been rolled back 
     *                    (or prepare returned "vote_rollback")
     * UNKNOWN          : Commit raised heuristics.
     * NO_TRANSACTION   : Cannot get the status value from JTM.
     * ---------------- : ----------------------------------------------
     * @serial
     */
    private int status = Status.STATUS_ACTIVE;

    /**
     * @serial
     */
    private boolean beforeCompletionDone = false;

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

    /**
     * constructor used by TransactionImpl
     */
    SubCoordinator(TransactionImpl tx, Xid xid) throws RemoteException {
	TraceTm.debugJta("SubCoordinator.SubCoordinator(TransactionImpl tx, Xid xid)");
	this.tx = tx;
	this.xid = xid;
	this.tm=Current.getTransactionManager();

	// increment counter for management
	Current.getCurrent().incrementBeginCounter();
    }

    // ------------------------------------------------------------------
    // Resource interface
    // ------------------------------------------------------------------

    /** 
     * phase 1 of the 2PC.
     *
     * @return int vote commit, rollback, or readonly.
     */
    public int prepare() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.prepare()");


	switch (status) {
	case Status.STATUS_MARKED_ROLLBACK:
	    doBeforeCompletion(false);
	    doRollback();
	    return Resource.VOTE_ROLLBACK;
	case Status.STATUS_COMMITTED:
	    return Resource.VOTE_COMMIT;
	default:
	    doBeforeCompletion(true);
	    break;
	}

	// Recheck Status after doBeforeCompletion
	if (status == Status.STATUS_MARKED_ROLLBACK) {
	    TraceTm.verbose("Rollback during beforeCompletion in SubCoordinator.prepare");
	    doRollback();
	    return Resource.VOTE_ROLLBACK;
	}

	int ret = doPrepare();
	if (ret == Resource.VOTE_READONLY) {
	    // Transaction completed for this Resource
	    doAfterCompletion();
	}
	return ret;
    }

    /** 
     * rollback transaction
     */
    public void rollback() throws RemoteException {

	switch (status) {
	case Status.STATUS_ACTIVE:
	case Status.STATUS_MARKED_ROLLBACK:
	    break;
	case Status.STATUS_ROLLEDBACK:
	    TraceTm.debugJta("rollback: already done.");
	    return;
	default:
	    TraceTm.error("rollback: bad status: "+stringStatus());
	    return;
	}

	TraceTm.debugJta("SubCoordinator.rollback");

	doBeforeCompletion(false);
	doRollback();
    }

    /** 
     * phase 2 of the 2PC.
     */
    public void commit() throws RemoteException {

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

	switch (status) {
	case Status.STATUS_PREPARED:
	    break;
	default:
	    TraceTm.error("commit: bad status: "+stringStatus());
	    return;
	}

	doCommit();
    }


    /** 
     * commit 1 phase. Called either from JTM (distributed transaction) or
     * from Transaction.commit (local transaction).
     */
    public void commit_one_phase() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.commit_one_phase()");
	

	switch (status) {
	case Status.STATUS_ROLLEDBACK:
	    throw new TransactionRolledbackException();
	case Status.STATUS_MARKED_ROLLBACK:
	    doBeforeCompletion(false);
	    doRollback();
	    throw new TransactionRolledbackException();
	case Status.STATUS_COMMITTED:
	    return;
	default:
	    doBeforeCompletion(true);
	    break;
	}

	// Recheck Status after doBeforeCompletion
	if (status == Status.STATUS_MARKED_ROLLBACK) {
	    TraceTm.verbose("Rollback during beforeCompletion in SubCoordinator.commit_one_phase");
	    doRollback();
	    throw new TransactionRolledbackException();
	}

	// only 1 Resource => 1 phase commit
	if (resourceList.size() == 1) {
	    doOnePhaseCommit();
	    return;
	}

	// 2 phase commit
	int vote = doPrepare();

	switch (vote) {
	case Resource.VOTE_COMMIT:
	    doCommit();
	    break;
	case Resource.VOTE_READONLY:
	    doAfterCompletion();
	    break;
	case Resource.VOTE_ROLLBACK:
	    doRollback();
	    throw new TransactionRolledbackException();
	}
    }

    /** 
     * forget heuristics about this transaction.
     */
    public void forget() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.forget()");
	doForget();
    }

    // ------------------------------------------------------------------
    // Other public methods (interface exposed to Transaction object)
    // ------------------------------------------------------------------

    /**
     * add a Synchronization to the list
     *
     * @param synchro 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.
     *
     */
    public void addSynchronization(Synchronization synchro) 
	throws RollbackException, IllegalStateException {

	TraceTm.debugJta("SubCoordinator.addSynchronization");

	// check status: should be ACTIVE.
	boolean markedRollback = false;
	switch(status) {
	case Status.STATUS_MARKED_ROLLBACK:
	    markedRollback = true;
	    break;
	case Status.STATUS_ACTIVE:
	    break;
	default:
	    TraceTm.error("SubCoordinator.addSynchronization: bad status = "+stringStatus());
	    throw new IllegalStateException("addSynchronization: Bad status");
	}

	// Add synchro to the list of local synchros
	synchroList.addElement(synchro);

	// If transaction marked rollback only, we add synchro but we throw
	// the correct exception because nobody can presume what will be done in the
	// Synchronization object, so we must send it beforeCompletion and afterCompletion
	// even in case of rollback.
	if (markedRollback) {
	    TraceTm.debugJta("SubCoordinator.addSynchronization: transaction rollback only");
	    throw new RollbackException();
	}
    }

    /**
     * add a XAResource to the list
     *
     * @param the XAResource to register
     *
     * @return true if this datasource was already known
     *
     * @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.
     */
    public boolean addResource(XAResource xares) 
	throws RollbackException, IllegalStateException {

	TraceTm.debugJta("SubCoordinator.addResource");

	// check status: should be ACTIVE.
	boolean markedRollback = false;
	switch(status) {
	case Status.STATUS_MARKED_ROLLBACK:
	    markedRollback = true;
	    break;
	case Status.STATUS_ACTIVE:
	    break;
	default:
	    TraceTm.error("SubCoordinator.addResource: bad status="+stringStatus());
	    throw new IllegalStateException("SubCoordinator.addResource: Bad status");
	}

	// Check if this DataSource is already known
	// -> we must register only ONE resource per DataSource.
	boolean found = false;
	for (int i = 0; i < resourceList.size(); i++) {
	    XAResource res = (XAResource) resourceList.elementAt(i);
	    try {
		if (res.isSameRM(xares)) {
		    found = true;
		    break;
		}
	    } catch (XAException e) {
		TraceTm.error("Exception on isSameRM:", e);
	    }
	}

	// add this XAResource to the list
	if (found == false) {
	    TraceTm.debugJta("SubCoordinator.addResource: new XAResource");
	    resourceList.addElement(xares);
	}

	// If transaction marked rollback only, we enlist resource but we throw
	// the correct exception. It is important to enlist the Resource because
	// if we don't, the database would be updated although transaction has been
	// rolled back.
	if (markedRollback) {
	    TraceTm.debugJta("SubCoordinator.addResource: transaction rollback only");
	    throw new RollbackException();
	}
	    
	return found;
    }

    /**
     * return the status of this transaction
     */
    public int getStatus() {
	if (TraceTm.isDebugJta()) 
	    TraceTm.debugJta("SubCoordinator.getStatus:"+ stringStatus());
	return status;
    }

    /**
     * set the transaction "rollback only"
     */
    public void setRollbackOnly() {
	TraceTm.debugJta("SubCoordinator.setRollbackOnly()");
	switch (status) {
	case Status.STATUS_ACTIVE:
	case Status.STATUS_UNKNOWN:
	case Status.STATUS_PREPARING:
	    status = Status.STATUS_MARKED_ROLLBACK;
	    break;
	case Status.STATUS_MARKED_ROLLBACK:
	case Status.STATUS_ROLLING_BACK:
	    break;
	case Status.STATUS_PREPARED:
	case Status.STATUS_COMMITTED:
	case Status.STATUS_ROLLEDBACK:
	case Status.STATUS_NO_TRANSACTION:
	case Status.STATUS_COMMITTING:
	    TraceTm.error("Cannot set transaction as rollback only");
	    TraceTm.error("Bad status="+stringStatus());
	    break;
	}
    }

    /**
     * return a string form of the transaction status (debugging)
     */
    public String stringStatus() {
	String ret = null;
	switch (status) {
	case Status.STATUS_ACTIVE:
	    ret = "ACTIVE";
	    break;
	case Status.STATUS_UNKNOWN:
	    ret = "UNKNOWN";
	    break;
	case Status.STATUS_PREPARING:
	    ret = "PREPARING";
	    break;
	case Status.STATUS_MARKED_ROLLBACK:
	    ret = "MARKED_ROLLBACK";
	    break;
	case Status.STATUS_PREPARED:
	    ret = "PREPARED";
	    break;
	case Status.STATUS_COMMITTED:
	    ret = "COMMITTED";
	    break;
	case Status.STATUS_ROLLEDBACK:
	    ret = "ROLLEDBACK";
	    break;
	case Status.STATUS_NO_TRANSACTION:
	    ret = "NO_TRANSACTION";
	    break;
	case Status.STATUS_COMMITTING:
	    ret = "COMMITTING";
	    break;
	case Status.STATUS_ROLLING_BACK:
	    ret = "ROLLING_BACK";
	    break;
	default:
	    ret = "BAD_VALUE";
	    break;
	}
	return ret;
    }

    // ------------------------------------------------------------------
    // private methods
    // ------------------------------------------------------------------

    /**
     * forget Transaction
     */
    private void doForget() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.doForget()");

	boolean exception = false;

	for (int i = 0; i < resourceList.size(); i++) {
	    // Tell this Resource to forget the transaction
	    // Remove from the list ? (XXX)
	    XAResource xar = (XAResource) resourceList.elementAt(i);
	    try {
		xar.forget(xid);
	    } catch (XAException e) {
		exception = true;
	    }
	}
	if (exception) {
	    throw new RemoteException("XAException on forget");
	}
	unexportObject(this,true); // usefull for Jeremie
    }

    /**
     * 2PC phase 1 (prepare)
     * Basically, send prepare on each XAResource and
     * collect the results.
     */
    private int doPrepare() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.doPrepare()");

	int ret = VOTE_READONLY;
	int errors = 0;

	// No resource
	if (resourceList.size() == 0) {	    
	    // increment counter for management
	    Current.getCurrent().incrementCommitCounter();
	    status = Status.STATUS_COMMITTED;
	    return ret;
	}

	// Creates a log for that transaction, where we will add all the
	// resources that replied VOTE_COMMIT to prepare.
	// Do not flush the log on disk before decision to commit.
	log = new SLog();

	// Sends prepare to each resource while no error
	// In case of prepare on sub-coord. we may have only 1 resource.
	status = Status.STATUS_PREPARING;
	for (int i = 0; i < resourceList.size(); i++) {
	    XAResource res = (XAResource) resourceList.elementAt(i);
	    if (errors > 0) {
		try {
		    res.rollback(xid);
		} catch (XAException e) {
		    // Nothing to do ?
		}
	    } else {
		try {
		    switch (res.prepare(xid)) {
		    case XAResource.XA_OK:
			log.addResource(res);
			ret = VOTE_COMMIT;
			break;
		    case XAResource.XA_RDONLY:
			break;
		    }
		} catch (XAException e) {
		    ret = VOTE_ROLLBACK;
		    errors++;
		}
	    }
	}

	// Update the status, depending on vote result
	// If all resources returned READ_ONLY, we can forget the transaction
	switch (ret) {
	case VOTE_READONLY:
	    // increment counter for management
	    Current.getCurrent().incrementCommitCounter();
	    status = Status.STATUS_COMMITTED;
	    break;
	case VOTE_COMMIT:
	    status = Status.STATUS_PREPARED;
	    break;
	case VOTE_ROLLBACK:
	    status = Status.STATUS_ROLLEDBACK;
	    break;
	}

	// return the global vote
	return ret;
    }

    /**
     * 2PC - phase 2 (commit)
     * See JTM for heuristics management XXX
     */
    private int doCommit() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.doCommit()");

	// First check that a log is initialized
	if (log == null) {
	    TraceTm.error("doCommit: no log");
	    return -1;
	}

	// Status Transaction = committing
	status = Status.STATUS_COMMITTING;

	// Send commit to each resource prepared
	int errors = 0;
	int commitnb = 0;
	int heuristicnb = 0;
	for (int i = 0; i < log.resourceLogged.size(); i++) {
	    XAResource res = (XAResource) log.resourceLogged.elementAt(i);

	    // aborts transaction if error and no resource committed yet
	    if (commitnb == 0 && errors > 0) {
		try {
		    res.rollback(xid);
		} catch (XAException e) {
		    commitnb++;
		    heuristicnb++;
		}
		continue;
	    }

	    // commit resource
	    try {
		res.commit(xid, false);
	    } catch (XAException e) {
		errors++;
		if (commitnb > 0)
		    heuristicnb++;
	    }
	}

	if (errors == 0) {
	    // increment counter for management
	    Current.getCurrent().incrementCommitCounter();
	    // Everything's fine.
	    status = Status.STATUS_COMMITTED;
	    doAfterCompletion();
	    log.forgetLog();
	    return 0;
	}

	if (heuristicnb == 0) {
	    // Transaction has been eventually rolled back
	    status = Status.STATUS_ROLLEDBACK;
	    doAfterCompletion();
	    log.forgetLog();
	    throw new TransactionRolledbackException();
	}

	// Log heuristics if errors
	// XXX
	status = Status.STATUS_UNKNOWN;
	doAfterCompletion();
	return -1;
    }

    /**
     * 1PC (commit one phase)
     */
    private void doOnePhaseCommit() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.doOnePhaseCommit()");

	// Only 1 resource: commit with onePhase=true
	status = Status.STATUS_COMMITTING;
	XAResource res = (XAResource) resourceList.elementAt(0);
	try {
	 
	    // increment counter for management
	    Current.getCurrent().incrementCommitCounter();   
	    res.commit(xid, true);
	    status = Status.STATUS_COMMITTED;
	} catch (XAException e) {
	    status = Status.STATUS_UNKNOWN;
	    if (e.errorCode == XAException.XA_RBROLLBACK) {
		throw new TransactionRolledbackException("XAException:"+e);
	    }
	    throw new RemoteException("XAException:"+e);
	} finally {
	    doAfterCompletion();
	}
    }

    /**
     * Rollback every resource involved
     */
    private void doRollback() throws RemoteException {

	TraceTm.debugJta("SubCoordinator.doRollback()");


	status = Status.STATUS_ROLLEDBACK;
	boolean hcomm = false;

	// rollback each resource
	for (int i = 0; i < resourceList.size(); i++) {
	    XAResource res = (XAResource) resourceList.elementAt(i);
	    try {
		res.rollback(xid);
	    } catch (XAException e) {
		switch (e.errorCode) {
		case XAException.XA_HEURCOM:
		    hcomm = true;
                    break;
		default:
		    TraceTm.error("Got unexpected XAException from resource: "+e);
		}
	    }
	}

	doAfterCompletion();
	// increment counter for management
	Current.getCurrent().incrementRollbackCounter();

	// raise exception if error on a resource
	if (hcomm) {
	    throw new HeuristicCommit();
	}
    }

    /**
     * before completion
     *
     * @param boolean true if completion ok, false if rollback
     */
    private void doBeforeCompletion(boolean committing) {

	if (beforeCompletionDone)
	    return;

	TraceTm.debugJta("SubCoordinator.doBeforeCompletion()");

	// Unset the timer for this transaction, if any
	tx.unsetTimer();

	// For each synchro, send beforeCompletion (not if rollback)
	if (committing && synchroList.size() > 0) {
	    // We must be in the correct transaction context
	    // See JTA spec. page 13 (3.3.2)
	    // because at beforeCompletion, the bean will write its cache

	    // Check the trivial case where we already have the correct tx context
	    Transaction mytx = null;
	    boolean suspended = false;
	    boolean resumed = false;
	    try {
		mytx = tm.getTransaction();
	    } catch (SystemException e) {
		if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot get transaction:"+e);
	    }

	    // Suspend if another tx context
	    if (mytx != null && mytx.equals(tx) == false) {
		try {
		    tm.suspend();
		    suspended = true;
		} catch (SystemException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot suspend transaction:"+e);
		}
	    }

	    // Resume the good tx context
	    if (mytx == null || suspended) {
		try {
		    tm.resume(tx);
		    resumed = true;
		} catch (SystemException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot resume transaction:"+e);
		} catch (InvalidTransactionException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot resume transaction:"+e);
		} catch (IllegalStateException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot resume transaction:"+e);
		}
	    }

	    // Call the synchronizations
	    // beforeCompletion may set the TX rollbackonly if something goes wrong
	    for (int i = 0; i < synchroList.size(); i++) {
		Synchronization sync = (Synchronization) synchroList.elementAt(i);
		sync.beforeCompletion();
	    }

	    // reset context as it was before
	    if (resumed) {
		try {
		    tm.suspend();
		} catch (SystemException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot suspend transaction:"+e);
		}
	    }
	    if (suspended) {
		try {
		    tm.resume(mytx);
		    resumed = true;
		} catch (SystemException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot resume transaction:"+e);
		} catch (InvalidTransactionException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot resume transaction:"+e);
		} catch (IllegalStateException e) {
		    if (TraceTm.isDebugJta()) TraceTm.debugJta("Cannot resume transaction:"+e);
		}
	    }
	}

	beforeCompletionDone = true;
    }

    /**
     * after completion
     */
    private void doAfterCompletion() {

	TraceTm.debugJta("SubCoordinator.doAfterCompletion()");

	// For each synchro, send afterCompletion
	/// CompletedTransactionListener has been replaced by Synchronization
	for (int i = 0; i < synchroList.size(); i++) {
	    Synchronization sync = (Synchronization) synchroList.elementAt(i);
	    TraceTm.debugJta("SubCoordinator sending afterCompletion to Synchronization");
	    sync.afterCompletion(status);
	}

	// Forget this transaction.
	// LATER: 
	// - Should not forget it in case of heuristics (for recovery)
	// - May be this could be deferred in case of retry from a client: use a timer.
	Current.getCurrent().forgetTx(tx.getXid());
	try {
	    unexportObject(this,true);	
	}catch(Exception e){
	}
    }
}
