/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2000-2009 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 mq/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 mq/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.
 */

package com.sun.messaging.bridge.service.jms.tx;

import java.util.Map;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.logging.Logger;
import java.util.logging.Level;
import javax.transaction.*;
import javax.transaction.xa.XAResource;
import java.lang.IllegalStateException;
import java.lang.SecurityException;
import com.sun.messaging.bridge.service.jms.tx.log.GlobalXidDecision;
import com.sun.messaging.bridge.service.jms.tx.log.BranchXidDecision;
import com.sun.messaging.bridge.service.jms.tx.log.LogRecord;
import com.sun.messaging.bridge.service.jms.tx.log.TxLog;

/**
 * Implements JTA Transaction interface 
 *
 * @author amyk
 */

public class TransactionImpl implements Transaction {

    private Logger _logger = null;

    private int _status = Status.STATUS_NO_TRANSACTION;

    private GlobalXid _gxid = null;

    private TransactionManagerImpl _tm = null;

    private ArrayList<BranchXid> _seenBranchXids = new ArrayList<BranchXid>();
    private ArrayList<XAResource> _associatedXAResources = new ArrayList<XAResource>();

    private Map<XAResource, XAParticipant> _participants = 
                            new LinkedHashMap<XAResource, XAParticipant>();

    private byte _branchCount = 0;
    private int _maxBranches;

    private TxLog _txlog = null;

    public TransactionImpl(GlobalXid xid, TransactionManagerImpl tm) throws SystemException {
        if (xid == null) {
            throw new SystemException("null xid");
        }
        _tm = tm;
        _logger = tm.getLogger();
        _txlog = tm.getTxLog();
        _gxid = xid;
        _maxBranches = tm.getMaxBranches();
        _branchCount = 0;
        _status = Status.STATUS_ACTIVE;
    }

    /**
     * Complete the transaction represented by this Transaction object.
     *
     * @exception RollbackException Thrown to indicate that
     *    the transaction has been rolled back rather than committed.
     *
     * @exception HeuristicMixedException Thrown to indicate that a heuristic
     *    decision was made and that some relevant updates have been committed
     *    while others have been rolled back.
     *
     * @exception HeuristicRollbackException Thrown to indicate that a
     *    heuristic decision was made and that all relevant updates have been
     *    rolled back.
     *
     * @exception SecurityException Thrown to indicate that the thread is
     *    not allowed to commit the transaction.
     *
     * @exception IllegalStateException Thrown if the transaction in the 
     *    target object is inactive.
     *
     * @exception SystemException Thrown if the transaction manager
     *    encounters an unexpected error condition.
     */
    public void commit() throws RollbackException,
                HeuristicMixedException, HeuristicRollbackException,
                SecurityException, IllegalStateException, SystemException {

        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, _tm+" commit "+this);
        }
        if (_status == Status.STATUS_MARKED_ROLLBACK) {
            rollback();
            throw new RollbackException(toString());
        }
        if (_status != Status.STATUS_ACTIVE) {
            throw new IllegalStateException(toString());
        }
        if (_associatedXAResources.size() != 0) {
            throw new IllegalStateException(
            "There are undelisted XAResources "+_associatedXAResources+" in "+this);  
        }

        boolean onePhase = false;
        if (_participants.size() == 1) {
            onePhase = true;
        }
        if (!onePhase) {
            _status = Status.STATUS_PREPARING;
            XAParticipant party  = null;
            boolean preparedOne = false;
            for (Map.Entry<XAResource, XAParticipant> pair: _participants.entrySet()) {
                party = pair.getValue();
                try { 
                    party.prepare();
                    preparedOne = true;
                } catch (IllegalStateException e) {
                    if (!preparedOne) throw e;
                    setRollbackOnly();
                    rollback();
                    RollbackException ex = new RollbackException(toString()+": "+e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } catch (Throwable t) {
                    setRollbackOnly();
                  
                    if (!(t instanceof RollbackException) && 
                        !(t instanceof SystemException)) {
                        _logger.log(Level.SEVERE, "Unexpected exception on prepare from "+party, t); 
                    }
                    rollback();
                    RollbackException ex = new RollbackException(t.getMessage());
                    ex.initCause(t);
                    throw ex;
                }
            }
            _status = Status.STATUS_PREPARED;
        }
        if (_status == Status.STATUS_MARKED_ROLLBACK) {
            rollback();
            throw new RollbackException(toString());
        }
        if (!onePhase) {
            try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(),  GlobalXidDecision.COMMIT);
                _txlog.logGlobalDecision(lr);
            } catch (Throwable t) {
                setRollbackOnly();
                _logger.log(Level.SEVERE, "Unable to log commit decision "+this, t);
                rollback();
                RollbackException ex = new RollbackException(toString()+": "+t.getMessage());
                ex.initCause(t);
                throw ex;
            }
            _status = Status.STATUS_PREPARED;
        }  
        _status = Status.STATUS_COMMITTING;
        Exception ex = null;
        XAParticipant party = null;
        boolean committedOne = false;
        for (Map.Entry<XAResource, XAParticipant> pair: _participants.entrySet()) {
            party= pair.getValue();
            try { 
                party.commit(onePhase);
                committedOne = true;
            } catch (IllegalStateException e) {
                if (!committedOne) throw e;
                ex = e;
                continue;
            } catch (RollbackException e) {
                if (!onePhase) {
                    _logger.log(Level.SEVERE, 
                    "Unexpected RollbackException on 2-phase commit from "+party, e);
                    ex = new HeuristicMixedException(e.getMessage());
                    ex.initCause(e);
                    continue;
                } else {
                    throw e;
                }
            } catch (HeuristicCommitException e) {
                try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(), GlobalXidDecision.COMMIT);
                BranchXid bxid = party.getBranchXid();
                lr.setBranchHeurCommit(bxid);
                _txlog.logHeuristicBranch(bxid, lr);
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Unable to log heuristic commit from "+party, t);
                }
                continue;
            } catch (HeuristicMixedException e) {
                ex = e;
                try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(), GlobalXidDecision.COMMIT);
                BranchXid bxid = party.getBranchXid();
                lr.setBranchHeurMixed(bxid);
                _txlog.logHeuristicBranch(bxid, lr);
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Unable to log heuristic mixed from "+party, t);
                }
                continue;
            } catch (HeuristicRollbackException e) {
                ex = e;
                try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(), GlobalXidDecision.COMMIT);
                BranchXid bxid = party.getBranchXid();
                lr.setBranchHeurRollback(bxid);
                _txlog.logHeuristicBranch(bxid, lr);
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Unable to log heuristic rollback from "+party, t);
                }
                continue;
            } catch (Throwable t) {
                if (!(t instanceof SystemException)) {
                    _logger.log(Level.SEVERE, "Unexpected exception on commit from "+party, t);
                    ex = new SystemException(t.getMessage());
                    ex.initCause(t);
                } else {
                   ex = (SystemException)t;
                }
                continue;
            }
        }
        _status =  Status.STATUS_COMMITTED;
        if (ex == null) {
            _status = Status.STATUS_NO_TRANSACTION;
            if (!onePhase) {
                try {
                _txlog.remove(_gxid); 
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Exception in removing comitted TM log record "+_gxid, t);
                }
            }
        }
        if (ex != null) {
            if (ex instanceof SystemException) throw (SystemException)ex;
            if (ex instanceof HeuristicMixedException) throw (HeuristicMixedException)ex;
            if (ex instanceof IllegalStateException) throw (IllegalStateException)ex;
            if (ex instanceof HeuristicRollbackException) throw (HeuristicRollbackException)ex;
            SystemException uex =  new SystemException(ex.getMessage()); //should not hapen
            uex.initCause(ex);
            throw uex;
        }
    }

    /**
     * Disassociate the resource specified from the transaction associated 
     * with the target Transaction object.
     *
     * @param xaRes The XAResource object associated with the resource 
     *              (connection).
     *
     * @param flag One of the values of TMSUCCESS, TMSUSPEND, or TMFAIL.
     *
     * @exception IllegalStateException Thrown if the transaction in the
     *    target object is inactive.
     *
     * @exception SystemException Thrown if the transaction manager
     *    encounters an unexpected error condition.
     *
     * @return <i>true</i> if the resource was delisted successfully; otherwise
     *	  <i>false</i>.
     *
     */
    public boolean delistResource(XAResource xaRes, int flag)
        throws IllegalStateException, SystemException {

        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, _tm+" dellistResource "+xaRes+" from "+this);
        }
        if (_status != Status.STATUS_ACTIVE && 
            _status != Status.STATUS_MARKED_ROLLBACK) {
            throw new IllegalStateException(toString());
        }

        XAParticipant party = _participants.get(xaRes);
        if (party == null) {
            throw new IllegalStateException(
            "XAResource "+xaRes+" had not associated to "+this);
        }

        int flags = flag;
        if (_status == Status.STATUS_MARKED_ROLLBACK) {
            flags = XAResource.TMFAIL;
        }
         
        try {
            party.end(flags);
            _associatedXAResources.remove(xaRes);
            return true;
        } catch (IllegalStateException e) {
            throw e;
        } catch (RollbackException e) {
            setRollbackOnly();
            return true;
        } catch (Throwable t) {
            setRollbackOnly();
            if (t instanceof SystemException) throw (SystemException)t;
            _logger.log(Level.SEVERE, "Unexpected exception occurred on end from "+party, t);
            SystemException ex = new SystemException(t.getMessage());
            ex.initCause(t);
            throw ex;
        }
    }

    /**
     * Enlist the resource specified with the transaction associated with the 
     * target Transaction object.
     *
     * @param xaRes The XAResource object associated with the resource 
     *              (connection).
     *
     * @return <i>true</i> if the resource was enlisted successfully; otherwise
     *    <i>false</i>.
     *
     * @exception RollbackException Thrown to indicate that
     *    the transaction has been marked for rollback only.
     *
     * @exception IllegalStateException Thrown if the transaction in the
     *    target object is in the prepared state or the transaction is
     *    inactive.
     *
     * @exception SystemException Thrown if the transaction manager
     *    encounters an unexpected error condition.
     *
     */
    public boolean enlistResource(XAResource xaRes)
        throws RollbackException, IllegalStateException,
        SystemException {

        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, _tm+" enlistResource "+xaRes+" to "+this);
        }

        if (_status ==  Status.STATUS_MARKED_ROLLBACK) {
            throw new RollbackException(toString());
        }

        if (_status != Status.STATUS_ACTIVE) { 
            throw new IllegalStateException(toString());
        }

        if (_participants.get(xaRes) != null) {
            throw new IllegalStateException("XAResource "+xaRes+" already enlisted in "+this);
        }

        String rm = null;
        try {
            rm = _tm.getRM(xaRes);
        } catch (Exception e) {
            String emsg = "Enlist XAResource "+xaRes+" failed";
            _logger.log(Level.SEVERE, emsg, e);
            SystemException ex = new SystemException(emsg);
            ex.initCause(e);
            throw ex;
        }
        if (rm == null) {
            setRollbackOnly();
            throw new SystemException(
            "No RM is registered for XAResource "+xaRes);
        }
        BranchXid bxid = _tm.genBranchXid(_gxid, rm, xaRes.getClass().getName(), getBranchCount());
        if (_seenBranchXids.contains(bxid)) {
            throw new SystemException(
            "Unexpected duplicated branch "+bxid+" for RM "+rm+" in "+_gxid); 
        }
        _seenBranchXids.add(bxid);
        XAParticipant party = new XAParticipant(rm, xaRes, bxid);
        party.setLogger(_logger);
        _participants.put(xaRes, party); 
        _associatedXAResources.add(xaRes); 
        try {
            party.start(XAResource.TMNOFLAGS);
            return true;
        } catch (IllegalStateException e) {
            throw e;
        } catch (Throwable t) {
            setRollbackOnly();
            if (t instanceof RollbackException) throw (RollbackException)t;
            if (t instanceof SystemException) throw (SystemException)t;
            _logger.log(Level.SEVERE, "Unexpected exception occurred on start from "+party, t);
            SystemException ex = new SystemException(t.getMessage());
            ex.initCause(t);
            throw ex;
        }
    }


    private synchronized byte getBranchCount() throws SystemException {
        _branchCount++;
        if (_branchCount > _maxBranches) {
            throw new SystemException(
            "Number of branches "+_branchCount+" exceeded max "+_maxBranches);
        }
        return _branchCount;
    }

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

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

    /**
     * Rollback the transaction represented by this Transaction object.
     *
     * @exception IllegalStateException Thrown if the transaction in the
     *    target object is in the prepared state or the transaction is
     *    inactive.
     *
     * @exception SystemException Thrown if the transaction manager
     *    encounters an unexpected error condition.
     *
     */
	public void rollback() throws IllegalStateException, SystemException {
        if (_logger.isLoggable(Level.FINE)) {
            _logger.log(Level.FINE, _tm+" rollback "+this);
        }
        if (_status != Status.STATUS_ACTIVE &&
            _status != Status.STATUS_MARKED_ROLLBACK) {
            throw new IllegalStateException(toString()); 
        }
        if (_associatedXAResources.size() > 0) {
            XAResource[] xars = _associatedXAResources.toArray(new XAResource[]{});
            for (int i = 0;  i < xars.length; i++) {
                try {
                    delistResource(xars[i], XAResource.TMFAIL);
                } catch (IllegalStateException e) {
                    throw e;
                } catch (Throwable t) {
                    _logger.log(Level.WARNING, "Unable to delist resource "+xars[i]+" for rollback", t);
                }
            }
        }
        _status = Status.STATUS_ROLLING_BACK;
        Exception ex = null;
        XAParticipant party = null;
        boolean rolledbackOne = false;
        for (Map.Entry<XAResource, XAParticipant> pair: _participants.entrySet()) {
            party = pair.getValue();
            try {
                party.rollback();
                rolledbackOne = true;
            } catch (IllegalStateException e) {
                if (!rolledbackOne) throw e;
                ex = new SystemException(e.getMessage());  
                ex.initCause(e);
                continue;
            } catch (HeuristicCommitException e) {
                ex = e;
                try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(), GlobalXidDecision.ROLLBACK);
                BranchXid bxid = party.getBranchXid();
                lr.setBranchHeurCommit(bxid);
                _txlog.logHeuristicBranch(bxid, lr);
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Unable to log heuristic commit from "+party, t);
                }
                continue;
            } catch (HeuristicRollbackException e) {
                try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(), GlobalXidDecision.ROLLBACK);
                BranchXid bxid = party.getBranchXid();
                lr.setBranchHeurRollback(bxid);
                _txlog.logHeuristicBranch(bxid, lr);
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Unable to log heuristic rollback from "+party, t);
                }
                continue;
            } catch (HeuristicMixedException e) {
                ex = e;
                try {
                LogRecord lr = new LogRecord(_gxid, _participants.values(), GlobalXidDecision.ROLLBACK);
                BranchXid bxid = party.getBranchXid();
                lr.setBranchHeurMixed(bxid);
                _txlog.logHeuristicBranch(bxid, lr);
                } catch (Throwable t) {
                _logger.log(Level.WARNING, "Unable to log heuristic mixed from "+party, t);
                }
                continue;
            } catch (Throwable t) {
                if (!(t instanceof SystemException)) {
                   ex = new SystemException(t.getMessage());
                   ex.initCause(t);
                } else {
                   ex = (SystemException)t;
                }
                continue;
            }
        }
        _status  = Status.STATUS_ROLLEDBACK;
        if (ex == null) _status = Status.STATUS_NO_TRANSACTION;
        if (ex != null) { 
            if (ex instanceof SystemException) throw (SystemException)ex;
            if (ex instanceof IllegalStateException) throw (IllegalStateException)ex;
            SystemException uex =  new SystemException(ex.getMessage()); 
            uex.initCause(ex);
            throw uex;
        }
    }

    /**
     * Modify the transaction associated with the target object such that
     * the only possible outcome of the transaction is to roll back the
     * transaction.
     *
     * @exception IllegalStateException Thrown if the target object is
     *    not associated with any transaction.
     *
     * @exception SystemException Thrown if the transaction manager
     *    encounters an unexpected error condition.
     *
     */
    public void setRollbackOnly() throws IllegalStateException, SystemException {
        _status = Status.STATUS_MARKED_ROLLBACK;
    }

    private String statusString(int status) {
        switch (status) {

        case Status.STATUS_ACTIVE: return "STATUS_ACTIVE";
        case Status.STATUS_COMMITTED: return "STATUS_COMMITTED";
        case Status.STATUS_COMMITTING: return "STATUS_COMMITTING";
        case Status.STATUS_MARKED_ROLLBACK: return "STATUS_MARKED_ROLLBACK";
        case Status.STATUS_NO_TRANSACTION: return "STATUS_NO_TRANSACTION";
        case Status.STATUS_PREPARED: return "STATUS_PREPARED";
        case Status.STATUS_PREPARING: return "STATUS_PREPARING";
        case Status.STATUS_ROLLEDBACK: return "STATUS_ROLLEDBACK";
        case Status.STATUS_ROLLING_BACK: return "STATUS_ROLLING_BACK";
        case Status.STATUS_UNKNOWN: return "STATUS_UNKNOWN";
        default: return "STATUS_UNKNOWN";

        }
    }

    public boolean equals(Object o) {
        if (!(o instanceof TransactionImpl)) return false;
        TransactionImpl that = (TransactionImpl)o;
        if (this == that) return true;
        return _gxid.equals(that._gxid);
    }

    public int hashCode() {
        return _gxid.hashCode();
    }

    public String toString() {
        return _gxid+"["+statusString(_status)+"]";
    }

    protected Logger getLogger() {
        return _logger;
    }

    public String getGXidString() { 
        return _gxid.toString();
    }
 
}
