/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * TransactionImpl.java
 *
 * Create on March 3, 2000
 */

package com.sun.jdo.spi.persistence.support.sqlstore.impl;

import javax.transaction.*;

import java.util.Hashtable;
import java.util.ArrayList;
import java.sql.Connection;
import javax.sql.DataSource;
import java.util.Locale;
import java.util.ResourceBundle;

import com.sun.jdo.api.persistence.support.ConnectionFactory;
import com.sun.jdo.api.persistence.support.JDOException;
import com.sun.jdo.api.persistence.support.JDOUnsupportedOptionException;
import com.sun.jdo.api.persistence.support.JDOFatalInternalException;
import com.sun.jdo.api.persistence.support.JDOUserException;
import com.sun.jdo.api.persistence.support.JDODataStoreException;

import com.sun.jdo.spi.persistence.support.sqlstore.PersistenceManager;
import com.sun.jdo.spi.persistence.support.sqlstore.PersistenceManagerFactory;
import com.sun.jdo.spi.persistence.utility.I18NHelper;

import com.sun.jdo.spi.persistence.support.sqlstore.connection.ConnectionImpl;

import com.sun.jdo.spi.persistence.support.sqlstore.utility.*;
import com.sun.jdo.spi.persistence.support.sqlstore.ejb.EJBHelper;
import com.sun.jdo.spi.persistence.utility.logging.Logger;
import com.sun.jdo.spi.persistence.support.sqlstore.LogHelperTransaction;

/**
 *
 * The Transaction interface allows operations to be performed against
 * the transaction in the target Transaction object. A Transaction
 * object is created corresponding to each global transaction creation.
 * The Transaction object can be used for resource enlistment,
 * synchronization registration, transaction completion and status
 * query operations.
 *
 * This implementation is completely internal.
 * All externally documented methods are on the Transaction interface.
 *
 * Note on synchronized(this):
 *        There are a number of places that calls are made outside the
 *        transaction system.  For example, Synchronization.beforeCompletion()
 *        and XAResource.start().  It is important that no locks are held
 *        when these callbacks are made.  This requires more state checking
 *        up return from these calls, and the methods are careful to check
 *        state transitions after release the lock and reacquiring it.
 *
 *      Also be careful NOT to call into a lower method with the lock, if that
 *      method may call out of the transaction system.
 *
 *        Take care if you make methods "synchronized" because:
 *            synchronized methodName()
 *        is not the same lock as a more localized:
 *            synchronized(this)
 *
 *        This is tested in the regression tests (see "Lock Test")
 */
public class TransactionImpl
    implements com.sun.jdo.spi.persistence.support.sqlstore.Transaction {
    /**
     * Trace level for sh:6.
     *
     * This is public to this transaction package and referenced:
     *         if (TransactionImpl.tracing)
     * This is reset by calling TransactionImpl.setTrace().
     */
    static boolean  tracing;
    private static final int    TRACE_THREADS            = 0x01;
    private static final int    TRACE_RESOURCES            = 0x02;
    private static final int    TRACE_SYNCHRONIZATIONS    = 0x04;
    private static final int    TRACE_ONE_PHASE            = 0x08;

    /**
     * Package-visible lock for static attributes.
     *
     * Note that globalLock can be a higher-level lock, in that it may be
     * locked before other lower-level objects are locked (i.e. the
     * transaction object).  It may NOT be locked the other way round.
     */
    static String    globalLock = "TranGlobalLock"; // NOI18N

    /**
     * Transaction status (from javax.transaction.Status).
     */
    private int            status;

    /**
     * Timeout for this transaction
     */
    private int         timeout;
    public static final int    TRAN_DEFAULT_TIMEOUT = 0;    // No timeout

    /**
     * Query and Update Statement timeouts for the datastore associated
     * with this transaction
     */
    private int         queryTimeout = 0;        // No limit
    private int         updateTimeout = 0;        // No limit

    /**
     * Number of threads participating in this transaction.
     */
    private int            threads;
    public static final int    TRAN_MAX_THREADS = 50;        // Public for test

    /**
     * The commit process has already begun (even though the status is still
     * STATUS_ACTIVE).  This is set during before-commit notification.
     */
    private boolean        startedCommit;

    /**
     * During prepare-processing we determine if we can commit this transaction
     * in one-phase. See check in commitPrepare().
     */
    private boolean        onePhase;

    /**
     * Array of registered Synchronization interfaces.
     */
    private    Synchronization    synchronization = null;

    /**
     * Array of registered Resource interfaces.
     */
    private    ArrayList    resources;
    private static final int    RESOURCE_START     = 0;
    private static final int    RESOURCE_END     = 1;

    /**
     * PersistenceManagerFactory associated with this transaction
     */
    private PersistenceManagerFactory pmFactory = null;

    /**
     * PersistenceManager associated with this transaction (1-1)
     */
    private PersistenceManager    persistenceManager     = null;

    /**
     * Connection Factory from which this transaction gets Connections
     */
    private Object     connectionFactory    = null;

    private javax.transaction.Transaction jta = null;

    /**
     * Type of the datasource. True if it javax.sql.DataSource
     */
    private boolean isDataSource = false;

    /** values for the datasource user and user password to access
     * security connections
     */
    private String         username     = null;
    private String         password     = null;

    /**
     * Associated Connection
     */
    private Connection         _connection         = null;

    /**
     * Number of users (or threads) currently using this connection.
     */
    private int _connectionReferenceCount = 0;

    /**
     * Flag that indicates how to handle objects after commit.
     * If true, at commit instances retain their values and the instances
     */
    private boolean retainValues = true;
    
    /**
     * Flag that indicates how to handle objects after roolback.
     * If true, at rollback instances restore their values and the instances
     */
    private boolean restoreValues = false;

    /**
     * Flag that indicates type of the transaction.
     * Optimistic transactions do not hold data store locks until commit time.
     */
    private boolean optimistic = true;

    /**
     * Flag that indicates if queries and navigation are allowed
     * without an active transaction
     */
    private boolean nontransactionalRead = true;

    /**
     * Possible values of txType
     */
    public static final int NON_MGD = 0;
    public static final int CMT = 1;
    public static final int BMT_UT = 2;
    public static final int BMT_JDO = 3;

    /**
     * Flag to indicate usage mode (non-managed vs. managed, etc.)
     */
    private int txType = -1;

    /**
     * The logger
     */
    private static Logger logger = LogHelperTransaction.getLogger();


    /**
     * I18N message handler
     */
    private final static ResourceBundle messages = I18NHelper.loadBundle(
            TransactionImpl.class);

    /**
     * Constructor
     */
    public TransactionImpl(PersistenceManager pm, String username, String password, int seconds) {
        this.status = Status.STATUS_NO_TRANSACTION;
        this.timeout = seconds;
        this.startedCommit = false;
        this.onePhase = false;
        this.resources = new ArrayList();

        persistenceManager = pm;
        this.username = username;
        this.password = password;

        pmFactory = (PersistenceManagerFactory)pm.getPersistenceManagerFactory();
        connectionFactory = pmFactory.getConnectionFactory();
        if (!(connectionFactory instanceof ConnectionFactory)) {
            isDataSource = true;
        }

        optimistic = pmFactory.getOptimistic();
        retainValues = pmFactory.getRetainValues();
        nontransactionalRead = pmFactory.getNontransactionalRead();
        queryTimeout = pmFactory.getQueryTimeout();
        updateTimeout = pmFactory.getUpdateTimeout();

    }

    /**
     * Set PersistenceManager
     */
    public void setPersistenceManager(PersistenceManager pm) {
    }

    /**
     * Returns PersistenceManager associated with this transaction
     */
    public com.sun.jdo.api.persistence.support.PersistenceManager getPersistenceManager() {
        return persistenceManager.getCurrentWrapper();
    }

    public boolean isActive() {
        return (this.status == Status.STATUS_ACTIVE ||
            this.status == Status.STATUS_MARKED_ROLLBACK);
    }

    public void setRetainValues(boolean flag) {
        //
        // First do a quick check to make sure the transaction is active.
        // This allows us to throw an exception immediately.
        // Cannot change flag to true inside an active pessimistic tx
        //
        if (isActive() && !optimistic && flag)
            throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N

        //
        // Now get an exclusive lock so we can modify the retainValues flag.
        //
        persistenceManager.acquireExclusiveLock();

        try {
            // Cannot change flag to true inside an active pessimistic tx
            if (isActive() && !optimistic && flag)
                throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                    "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N

            this.retainValues = flag;

            // Adjust depending flags
            if (flag) {
                nontransactionalRead = flag;
                persistenceManager.notifyNontransactionalRead(flag);
            }
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    public boolean getRetainValues() {
        return retainValues;
    }
    
    public void setRestoreValues(boolean flag) {
        //
        // First do a quick check to make sure the transaction is active.
        // This allows us to throw an exception immediately.
        // Cannot change flag to true inside an active tx
        //
        if (isActive())
            throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N

        //
        // Now get an exclusive lock so we can modify the restoreValues flag.
        //
        persistenceManager.acquireExclusiveLock();

        try {
            // Cannot change flag to true inside an active  tx
            if (isActive())
                throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                    "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N

            this.restoreValues = flag;

        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    public boolean getRestoreValues() {
        return restoreValues;
    }
    

    public void setNontransactionalRead (boolean flag) {
        //
        // First do a quick check to make sure the transaction is active.
        // This allows us to throw an exception immediately.
        // Cannot change flag to false inside an active optimistic tx
        //
        if (isActive() && optimistic && !flag)
            throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N

        //
        // Now get an exclusive lock so we can modify the nontransactionalRead flag.
        //
        persistenceManager.acquireExclusiveLock();

        try {
            // Cannot change flag to false inside an active optimistic tx
            if (isActive() && optimistic && !flag)
                throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                    "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N

            this.nontransactionalRead = flag;
            persistenceManager.notifyNontransactionalRead(flag);

            // Adjust depending flags
            if (flag == false) {
                retainValues = flag;
                optimistic = flag;

                // Notify PM about Tx type change
                persistenceManager.notifyOptimistic(flag);
            }
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    public boolean getNontransactionalRead() {
        return nontransactionalRead;
    }

    /**
     * Sets the number of seconds to wait for a query statement
     * to execute in the datastore associated with this  Transaction instance
     * @param timeout          new timout value in seconds; zero means unlimited
     */
    public void setQueryTimeout(int timeout) {
        queryTimeout = timeout;
    }

    /**
     * Gets the number of seconds to wait for a query statement
     * to execute in the datastore associated with this  Transaction instance
     * @return      timout value in seconds; zero means unlimited
     */
    public int getQueryTimeout() {
        return queryTimeout;
    }

    /**
     * Sets the number of seconds to wait for an update statement
     * to execute in the datastore associated with this  Transaction instance
     * @param timeout          new timout value in seconds; zero means unlimited
     */
    public void setUpdateTimeout(int timeout) {
        updateTimeout = timeout;
    }

    /**
     * Gets the number of seconds to wait for an update statement
     * to execute in the datastore associated with this  Transaction instance
     * @return      timout value in seconds; zero means unlimited
     */
    public int getUpdateTimeout() {
        return updateTimeout;
    }

    public void setOptimistic(boolean flag) {
        //
        // First do a quick check to make sure the transaction is active.
        // This allows us to throw an exception immediately.
        //
        if (!isTerminated()) {
            throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                  "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N
        }

        //
        // Now, get an exclusive lock in order to actually modify the optimistic flag.
        //
        persistenceManager.acquireExclusiveLock();

        try {
            if (isTerminated()) {
                this.optimistic = flag;

                // Adjust depending flags
                if (flag) {
                    nontransactionalRead = flag;
                    persistenceManager.notifyNontransactionalRead(flag);
                }
            } else {
                throw new JDOUnsupportedOptionException(I18NHelper.getMessage(messages,
                      "transaction.transactionimpl.setoptimistic.notallowed")); // NOI18N
            }

            // Notify PM about Tx type change
            persistenceManager.notifyOptimistic(flag);
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    public boolean getOptimistic() {
        return optimistic;
    }

    public void setSynchronization(Synchronization sync) {
        if (this.tracing)
        this.traceCall("setSynchronization"); // NOI18N

        persistenceManager.acquireExclusiveLock();

        try {
            synchronization = sync;

            if (this.tracing) {
                this.traceCallInfo("setSynchronization", // NOI18N
                    TRACE_SYNCHRONIZATIONS, null);
            }

        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    public Synchronization getSynchronization() {
        persistenceManager.acquireShareLock();

        try {
            return synchronization;
        } finally {
            persistenceManager.releaseShareLock();
        }
    }

    public int getTransactionType() {
        return txType;
    }

    /** Verify that username and password are equal to ones stored before
     *
     * @param username as String
     * @param password as String
     * @return true if they are equal
     */
    public boolean verify(String username, String password)
    {
        if ((this.username != null && !this.username.equals(username)) ||
            (this.username == null && username != null) ||
            (this.password != null && !this.password.equals(password)) ||
            (this.password  == null && password != null)) {
            return false;
        }
        return true;
    }

    //
    // ----- Methods from javax.transaction.Transaction interface ------
    //

    /**
     * Begin a transaction.
     */
    public void begin() {

        persistenceManager.acquireExclusiveLock();

        try {

            // Check and set status...
            beginInternal();

            // BMT with JDO Transaction
            if (EJBHelper.isManaged()) {
                txType = BMT_JDO;
                try {
                    TransactionManager tm = EJBHelper.getLocalTransactionManager();

                    tm.begin();
                    jta = tm.getTransaction();
                    EJBHelper.registerSynchronization(jta, this);
                    pmFactory.registerPersistenceManager(persistenceManager, jta);

                } catch (JDOException e) {
                    throw e;     // re-throw it.

                } catch (Exception e) {
                    throw new JDOFatalInternalException(I18NHelper.getMessage(
                        messages, "transaction.transactionimpl.begin.failedlocaltx"), e); // NOI18N
                }
            } else {
                txType = NON_MGD;
            }
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    /**
     * Status change and validation
     */
    private void beginInternal() {
        this.setTrace();

        if (this.tracing)
            this.traceCall("begin");  // NOI18N

        // RESOLVE: need to reset to NO_TX
        if (this.isActive()) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.begin.notnew",  // NOI18N
                this.statusString(this.status)));

        }
        this.setStatus(Status.STATUS_ACTIVE);
        this.threads = 1;
    }

    /**
     * Begin a transaction in managed environment
     */
    public void begin(javax.transaction.Transaction t) {

        persistenceManager.acquireExclusiveLock();

        try {
            beginInternal();
            try {
                jta = t;
                EJBHelper.registerSynchronization(jta, this);
            } catch (Exception e) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(
                    messages, "transaction.transactionimpl.begin.registersynchfailed"), e); // NOI18N
            }

            txType = CMT;
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    /**
     * Commit the transaction represented by this Transaction object
     *
     */
    public void commit() {

        persistenceManager.acquireExclusiveLock();

        try {
            if (txType == CMT || txType == BMT_UT) {
                // Error - should not be called
                throw new JDOUserException(I18NHelper.getMessage(messages,
                     "transaction.transactionimpl.mgd", "commit")); //NOI18N
            } else if (txType == BMT_JDO) {
                // Send request to the container:
                try {
                    EJBHelper.getLocalTransactionManager().commit();
                    return;
                } catch (Exception e) {
                    throw new JDOException("", e); // NOI18N
                }
            }

            this.setTrace();

            if (this.tracing)
                this.traceCall("commit"); // NOI18N

            this.commitBefore();
            this.commitPrepare();
            this.commitComplete();
            this.notifyAfterCompletion();
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    /**
     * Called in the managed environment only for transaction completion
     */
    public void beforeCompletion() {

        if (txType == NON_MGD) {
            // Error - should not be called
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.nonmgd", "beforeCompletion")); //NOI18N
        }

        Object o = null;

        try {
            o = EJBHelper.preInvoke(new Object[] {this, persistenceManager, jta});
            this.commitBefore(); // do actual beforeComplition
            this.commitPrepare(); // check internal status
            this.commitComplete();
        } finally {
            this.closeConnection();
            EJBHelper.postInvoke(o);
        }
    }

    /**
     * Called in the managed environment only for transaction completion
     */
    public void afterCompletion(int st) {

        if (txType == NON_MGD) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                        "transaction.transactionimpl.nonmgd", "afterCompletion")); //NOI18N
        }
        st = EJBHelper.translateStatus(st); // translate Status

        if (this.tracing) {
            this.traceCallInfo("afterCompletion", TRACE_SYNCHRONIZATIONS, //NOI18N
            this.statusString(st));
        }

        if (st == Status.STATUS_ROLLEDBACK) {
            this.setStatus(Status.STATUS_ROLLING_BACK);
            this.internalRollback();
        }

        if (st != this.status) {
            if (synchronization != null) {
                // Allow user to do any cleanup.
                try {
                    synchronization.afterCompletion(st);
                } catch (Exception ex) {
                    logger.log(Logger.WARNING, I18NHelper.getMessage(
                           messages,
                           "transaction.transactionimpl.syncmanager.aftercompletion", // NOI18N
                           ex.getMessage()));
                }
            }

            // Force to close the persistence manager. 
            persistenceManager.forceClose();

            throw new JDOFatalInternalException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.commitprepare.wrongstatus", // NOI18N
                "afterCompletion", this.statusString(this.status),  // NOI18N
                this.statusString(st)));
        }

        this.notifyAfterCompletion();
    }

    /**
     * Lower-level before-commit method - phase 0.
     *
     * This is called before commit processing actually begins.
     * State transition:
     *        STATUS_ACTIVE        starting state
     *        startedCommit        set to avoid concurrent commits
     *        beforeCompletion()    called while still active
     *        STATUS_PREPARING    no longer active, about to "really" commit
     *
     * The startedCommit status is an "internal state" which is still active
     * but prevents concurrent commits and some other operations.
     *
     * For exceptions see commit() method.
     */
    private void commitBefore() {
        boolean        rollbackOnly = false; //marked for rollback
        boolean        notified = false;

        if (this.tracing)
            this.traceCall("commitBefore"); // NOI18N
        //
        // Validate transaction state before we commit
        //

        if ((this.status == Status.STATUS_ROLLING_BACK)
            ||    (this.status == Status.STATUS_ROLLEDBACK)) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.rolledback", // NOI18N
                "commit", // NOI18N
                this.statusString(this.status)));
        }

        if (this.status == Status.STATUS_MARKED_ROLLBACK) {
            rollbackOnly = true;
        } else if (this.status != Status.STATUS_ACTIVE) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.commit_rollback.notactive", // NOI18N
                "commit", // NOI18N
                this.statusString(this.status)));
        }

        if (this.startedCommit) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.commitbefore.incommit", // NOI18N
                "commit")); // NOI18N
        }
        this.startedCommit = true;

        //
        // User notifications done outside of lock - check for concurrent
        // rollback or setRollbackOnly during notification.
        //
        if (!rollbackOnly) {
            this.notifyBeforeCompletion();
            notified = true;

            if (this.status == Status.STATUS_ACTIVE) {        // All ok
                this.setStatus(Status.STATUS_PREPARING);
            } else if (this.status == Status.STATUS_MARKED_ROLLBACK) {
                rollbackOnly = true;
            } else {    // Must have been concurrently rolled back
                throw new JDOUserException(I18NHelper.getMessage(messages,
                    "transaction.transactionimpl.commitbefore.rolledback")); // NOI18N
            }
        }
        if (rollbackOnly && txType == NON_MGD) {
            this.rollback();

            throw new JDOUserException(I18NHelper.getMessage(messages,
                notified ?
                   "transaction.transactionimpl.commitbefore.rollbackonly_insync" : // NOI18N
                   "transaction.transactionimpl.commitbefore.rollbackonly")); // NOI18N
        }
    }

    /**
     * Lower-level prepare-commit method - phase 1.
     *
     * This is called once we've started "real commit" processing.
     * State transition:
     *        STATUS_PREPARING    starting state
     *        prepareResources()        if not one-phase
     *        STATUS_PREPARED
     *
     * For exceptions see commit() method.
     */
    private void commitPrepare() {
        if (this.tracing)
            this.traceCall("commitPrepare"); // NOI18N
        //
        // Once we've reached the Status.STATUS_PREPARING state we do not need
        // to check for concurrent state changes.  All user-level methods
        // (rollback, setRollbackOnly, register, enlist, etc) are no longer
        // allowed.
        //

        //
        // Validate initial state
        //
        if (this.status != Status.STATUS_PREPARING) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
               "transaction.transactionimpl.commitprepare.wrongstatus", // NOI18N
                "commitPrepare",  // NOI18N
                "STATUS_PREPARING", // NOI18N
                this.statusString(this.status)));
        }

        //
        // If there is at most one resource then we can do this in 1-phase.
        //
        if (this.resources.size() <= 1)
            this.onePhase = true;

        /*
        //
        // Prepare resources if not one-phase
        //
        if (!this.onePhase) {
            int    error = this.prepareResources();
            if (error != XAResource.XA_OK) {
                this.forceRollback();

                throw (RollbackException)ErrorManager.createFormatAdd(
                        RollbackException.class,
                        ErrorManager.USER,
                        TransactionMsgCat.SH_ERR_TX_TR_CMT_RB_IN_PREP_XA,
                        this.XAErrorString(error));
            }
        }
        */

        this.setStatus(Status.STATUS_PREPARED);
    }

    /**
     * Lower-level complete-commit method - phase 2.
     *
     * State transition:
     *        STATUS_PREPARED        starting state
     *        STATUS_COMMITTING    starting to do final phase
     *        commitResources()        maybe one-phase
     *        STATUS_COMMITTED
     *        afterCompletion()    no longer active
     *
     * For exceptions see commit() method.
     */
    private void commitComplete() {
        if (this.tracing)
            this.traceCallInfo("commitComplete", TRACE_ONE_PHASE, null); // NOI18N

        //
        // Validate initial state
        //
        if (this.status == Status.STATUS_ROLLING_BACK) {
            this.setStatus(Status.STATUS_ROLLING_BACK); //st);
            internalRollback();
        } else if (this.status == Status.STATUS_PREPARED) {
            this.setStatus(Status.STATUS_COMMITTING);
            internalCommit();
        } else {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.commitprepare.wrongstatus", // NOI18N
                "commitComplete",  // NOI18N
                "STATUS_PREPARED", // NOI18N
                this.statusString(this.status)));
        }
    }

    /**
     * Internal method to process commit operation
     */
    private void internalCommit() {
        /*
         * CODE FOR XAResource support
        //
        // Commit resources.  If onePhase then we can expect a rollback
        // indication so be prepared to deal with that.
        //
        int    error = this.commitResources();
        if (error != XAResource.XA_OK) {
            this.forceRollback();

            throw (RollbackException)ErrorManager.createFormatAdd(
                    RollbackException.class,
                    ErrorManager.USER,
                    TransactionMsgCat.SH_ERR_TX_TR_CMT_RB_IN_CMT_COMP_XA,
                    this.XAErrorString(error));
        }
         * END CODE FOR XAResource support
         */

        if (txType == NON_MGD) {
            int     error = this.commitConnection();
            if (error != INTERNAL_OK) {
                this.forceRollback();
                throw new JDOUserException(I18NHelper.getMessage(messages,
                    "transaction.transactionimpl.commitcomplete.error", // NOI18N
                    "Connection Error")); // NOI18N
            }
            this.closeConnection();
        }
        this.setStatus(Status.STATUS_COMMITTED);
    }

    /**
     * Rollback the transaction represented by this transaction object.
     *
     */
    public void rollback() {

        persistenceManager.acquireExclusiveLock();

        try {
            if (txType == CMT || txType == BMT_UT) {
                // Error - should not be called
                throw new JDOUserException(I18NHelper.getMessage(messages,
                     "transaction.transactionimpl.mgd", "rollback")); //NOI18N
            }

            this.setTrace();

            if (this.tracing)
                this.traceCall("rollback"); // NOI18N

            if ((this.status != Status.STATUS_ACTIVE)
                &&    (this.status != Status.STATUS_MARKED_ROLLBACK)) {
                //
                // Once commit processing has started (PREPARING, COMMITTING
                // or COMITTED) the only way to rollback a transaction is
                // via the registered resource interface throwing an
                // XAException, which will use a lower-level rollback.
                //

                throw new JDOUserException(I18NHelper.getMessage(messages,
                    "transaction.transactionimpl.commit_rollback.notactive", // NOI18N
                    "rollback", // NOI18N
                    this.statusString(this.status)));
            }

            this.setStatus(Status.STATUS_ROLLING_BACK);

            this.internalRollback();
            this.closeConnection();

            if (txType == BMT_JDO) {
                // Send request to the container:
                try {
                    EJBHelper.getLocalTransactionManager().rollback();
                } catch (Exception e) {
                    throw new JDOException("", e); // NOI18N
                }
            } else { //NON_MGD
                //This has effect of rolling back changes also
                //which would not happen in case of BMT_JDO
                //Is this the desired behavior ?
                //TransactionImpl.notifyAfterCompletion()
                //PersistenceManagerImp.afterCompletion()
                //SQLStateManager.rollback()
                this.notifyAfterCompletion();
            }
        } finally {
            persistenceManager.releaseExclusiveLock();
        }
    }

    /**
     * Lower-level rollback method.  This is expected to be called once
     * the caller has confirmed that itself has set the status to
     * STATUS_ROLLING_BACK.  This is to avoid concurrent rollbacks.
     *
     */
    private void internalRollback() {
        if (this.tracing)
            this.traceCall("internalRollback"); // NOI18N

        if (this.status == Status.STATUS_ROLLEDBACK) {
            return;
        }

        if (this.status != Status.STATUS_ROLLING_BACK) {
            throw new JDOUserException(I18NHelper.getMessage(messages,
                "transaction.transactionimpl.commitprepare.wrongstatus", // NOI18N
                "internalRollback",  // NOI18N
                "STATUS_ROLLING_BACK", // NOI18N
                this.statusString(this.status)));
        }

        if (txType == NON_MGD) {
            this.rollbackConnection();
        }

        this.setStatus(Status.STATUS_ROLLEDBACK);
    }

    /**
     * [package-visible]
     *
     * Force rollback.  This is called when something goes wrong during
     * a late state check (i.e. some failure occurred during the prepare
     * stage).  Unless we're not already rolling back (or rolled back) this
     * will blindly change the state of the transaction and complete the
     * latter stage of rollback.
     *
     * @return the final status of the transaction.
     *
     * See internalRollback() for exceptions
     */
    int forceRollback() {
        if (this.tracing)
            this.traceCall("forceRollback"); // NOI18N

        if ((this.status == Status.STATUS_ROLLING_BACK)        // Already
            ||    (this.status == Status.STATUS_ROLLEDBACK)        // Done
            ||    (this.status == Status.STATUS_COMMITTED)        // Too late
            ||    (this.status == Status.STATUS_NO_TRANSACTION)    // Never was
           ) {
            return this.status;
        }
        this.internalRollback();
        this.notifyAfterCompletion();

        return this.status;
    }

    /**
     * Modify the transaction object such that the only possible outcome of
     * the transaction is to roll back.
     *
     */
    public void setRollbackOnly() {
        if (this.tracing)
            this.traceCall("setRollbackOnly"); // NOI18N

        if ((this.status == Status.STATUS_ROLLING_BACK)
                ||    (this.status == Status.STATUS_ROLLEDBACK)
                ||     (this.status == Status.STATUS_MARKED_ROLLBACK)) {
            //
            // Already rolled back, rollback in progress or already marked.
            //
            return;
        }

        if (txType != NON_MGD) {
            try {
                jta.setRollbackOnly();
            } catch (Exception e) {
                throw new JDOException("", e); // NOI18N
            }
        } else {
            this.setStatus(Status.STATUS_MARKED_ROLLBACK);
        }

    }

    /**
     * Obtain the status of this transaction object.
     *
     * @return The transaction status.
     *
     */
    public int getStatus() {
        //
        // Leave this as "synchronized" as I want to test that from callbacks
        // (i.e. beforeCompletion, xaRes.prepare) the lock is released, and
        // that other threads can access this object.  See note at top of file.
        //
        synchronized (this.globalLock) {
            return this.status;
        }
    }

    //
    // ----- Methods introduced in com.forte.transaction.ForteTran ------
    //


    /**
     * Confirm that transaction is terminated.
     *
     * @return True if transaction is completed.
     */
    boolean isTerminated() {
        synchronized (this.globalLock) {
            return ((this.status == Status.STATUS_COMMITTED)
                ||    (this.status == Status.STATUS_ROLLEDBACK)
                ||    (this.status == Status.STATUS_NO_TRANSACTION));
        }
    }

    //
    // ----- Private methods ------
    //

    /**
     * Notify registered Synchronization interfaces with beforeCompletion().
     * This method is only called before a commit so we stop iterating if
     * we encounter an invalid status.  Which could have been set by a
     * rollback during (or concurrent with) the notification.
     */
    //
    // CHANGES
    //     18-jan-1999
    //        written (ncg)
    //
    private void notifyBeforeCompletion() {
        if (synchronization != null) {
            try {
                synchronization.beforeCompletion();
            } catch (Exception ex) {
                //
                // RESOLVE: ignored ?
                //
            }
        }
        persistenceManager.beforeCompletion();
    }

    /**
     * Notify registered Synchronization interfaces with afterCompletion().
     * We assume that no status changes occur while executing this method.
     */
    private void notifyAfterCompletion() {
        try {
            persistenceManager.afterCompletion(this.status);

        } finally {
            if (synchronization != null) {
                try {
                    synchronization.afterCompletion(this.status);
                } catch (Exception ex) {
                    logger.log(Logger.WARNING, I18NHelper.getMessage(
                         messages,
                         "transaction.transactionimpl.syncmanager.aftercompletion", // NOI18N
                         ex.getMessage()));
                }
            }
        }

        this.forget();
    }



    /**
     * Set status under lock (may be a nested lock which is ok)
     */
    //
    // CHANGES
    //     27-jan-1999
    //        created (ncg)
    //
    private void setStatus(int status) {
        synchronized(this.globalLock) {
            if (this.tracing) {
                Object[] items= new Object[] {Thread.currentThread(),this.toString(), 
                    this.statusString(this.status), this.statusString(status), persistenceManager};
                logger.finest("sqlstore.transactionimpl.status",items); // NOI18N
            }
            this.status = status;
            persistenceManager.notifyStatusChange(isActive());
        }
    }

    /**
     * Forget this transaction
     */
    private void forget() {
        if (this.tracing)
            this.traceCall("forget"); // NOI18N

        //
        // Do not clear:
        //
        //    .tid            -- users can still browse transaction id
        //    .status            -- users can still check status
        //    .timeout        -- users can still check timeout
        //    .onePhase         -- remember if was committed 1-phase
        //
        this.threads = 0;
        this.startedCommit = false;

        // SHOULD BE CLOSED: closeConnection();
        if (_connection != null) {
            try {
                if (!_connection.isClosed()) {
                    closeConnection();
                    throw new JDOFatalInternalException(I18NHelper.getMessage(
                        messages, 
                        "transaction.transactionimpl.forget.connectionnotclosed")); // NOI18N
                }
            } catch (Exception e) {
            }
            _connection = null;
        }
        _connectionReferenceCount = 0;

        //
        // Do we need to invoke forget on the resource?
        //
        this.resources.clear();
            if (txType != NON_MGD) {
            persistenceManager.close();
        }

        jta = null;
        txType = NON_MGD;       // Restore the flag

        this.setTrace();
    }

    //
    // ----- Debugging utilities -----
    //

    /**
     * Set the global transaction tracing.
     */
    static void setTrace() {
        TransactionImpl.tracing = logger.isLoggable(Logger.FINEST);
    }

    /**
     * Trace method call.
     */
    private void traceCall(String call) {
        Object[] items = new Object[]{Thread.currentThread(),this.toString(),call,
             this.statusString(this.status),txTypeString(), persistenceManager};
        logger.finest("sqlstore.transactionimpl.call",items); // NOI18N
    }

    /**
     * Trace method call with extra info.
     */
    //
    // CHANGES
    //     11-jan-1999
    //        created (ncg)
    //
    private void traceCallInfo(String call, int info, String s) {

        //TODO : Optimize this when converting to resource budles
        StringBuffer logMessage = new StringBuffer();
        logMessage.append("Thread.currentThread()").append("Tran[") // NOI18N
            .append(this.toString()).append("].").append(call) // NOI18N
            .append(": status = ").append(this.statusString(this.status)); // NOI18N

        if ((info & TRACE_THREADS) != 0)
            logMessage.append(", threads = " + this.threads); // NOI18N
        if ((info & TRACE_SYNCHRONIZATIONS) != 0)
            logMessage.append(", sync = " + this.synchronization); // NOI18N
        if ((info & TRACE_RESOURCES) != 0)
            logMessage.append(", resources = " + this.resources.size()); // NOI18N
        if ((info & TRACE_ONE_PHASE) != 0 && this.onePhase)
            logMessage.append(", onePhase = true"); // NOI18N
        if (s != null)
            logMessage.append(", " + s + " for " + persistenceManager); // NOI18N
        
        logger.finest("sqlstore.transactionimpl.general",logMessage.toString()); // NOI18N
    }

    /**
     * Trace method call with extra string.
     */
    private void traceCallString(String call, String info) {
      Object[] items = new Object[] {Thread.currentThread(),this.toString(),call,info,persistenceManager};
      logger.finest("sqlstore.transactionimpl.call.info",items); // NOI18N
    }

    /**
     * Translates a txType value into a string.
     *
     * @return  Printable String for a txType value
     */
    private String txTypeString() {
        switch (txType) {
            case NON_MGD:                   return "NON_MGD"; // NOI18N
            case CMT:                       return "CMT"; // NOI18N
            case BMT_UT:                    return "BMT_UT"; // NOI18N
            case BMT_JDO:                   return "BMT_JDO"; // NOI18N
            default:                        break;
        }
        return "UNKNOWN"; // NOI18N
    }

    /**
     * Translates a javax.transaction.Status value into a string.
     *
     * @param   status   Status object to translate.
     * @return  Printable String for a Status object.
     */
    public static String statusString(int status) {
        switch (status) {
            case Status.STATUS_ACTIVE:            return "STATUS_ACTIVE"; // NOI18N
            case Status.STATUS_MARKED_ROLLBACK:   return "STATUS_MARKED_ROLLBACK"; // NOI18N
            case Status.STATUS_PREPARED:          return "STATUS_PREPARED"; // NOI18N
            case Status.STATUS_COMMITTED:         return "STATUS_COMMITTED"; // NOI18N
            case Status.STATUS_ROLLEDBACK:        return "STATUS_ROLLEDBACK"; // NOI18N
            case Status.STATUS_UNKNOWN:           return "STATUS_UNKNOWN"; // NOI18N
            case Status.STATUS_NO_TRANSACTION:    return "STATUS_NO_TRANSACTION"; // NOI18N
            case Status.STATUS_PREPARING:         return "STATUS_PREPARING"; // NOI18N
            case Status.STATUS_COMMITTING:        return "STATUS_COMMITTING"; // NOI18N
            case Status.STATUS_ROLLING_BACK:      return "STATUS_ROLLING_BACK"; // NOI18N
            default:                              break;
        }
        return "STATUS_Invalid[" + status + "]"; // NOI18N
    }

    /**
     * Returns a Connection. If there is no existing one, asks
     * ConnectionFactory for a new Connection
     */
    public synchronized Connection getConnection() {
        boolean debug = logger.isLoggable(Logger.FINEST);

        if (_connection == null) {
            // find a new connection
            if (connectionFactory == null) {
                throw new JDOFatalInternalException(I18NHelper.getMessage(messages, 
                    "transaction.transactionimpl.getconnection.nullcf")); // NOI18N
            }

            _connection = this.getConnectionInternal();
        }

        _connectionReferenceCount++;

        if (debug) {
            Object[] items = new Object[] {_connection, Boolean.valueOf(optimistic), 
                new Integer(_connectionReferenceCount) , persistenceManager};
            logger.finest("sqlstore.transactionimpl.getconnection",items); // NOI18N
        }

        // We cannot depend on NON_MGD flag here as this method can be called
        // outside of an active transaction.
        if (!EJBHelper.isManaged()) {
            try {
                //
                // For active pessimistic transaction or a committing transaction, we need to set
                // auto-commit feature off.
                //
                if ((!optimistic && isActive()) || startedCommit) {
                    // Set autocommit to false *only* if it's true
                    // I.e., don't set to false multiple times (Sybase
                    // throws exception in that case).
                    if (_connection.getAutoCommit()) {
                        _connection.setAutoCommit(false);
                    }
                } else {
                    _connection.setAutoCommit(true);
                }
            } catch (java.sql.SQLException e) {
                logger.log(Logger.WARNING,"sqlstore.exception.log",e);  // NOI18N
            }
        }

        return _connection;
    }

    /**
     * Replace a connection. Used in a managed environment only.
     * In a J2EE RI Connection need to be replaced at the beforeCompletion.
     */
    public void replaceConnection() {
        if (EJBHelper.isManaged()) {
            this.releaseConnection();
            this.closeConnection();
            this.getConnection();
        }
    }

    /**
     * Close a connection.
     * Connection cannot be closed if it is part of the commit/rollback
     * operation or inside a pessimistic transaction
     */
    public synchronized void releaseConnection() {
        boolean debug = logger.isLoggable(Logger.FINEST);

        if (_connectionReferenceCount > 0) {
            _connectionReferenceCount--;
        }

        if (debug) {
            Object[] items = new Object[] {Boolean.valueOf(optimistic),
                Boolean.valueOf(startedCommit), 
                new Integer(_connectionReferenceCount) , persistenceManager};
            logger.finest("sqlstore.transactionimpl.releaseconnection",items); // NOI18N
        }

        // Fix for bug 4479807: Do not keep connection in the managed environment.
        if ( (!EJBHelper.isManaged() && optimistic == false) || startedCommit ) {
            // keep Connection. Do not close.
            return;
        }

        if (_connectionReferenceCount == 0) {
            //
            // For optimistic transaction, we only release the connection when
            // no one is using it.
            //
            closeConnection();
        }
    }

    private Connection getConnectionInternal() {
        if (isDataSource) {
            try {
                if (EJBHelper.isManaged()) {
                    // Delegate to the EJBHelper for details.
                    if (isActive()) {
                        return EJBHelper.getConnection(connectionFactory,
                            username, password);
                    } else {
                        return EJBHelper.getNonTransactionalConnection(
                            connectionFactory, username, password);
                    }
                } else if (username != null) {
                    return ((DataSource)connectionFactory).getConnection(
                        username, password);
                } else {
                     return ((DataSource)connectionFactory).getConnection();
                }

            } catch (java.sql.SQLException e) {
                String sqlState = e.getSQLState();
                int  errorCode = e.getErrorCode();

                if (sqlState == null) {
                    throw new JDODataStoreException(
                        I18NHelper.getMessage(messages,
                            "connectionefactoryimpl.sqlexception", // NOI18N
                            "null", "" + errorCode), e); // NOI18N
                } else {
                    throw new JDODataStoreException(
                        I18NHelper.getMessage(messages,
                            "connectionefactoryimpl.sqlexception", // NOI18N
                            sqlState, "" + errorCode), e); // NOI18N
                }
            }
        } else {
            return ((ConnectionFactory)connectionFactory).getConnection();
        }
    }

    /**
     * Always Close a connection
     */
    private void closeConnection() {
        boolean debug = logger.isLoggable(Logger.FINEST);
        if (debug) {
            Object[] items = new Object[] {_connection , persistenceManager};
            logger.finest("sqlstore.transactionimpl.closeconnection",items); // NOI18N
        }
        try {
            if (_connection != null) {
                _connection.close();
            }
        } catch (Exception e) {
            // Recover?
        }
        _connection = null;
    }

    /**
     * replaces rollbackResources() in ForteTran
     */
    private void rollbackConnection() {
        boolean debug = logger.isLoggable(Logger.FINEST);
        if (debug) {
            Object[] items = new Object[] {_connection , persistenceManager};
            logger.finest("sqlstore.transactionimpl.rollbackconnection",items); // NOI18N
        }
        if (_connection != null) {
            try {
                if (isDataSource)
                    _connection.rollback();
                else
                    ((ConnectionImpl)_connection).internalRollback();
            } catch (Exception e) {
                //Recover?
            }
        }
    }
    private int INTERNAL_ERROR = 1;
    private int INTERNAL_OK = 0;

    /**
     * replaces commitResources() in ForteTran
     */
    private int commitConnection() {
        if (_connection != null) {
            try {
                if (isDataSource)
                    _connection.commit();
                else
                    ((ConnectionImpl)_connection).internalCommit();
            } catch (Exception e) {
                return INTERNAL_ERROR;
            }
        }
        return INTERNAL_OK;
    }


    /**
     * Returns a string representation of this transaction object.
     *
     * @return  String describing contents of this Transaction object.
     */
    public String toString() {
        int        i;
        Object    o;

        String    s = "  Transaction: \n   status        = " + this.statusString(this.status)+ "\n" // NOI18N
                  +    "   Transaction Object       = Transaction@" + this.hashCode() + "\n" // NOI18N
                  +    "   threads       = " + this.threads + "\n"; // NOI18N

        if (this.timeout != 0)
            s = s + "   timeout       = " + this.timeout + "\n"; // NOI18N
        if (this.startedCommit)
            s = s +    "   startedCommit = true\n"; // NOI18N
        if (this.onePhase)
            s = s +    "   onePhase      = true\n"; // NOI18N

        if (synchronization != null) {
            s = s + "sync:     " + synchronization + "\n"; // NOI18N
        }
        if (!this.resources.isEmpty()) {
            s = s +    "   # resources   = " + this.resources.size() + "\n"; // NOI18N
            /*
            for (i = 0; i < this.resources.size(); i++) {
                XAResource        res = (XAResource)this.resources.get(i);
                //
                // Make be null if vote readonly during commit processing
                // Bug 48325: avoid recursive toString() calls
                //
                if (res != null) {
                    s = s + "    [" + i + "] " + // NOI18N
                        res.getClass().getName() + "\n"; // NOI18N
                }
            }
            */
        }
        return s;

    }
}
