/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/store/txfile/AbstractTxFileStoreService.java,v 1.20 2004/12/19 00:17:51 ozeigermann Exp $
 * $Revision: 1.20 $
 * $Date: 2004/12/19 00:17:51 $
 *
 * ====================================================================
 *
 * 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.store.txfile;

import org.apache.commons.transaction.file.FileResourceManager;
import org.apache.commons.transaction.file.ResourceIdToPathMapper;
import org.apache.commons.transaction.file.ResourceManager;
import org.apache.commons.transaction.file.ResourceManagerException;
import org.apache.commons.transaction.file.URLEncodeIdMapper;
import org.apache.commons.transaction.util.xa.XidWrapper;
import org.apache.slide.common.*;

import java.io.File;
import java.util.Hashtable;

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

import org.apache.slide.store.ConcurrencyConflictError;
import org.apache.slide.util.logger.TxLogger;
import org.apache.slide.util.logger.Logger;

/**
 * Abstract base for transactional file stores. 
 *
 * Even though the <code>XAResource</code> interface is implemented,
 * this code does not provide conformance to the JTA/JTS or XA spec. 
 *
 * It is only intended for use inside of Slide's transaction manager.  
 *  
 * 
 */
public abstract class AbstractTxFileStoreService extends AbstractServiceBase implements Status {

    protected static final int DEBUG_LEVEL = Logger.DEBUG;

    protected static final String STORE_DIR_PARAMETER = "rootpath";
    protected static final String WORK_DIR_PARAMETER = "workpath";
    protected static final String TIMEOUT_PARAMETER = "timeout";
    protected static final String URLENCODE_PATH_PARAMETER = "url-encode-path";
    protected static final String DEBUG_MODE_PARAMETER = "debug";
    protected static final String USE_PORTABLE_PATH_PARAMETER = "use-portable-path";

    protected FileResourceManager rm;
    protected boolean started = false;
    protected String storeDir;
    protected String workDir;

    // there might be at least one active transaction branch per thread
    protected ThreadLocal activeTransactionBranch = new ThreadLocal();

    public void setParameters(Hashtable parameters)
        throws ServiceParameterErrorException, ServiceParameterMissingException {

        storeDir = (String) parameters.get(STORE_DIR_PARAMETER);
        workDir = (String) parameters.get(WORK_DIR_PARAMETER);

        if (storeDir == null) {
            throw new ServiceParameterMissingException(this, STORE_DIR_PARAMETER);
        }
        if (workDir == null) {
            throw new ServiceParameterMissingException(this, WORK_DIR_PARAMETER);
        }

        new File(storeDir).mkdirs();
        new File(workDir).mkdirs();

        boolean debug = false;
        String debugString = (String) parameters.get(DEBUG_MODE_PARAMETER);
        if (debugString != null) {
            debug = "true".equals(debugString);
        }

        ResourceIdToPathMapper idMapper = null;
        
        String urlEncodePathString = (String) parameters.get(URLENCODE_PATH_PARAMETER);
        if (urlEncodePathString != null) {
            boolean urlEncodePath = "true".equals(urlEncodePathString);
            if (urlEncodePath) idMapper = new URLEncodeIdMapper();
        }

        String usePortablePathString = (String) parameters.get(USE_PORTABLE_PATH_PARAMETER);
        if (usePortablePathString != null) {
            boolean usePortablePath = "true".equals(usePortablePathString);
            if (usePortablePath) idMapper = new PortableIdMapper();
        }
        
        rm =
            new FileResourceManager(
                storeDir,
                workDir,
                idMapper,
                new TxLogger(getLogger(), FileResourceManager.class.getName()), 
                debug);

        getLogger().log(
            "File Store configured to " + storeDir + ", working directory " + workDir,
            getLogChannel(),
            Logger.INFO);

        String timeoutString = (String) parameters.get(TIMEOUT_PARAMETER);
        if (timeoutString != null) {
            try {
                int timeout = Integer.parseInt(timeoutString);
                rm.setDefaultTransactionTimeout(timeout * 1000);
                getLogger().log("Set timeout to " + timeoutString, getLogChannel(), Logger.INFO);
            } catch (NumberFormatException nfe) {
                getLogger().log(
                    "Can not set timeout, '" + timeoutString + "' must be an integer!",
                    getLogChannel(),
                    Logger.WARNING);
            }
        }

    }

    public String toString() {
        return "TxFileStore at " + storeDir + "  working on " + workDir;
    }

    public void connect() throws ServiceConnectionFailedException {
        try {
            rm.start();
            started = true;
        } catch (ResourceManagerException e) {
            throw new ServiceConnectionFailedException(this, e);
        }
    }

    public void disconnect() throws ServiceDisconnectionFailedException {
        try {
            if (!rm.stop(ResourceManager.SHUTDOWN_MODE_NORMAL)) {
                throw new ServiceDisconnectionFailedException(this, "Shut down timed out");
            }
            started = false;
        } catch (ResourceManagerException e) {
            throw new ServiceDisconnectionFailedException(this, e);
        }
    }

    public boolean isConnected() throws ServiceAccessException {
        return started;
    }

    public void reset() {
        rm.reset();
    }

    public int getTransactionTimeout() throws XAException {
        try {
            long msecs = rm.getTransactionTimeout(getActiveTxId());
            return Math.round(msecs / (float) 1000);
        } catch (ResourceManagerException e) {
            throw createXAException(e);
        }
    }

    public boolean setTransactionTimeout(int seconds) throws XAException {
        try {
            rm.setTransactionTimeout(getActiveTxId(), seconds * 1000);
            return true;
        } catch (ResourceManagerException e) {
            throw createXAException(e);
        }
    }

    public boolean isSameRM(XAResource xares) throws XAException {
        return (xares instanceof AbstractTxFileStoreService && ((AbstractTxFileStoreService) xares).rm.equals(this.rm));
    }

    public synchronized Xid[] recover(int flag) throws XAException {
        // do not care as this never is called by Slide
        return null;
    }

    public synchronized void forget(Xid xid) throws XAException {
        // there is nothing we remember, so there is nothing to forget
    }

    public synchronized int prepare(Xid xid) throws XAException {
        Object txId = wrap(xid);
        Thread currentThread = Thread.currentThread(); 
        getLogger().log(
            "Thread " + currentThread + " prepares transaction branch " + txId,
            getLogChannel(),
            DEBUG_LEVEL);
        try {
            int status = rm.prepareTransaction(txId);
            switch (status) {
                case ResourceManager.PREPARE_SUCCESS_READONLY :
                    return XA_RDONLY;
                case ResourceManager.PREPARE_SUCCESS :
                    return XA_OK;
                default :
                    throw new XAException(XAException.XA_RBROLLBACK);
            }
        } catch (ResourceManagerException e) {
            getLogger().log(
                "Thread " + currentThread + " failed to prepare transaction branch " + txId,
                e,
                getLogChannel(),
                Logger.CRITICAL);
            throw createXAException(e);
        }
    }

    public synchronized void rollback(Xid xid) throws XAException {
        Object txId = wrap(xid);
        Thread currentThread = Thread.currentThread(); 
        getLogger().log(
            "Thread " + currentThread + " rolls back transaction branch " + txId,
            getLogChannel(),
            DEBUG_LEVEL);

        try {
            rm.rollbackTransaction(txId);
            activeTransactionBranch.set(null);
        } catch (ResourceManagerException e) {
            getLogger().log(
                "Thread " + currentThread + " failed to roll back transaction branch " + txId,
                e,
                getLogChannel(),
                Logger.CRITICAL);
            throw createXAException(e);
        }
    }

    public synchronized void commit(Xid xid, boolean onePhase) throws XAException {
        Object txId = wrap(xid);
        Thread currentThread = Thread.currentThread(); 
        getLogger().log(
            "Thread " + currentThread + " commits transaction branch " + txId,
            getLogChannel(),
            DEBUG_LEVEL);

        try {
            if (!onePhase && rm.getTransactionState(txId) != STATUS_PREPARED) {
                throw new XAException(XAException.XAER_INVAL);
            }

            rm.commitTransaction(txId);
            activeTransactionBranch.set(null);
        } catch (ResourceManagerException e) {
            getLogger().log(
                "Thread " + currentThread + " failed to commit transaction branch " + txId,
                e,
                getLogChannel(),
                Logger.CRITICAL);
            throw createXAException(e);
        }
    }

    public synchronized void end(Xid xid, int flags) throws XAException {
        Object txId = wrap(xid);
        Thread currentThread = Thread.currentThread(); 
        getLogger().log(
            "Thread "
                + currentThread
                + (flags == TMSUSPEND ? " suspends" : flags == TMFAIL ? " fails" : " ends")
                + " work on behalf of transaction branch "
                + txId,
            getLogChannel(),
            DEBUG_LEVEL);

        switch (flags) {
            case TMSUSPEND :
                activeTransactionBranch.set(null);
                break;
            case TMFAIL :
                try {
                    rm.markTransactionForRollback(wrap(xid));
                } catch (ResourceManagerException e) {
                    throw createXAException(e);
                }
                break;
            case TMSUCCESS :
                // not ineresting for us
                break;
        }
    }

    public synchronized void start(Xid xid, int flags) throws XAException {
        Object txId = wrap(xid);
        Thread currentThread = Thread.currentThread(); 
        getLogger().log(
            "Thread "
                + currentThread
                + (flags == TMNOFLAGS ? " starts" : flags == TMJOIN ? " joins" : " resumes")
                + " work on behalf of transaction branch "
                + txId,
            getLogChannel(),
            DEBUG_LEVEL);

        switch (flags) {
            // a new transaction
            case TMNOFLAGS :
                if (getActiveTxId() != null) {
                    throw new XAException(XAException.XAER_INVAL);
                }
                try {
                    rm.startTransaction(txId);
                    activeTransactionBranch.set(txId);
                } catch (ResourceManagerException e) {
                    throw createXAException(e);
                }
                break;
            case TMJOIN :
                if (getActiveTxId() != null) {
                    throw new XAException(XAException.XAER_INVAL);
                }
                try {
                    if (rm.getTransactionState(txId) == STATUS_NO_TRANSACTION) {
                        throw new XAException(XAException.XAER_INVAL);
                    }
                } catch (ResourceManagerException e) {
                    throw createXAException(e);
                }
                activeTransactionBranch.set(txId);
                break;
            case TMRESUME :
                activeTransactionBranch.set(txId);
                break;
        }
    }

    public synchronized void throwInternalError(String cause) throws ServiceAccessException {
        Object txId = getActiveTxId();

        getLogger().log(
            "Thread "
                + Thread.currentThread()
                + " marked transaction branch "
                + txId
                + " for rollback. Cause: "
                + cause,
            getLogChannel(),
            Logger.WARNING);

        try {
            rm.markTransactionForRollback(txId);
        } catch (ResourceManagerException re) {
            throw new ServiceAccessException(this, re);
        }

        throw new ServiceAccessException(this, cause);

    }

    public synchronized void throwInternalError(Throwable cause) throws ServiceAccessException {
        Object txId = getActiveTxId();

        getLogger().log(
            "Thread "
                + Thread.currentThread()
                + " marked transaction branch "
                + txId
                + " for rollback. Cause: "
                + cause,
            getLogChannel(),
            Logger.WARNING);

        try {
            rm.markTransactionForRollback(txId);
        } catch (ResourceManagerException re) {
            throw new ServiceAccessException(this, re);
        }

        throw new ServiceAccessException(this, cause);

    }

    public synchronized void throwInternalError(Throwable cause, String uri) throws ServiceAccessException {
        Object txId = getActiveTxId();

        if ((cause instanceof ResourceManagerException)
                && (((ResourceManagerException) cause).getStatus() == ResourceManagerException.ERR_NO_LOCK || ((ResourceManagerException) cause)
                        .getStatus() == ResourceManagerException.ERR_DEAD_LOCK)) {

            if (txId != null) {
                try {
                    rm.markTransactionForRollback(txId);
                } catch (ResourceManagerException re) {
                    throw new ServiceAccessException(this, re);
                }
            }
            getLogger().log(
                "DEADLOCK VICTIM: Thread "
                    + Thread.currentThread()
                    + " marked transaction branch "
                    + txId
                    + " for rollback",
                getLogChannel(),
                Logger.INFO);

//            throw new ServiceAccessException(this, new ConflictException(uri));
            throw new ConcurrencyConflictError(((ResourceManagerException) cause).getStatus(),
                    ((ResourceManagerException) cause).getMessage(), uri);

        } else {

            getLogger().log(
                "Could not process URI '"
                    + uri
                    + "'! Thread "
                    + Thread.currentThread()
                    + " marking transaction branch "
                    + txId
                    + " for rollback",
                cause,
                getLogChannel(),
                Logger.WARNING);

            if (txId != null) {
                try {
                    rm.markTransactionForRollback(txId);
                } catch (ResourceManagerException re) {
                    throw new ServiceAccessException(this, re);
                }
            }

            throw new ServiceAccessException(this, cause);

        }
    }

    protected Object getActiveTxId() {
        Object txId = activeTransactionBranch.get();
        return txId;
    }

    protected XAException createXAException(ResourceManagerException e) {
        if (e.getStatus() == ResourceManagerException.ERR_DUP_TX) {
            return new XAException(XAException.XAER_DUPID);
        } else if (e.getStatus() == ResourceManagerException.ERR_TXID_INVALID) {
            return new XAException(XAException.XAER_NOTA);
        } else {
            return new XAException(e.toString());
        }
    }

    protected String wrap(Xid xid) {
        String sxid = XidWrapper.wrap(xid).toString();
        // XXX for tx file store replace :, ', \ and / which might be part of the identifier with chars not 
        // offensive to any file system specific chars
        sxid = sxid.replace('\'', '.').replace('"', '.').replace(':', '.').replace('/', '.').replace('\\', '.');
        return sxid;
    }
    
    abstract protected String getLogChannel();
}
