/*
 * 
 * Copyright 2004 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source
 * License v3.0/Jini Technology Specific Attachment v 1.0 (the
 * "License"). You may not use this file except in compliance with the
 * License. You may obtain a copy of the License at
 * http://wwws.sun.com/software/jini/licensing/SCSL3_JiniTSA1.html.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 * 
 * The Reference Code is Jini Technology Core Platform code, v2.0. The
 * Developer of the Reference Code is Sun Microsystems, Inc.
 * 
 * Contributor(s): Sun Microsystems, Inc.
 * 
 * The contents of this file comply with the Jini Technology Core
 * Platform Compatibility Kit, v2.0A.
 * 
 * Tester(s): Sun Microsystems, Inc.
 * 
 * Test Platform(s):
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Solaris(TM)SPARC/x86
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Linux (Intel x86)
 * Java 2 SDK, Standard Edition, Version 1.4.2_03 for Windows (Intel Platform)
 * 
 */
package com.sun.jini.mahalo;

import net.jini.id.Uuid;
import net.jini.core.lease.Lease;
import net.jini.core.transaction.*;
import net.jini.core.transaction.server.*;
import net.jini.security.ProxyPreparer;

import com.sun.jini.mahalo.log.*;

import com.sun.jini.constants.TimeConstants;
import com.sun.jini.constants.TxnConstants;
import com.sun.jini.landlord.LeasedResource;
import com.sun.jini.logging.Levels;
import com.sun.jini.thread.TaskManager;
import com.sun.jini.thread.WakeupManager;

import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.util.Enumeration;

import java.io.Serializable;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Map;
import java.util.Vector;

/**
 * TxnManagerTransaction is a class which
 * captures the internal representation of a transaction
 * in the TxnManagerImpl server.  This class is associated
 * with a transaction id.
 * The information encapsulated includes the list of participants
 * which have joined the transaction, the state of the
 * transaction, the crash.
 *
 * The user of a ParticipantHolder must make the association
 * between an instance of a ParticipantHolder and some sort
 * of key or index.
 *
 * @author Sun Microsystems, Inc.
 *
 */
class TxnManagerTransaction
    implements TransactionConstants, TimeConstants, Serializable, 
    LeasedResource
{
    static final long serialVersionUID = -2088463193687796098L;

   /*
    * Table of valid state transitions which a
    * TransactionManager may make.
    *
    * This represents the following diagram from the
    * Jini(TM) Transaction Spec:
    *
    *  ACTIVE ----> VOTING ------->COMMITTED
    *     \           |
    *      \          |
    *       ---------------------->ABORTED
    *
    *
    *		    ACTIVE  VOTING  PREPARED  NOTCHANGED  COMMITTED  ABORTED
    * ----------------------------------------------------------------------
    * ACTIVE	    true     true   false     false       false       true
    * VOTING        false    true   false     false       true        true
    * PREPARED      false    false  false     false       false       false
    * NOTCHANGED    false    false  false     false       false       false
    * COMMITTED     false    false  false     false       true        false
    * ABORTED       false    false  false     false       false       true
    *
    * The table is indexed using the ordered pair
    * <current_state, next_state>.  A value of true means
    * that the transition is possible, while a false
    * means that the transition is not possible.
    *
    * Note:  Some rows are "{false, false, false, false}" as
    * 	     unused filler to account for the fact that the
    *	     TransactionManager's valid states are a subset
    *	     of the TransactionConstants.
    *
    *    <zero>
    *    ACTIVE = 1
    *    VOTING = 2
    *    PREPARED = 3
    *    NOTCHANGED = 4
    *    COMMITTED = 5
    *    ABORTED = 6
    */
    private static final boolean states[][] = {
	/* <zero>    */ {false, false, false, false, false, false, false},
	/* ACTIVE    */ {false, true,  true,  false, false, false, true},
	/* VOTING    */ {false, false, true,  false, false, true,  true},
	/* PREPARED  */ {false, false, false, false, false, false, false},
	/* NOTCHANGED*/ {false, false, false, false, false, false, false},
	/* COMMITTED */ {false, false, false, false, false, true,  false},
	/* ABORTED   */ {false, false, false, false, false, false, true}};

    /**
     * @serial
     */
    private List parts = new Vector();

    /**
     * @serial
     */
    private final ServerTransaction str;

    /**
     * @serial
     */
    private int trstate;

    /**
     * @serial
     */
    private long expires;		//expiration time

    /**
     * @serial
     */
    private LogManager logmgr;


   /**
    * "Parallelizing" the interaction between the manager
    * and participants means using threads to interact with
    * participants on behalf of the manager. In the thread
    * pool model, a TaskManager provides a finite set of
    * threads used to accomplish a variety of tasks.
    * A Job encapsulates a body of work which needs to be
    * performed during the two-phase commit: preparing,
    * committing and aborting.  Each work item is broken
    * into smaller pieces of work- interactions with a
    * single participant- each assigned to a task.
    *
    * When a transaction is committing, a PrepareJob is
    * created and its tasks are scheduled.  After completion,
    * the PrepareJob's outcome is computed. Depending on
    * the outcome, either an AbortJob or CommitJob is
    * scheduled.  When a transaction is aborted, an AbortJob
    * is scheduled.
    *
    * A caller may specify a timeout value for commit/abort.
    * The timeout represents the length of time a caller is
    * willing to wait for participants to be instructed to
    * roll-forward/back.  Should this timeout expire, a
    * TimeoutExpiredException is thrown.  This causes the
    * caller's thread to return back to the caller.  Someone
    * needs to finish contacting all the participants.  This
    * is accomplished by the SettlerTask.  SettlerTasks must
    * use a different thread pool from what is used by the
    * various Job types, otherwise deadlock will occur.
    *
    * @serial
    */
    private TaskManager threadpool;

    /**
     * @serial
     */
    private WakeupManager wm;

    /**
     * @serial
     */
    private TxnSettler settler;

    /**
     * @serial
     */
    private Job job;

    /**
     * @serial
     */
    private Uuid uuid;

   /**
    * Interlock for the expiration time since
    * lease renewal which set it may compete
    * against lease checks which read it.
    *
    * @serial
    */
    private Object leaseLock = new Object();


   /**
    * Interlock for Jobs is needed since many
    * threads on behalf of many clients can
    * simultaneously access or modify the Job
    * associated with this transaction when
    * when attempting to prepare, roll forward
    * or roll back participants.
    *
    * @serial
    */
    private Object jobLock = new Object();


   /**
    * Interlock for transaction state needed
    * since many threads on behalf of many
    * clients can simultaneously access or
    * attempt to modify this transaction's
    * state as a side effect of calling
    * commit or abort.
    *
    * @serial
    */
    private Object stateLock = new Object();


    /** Logger for operation related messages */
    private static final Logger operationsLogger = 
        TxnManagerImpl.operationsLogger;

    /** Logger for transaction related messages */
    private static final Logger transactionsLogger = 
        TxnManagerImpl.transactionsLogger;

    /**
     * Constructs a <code>TxnManagerTransaction</code>
     *
     * @param mgr	<code>TransactionManager</code> which owns
     *			this internal representation.
     * @param logmgr	<code>LogManager</code> responsible for
     *			recording COMMITTED and ABORTED transactions
     *			to stable storage.
     *
     * @param id	The transaction id
     *
     * @param threadpool The <code>TaskManager</code> which provides
     *			 the pool of threads used to interact with
     *			 participants.
     *
     * @param settler	TxnSettler responsible for this transaction if
     *			unsettled.
     */
    TxnManagerTransaction(TransactionManager mgr, LogManager logmgr, long id,
        TaskManager threadpool, WakeupManager wm, TxnSettler settler,
	Uuid uuid) 
    {
	if (logmgr == null)
	    throw new IllegalArgumentException("TxnManagerTransaction: " +
			    "log manager must be non-null");
	if (mgr == null)
	    throw new IllegalArgumentException("TxnManagerTransaction: " +
			    "transaction manager must be non-null");

	if (threadpool == null)
	    throw new IllegalArgumentException("TxnManagerTransaction: " +
			    "threadpool must be non-null");

	if (wm == null)
	    throw new IllegalArgumentException("TxnManagerTransaction: " +
			    "wakeup manager must be non-null");

	if (settler == null)
	    throw new IllegalArgumentException("TxnManagerTransaction: " +
			    "settler must be non-null");

	if (uuid == null)
	    throw new IllegalArgumentException("TxnManagerTransaction: " +
			    "uuid must be non-null");

	this.threadpool = threadpool;
	this.wm = wm;
	this.logmgr = logmgr ;
	str = new ServerTransaction(mgr, id);
	this.settler = settler;
	this.uuid = uuid;

	trstate = ACTIVE;  //this is implied since ACTIVE is initial state
	// Expires is set after object is created when the associated
	// lease is constructed.
    }


    /**
     * Convenience method which adds a given <code>ParticipantHandle</code> 
     * to the set of <code>ParticpantHandle</code>s associated with this
     * transaction.
     *
     * @param handle The added handle
     */
    void add(ParticipantHandle handle)
        throws InternalManagerException
    {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "add", handle);
	}
	
	if (handle == null)
	    throw new NullPointerException("ParticipantHolder: add: " +
					   "cannot add null handle");

        //NOTE: if the same participant re-joins, then that is
        //      fine.

	try {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Adding ParticipantHandle: {0}", handle);
            }
	    parts.add(handle);
	} catch (Exception e) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Unable to add ParticipantHandle", e);
	    }
	    throw new InternalManagerException("TxnManagerTransaction: " +
					    	     "add: " + e.getMessage());
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "add");
	}
    }

    /**
     * Convenience method which allows the caller to modify the
     * prepState associated with a given <code>ParticipantHandle</code>
     *
     * @param handle The <code>ParticipantHandle</code> being modified
     *
     * @param state The new prepstate
     */
    void modifyParticipant(ParticipantHandle handle, int state) {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "modifyParticipant", new Object[] {handle, new Integer(state)});
	}
	ParticipantHandle ph = null;

	if (handle == null)
	    throw new NullPointerException("ParticipantHolder: " +
			"modifyParticipant: cannot modify null handle");

	if (parts.contains(ph))
	    ph = (ParticipantHandle) parts.get(parts.indexOf(handle));	

	if (ph == null) {
            if (operationsLogger.isLoggable(Level.FINER)) {
                operationsLogger.exiting(
	            TxnManagerTransaction.class.getName(), 
		    "modifyParticipant");
	    }
//TODO - ignore??	    
	    return;
	}

	ph.setPrepState(state);
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "modifyParticipant");
	}
    }


    /**
     * Changes the manager-side state of the transaction.  This
     * method makes only valid state changes and informs the
     * caller if the change was successful. Calls to this method
     * synchronize around the manager-side state variable appropriately.
     *
     * @param int state the new desired state
     */
    boolean modifyTxnState(int state) {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "modifyTxnState", new Integer(state));
	}
	boolean result = false;
	synchronized (stateLock) {
	    switch (state) {
		case ACTIVE:
		case VOTING:
		case COMMITTED:
		case ABORTED:
	          result = states[trstate][state];
		  break;

	      default:
	        throw new IllegalArgumentException("TxnManagerTransaction: " +
				            "modifyTxnState: invalid state");
	    }

	    if (result)
		trstate = state;
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "modifyTxnState", new Boolean(result));
	}
	return result;
    }


    /**
     * Implementation of the join method. 
     *
     * @param part The joining <code>TransactionParticpant</code>
     *
     * @param crashCount The crashcount associated with the joining
     *			 <code>TransactionParticipant</code>
     *
     * @see net.jini.core.transaction.server.TransactionParticipant
     */
    public void
	join(TransactionParticipant preparedPart, long crashCount)
	    throws CannotJoinException, CrashCountException, RemoteException
    {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "join", new Object[] {preparedPart, new Long(crashCount)});
	}
	//if the lease has expired, or the state is not
	//amenable there is no need to continue

	if (getState() != ACTIVE)
	    throw new CannotJoinException("not active");

	if ((getState() == ACTIVE) && (ensureCurrent() == false)) {
	    doAbort(0);
	    throw new CannotJoinException("Lease expired");
	}

	//Create a ParticipantHandle for the new participant
	//and mark the transactional state as ACTIVE
	try {
	    ParticipantHandle ph = 
	        new ParticipantHandle(preparedPart, crashCount);
	    ParticipantHandle phtmp = (ParticipantHandle) 	
	        ((parts.contains(ph))?
		    parts.get(parts.indexOf(ph)):
		    null);	

            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Retrieved ParticipantHandle: {0}", phtmp);
            }
	    if (phtmp != null) {
		long oldcount = phtmp.getCrashCount();
		if (oldcount == crashCount) {
		    return;
		} else {
		    throw new CrashCountException("TxnManagerTransaction: " +
					    "join: old = " + oldcount +
					    " new = " + crashCount);
		}
	    }

	    add(ph);

	} catch (InternalManagerException ime) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "TransactionParticipant unable to join", ime);
            }
	    throw ime;
	} catch (RemoteException re) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "TransactionParticipant unable to be stored", re);
            }
	    throw re;
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "join");
	}
    }


   /**
    * This method returns the state of the transaction.
    * Since the purpose of the set of ParticipantHolders is
    * to associate a Transaction with a group of
    * participants joined the transaction, we would like
    * to get the state of the transaction associated with
    * the aforementioned set.
    *
    */
    public int getState() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "getState");
	}
	synchronized (stateLock) {
            if (operationsLogger.isLoggable(Level.FINER)) {
                operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	            "getState", new Integer(trstate));
	    }
            return trstate;
	}
    }


    /**
     * Commits the transaction.
     * This initiates the two-phase commit protocol.  First,
     * each <code>net.jini.core.transaction.server.TransactionParticipant</code>
     * in the set of participants joined in the
     * <code>net.jini.core.transaction.server.Transaction</code>
     * is instructed to vote and the votes are tallied. This is the
     * first phase (prepare phase).
     *
     * Depending on the outcome of the votes, the transaction
     * is considered committed or aborted.  Once commit/abort
     * status is known, the participants are notified with
     * a message to either roll-forward (commit case) or 
     * roll-back (abort case).  This is the roll-phase.
     * 
     * Since there may be a one-to-many relationship between
     * a transaction and its participants,
     * <code>com.sun.jini.thread.TaskManager</code>s are used
     * as a generic mechanism to provide the threads needed
     * to interact with the participants.
     * 
     */
    void commit(long waitFor)
        throws CannotCommitException, TimeoutExpiredException
    {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "commit", new Long(waitFor));
	}
	long starttime = System.currentTimeMillis();

	//If the transaction has already expired or the state
	//is not amenable, don't even try to continue

	if ((getState() == ACTIVE) && (ensureCurrent() == false)) {
	    doAbort(0);
	    throw new CannotCommitException("Lease expired");
	}

	if (getState() == ABORTED)
	    throw new CannotCommitException("attempt to commit " +
					    "ABORTED transaction");


	//Check to see if anyone joined the transaction.  Even
	//if no one has joined, at this point, attempt to
	//get to the COMMITTED state through valid state changes

        Vector joinvec = parthandles();
 
        if (joinvec == null) {
	    if (!modifyTxnState(VOTING))
		throw new CannotCommitException("attempt to commit " +
						"ABORTED transaction");

	    if (modifyTxnState(COMMITTED))
                return;
	    else
		throw new CannotCommitException("attempt to commit " +
						"ABORTED transaction");
        }

	try {
	    Enumeration joined = joinvec.elements();
	    int numparts = joinvec.size();
	    ParticipantHandle[] phs = new ParticipantHandle[numparts];
	    joinvec.copyInto(phs);

            long now = starttime;
            long transpired = 0;
            long remainder = 0;

	    ClientLog log = logmgr.logFor(str.id);

            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "{0} TransactionParticipants have joined", 
		new Integer(numparts));
            }

	    //If commit is called after recovery, do not
	    //log a CommitRecord since it already exists
	    //exists in the Log file for this transaction.
	    //Remember that a log is not invalidated until
	    //after the transaction is committed.
	    //
	    //Only new occurrences of activities requiring
	    //logging which happen after recovery should
	    //be added to the log file.  So, we add records
	    //for voting and roll forward/back activity for
	    //ACTIVE participants.
	    //
	    //If the state cannot validly transition to VOTING,
	    //it is either because someone already aborted or
	    //committed.  Only throw an exception if  someone
	    //has previously aborted.  In the case of a prior
	    //commit, fall through and wait for the CommitJob
	    //to complete.

	    int oldstate = getState();
	    Integer result = new Integer(ABORTED);



	    //On an ACTIVE to VOTING transition, create
	    //and schedule a Prepare or PrepareAndCommitJob.
	    //If the state transition is VOTING to VOTING,
	    //then the PrepareJob has already been created
	    //in the past, so just fall through and wait
	    //for it to complete.
	    //
	    //Only log the commit on ACTIVE to VOTING
	    //transitions.

	    if (modifyTxnState(VOTING)) {

		if (oldstate == ACTIVE)
		    log.write(new CommitRecord(phs));


		//preparing a participant can never override
		//the other activities (abort or commit),
		//so only set when the job is null.

	        synchronized (jobLock) {
		    if (job == null) {
	                if (phs.length == 1)
		            job = new
			      PrepareAndCommitJob(
				  str, threadpool, wm, log, phs[0]);
	                else
	                    job = new PrepareJob(str, threadpool, wm, log, phs);

	                job.scheduleTasks();
		    }
		}

		//Wait for the PrepareJob to complete.
		//PrepareJobs are given maximum time for
		//completion.  This is required in order to
		//know the transaction's completion status.
		//Remember that the timeout ONLY controls how
		//long the caller is willing to wait to inform
		//participants.  This means that a completion
		//status for the transaction MUST be computed
		//before consulting the timeout.
		//Timeout is ignored until completion status
		//is known.  If extra time is left, wait for
		//the remainder to inform participants.

		//We must explicitly check for Job type
		//because someone else could have aborted
		//the transaction at this point.

                synchronized (jobLock) {
		    if ((job instanceof PrepareJob) ||
			    (job instanceof PrepareAndCommitJob)) {
                        try {
                            if (job.isCompleted(Long.MAX_VALUE))
                                result = (Integer) job.computeResult();
                        } catch (JobNotStartedException jnse) {
                            //no participants voted, so do nothing
                            result = new Integer(NOTCHANGED);
                        } catch (ResultNotReadyException rnre) {
                            //consider aborted
                        } catch (JobException je) {
                            //consider aborted
                        }
		    }
		}
	    } else {
		//Cannot be VOTING, so we either have
		//an abort or commit in progress.

		if (getState() == ABORTED)
		    throw new CannotCommitException("transaction ABORTED");

 
                //If a CommitJob is already in progress 
		//(the state is COMMITTED) cause a fall
		//through to the code which waits for
                //the CommitJob to complete.

                if (getState() == COMMITTED)
                    result = new Integer(COMMITTED);
	    }

            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Voting result: {0}", 
		TxnConstants.getName(result.intValue()));
            }

	    switch (result.intValue()) {
	      case NOTCHANGED:
		break;

	      case ABORTED:
                now = System.currentTimeMillis();
                transpired = now - starttime;
                remainder = waitFor - transpired;

		if (remainder >=0)
		    doAbort(remainder);
		else
		    doAbort(0);

                throw new CannotCommitException("Unable to commit " +
                                                            "transaction");

	      case PREPARED:
		//This entrypoint is entered if a PrepareJob
		//tallied the votes with an outcome of
		//PREPARED.  In order to inform participants,
		//a CommitJob must be scheduled.

		if(modifyTxnState(COMMITTED)) {
//TODO - log committed state record?		
                    synchronized (jobLock) {
                        job = new CommitJob(str, threadpool, wm, log, phs);
                        job.scheduleTasks();
                    }
		} else {
		    throw new CannotCommitException("attempt to commit " +
					        "ABORTED transaction");
		}

		//Fall through to code with waits
		//for CommitJob to complete.


	      case COMMITTED:
		//This entrypoint is the starting place for the code
		//which waits for a CommitJob to complete and
		//computes its resulting outcome. In addition,
		//the wait time is enforced.  Should the wait time
		//expire, a SettlerTask is scheduled on a thread 
		//pool.  The SettlerTask is needed to complete
		//the commit (instruct participants to roll-forward)
		//on behalf of the thread which exits when
		//the TimeoutExpiredException is thrown.
		//
                //It is reached when...
		//
		// a) A commit was called on the same transaction twice.
		//    When the thread comes in on the commit call, a
		//    CommitJob already exists and the state is COMMITTED.
		//
		// b) The normal case where a PrepareJob was found to
		//    have prepared the transaction and the tally of
		//    votes resulted in a PREPARED outcome.  This causes
		//    the state to be changed to COMMITTED and a
		//    CommitJob to be created.
		//
		// c) A PrepareAndCommitJob has successfully prepared
		//    a participant which rolled its changes forward.
		//
                //Note:  By checking to see if the CommitJob is already
                //       present, this check allows us to use the same
		//	 wait-for-CommitJob code for the regular
		//       PREPARE/COMMIT, the COMMIT/COMMIT and
		//       the PREPAREANDCOMMIT cases.
 
		  synchronized (jobLock) {
		      //A prepareAndCommitJob is done at this
		      //point since the TransactionParticipant
		      //would have instructed itself to roll
		      //forward.

		      if (job instanceof PrepareAndCommitJob) {
		          if(!modifyTxnState(COMMITTED))
			      throw new CannotCommitException("transaction " +
							    	"ABORTED");	
		          break;
		      }
 

		      //If the abort already arrived, then stop

		      if (job instanceof AbortJob) 
			   throw new CannotCommitException("transaction " +
								"ABORTED");
		  }

                  if (getState() != COMMITTED)
                        throw new
                            InternalManagerException("TxnManagerTransaction: " +
                                    "commit: " + job + " got bad state: " +
                                    TxnConstants.getName(result.intValue()));


		now = System.currentTimeMillis();
		transpired = now - starttime;

		boolean committed = false;

		//If the commit is asynchronous then...
		//
		// a) check to see if the wait time has transpired
		//
		// b) If it hasn't, sleep for what's left from the wait time

		try {
		    remainder = waitFor - transpired;
		    synchronized (jobLock) {
		        if (remainder <= 0 || !job.isCompleted(remainder)) {
/*
 * Note - SettlerTask will kick off another Commit/Abort task for the same txn
 * which will try go through the VOTING->Commit states again.
 */
//TODO - Kill off existing task? Postpone SettlerTask? 			
		            settler.noteUnsettledTxn(str.id);
			    throw new TimeoutExpiredException(
					    "timeout expired", true);
			} else {
			    result = (Integer) job.computeResult();
			    committed = true;
			}
		    }
		} catch (ResultNotReadyException rnre) {
		    //this should not happen, so flag
		    //as an error.
		} catch (JobNotStartedException jnse) {
		    //an error
		} catch (JobException je) {
		    //an error
		}

		if (committed)
		    break;

	      default:
                throw new InternalManagerException("TxnManagerTransaction: " +
	    			    "commit: " + job + " got bad state: " +
				    TxnConstants.getName(result.intValue()));
	    }

	    //We don't care about the result from
	    //the CommitJob
	    log.invalidate();
	} catch (RuntimeException rte) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Problem committing transaction", 
	         rte);
            }
            throw rte;
	} catch (LogException le) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Problem persisting transaction", 
	         le);
            }
	    throw new CannotCommitException("Unable to log");
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "commit");
	}
    }


    /**
     * Aborts the transaction.
     * This method attempts to set the state of the transaction
     * to the ABORTED state.  If successful, it then creates
     * an AbortJob which schedules tasks executed on a
     * thread pool.  These tasks interact with the participants
     * joined in this transaction to inform them to roll back.
     * 
     * @param waitFor Timeout value which controls how long,
     *		      the caller is willing to wait for the
     *		      participants joined in the transaction 
     *		      to be instructed to roll-back.
     *
     * @see com.sun.jini.mahalo.AbortJob
     * @see com.sun.jini.mahalo.ParticipantTask
     * @see com.sun.jini.thread.TaskManager
     * @see net.jini.core.transaction.server.TransactionParticipant
     */
    void abort(long waitFor)
	throws CannotAbortException, TimeoutExpiredException
    {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "abort", new Long(waitFor));
	}
	long starttime = System.currentTimeMillis();

	/*
	 * Since lease cancellation process sets expiration to 0 
	 * and then calls abort, can't reliably check expiration 
	 * at this point.
	 */
//TODO - Change internal, lease logic to call overload w/o expiration check
//TODO - Add expiration check to abort for external clients     
	try {
            Vector joinvec = parthandles();

	    if (joinvec == null) {
		if (modifyTxnState(ABORTED))
		    return;
		else 
		    throw new
			CannotAbortException("Transaction already COMMITTED");
	    }

            Enumeration joined = joinvec.elements();
            int numparts = joinvec.size();
            ParticipantHandle[] phs = new ParticipantHandle[numparts];
            joinvec.copyInto(phs);

            ClientLog log = logmgr.logFor(str.id);


	    //When attempting to abort, if someone has already
	    //committed the transaction, then throw an Exception.
	    //
	    //If an abort is possible, and you find that an AbortJob
	    //is already in progress, let the existing AbortJob
	    //proceed.
	    //
	    //If an abort is possible, but a PrepareJob is in progress,
	    //go ahead an halt the PrepareJob and replace it with
	    //an AbortJob.

	    if (modifyTxnState(ABORTED)) {
                log.write(new AbortRecord(phs));

	        synchronized (jobLock) {
	            if (!(job instanceof AbortJob)) {
		        if (job != null)
		            job.stop();
	                job = new AbortJob(str, threadpool, wm, log, phs);
	                job.scheduleTasks();
	            }
	        }
	    } else {
		throw new CannotAbortException("Transaction already COMMITTED");
	    }


	    //This code waits for an AbortJob to complete and
	    //computes its resulting outcome. In addition,
	    //the wait time is enforced.  Should the wait time
	    //expire, a SettlerTask is scheduled on a thread
	    //pool.  The SettlerTask is needed to complete
	    //the abort (instruct participants to roll-back)
	    //on behalf of the thread which exits when
	    //the TimeoutExpiredException is thrown.

	    long now = System.currentTimeMillis();
	    long transpired = now - starttime;

	    Integer result = new Integer(ACTIVE);
	    boolean aborted = false;

	    long remainder = waitFor - transpired;

	    try {
		synchronized (jobLock) {
	            if (remainder<= 0 || !job.isCompleted(remainder)) {
		        settler.noteUnsettledTxn(str.id);
		        throw new TimeoutExpiredException(
				        "timeout expired",false);
		    } else {
	    	       	result = (Integer) job.computeResult();
	    	       	aborted = true;
		    }
		}
	    }  catch (ResultNotReadyException rnre) {
		//should not happen, so flag as error
	    } catch (JobNotStartedException jnse) {
		//error
	    } catch (JobException je) {
	        settler.noteUnsettledTxn(str.id);
		throw new TimeoutExpiredException("timeout expired", false);
	    }


	    if (!aborted)
                throw new InternalManagerException("TxnManagerTransaction: " +
                                "abort: AbortJob got bad state: " +
                		TxnConstants.getName(result.intValue()));

	    log.invalidate();
        } catch (RuntimeException rte) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Problem aborting transaction", 
	         rte);
            }
	    throw new InternalManagerException("TxnManagerTransaction: " +
						    "abort: fatal error");
        } catch (LogException le) {
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Problem persisting transaction", 
	         le);
            }
            throw new CannotAbortException("Unable to log");
        }
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "abort");
	}
    }

    public Transaction getTransaction() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "getTransaction");
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "getTransaction", str);
	}
	return str;
    }

    public long getExpiration() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "getExpiration");
	}
	synchronized (leaseLock) {
            if (operationsLogger.isLoggable(Level.FINER)) {
                operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	            "getExpiration", new Date(expires));
	    }
            return expires;
	}
    }

    public void setExpiration(long newExpiration) {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "setExpiration", new Date(newExpiration));
	}
	synchronized (leaseLock) {
	    expires = newExpiration;
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "setExpiration");
	}
    }

    public Uuid getCookie() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "getCookie");
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "getCookie", uuid);
	}
	return uuid;
    }

    private void doAbort(long timeout) {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "doAbort", new Long(timeout));
	}
        try {
            str.abort(timeout);
        } catch (RemoteException re) {
            //abort must have happened, so ignore
            if (transactionsLogger.isLoggable(Levels.HANDLED)) {
                transactionsLogger.log(Levels.HANDLED,
                "Trouble aborting  transaction", re);
	    }
        } catch (TimeoutExpiredException te) {
	    //Swallow this because we really only
	    //care about a scheduling a SettlerTask
            if (transactionsLogger.isLoggable(Levels.HANDLED)) {
                transactionsLogger.log(Levels.HANDLED,
                "Trouble aborting  transaction", te);
	    }
        } catch (TransactionException bte) {
            //If abort has problems, swallow
            //it because the abort must have
            //happened
            if (transactionsLogger.isLoggable(Levels.HANDLED)) {
                transactionsLogger.log(Levels.HANDLED,
                "Trouble aborting  transaction", bte);
	    }
	}
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "doAbort");
	}

    }

    synchronized boolean ensureCurrent() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "ensureCurrent");
	}
	long cur = System.currentTimeMillis();
	boolean result = false;
	long useby = getExpiration();

	if (useby > cur)
	    result = true;
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "ensureCurrent", new Boolean(result));
	}
	return result;
    }


    private Vector parthandles() {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "parthandles");
	}
	if ( (parts == null ) || ( parts.size() == 0 ) )
	    return null;

        Vector vect = new Vector(parts);
 
        if (transactionsLogger.isLoggable(Level.FINEST)) {
            transactionsLogger.log(Level.FINEST,
            "Retrieved {0} participants", 
	     new Integer(vect.size()));
        }
	
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "parthandles");
	}

	return vect;
    }

    void restoreTransientState(ProxyPreparer preparer) 
        throws RemoteException
    {
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.entering(TxnManagerTransaction.class.getName(), 
	        "restoreTransientState");
	}
    	if ( (parts == null ) || ( parts.size() == 0 ) )
	    return;

	ParticipantHandle[] handles = (ParticipantHandle[])
	    parts.toArray(new ParticipantHandle[parts.size()]);
        for (int i=0; i < handles.length; i++) {
	    handles[i].restoreTransientState(preparer);
            if (transactionsLogger.isLoggable(Level.FINEST)) {
                transactionsLogger.log(Level.FINEST,
                "Restored transient state for {0}", 
	         handles[i]);
            }
	}
	
        if (operationsLogger.isLoggable(Level.FINER)) {
            operationsLogger.exiting(TxnManagerTransaction.class.getName(), 
	        "restoreTransientState");
	}
    }
}
