/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Exoffice Technologies.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Exoffice Technologies. Exolab is a registered
 *    trademark of Exoffice Technologies.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY EXOFFICE TECHNOLOGIES AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * EXOFFICE TECHNOLOGIES OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000-2004 (C) Exoffice Technologies Inc. All Rights Reserved.
 *
 * $Id: ResourceManager.java,v 1.1 2010/06/18 16:44:19 smhuang Exp $
 */
package org.exolab.jms.messagemgr;

import java.io.File;
import java.io.FilenameFilter;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.TreeSet;
import java.util.Vector;
import javax.jms.JMSException;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.exolab.jms.client.JmsDestination;
import org.exolab.jms.common.uuid.UUID;
import org.exolab.jms.message.MessageImpl;
import org.exolab.jms.persistence.DatabaseService;
import org.exolab.jms.persistence.PersistenceException;
import org.exolab.jms.service.Service;
import org.exolab.jms.tranlog.DataTransactionLogEntry;
import org.exolab.jms.tranlog.ExternalXid;
import org.exolab.jms.tranlog.StateTransactionLogEntry;
import org.exolab.jms.tranlog.TransactionLog;
import org.exolab.jms.tranlog.TransactionLogException;
import org.exolab.jms.tranlog.TransactionState;


/**
 * The resource manager provides XA support for the JMS Server.
 * <p/>
 * The resource manager is responsible for managing the various transaction
 * identifiers and managing the association between transaction ids and
 * connections.
 * <p/>
 * The resource manager will store the global XID's and their state in the
 * database for recovery purposes.
 * <p/>
 * Messages that arrive, and are associated with an XID are not processed
 * through the {@link MessageMgr}. Instead they are routed to this resource
 * managers where they are cached until the associated XID is committed or
 * rolled back. If the transaction is successfully committed, through the 2PC
 * protocol the messages will pass through the system.
 * <p/>
 * Similarly, messages that are sent to consumers, either synchronously or
 * asynchronously are also cached by the resource manager until the global
 * transaction completes.
 * <p/>
 * On startup the resource manager will read all incomplete transactions, which
 * are incompleted into memory. It will then process trnasactions that have
 * timed out.
 * <p/>
 * The transaction manager will call the {@link #recover} method  and obtain a
 * list of incomplete transaction for the purpose of completing them where
 * possible.
 *
 * @author <a href="mailto:jima@intalio.com">Jim Alateras</a>
 * @version $Revision: 1.1 $ $Date: 2010/06/18 16:44:19 $
 */
public class ResourceManager extends Service {

    /**
     * The extension for all transaction log files
     */
    public final static String RM_LOGFILE_EXTENSION = ".log";

    /**
     * This is used to indicate the garbage collection has been disabled and
     * that the client will take responsibility for all aspects of log file
     * management. This is useful in situations where the client wants to
     * archive the transaction log files
     * <p/>
     * This is the default mode for GC.
     */
    public static final int GC_DISABLED = 0;

    /**
     * Synchronous gabrage collection is used to remove processed log files when
     * the last trnasaction, in that log file, has been successfully processed.
     * This is more efficient means since the log files does not need to be
     * scanned asynchronously to determine whether all the transactions have
     * been processed.
     */
    public static final int GC_SYNCHRONOUS = 1;

    /**
     * Asynchronous garbage collection is used to remove processed log files
     * asynchronous (i.e in a different thread context). This is rather
     * expensive since it must manually scan each log file and determine whether
     * all transactions, in that file, have been closed. If this is the case
     * then it will remove the log file.
     */
    public static final int GC_ASYNCHRONOUS = 2;

    /**
     * The message manager.
     */
    private final MessageManager _messages;

    /**
     * The destination manager.
     */
    private final DestinationManager _destinations;

    /**
     * This is the maximum size, in bytes, of each transaction log file. The
     * value can be overriden by the user
     */
    private int _logFileSize = 1000000;

    /**
     * Maintains a collection of transaction log files currently in use by this
     * resource manager
     */
    private TreeSet _logs = new TreeSet(new TranLogFileComparator());

    /**
     * Maintain a mapping between the TRID (transaction id and the log file it
     * is associated with.
     */
    private HashMap _tridToLogCache = new HashMap();

    /**
     * Maintain a list of open TRIDs for a particular  {@link TransactionLog}
     */
    private HashMap _logToTridCache = new HashMap();

    /**
     * This attribute is used to synchronize the modifications to the _tridToLog
     * _logToTrid attributes
     */
    private final Object _cacheLock = new Object();

    /**
     * This maintains a cache of all open transactions and the corresponding
     * data. The key is the transaction identifier and the object is a
     * LinkedList transaction entries, which includes both state and data
     */
    private HashMap _activeTransactions = new HashMap();

    /**
     * The directory where the log files are stored. This can be set by the
     * client
     */
    private String _logDirectory = ".";

    /**
     * This is the number of the last log file created by the ResourceManager
     */
    private long _lastLogNumber = 0;

    /**
     * The expiry time for transaction associated with this resource manager.
     * This will either be configured or passed in with the transaction context
     * The value is specified in seconds.
     */
    private int _txExpiryTime = 120;

    /**
     * This attribute caches the garbage collection mode for the resouce
     * managers. Valid values are specified by the GC_* constants.
     * <p/>
     * By default garbage collection is disabled.
     */
    private int _gcMode = GC_SYNCHRONOUS;

    /**
     * This is the id associated with this resource...need to work out who or
     * what sets this.
     */
    private String _rid = UUID.next();

    /**
     * The name of the service
     */
    private final static String RM_SERVICE_NAME = "XAResourceManager";

    /**
     * The prefix used for all transaction log files, which are created and
     * managed by the {@link TransactionLog}
     */
    private final static String RM_LOGFILE_PREFIX = "ojmsrm";

    /**
     * The logger
     */
    private static final Log _log = LogFactory.getLog(ResourceManager.class);


    /**
     * Construct a resource manager using the default directory for its log
     * files.
     * <p/>
     * If the directory does not exist or there is no permisssion to access it,
     * then throw a ResourceManagerException.
     *
     * @param messages     the message manager
     * @param destinations the destination manager
     * @param database     the database service
     * @throws ResourceManagerException
     */
    public ResourceManager(MessageManager messages,
                           DestinationManager destinations,
                           DatabaseService database)
            throws ResourceManagerException {
        super(RM_SERVICE_NAME);

        _messages = messages;
        _destinations = destinations;
        /*

        final String dir = "./logs";
        _logDirectory = dir;
        File file = new File(dir);
        if ((!file.exists()) ||
                (!file.isDirectory())) {
            throw new ResourceManagerException(dir
                                               +
                                               " does not exist or is not a directory");
        }

        // build the list of existing log files.
        buildLogFileList();

        // recover te list of log files
        recover();
        */
    }

    /**
     * Check whether garbage collection has been disabled
     *
     * @return boolean - true if gc is disabled
     */
    public boolean gcDisabled() {
        return (_gcMode == GC_DISABLED) ? true : false;
    }

    /**
     * Log this published message so that it can be passed through the system
     * when the associated global transaction commits.
     *
     * @param xid     - the global transaction identity
     * @param message - the message published
     * @throws TransactionLogException  - error adding the entry
     * @throws ResourceManagerException - error getting the trnasaction log
     * @throws JMSException             - if there is an issue with prep'ing the
     *                                  message
     */
    public synchronized void logPublishedMessage(Xid xid, MessageImpl message)
            throws TransactionLogException, ResourceManagerException,
            JMSException {
        _messages.prepare(message);
        logTransactionData(new ExternalXid(xid), _rid,
                           createPublishedMessageWrapper(message));
    }

    /**
     * Log that this message handle was sent to the consumer within the
     * specified global transaction identity. The message will be acknowledged
     * when the global transaction commits. Alternatively, if the global
     * transaction is rolled back the message handle will be returned to the
     * destination
     *
     * @param xid    the global transaction identity
     * @param id     the consumer receiving this message
     * @param handle - the handle of the message received
     * @throws TransactionLogException  - error adding the entry
     * @throws ResourceManagerException - error getting the transaction log
     */
    public synchronized void logReceivedMessage(Xid xid, long id,
                                                MessageHandle handle)
            throws TransactionLogException, ResourceManagerException {
        logTransactionData(new ExternalXid(xid), _rid,
                           createReceivedMessageWrapper(id, handle));
    }

    /**
     * Add an {@link StateTransactionLogEntry} using the specified txid, rid and
     * state
     *
     * @param xid   - the transaction identifier
     * @param state - the transaction log state
     * @throws TransactionLogException  - error adding the entry
     * @throws ResourceManagerException - error getting the trnasaction log
     */
    public synchronized void logTransactionState(Xid xid,
                                                 TransactionState state)
            throws TransactionLogException, ResourceManagerException {
        ExternalXid txid = new ExternalXid(xid);
        switch (state.getOrd()) {
            case TransactionState.OPENED_ORD:
                {
                    TransactionLog log = getCurrentTransactionLog();
                    addTridLogEntry(txid, log);
                    log.logTransactionState(txid, _txExpiryTime * 1000, _rid,
                                            state);

                    // cache the transaction state
                    _activeTransactions.put(txid, new LinkedList());
                }
                break;

            case TransactionState.PREPARED_ORD:
                // cache the transaction state
                LinkedList list = (LinkedList) _activeTransactions.get(txid);
                if (list != null) {
                    list.add(state);
                } else {
                    throw new ResourceManagerException("Trasaction " + txid +
                                                       " is not active.");
                }
                break;

            case TransactionState.CLOSED_ORD:
                {
                    TransactionLog log = getTransactionLog(txid);
                    log.logTransactionState(txid, _txExpiryTime * 1000, _rid,
                                            state);
                    removeTridLogEntry(txid, log);

                    // check whether this log has anymore open transactions
                    synchronized (_cacheLock) {
                        if ((_logToTridCache.get(log) == null) &&
                                (!isCurrentTransactionLog(log))) {
                            log.close();

                            // now check if gc mode is GC_SYNCHRONOUS. If it is
                            // remove the log file
                            if (_gcMode == GC_SYNCHRONOUS) {
                                try {
                                    log.destroy();
                                } catch (TransactionLogException exception) {
                                    exception.printStackTrace();
                                }
                            }
                        }
                    }

                    // we also want to remove this entry from the list
                    // of active transactions
                    _activeTransactions.remove(txid);
                }
                break;

            default:
                throw new ResourceManagerException("Cannot process tx state " +
                                                   state);
        }
    }

    /**
     * Add an {@link DataTransactionLogEntry} using the specified txid, rid and
     * data
     *
     * @param txid - the transaction identifier
     * @param rid  - the resource identifier
     * @throws TransactionLogException  - error adding the entry
     * @throws ResourceManagerException - error getting the trnasaction log
     */
    synchronized void logTransactionData(ExternalXid txid, String rid,
                                         Object data)
            throws ResourceManagerException, TransactionLogException {
        getTransactionLog(txid).logTransactionData(txid, _txExpiryTime * 1000,
                                                   rid, data);

        // we also want to add this to the transaction data for that
        // txid
        LinkedList list = (LinkedList) _activeTransactions.get(txid);
        if (list != null) {
            list.add(data);
        } else {
            throw new ResourceManagerException("Trasaction " + txid +
                                               " is not active.");
        }
    }

    /**
     * This is the entry point for the garbage collection callback. It scans
     * through the each transaction log file and determines whether it can be
     * garbage collected. If it can then it simply destroys the corresponding
     * TransactionLog.
     */
    public void garbageCollect() {
        try {
            int gcfiles = 0;

            // if there are no transaction log files then return
            if (_logs.size() == 0) {
                return;
            }

            TreeSet copy = null;
            synchronized (_logs) {
                copy = new TreeSet(_logs);
            }

            // remove the current log file, since this is likely to be the
            // current log file
            copy.remove(_logs.last());

            // process each of the remaining log files
            while (copy.size() > 0) {
                TransactionLog log = (TransactionLog) copy.first();
                copy.remove(log);
                if (log.canGarbageCollect()) {
                    // destroy the log
                    log.destroy();

                    // remove it from the log cache
                    synchronized (_logs) {
                        _logs.remove(log);
                    }

                    // increment the number of garbafe collected files
                    ++gcfiles;
                }
            }

            // print an informative message
            _log.info("[RMGC] Collected " + gcfiles + " files.");
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    /**
     * Ensure that a transaction with the specified xid is currently active. If
     * this is the case then commit the transaction based onb the value of the
     * onePhase flag.
     * <p/>
     * This will have the effect of passing all messages through
     *
     * @param id       - the xa transaction identity
     * @param onePhase - treu if it is a one phase commit
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized void commit(Xid id, boolean onePhase)
            throws XAException {
        // check that the xid is not null
        if (id == null) {
            throw new XAException(XAException.XAER_NOTA);
        }

        // covert to our internal representation of an xid
        ExternalXid xid = new ExternalXid(id);

        // check to see that the transaction is active and open. We should
        // not be allowed to commit a committed transaction.
        if (!isTransactionActive(xid)) {
            throw new XAException(XAException.XAER_PROTO);
        }

        // process all the messages associated with this global transaction
        // If a message has been  published then sent it to the message mgr
        // for processing. If a message has been consumed then remove it
        // from the list of unconsumed messages.
        try {
            // retrieve a list of recrods for the specified global transaction
            // and process them. Ignore the state records and only process the
            // data records, which are of type TransacitonalObjectWrapper.
            Object[] records = getTransactionRecords(xid, _rid);
            for (int index = 0; index < records.length; index++) {
                if (records[index] instanceof TransactionalObjectWrapper) {
                    TransactionalObjectWrapper wrapper =
                            (TransactionalObjectWrapper) records[index];
                    if (wrapper.isPublishedMessage()) {
                        // send the published message to the message manager
                        MessageImpl message = (MessageImpl) wrapper.getObject();
                        _messages.add(message);

                    } else if (wrapper.isReceivedMessage()) {
                        // if it is a received message handle then simply
                        // delete it and mark it as acknowledged
                        MessageHandle handle = ((ReceivedMessageWrapper) (wrapper)).getMessageHandle();
                        handle.destroy();
                    }
                } else {
                    // ignore since it is a state records.
                }
            }
        } catch (Exception exception) {
            _log.error(exception, exception);
            throw new XAException("Failed in ResourceManager.commit : " +
                                  exception.toString());
        } finally {
            // and now mark the transaction as closed
            try {
                logTransactionState(xid, TransactionState.CLOSED);
            } catch (Exception exception) {
                throw new XAException("Error processing commit : " + exception);
            }
        }
    }

    /**
     * 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 completedCommits an XA transaction
     * that is in progress.
     *
     * @param id    - the xa transaction identity
     * @param flags - one of TMSUCCESS, TMFAIL, or TMSUSPEND
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized void end(Xid id, int flags)
            throws XAException {
        //check the xid is not null
        if (id == null) {
            throw new XAException(XAException.XAER_NOTA);
        }

        // covert to our internal representation of an xid
        ExternalXid xid = new ExternalXid(id);

        // check that the flags are valid for this method
        if ((flags != XAResource.TMSUSPEND) ||
                (flags != XAResource.TMSUCCESS) ||
                (flags != XAResource.TMFAIL)) {
            throw new XAException(XAException.XAER_PROTO);
        }

        switch (flags) {
            case XAResource.TMFAIL:
                // check that the transaction exists
                if (!isTransactionActive(xid)) {
                    throw new XAException(XAException.XAER_PROTO);
                }

                // do not process that associated data, simply rollback
                rollback(xid);
                break;

            case XAResource.TMSUSPEND:
                // check that the transaction is opened
                if (!isTransactionActive(xid)) {
                    throw new XAException(XAException.XAER_PROTO);
                }
                break;

            case XAResource.TMSUCCESS:
                // nothing to do here but check that the resource manager is
                // in a consistent state wrt to this xid. The xid should not
                // be active if it received the commit, forget etc.
                if (isTransactionActive(xid)) {
                    throw new XAException(XAException.XAER_PROTO);
                }
                break;
        }
    }

    /**
     * Tell the resource manager to forget about a heuristically completed
     * transaction branch.
     *
     * @param id - the xa transaction identity
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized void forget(Xid id)
            throws XAException {
        //check the xid is not null
        if (id == null) {
            throw new XAException(XAException.XAER_NOTA);
        }

        // covert to our internal representation of an xid
        ExternalXid xid = new ExternalXid(id);

        // check to see that the xid actually exists
        if (!isTransactionActive(xid)) {
            throw new XAException(XAException.XAER_PROTO);
        }

        // call rollback to complete the work
        rollback(id);
    }

    /**
     * Return the transaction timeout for this instance of the resource
     * manager.
     *
     * @return int - the timeout in seconds
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized int getTransactionTimeout()
            throws XAException {
        return _txExpiryTime;
    }

    /**
     * Ask the resource manager to prepare for a transaction commit of the
     * transaction specified in xid
     *
     * @param xares
     * @return int - XA_RDONLY or XA_OK
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized boolean isSameRM(XAResource xares)
            throws XAException {
        boolean result = false;

        if ((xares == this) ||
                ((xares instanceof ResourceManager) &&
                (((ResourceManager) xares)._rid.equals(_rid)))) {
            result = true;
        }

        return result;
    }

    /**
     * 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.
     *
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized int prepare(Xid id)
            throws XAException {
        //check the xid is not null
        if (id == null) {
            throw new XAException(XAException.XAER_NOTA);
        }

        // covert to our internal representation of an xid
        ExternalXid xid = new ExternalXid(id);

        // check to see that the xid actually exists
        if (!isTransactionActive(xid)) {
            throw new XAException(XAException.XAER_PROTO);
        }

        // can a prepare for the same resource occur multiple times
        // ????

        try {
            logTransactionState(xid, TransactionState.PREPARED);
        } catch (Exception exception) {
            throw new XAException("Error processing prepare : " + exception);
        }

        return XAResource.XA_OK;
    }

    /**
     * Inform the resource manager to roll back work done on behalf of a
     * transaction branch
     *
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized Xid[] recover(int flag)
            throws XAException {

        Xid[] result = new Xid[0];

        if ((flag == XAResource.TMNOFLAGS) ||
                (flag == XAResource.TMSTARTRSCAN) ||
                (flag == XAResource.TMENDRSCAN)) {
            LinkedList xids = new LinkedList();
            Iterator iter = _activeTransactions.keySet().iterator();
            while (iter.hasNext()) {
                Xid xid = (Xid) iter.next();
                LinkedList list = (LinkedList) _activeTransactions.get(xid);
                if (list.size() > 1) {
                    // need at least a start in the chain.
                    Object last = list.getLast();
                    if ((last instanceof StateTransactionLogEntry)
                            &&
                            (((StateTransactionLogEntry) last).getState()
                            .isPrepared())) {
                        xids.add(xid);
                    }
                }

            }
            result = (Xid[]) xids.toArray();
        }

        return result;
    }

    /**
     * Set the current transaction timeout value for this XAResource instance.
     *
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized void rollback(Xid id)
            throws XAException {
        //check the xid is not null
        if (id == null) {
            throw new XAException(XAException.XAER_NOTA);
        }

        // covert to our internal representation of an xid
        ExternalXid xid = new ExternalXid(id);

        // check to see that the xid actually exists
        if (!isTransactionActive(xid)) {
            throw new XAException(XAException.XAER_PROTO);
        }

        // process the data in that transaction. If it was a published message
        // then drop it. If it was a consumed message then return it back to
        // the destination.
        try {
            // retrieve a list of recrods for the specified global transaction
            // and process them. Ignore the state records and only process the
            // data records, which are of type TransacitonalObjectWrapper.
            Object[] records = getTransactionRecords(xid, _rid);
            for (int index = 0; index < records.length; index++) {
                if (records[index] instanceof TransactionalObjectWrapper) {
                    TransactionalObjectWrapper wrapper =
                            (TransactionalObjectWrapper) records[index];
                    if (wrapper.isPublishedMessage()) {
                        // we don't need to process these messages since the
                        // global transaction has been rolled back.
                    } else if (wrapper.isReceivedMessage()) {
                        ReceivedMessageWrapper rmsg_wrapper =
                                (ReceivedMessageWrapper) wrapper;
                        MessageHandle handle =
                                (MessageHandle) rmsg_wrapper.getObject();
                        JmsDestination dest = handle.getDestination();
                        DestinationCache cache =
                                _destinations.getDestinationCache(dest);
                        cache.returnMessageHandle(handle);
                    }
                } else {
                    // ignore since it is a state records.
                }
            }
        } catch (Exception exception) {
            throw new XAException("Failed in ResourceManager.rollback : " +
                                  exception.toString());
        } finally {
            // and now mark the transaction as closed
            try {
                logTransactionState(xid, TransactionState.CLOSED);
            } catch (Exception exception) {
                throw new XAException(
                        "Error processing rollback : " + exception);
            }
        }
    }

    /**
     * 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
     *
     * @throws XAException - if there is a problem completing the call
     */
    public synchronized boolean setTransactionTimeout(int seconds)
            throws XAException {
        _txExpiryTime = seconds;
        return true;
    }

    // implementation of XAResource.start
    public synchronized void start(Xid id, int flags)
            throws XAException {

        //check the xid is not null
        if (id == null) {
            throw new XAException(XAException.XAER_NOTA);
        }

        // covert to our internal representation of an xid
        ExternalXid xid = new ExternalXid(id);

        // check that the flags are valid for this method
        if ((flags != XAResource.TMNOFLAGS) ||
                (flags != XAResource.TMJOIN) ||
                (flags != XAResource.TMRESUME)) {
            throw new XAException(XAException.XAER_PROTO);
        }

        switch (flags) {
            case XAResource.TMNOFLAGS:
                // check to see that the xid does not already exist
                if (isTransactionActive(xid)) {
                    throw new XAException(XAException.XAER_DUPID);
                }

                // otherwise log the start of the transaction
                try {
                    logTransactionState(xid, TransactionState.OPENED);
                } catch (Exception exception) {
                    throw new XAException(
                            "Error processing start : " + exception);
                }
                break;

            case XAResource.TMJOIN:
            case XAResource.TMRESUME:
                // joining a transaction previously seen by the resource
                // manager
                if (!isTransactionActive(xid)) {
                    throw new XAException(XAException.XAER_PROTO);
                }
                break;
        }
    }

    /**
     * Return the resource manager identity
     *
     * @return the resource manager identity
     */
    public String getResourceManagerId() {
        return _rid;
    }

    /**
     * Create the next {@link TransactionLog} and add it to the list of managed
     * transaction logs.
     * <p/>
     * The method will throw ResourceManagerException if there is a problem
     * completing the request.
     *
     * @throws ResourceManagerException
     */
    protected TransactionLog createNextTransactionLog()
            throws ResourceManagerException {
        TransactionLog newlog = null;

        synchronized (_logs) {
            try {
                // get the last log number
                long last = 1;
                if (!_logs.isEmpty()) {
                    last
                            = getSequenceNumber(
                                    ((TransactionLog) _logs.last()).getName());
                }

                // now that we have the last log number, increment it and use
                // it to build the name of the next log file.
                String name = _logDirectory
                        + System.getProperty("file.separator") +
                        RM_LOGFILE_PREFIX + Long.toString(++last)
                        + RM_LOGFILE_EXTENSION;

                // create a transaction log and add it to the collection
                newlog = new TransactionLog(name, true);
                _logs.add(newlog);
            } catch (TransactionLogException exception) {
                throw new ResourceManagerException(
                        "Error in createNextTransactionLog " + exception);
            }
        }

        return newlog;
    }

    /**
     * Build a list of all log files in the specified log directory
     *
     * @throws IllegalArgumentException - if the directory does not exist.
     */
    protected void buildLogFileList() {
        File dir = new File(_logDirectory);
        if ((!dir.exists()) ||
                (!dir.isDirectory())) {
            throw new IllegalArgumentException(_logDirectory +
                                               " is not a directory");
        }

        try {
            File[] list = dir.listFiles(new FilenameFilter() {

                // implementation of FilenameFilter.accept
                public boolean accept(File dir, String name) {
                    boolean result = false;

                    if ((name.startsWith(RM_LOGFILE_PREFIX)) &&
                            (name.endsWith(RM_LOGFILE_EXTENSION))) {
                        result = true;
                    }

                    return result;
                }
            });

            // add the files to the list
            synchronized (_logs) {
                for (int index = 0; index < list.length; index++) {
                    _logs.add(new TransactionLog(list[index].getPath(), false));
                }
            }
        } catch (Exception exception) {
            // replace this with the exception strategy
            exception.printStackTrace();
        }

    }

    /**
     * This method will process all the transaction logs, in the log diretory
     * and call recover on each of them.
     *
     * @throws ResourceManagerException - if there is a problem recovering
     */
    private synchronized void recover()
            throws ResourceManagerException {
        try {
            if (!_logs.isEmpty()) {
                Iterator iter = _logs.iterator();
                while (iter.hasNext()) {
                    TransactionLog log = (TransactionLog) iter.next();
                    HashMap records = log.recover();
                }
            }
        } catch (Exception exception) {
            throw new ResourceManagerException("Error in recover " +
                                               exception.toString());
        }
    }

    /**
     * Retrieve the transaction log for the specified transaction id
     *
     * @param txid - the transaction identity
     * @return TransactionLog
     * @throws TransactionLogException  - if there is tx log exception
     * @throws ResourceManagerException - if there is a resource problem.
     */
    private TransactionLog getTransactionLog(ExternalXid txid)
            throws TransactionLogException, ResourceManagerException {
        TransactionLog log = (TransactionLog) _tridToLogCache.get(txid);
        if (log == null) {
            log = getCurrentTransactionLog();
            addTridLogEntry(txid, log);
        }

        return log;
    }

    /**
     * Get the current transaction log. It will check the last transaction log
     * opened by the resource manager and determine whether there is space
     * enough to process another transaction.
     * <p/>
     * If there is space enough then it will return that transaction, otherwise
     * it will create a new transaction log for the resource
     *
     * @return TransactionLog - the transaction log to use
     * @throws ResourceManagerException
     * @throws TransactionLogException
     */
    private TransactionLog getCurrentTransactionLog()
            throws TransactionLogException, ResourceManagerException {
        TransactionLog log = null;

        synchronized (_logs) {
            if (_logs.size() > 0) {
                log = (TransactionLog) _logs.last();
            }

            if ((log == null) ||
                    (log.size() > _logFileSize)) {
                log = createNextTransactionLog();
            }
        }

        return log;
    }

    /**
     * Add an entry to the trid log cache table for the specified trid and
     * transaction log mapping.
     *
     * @param trid - the transaction identifier
     * @param log  - the transaction log
     */
    private void addTridLogEntry(ExternalXid trid, TransactionLog log) {
        synchronized (_cacheLock) {
            // one to one relationship
            _tridToLogCache.put(trid, log);

            // one to many relationship
            Vector trids = (Vector) _logToTridCache.get(log);
            if (trids == null) {
                trids = new Vector();
                _logToTridCache.put(log, trids);
            }
            trids.addElement(trid);
        }
    }

    /**
     * Check whether the specified log is also the current log
     *
     * @param log - the log to check
     * @return boolean - true if it is
     */
    private boolean isCurrentTransactionLog(TransactionLog log) {
        boolean result = false;

        if (_logs.size() > 0) {
            result = log.equals(_logs.last());
        }

        return result;
    }

    /**
     * Remove an entry to the trid log cache table for the specified trid and
     * transaction log mapping.
     *
     * @param trid - the transaction identifier
     * @param log  - the transaction log
     */
    private void removeTridLogEntry(ExternalXid trid, TransactionLog log) {
        synchronized (_cacheLock) {

            // one to one relationship
            _tridToLogCache.remove(trid);

            // one to many relationship
            Vector trids = (Vector) _logToTridCache.get(log);
            if (trids != null) {
                trids.remove(trid);
                if (trids.size() == 0) {
                    _logToTridCache.remove(log);
                }
            }
        }
    }

    /**
     * Return an arrya of records, both state and date, for the specified global
     * transaction
     *
     * @param xid - the global transaction id
     * @param rid - the resource id
     * @return Object[] - array of records
     */
    protected Object[] getTransactionRecords(ExternalXid xid, String rid) {
        Object[] records;

        // we also want to add this to the transaction data for that
        // txid
        LinkedList list = (LinkedList) _activeTransactions.get(xid);
        if (list != null) {
            records = list.toArray();
        } else {
            records = new Object[0];
        }

        return records;
    }


    /**
     * Return the sequence number of the file files are associated with a unique
     * number
     *
     * @param name - the file name to investigate
     * @return long - the transaction log number
     * @throws ResourceManagerException
     */
    protected long getSequenceNumber(String name)
            throws ResourceManagerException {
        int start = name.indexOf(RM_LOGFILE_PREFIX) +
                RM_LOGFILE_PREFIX.length();
        int end = name.indexOf(RM_LOGFILE_EXTENSION);

        // the number must be between the start and end positions
        try {
            return Long.parseLong(name.substring(start, end));
        } catch (NumberFormatException exception) {
            throw new ResourceManagerException(
                    "Invalid name assigned to resource manager file " + name);
        }
    }

    /**
     * Return true if the specified transaction is active
     *
     * @param xid - the gobal transaction identifier
     */
    private synchronized boolean isTransactionActive(ExternalXid xid) {
        return _activeTransactions.containsKey(xid);
    }

    /**
     * Dump the specified records to the screen
     */
    private void dumpRecovered(HashMap records) {
        Iterator iter = records.keySet().iterator();
        while (iter.hasNext()) {
            ExternalXid txid = (ExternalXid) iter.next();
            LinkedList list = (LinkedList) records.get(txid);
            Iterator oiter = list.iterator();
            while (oiter.hasNext()) {
                Object object = oiter.next();
                if (object instanceof StateTransactionLogEntry) {
                    System.err.println(
                            "Recovered [" + txid + "] Class " +
                            object.getClass().getName()
                            + " ["
                            +
                            ((StateTransactionLogEntry) object).getState()
                            .toString()
                            + "]");
                } else {
                    System.err.println("Recovered [" + txid + "] Class " +
                                       object.getClass().getName());
                }
            }
        }
    }


    /**
     * Helper and type-safe method for creating a wrapper object for published
     * messages
     *
     * @param message - the message published
     * @return PublishedMessageWrapper
     */
    private PublishedMessageWrapper createPublishedMessageWrapper(
            MessageImpl message) {
        return new PublishedMessageWrapper(message);
    }

    /**
     * Helper and type-safe method for creating a wrapper object for received
     * messages
     *
     * @param id     - the identity of the consumer receiving the message
     * @param handle - the handle of the message received
     * @return ReceivedMessageWrapper
     */
    private ReceivedMessageWrapper createReceivedMessageWrapper(long id,
                                                                MessageHandle handle) {
        return new ReceivedMessageWrapper(id, handle);
    }

    /**
     * This functor is used by various collections to order the transaction log
     * files created by this resource manager. The resource manager will create
     * log files with sequentially increasing numbers (i.e xxx01.log, xxx2.log
     */
    private class TranLogFileComparator
            implements Comparator {

        // implementation of Comparator.comapre
        public int compare(Object o1, Object o2) {
            int result = -1;

            try {
                if ((o1 instanceof TransactionLog) &&
                        (o2 instanceof TransactionLog)) {
                    long seq1 = getSequenceNumber(
                            ((TransactionLog) o1).getName());
                    long seq2 = getSequenceNumber(
                            ((TransactionLog) o2).getName());

                    if (seq1 > seq2) {
                        result = 1;
                    } else if (seq1 < seq2) {
                        result = -1;
                    } else {
                        result = 0;
                    }
                } else {
                    throw new ClassCastException("o1 = " +
                                                 o1.getClass().getName() + " and o2 = " +
                                                 o2.getClass().getName());
                }
            } catch (Exception exception) {
                throw new RuntimeException("Error in ResourceManager.compare " +
                                           exception.toString());
            }

            return result;
        }

        // implementation of Comparator.equals
        public boolean equals(Object obj) {
            if (obj instanceof TranLogFileComparator) {
                return true;
            }

            return false;
        }
    }


    /**
     * This private member class is used to wrap the transactional object, which
     * for this particular resource manager is a published message or a received
     * message handle.
     */
    abstract private class TransactionalObjectWrapper {

        /**
         * The transactional object instance
         */
        private Object _object;

        /**
         * Create an instance of the wrapper using the type and the object
         *
         * @param object - the associated object
         */
        public TransactionalObjectWrapper(Object object) {
            _object = object;
        }

        /**
         * Check whether the wrapper contains a published message. Note that a
         * published message has a {@link MessageImpl} a the transactional
         * object.
         *
         * @return boolean - true if it is
         */
        public boolean isPublishedMessage() {
            return this instanceof PublishedMessageWrapper;
        }

        /**
         * Check whether the wrapper contains a received message handle. Note
         * that a received message contains a {@link MessageHandle} as the
         * transactional object.
         *
         * @return boolean - true if it does
         */
        public boolean isReceivedMessage() {
            return this instanceof ReceivedMessageWrapper;
        }

        /**
         * Return the transaction object
         *
         * @return Object
         */
        public Object getObject() {
            return _object;
        }

    }


    /**
     * This private member class is used to wrap a published message
     */
    private class PublishedMessageWrapper extends TransactionalObjectWrapper {

        /**
         * Create an instance of the wrapper using the specified message
         *
         * @param message - the message to wrap
         */
        public PublishedMessageWrapper(MessageImpl message) {
            super(message);
        }

        /**
         * Return an instance of the message object
         *
         * @return MessageImpl
         */
        public MessageImpl getMessage() {
            return (MessageImpl) super.getObject();
        }
    }


    /**
     * This private member class is used to wrap a received message
     */
    private class ReceivedMessageWrapper extends TransactionalObjectWrapper {

        /**
         * Caches the id of the {@link ConsumerEndpoint} that is processed this
         * handle
         */
        private long _consumerId;

        /**
         * Create an instance of the wrapper using the specified message
         *
         * @param id     - the identity of the consumer endpoint
         * @param handle - the handle to the message
         */
        public ReceivedMessageWrapper(long id, MessageHandle handle) {
            super(handle);
            _consumerId = id;
        }

        /**
         * Return a reference to the  consumer identity
         *
         * @return String
         */
        public long getConsumerId() {
            return _consumerId;
        }

        /**
         * Return an instance of the message handle
         *
         * @return MessageHandle
         */
        public MessageHandle getMessageHandle() {
            return (MessageHandle) super.getObject();
        }
    }

}
