/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/external-tm/src/share/org/apache/slide/common/AbstractService.java,v 1.1 2004/09/20 09:10:02 ozeigermann Exp $
 * $Revision: 1.1 $
 * $Date: 2004/09/20 09:10:02 $
 *
 * ====================================================================
 *
 * 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.common;

import java.util.Hashtable;
import java.util.Vector;
import java.util.Arrays;

import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.apache.slide.transaction.SlideTransactionManager;

/**
 * Slide Service abstract implementation with support for multiple
 * simultaneous transaction context.
 *
 * @version $Revision: 1.1 $
 */
public abstract class AbstractService extends AbstractServiceBase
    implements Service {


    // -------------------------------------------------------------- Constants


    public static final int TX_IDLE = 0;
    public static final int TX_PREPARED = 1;
    public static final int TX_SUSPENDED = 2;


    // ----------------------------------------------------- Instance Variables


    /**
     * Current transaction context.
     */
    protected Hashtable currentContexts = new Hashtable();


    // ----------------------------------------------------- XAResource Mathods


    /**
     * Commit the global transaction specified by xid.
     *
     * @param xid A global transaction identifier
     * @param onePhase If true, the resource manager should use a one-phase
     * commit protocol to commit the work done on behalf of xid.
     * @exception XAException An error has occurred. Possible XAExceptions
     * are XA_HEURHAZ, XA_HEURCOM, XA_HEURRB, XA_HEURMIX, XAER_RMERR,
     * XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or XAER_PROTO. If the resource
     * manager did not commit the transaction and the paramether onePhase is
     * set to true, the resource manager may throw one of the XA_RB*
     * exceptions. Upon return, the resource manager has rolled back the
     * branch's work and has released all held resources.
     */
    public void commit(Xid xid, boolean onePhase)
        throws XAException {

        ContextTuple currentContextTuple =
            (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
            currentContextTuple!=null?currentContextTuple.getXid():null;

        if (currentContext == null)
            throw new XAException(XAException.XAER_NOTA);
        if (xid == null)
            throw new XAException(XAException.XAER_INVAL);
        if (!Arrays.equals(currentContext.getGlobalTransactionId(),
            xid.getGlobalTransactionId()))
            throw new XAException(XAException.XAER_PROTO);

        if (!onePhase && currentContextTuple.getStatus() != TX_PREPARED)
            throw new XAException(XAException.XAER_PROTO);
        if (onePhase &&
            (!((currentContextTuple.getStatus() == TX_IDLE) ||
               (currentContextTuple.getStatus() == TX_SUSPENDED))))
            throw new XAException(XAException.XAER_PROTO);

        if (((ContextTuple) currentContexts.get(Thread.currentThread()))
            .getRollbackOnly()) {
            rollback(xid);
            throw new XAException(XAException.XA_RBROLLBACK);
        }

        currentContexts.remove(Thread.currentThread());

    }


    /**
     * Ends the work performed on behalf of a transaction branch. The
     * resource manager disassociates the XA resource from the transaction
     * branch specified and let the transaction be completed.
     * If TMSUSPEND is specified in flags, the transaction branch is
     * temporarily suspended in incomplete state. The transaction context is
     * in suspened state and must be resumed via start with TMRESUME specified.
     * If TMFAIL is specified, the portion of work has failed. The resource
     * manager may mark the transaction as rollback-only.
     * If TMSUCCESS is specified, the portion of work has completed
     * successfully.
     *
     * @param xid A global transaction identifier that is the same as what
     * was used previously in the start method.
     * @param flags One of TMSUCCESS, TMFAIL, or TMSUSPEND
     * @exception XAException An error has occurred. Possible XAException
     * values are XAER_RMERR, XAER_RMFAILED, XAER_NOTA, XAER_INVAL,
     * XAER_PROTO, or XA_RB*.
     */
    public void end(Xid xid, int flags)
        throws XAException {

        ContextTuple currentContextTuple =
            (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
            currentContextTuple!=null?currentContextTuple.getXid():null;

        if (currentContext == null)
            throw new XAException(XAException.XAER_NOTA);
        if (xid == null)
            throw new XAException(XAException.XAER_INVAL);
        if (!Arrays.equals(currentContext.getGlobalTransactionId(),
            xid.getGlobalTransactionId()))
            throw new XAException(XAException.XAER_PROTO);

        if (flags == XAResource.TMSUSPEND)
            currentContextTuple.setStatus(TX_SUSPENDED);

        if (flags == XAResource.TMFAIL)
            ((ContextTuple) currentContexts.get(Thread.currentThread()))
                .setRollbackOnly(true);

    }


    /**
     * Tell the resource manager to forget about a heuristically completed
     * transaction branch.
     *
     * @param xid A global transaction identifier
     * @exception XAException An error has occurred. Possible exception values
     * are XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL, or XAER_PROTO.
     */
    public void forget(Xid xid)
        throws XAException {

        ContextTuple currentContextTuple =
            (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
            currentContextTuple!=null?currentContextTuple.getXid():null;

        if (currentContext == null)
            throw new XAException(XAException.XAER_NOTA);
        if (xid == null)
            throw new XAException(XAException.XAER_INVAL);
        if (!Arrays.equals(currentContext.getGlobalTransactionId(),
            xid.getGlobalTransactionId()))
            throw new XAException(XAException.XAER_PROTO);

        currentContexts.remove(Thread.currentThread());

    }


    /**
     * Obtain the current transaction timeout value set for this XAResource
     * instance. If XAResource.setTransactionTimeout was not use prior to
     * invoking this method, the return value is the default timeout set for
     * the resource manager; otherwise, the value used in the previous
     * setTransactionTimeout call is returned.
     *
     * @return the transaction timeout value in seconds.
     * @exception XAException An error has occurred. Possible exception
     * values are XAER_RMERR, XAER_RMFAIL.
     */
    public int getTransactionTimeout()
        throws XAException {

        return ((ContextTuple) currentContexts.get(Thread.currentThread()))
            .getTransactionTimeout();

    }


    /**
     * This method is called to determine if the resource manager instance
     * represented by the target object is the same as the resouce manager
     * instance represented by the parameter xares.
     *
     * @param xares An XAResource object whose resource manager instance is
     * to be compared with the resource manager instance of the target object.
     * @return true if it's the same RM instance; otherwise false.
     * @exception XAException An error has occurred. Possible exception values
     * are XAER_RMERR, XAER_RMFAIL.
     */
    public boolean isSameRM(XAResource xares)
        throws XAException {

        if (xares == null)
            return false;

        return (this == xares);

    }


    /**
     * Ask the resource manager to prepare for a transaction commit of the
     * transaction specified in xid.
     *
     * @param xid A global transaction identifier
     * @return A value indicating the resource manager's vote on the outcome
     * of the transaction. The possible values are: XA_RDONLY or XA_OK. If
     * the resource manager wants to roll back the transaction, it should do
     * so by raising an appropriate XAException in the prepare method.
     * @exception XAException An error has occurred. Possible exception
     * values are: XA_RB*, XAER_RMERR, XAER_RMFAIL, XAER_NOTA, XAER_INVAL,
     * or XAER_PROTO.
     */
    public int prepare(Xid xid)
        throws XAException {

        ContextTuple currentContextTuple =
            (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
            currentContextTuple!=null?currentContextTuple.getXid():null;

        if (currentContext == null)
            throw new XAException(XAException.XAER_NOTA);
        if (xid == null)
            throw new XAException(XAException.XAER_INVAL);

        if (!Arrays.equals(currentContext.getGlobalTransactionId(),
            xid.getGlobalTransactionId()))
            throw new XAException(XAException.XAER_PROTO);

        if (!((currentContextTuple.getStatus() == TX_IDLE) ||
              (currentContextTuple.getStatus() == TX_SUSPENDED)))
            throw new XAException(XAException.XAER_PROTO);

        if (((ContextTuple) currentContexts.get(Thread.currentThread()))
            .getRollbackOnly()) {
            // FIXME: Don't know if should be automatically rollbacked in that
            // case
            throw new XAException(XAException.XA_RBROLLBACK);
        }

        currentContextTuple.setStatus(TX_PREPARED);

        return XAResource.XA_OK;

    }


    /**
     * Obtain a list of prepared transaction branches from a resource manager.
     * The transaction manager calls this method during recovery to obtain the
     * list of transaction branches that are currently in prepared or
     * heuristically completed states.
     *
     * @param flag One of TMSTARTRSCAN, TMENDRSCAN, TMNOFLAGS. TMNOFLAGS must
     * be used when no other flags are set in flags.
     * @return The resource manager returns zero or more XIDs for the
     * transaction branches that are currently in a prepared or heuristically
     * completed state. If an error occurs during the operation, the resource
     * manager should throw the appropriate XAException.
     * @exception XAException An error has occurred. Possible values are
     * XAER_RMERR, XAER_RMFAIL, XAER_INVAL, and XAER_PROTO.
     */
    public Xid[] recover(int flag)
        throws XAException {

        ContextTuple currentContextTuple =
           (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
           currentContextTuple!=null?currentContextTuple.getXid():null;

        Vector list = new Vector();

        if ((currentContextTuple.getStatus() == TX_PREPARED) &&
            (currentContext != null))
           list.addElement(currentContext);

        return (Xid[]) list.toArray(new Xid[list.size()]);

    }


    /**
     * Inform the resource manager to roll back work done on behalf of a
     * transaction branch.
     *
     * @param xid A global transaction identifier
     * @exception XAException An error has occurred
     */
    public void rollback(Xid xid)
        throws XAException {

        ContextTuple currentContextTuple =
            (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
            currentContextTuple!=null?currentContextTuple.getXid():null;

        if (currentContext == null)
            throw new XAException(XAException.XAER_NOTA);
        if (xid == null)
            throw new XAException(XAException.XAER_INVAL);
        if (!Arrays.equals(currentContext.getGlobalTransactionId(),
            xid.getGlobalTransactionId()))
            throw new XAException(XAException.XAER_PROTO);

        currentContexts.remove(Thread.currentThread());

    }


    /**
     * Set the current transaction timeout value for this XAResource instance.
     *
     * @param seconds the transaction timeout value in seconds.
     * @return true if transaction timeout value is set successfully;
     * otherwise false.
     * @exception XAException An error has occurred. Possible exception
     * values are XAER_RMERR, XAER_RMFAIL, or XAER_INVAL.
     */
    public boolean setTransactionTimeout(int seconds)
        throws XAException {

        ((ContextTuple) currentContexts.get(Thread.currentThread()))
            .setTransactionTimeout(seconds);
        return true;

    }


    /**
     * Start work on behalf of a transaction branch specified in xid If
     * TMJOIN is specified, the start is for joining a transaction previously
     * seen by the resource manager. If TMRESUME is specified, the start is
     * to resume a suspended transaction specified in the parameter xid. If
     * neither TMJOIN nor TMRESUME is specified and the transaction specified
     * by xid has previously been seen by the resource manager, the resource
     * manager throws the XAException exception with XAER_DUPID error code.
     *
     * @param xid A global transaction identifier to be associated with the
     * resource
     * @param flags One of TMNOFLAGS, TMJOIN, or TMRESUME
     * @exception XAException An error has occurred. Possible exceptions are
     * XA_RB*, XAER_RMERR, XAER_RMFAIL, XAER_DUPID, XAER_OUTSIDE, XAER_NOTA,
     * XAER_INVAL, or XAER_PROTO.
     */
    public void start(Xid xid, int flags)
        throws XAException {

        ContextTuple currentContextTuple =
            (ContextTuple) currentContexts.get(Thread.currentThread());
        Xid currentContext =
            currentContextTuple!=null?currentContextTuple.getXid():null;

        if (xid == null)
            throw new XAException(XAException.XAER_INVAL);
        if ( (currentContext != null) &&
             (!Arrays.equals(currentContext.getGlobalTransactionId(),
              xid.getGlobalTransactionId())) )
            throw new XAException(XAException.XAER_OUTSIDE);

        switch (flags) {
        case XAResource.TMNOFLAGS:
            if (currentContext != null)
                throw new XAException(XAException.XAER_INVAL);
            currentContext = xid;
            // is the idle status really ok ???
            currentContexts.put
                (Thread.currentThread(),new ContextTuple
                    (xid, TX_IDLE,
                     SlideTransactionManager.DEFAULT_TRANSACTION_TIMEOUT,
                     false));
            break;
        case XAResource.TMJOIN:
            if (currentContext == null)
                throw new XAException(XAException.XAER_NOTA);
            if (!Arrays.equals(currentContext.getGlobalTransactionId(),
                xid.getGlobalTransactionId()))
                throw new XAException(XAException.XAER_INVAL);
            break;
        case XAResource.TMRESUME:
            if (currentContext == null)
                throw new XAException(XAException.XAER_NOTA);
            if (currentContextTuple.getStatus() != TX_SUSPENDED)
                throw new XAException(XAException.XAER_INVAL);
            currentContextTuple.setStatus(TX_IDLE);
            break;
        }

    }


    // ------------------------------------------------ ContextTuple InnerClass


    /**
     * inner class to pack both the xid and the status
     **/
    private class ContextTuple {
        private Xid xid;
        private int status;
        private int transactionTimeout;
        private boolean rollbackOnly;
        public ContextTuple(Xid xid, int status, int transactionTimeout,
                            boolean rollbackOnly) {
            this.xid    = xid;
            setStatus(status);
            setTransactionTimeout(transactionTimeout);
            setRollbackOnly(rollbackOnly);
        }
        public Xid getXid() {
            return xid;
        }
        public int getStatus() {
            return status;
        }
        public void setStatus(int status) {
            this.status = status;
        }
        public int getTransactionTimeout() {
            return transactionTimeout;
        }
        public void setTransactionTimeout(int transactionTimeout) {
            this.transactionTimeout = transactionTimeout;
        }
        public boolean getRollbackOnly() {
            return rollbackOnly;
        }
        public void setRollbackOnly(boolean rollbackOnly) {
            this.rollbackOnly = rollbackOnly;
        }

    }


}