/*
 * $Header: /home/cvspublic/jakarta-slide/src/share/org/apache/slide/transaction/SlideTransaction.java,v 1.24 2004/11/26 23:37:42 ozeigermann Exp $
 * $Revision: 1.24 $
 * $Date: 2004/11/26 23:37:42 $
 *
 * ====================================================================
 *
 * Copyright 1999-2002 The Apache Software Foundation 
 *
 * Licensed under the Apache License, Version 2.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://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.apache.slide.transaction;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.RollbackException;
import javax.transaction.Status;
import javax.transaction.Synchronization;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.apache.slide.util.Messages;
import org.apache.slide.util.logger.Logger;

/**
 * JTA Transaction implementation.
 *
 * @version $Revision: 1.24 $
 */
public final class SlideTransaction implements Transaction {
    
    
    // -------------------------------------------------------------- Constants
    
    
    protected static final String LOG_CHANNEL =
        SlideTransaction.class.getName();
    
    
    // ------------------------------------------------------------ Constructor
    
    
    /**
     * Constructor.
     */
    public SlideTransaction(SlideTransactionManager transactionManager) {
        // Generate the transaction id
        globalCreatedTransactions++;
        currentTransactionNumber  = globalCreatedTransactions;
        currentThreadName         = Thread.currentThread().getName();
        xid = new SlideXid
            ((currentThreadName + "-" + System.currentTimeMillis() + "-"
                  + currentTransactionNumber).getBytes(),
             0, new byte[0]);
        this.transactionManager = transactionManager;
    }
    
    
    // ----------------------------------------------------- Instance Variables
    
    
    /**
     * Global transaction id.
     */
    private SlideXid xid;
    
    
    /**
     * Branches.
     * Keyed : branch xid -> resource manager.
     */
    private Hashtable branches = new Hashtable();
    
    
    /**
     * Active branches.
     * Keyed : resource manager -> branches xid.
     */
    private Hashtable activeBranches = new Hashtable();
    
    
    /**
     * Enlisted resources.
     */
    private Vector enlistedResources = new Vector();
    
    
    /**
     * Suspended resources.
     * Keyed : resource manager -> branches xid.
     */
    private Hashtable suspendedResources = new Hashtable();
    
    
    /**
     * Transaction status.
     */
    private int status = Status.STATUS_ACTIVE;
    
    
    /**
     * Synchronization objects.
     */
    private Vector synchronizationObjects = new Vector();
    
    
    /**
     * Branch counter.
     */
    private int branchCounter = 1;
    
    
    /**
     * Number of transactions created.
     */
    private static int globalCreatedTransactions = 0;
    
    
    /**
     * Transaction number.
     */
    private int currentTransactionNumber = 0;
    
    
    /**
     * Name of the thread bound to the transaction.
     */
    private String currentThreadName = null;
    
    
    /**
     * Associated transaction manager.
     */
    private SlideTransactionManager transactionManager;
    
    
    // ------------------------------------------------------------- Properties
    
    
    // ---------------------------------------------------- Transaction Methods
    
    
    /**
     * 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 some relevant updates have been
     * rolled back.
     * @exception SecurityException Thrown to indicate that the thread is not
     * allowed to commit the transaction.
     * @exception IllegalStateException Thrown if the current thread is not
     * associated with a transaction.
     * @exception SystemException Thrown if the transaction manager encounters
     * an unexpected error condition.
     */
    public void commit()
        throws RollbackException, HeuristicMixedException,
        HeuristicRollbackException, SecurityException, IllegalStateException,
        SystemException {
        
        //System.out.println(this + "  COMMIT ");
        
        if (status == Status.STATUS_MARKED_ROLLBACK) {
            rollback();
            return;
        }
        
        // Check status ACTIVE
        if (status != Status.STATUS_ACTIVE)
            throw new IllegalStateException();
        
        // Call synchronized objects beforeCompletion
        Enumeration syncList = synchronizationObjects.elements();
        while (syncList.hasMoreElements()) {
            Synchronization sync = (Synchronization) syncList.nextElement();
            sync.beforeCompletion();
        }
        
        Vector exceptions = new Vector();
        boolean fail = false;
        
        Enumeration keys = branches.keys();
        
        if (enlistedResources.size() == 1) {
            
            // One phase commit
            status = Status.STATUS_COMMITTING;
            while (keys.hasMoreElements()) {
                Object key = keys.nextElement();
                XAResource resourceManager =
                    (XAResource) branches.get(key);
                try {
                    if (!fail)
                        resourceManager.commit(xid, true);
                    else
                        resourceManager.rollback(xid);
                } catch (Throwable e) {
                    // Adding the exception to the error code list
                    String logMessage = Messages.format
                        (SlideTransaction.class.getName() + ".commitFail",
                         resourceManager, getXAErrorCode(e), toString());
                    transactionManager.getLogger().log
                        (logMessage, e, LOG_CHANNEL, Logger.WARNING);
                    exceptions.addElement(e);
                    fail = true;
                    status = Status.STATUS_MARKED_ROLLBACK;
                }
            }
            if (!fail) {
                status = Status.STATUS_COMMITTED;
            } else {
                status = Status.STATUS_ROLLEDBACK;
            }
            
        } else if (enlistedResources.size() != 0) {
            
            // Prepare each enlisted resource
            status = Status.STATUS_PREPARING;
            while ((!fail) && (keys.hasMoreElements())) {
                Object key = keys.nextElement();
                XAResource resourceManager =
                    (XAResource) branches.get(key);
                try {
                    // Preparing the resource manager using its branch xid
                    resourceManager.prepare((Xid) key);
                } catch (Throwable e) {
                    String logMessage = Messages.format
                        (SlideTransaction.class.getName() + ".prepareFail",
                         resourceManager, getXAErrorCode(e), toString());
                    transactionManager.getLogger().log
                        (logMessage, e, LOG_CHANNEL, Logger.WARNING);
                    // Adding the exception to the error code list
                    exceptions.addElement(e);
                    fail = true;
                    status = Status.STATUS_MARKED_ROLLBACK;
                }
            }
            
            if (!fail)
                status = Status.STATUS_PREPARED;
            
            // If fail, rollback
            if (fail) {
                status = Status.STATUS_ROLLING_BACK;
                fail = false;
                // Rolling back all the prepared (and unprepared) branches
                keys = branches.keys();
                while (keys.hasMoreElements()) {
                    Object key = keys.nextElement();
                    XAResource resourceManager =
                        (XAResource) branches.get(key);
                    try {
                        resourceManager.rollback((Xid) key);
                    } catch(Throwable e) {
                        String logMessage = Messages.format
                            (SlideTransaction.class.getName() + ".rollbackFail",
                             resourceManager, getXAErrorCode(e), toString());
                        transactionManager.getLogger().log
                            (logMessage, e, LOG_CHANNEL, Logger.WARNING);
                        exceptions.addElement(e);
                        fail = true;
                    }
                }
                status = Status.STATUS_ROLLEDBACK;
            } else {
                status = Status.STATUS_COMMITTING;
                // Commit each enlisted resource
                keys = branches.keys();
                while (keys.hasMoreElements()) {
                    Object key = keys.nextElement();
                    XAResource resourceManager =
                        (XAResource) branches.get(key);
                    try {
                        resourceManager.commit((Xid) key, false);
                    } catch(Throwable e) {
                        String logMessage = Messages.format
                            (SlideTransaction.class.getName() + ".commitFail",
                             resourceManager, getXAErrorCode(e), toString());
                        transactionManager.getLogger().log
                            (logMessage, e, LOG_CHANNEL, Logger.WARNING);
                        exceptions.addElement(e);
                        fail = true;
                    }
                }
                status = Status.STATUS_COMMITTED;
            }
            
        }
        
        // Call synchronized objects afterCompletion
        syncList = synchronizationObjects.elements();
        while (syncList.hasMoreElements()) {
            Synchronization sync =
                (Synchronization) syncList.nextElement();
            sync.afterCompletion(status);
        }
        
        // Parsing exception and throwing an appropriate exception
        keys = exceptions.elements();
        if (keys.hasMoreElements()) {
            if ((status == Status.STATUS_ROLLEDBACK) && (!fail))
                throw new RollbackException();
            if (status == Status.STATUS_ROLLEDBACK)
                throw new HeuristicRollbackException();
            if ((status == Status.STATUS_COMMITTED) && (fail))
                throw new HeuristicMixedException();
        }
        
    }
    
    
    /**
     * Delist the resource specified from the current transaction associated
     * with the calling thread.
     *
     * @param xaRes The XAResource object representing the resource to delist
     * @param flag One of the values of TMSUCCESS, TMSUSPEND, or TMFAIL
     * @exception IllegalStateException Thrown if the transaction in the
     * target object is inactive.
     * @exception SystemException Thrown if the transaction manager encounters
     * an unexpected error condition.
     */
    public boolean delistResource(XAResource xaRes, int flag)
        throws IllegalStateException, SystemException {
        
        //System.out.println(this + "  DELIST " + xaRes);
        
        // Check status ACTIVE
        if (status != Status.STATUS_ACTIVE)
            throw new IllegalStateException();
        
        Xid xid = (Xid) activeBranches.get(xaRes);
        
        if (xid == null)
            throw new IllegalStateException();
        
        activeBranches.remove(xaRes);
        
        if (transactionManager.getLogger().isEnabled
                (LOG_CHANNEL, Logger.DEBUG)) {
            String logMessage = Messages.format
                (SlideTransaction.class.getName() + ".delist", xaRes,
                 getXAFlag(flag), toString());
            transactionManager.getLogger().log
                (logMessage, LOG_CHANNEL, Logger.DEBUG);
        }
        
        XAException exception = null;
        
        try {
            xaRes.end(xid, flag);
        } catch (XAException e) {
            exception = e;
        }
        
        if (exception != null) {
            String logMessage = Messages.format
                (SlideTransaction.class.getName() + ".delistFail",
                 xaRes, getXAErrorCode(exception), toString());
            transactionManager.getLogger().log
                (logMessage, LOG_CHANNEL, Logger.WARNING);
            return false;
        }
        
        if (flag == XAResource.TMSUSPEND)
            suspendedResources.put(xaRes, xid);
        
        //System.out.println("Delisted ok(" + this + ") = " + xaRes + " xid: " + xid);
        
        return true;
        
    }
    
    
    /**
     * Enlist the resource specified with the current transaction context of
     * the calling thread.
     *
     * @param xaRes The XAResource object representing the resource to delist
     * @return true if the resource was enlisted successfully; otherwise false.
     * @exception RollbackException Thrown to indicate that the transaction
     * has been marked for rollback only.
     * @exception IllegalStateException Thrown if the transaction in the
     * target object is in prepared state or the transaction is inactive.
     * @exception SystemException Thrown if the transaction manager
     * encounters an unexpected error condition.
     */
    public boolean enlistResource(XAResource xaRes)
        throws RollbackException, IllegalStateException, SystemException {
        
        //System.out.println(this + "  ENLIST " + xaRes);
        
        if (status == Status.STATUS_MARKED_ROLLBACK)
            throw new RollbackException();
        
        // Check status ACTIVE
        if (status != Status.STATUS_ACTIVE)
            throw new IllegalStateException();
        
        // Preventing two branches from being active at the same time on the
        // same resource manager
        Xid activeXid = (Xid) activeBranches.get(xaRes);
        if (activeXid != null)
            return false;
        
        boolean alreadyEnlisted = false;
        int flag = XAResource.TMNOFLAGS;
        
        Xid branchXid = (Xid) suspendedResources.get(xaRes);
        
        if (branchXid == null) {
            Enumeration elms = enlistedResources.elements();
            while ((!alreadyEnlisted) && (elms.hasMoreElements())) {
                XAResource resourceManager = (XAResource) elms.nextElement();
                try {
                    if (resourceManager.isSameRM(xaRes)) {
                        flag = XAResource.TMJOIN;
                        alreadyEnlisted = true;
                    }
                } catch (XAException e) {
                }
            }
            branchXid = this.xid.newBranch(branchCounter++);
            
            //System.out.println(this + "  Creating new branch for " + xaRes);
            
        } else {
            alreadyEnlisted = true;
            flag = XAResource.TMRESUME;
            suspendedResources.remove(xaRes);
        }
        
        if (transactionManager.getLogger().isEnabled
                (LOG_CHANNEL, Logger.DEBUG)) {
            String logMessage = Messages.format
                (SlideTransaction.class.getName() + ".enlist", xaRes,
                 getXAFlag(flag), toString());
            transactionManager.getLogger().log
                (logMessage, LOG_CHANNEL, Logger.DEBUG);
        }
        
        try {
            //System.out.println("Starting(" + this + ") = " + xaRes + " Branch: " + branchXid + " Flag: " + flag);
            
            xaRes.start(branchXid, flag);
        } catch (XAException e) {
            String logMessage = Messages.format
                (SlideTransaction.class.getName() + ".enlistFail",
                 xaRes, getXAErrorCode(e), toString());
            transactionManager.getLogger().log
                (logMessage, LOG_CHANNEL, Logger.WARNING);
            return false;
        }
        
        if (!alreadyEnlisted) {
            enlistedResources.addElement(xaRes);
        }
        
        branches.put(branchXid, xaRes);
        activeBranches.put(xaRes, branchXid);
        
        return true;
        
    }
    
    
    /**
     * Roll back the transaction associated with the current thread. When
     * this method completes, the thread becomes associated with no
     * transaction.
     *
     * @exception SecurityException Thrown to indicate that the thread is not
     * allowed to commit the transaction.
     * @exception IllegalStateException Thrown if the current thread is not
     * associated with a transaction.
     * @exception SystemException Thrown if the transaction manager encounters
     * an unexpected error condition.
     */
    public void rollback()
        throws SecurityException, IllegalStateException, SystemException {
        
        //System.out.println(this + "  ROLLBACK ");
        
        // Check status ACTIVE
        if (status != Status.STATUS_ACTIVE && status != Status.STATUS_MARKED_ROLLBACK)
            throw new IllegalStateException();
        
        Vector exceptions = new Vector();
        
        Enumeration keys = branches.keys();
        
        status = Status.STATUS_ROLLING_BACK;
        while (keys.hasMoreElements()) {
            Xid xid = (Xid) keys.nextElement();
            XAResource resourceManager = (XAResource) branches.get(xid);
            try {
                resourceManager.rollback(xid);
            } catch (Throwable e) {
                exceptions.addElement(e);
                String logMessage = Messages.format
                    (SlideTransaction.class.getName() + ".rollbackFail",
                     resourceManager, getXAErrorCode(e), toString());
                transactionManager.getLogger().log
                    (logMessage, LOG_CHANNEL, Logger.WARNING);
            }
        }
        status = Status.STATUS_ROLLEDBACK;
        
    }
    
    
    /**
     * Modify the transaction associated with the current thread such that
     * the only possible outcome of the transaction is to roll back the
     * transaction.
     *
     * @exception IllegalStateException Thrown if the current thread is not
     * associated with a transaction.
     * @exception SystemException Thrown if the transaction manager encounters
     * an unexpected error condition.
     */
    public void setRollbackOnly()
        throws IllegalStateException, SystemException {
        status = Status.STATUS_MARKED_ROLLBACK;
    }
    
    
    /**
     * Obtain the status of the transaction associated with the current thread.
     *
     * @exception SystemException Thrown if the transaction manager encounters
     * an unexpected error condition.
     * @return The transaction status. If no transaction is associated with
     * the current thread, this method returns the Status.NoTransaction value.
     */
    public int getStatus()
        throws SystemException {
        return status;
    }
    
    
    /**
     * Register a synchronization object for the transaction currently
     * associated with the calling thread. The transction manager invokes the
     * beforeCompletion method prior to starting the transaction commit
     * process. After the transaction is completed, the transaction manager
     * invokes the afterCompletion method.
     *
     * @param sync The Synchronization object for the transaction associated
     * with the target object.
     * @exception RollbackException Thrown to indicate that the transaction
     * has been marked for rollback only.
     * @exception IllegalStateException Thrown if the transaction in the
     * target object is in prepared state or the transaction is inactive.
     * @exception SystemException Thrown if the transaction manager encounters
     * an unexpected error condition.
     */
    public void registerSynchronization(Synchronization sync)
        throws RollbackException, IllegalStateException, SystemException {
        
        if (status == Status.STATUS_MARKED_ROLLBACK)
            throw new RollbackException();
        
        if (status != Status.STATUS_ACTIVE)
            throw new IllegalStateException();
        
        synchronizationObjects.addElement(sync);
        
    }
    
    
    // --------------------------------------------------------- Public Methods
    
    
    /**
     * Return a String representation of the error code contained in a
     * XAException.
     */
    public static String getXAErrorCode(Throwable throww) {
        String result = null;
        if (throww instanceof XAException)
            result = getXAErrorCode((XAException)throww);
        else {
            StringWriter sw = new StringWriter();
            throww.printStackTrace( new PrintWriter(sw, true) ); //autoFlush=true
            result = sw.toString();
        }
        return result;
    }
    /**
     * Return a String representation of the error code contained in a
     * XAException.
     */
    public static String getXAErrorCode(XAException xae) {
        switch (xae.errorCode) {
            case XAException.XA_HEURCOM:
                return "XA_HEURCOM";
            case XAException.XA_HEURHAZ:
                return "XA_HEURHAZ";
            case XAException.XA_HEURMIX:
                return "XA_HEURMIX";
            case XAException.XA_HEURRB:
                return "XA_HEURRB";
            case XAException.XA_NOMIGRATE:
                return "XA_NOMIGRATE";
            case XAException.XA_RBBASE:
                return "XA_RBBASE";
            case XAException.XA_RBCOMMFAIL:
                return "XA_RBCOMMFAIL";
            case XAException.XA_RBDEADLOCK:
                return "XA_RBBEADLOCK";
            case XAException.XA_RBEND:
                return "XA_RBEND";
            case XAException.XA_RBINTEGRITY:
                return "XA_RBINTEGRITY";
            case XAException.XA_RBOTHER:
                return "XA_RBOTHER";
            case XAException.XA_RBPROTO:
                return "XA_RBPROTO";
            case XAException.XA_RBTIMEOUT:
                return "XA_RBTIMEOUT";
            case XAException.XA_RDONLY:
                return "XA_RDONLY";
            case XAException.XA_RETRY:
                return "XA_RETRY";
            case XAException.XAER_ASYNC:
                return "XAER_ASYNC";
            case XAException.XAER_DUPID:
                return "XAER_DUPID";
            case XAException.XAER_INVAL:
                return "XAER_INVAL";
            case XAException.XAER_NOTA:
                return "XAER_NOTA";
            case XAException.XAER_OUTSIDE:
                return "XAER_OUTSIDE";
            case XAException.XAER_PROTO:
                return "XAER_PROTO";
            case XAException.XAER_RMERR:
                return "XAER_RMERR";
            case XAException.XAER_RMFAIL:
                return "XAER_RMFAIL";
            default:
                return "UNKNOWN";
        }
    }
    
    
    /**
     * Return a String representation of a flag.
     */
    public static String getXAFlag(int flag) {
        switch (flag) {
            case XAResource.TMENDRSCAN:
                return "TMENDRSCAN";
            case XAResource.TMFAIL:
                return "TMFAIL";
            case XAResource.TMJOIN:
                return "TMJOIN";
            case XAResource.TMNOFLAGS:
                return "TMNOFLAGS";
            case XAResource.TMONEPHASE:
                return "TMONEPHASE";
            case XAResource.TMRESUME:
                return "TMRESUME";
            case XAResource.TMSTARTRSCAN:
                return "TMSTARTRSCAN";
            case XAResource.TMSUCCESS:
                return "TMSUCCESS";
            case XAResource.TMSUSPEND:
                return "TMSUSPEND";
            default:
                return "UNKNOWN";
        }
    }
    
    
    /**
     * Print the Transaction object in a debugger friendly manner
     */
    public String toString() {
        return "Transaction " + currentTransactionNumber
            + " xid " + xid + " in thread " + currentThreadName +
            (currentThreadName.equals(Thread.currentThread().getName())?"":
                 " current= " + Thread.currentThread().getName());
    }
    
    
}


