/*
 * $Header: /home/cvs/jakarta-slide/src/stores/org/apache/slide/store/txfile/AbstractTxFileStoreService.java,v 1.16 2004/08/09 09:35:20 ozeigermann Exp $
 * $Revision: 1.16 $
 * $Date: 2004/08/09 09:35:20 $
 *
 * ====================================================================
 *
 * 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.esupportail.webdavserver.store.txhashfile;
 
import org.apache.commons.transaction.file.FileResourceManager;
/* ESUP */
import org.apache.commons.transaction.file.ResourceIdToPathMapper;
/* FIN ESUP */
import org.apache.commons.transaction.file.ResourceManager;
import org.apache.commons.transaction.file.ResourceManagerException;
import org.apache.commons.transaction.util.xa.XidWrapper;
import org.apache.slide.common.*;
import org.apache.slide.macro.ConflictException;

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.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";

    /* ESUP */
    protected static final String HASH_METHOD_PARAMETER = "hash-method";
    protected static final String STORE_ROOT_PARAMETER = "store-root";
    
    protected String hashMethod; // HASH_METHOD_HASH | HASH_METHOD_HASH_REVERSE 
    protected String storeRoot;     
    /* FIN ESUP */
    
    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);
        }

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

        /* ESUP */
        storeRoot = (String) parameters.get(STORE_ROOT_PARAMETER);        
        if (storeRoot == null) {
            throw new ServiceParameterMissingException(this, STORE_ROOT_PARAMETER);
        }
        
        hashMethod = (String) parameters.get(HASH_METHOD_PARAMETER);        
        if (hashMethod == null) {
            throw new ServiceParameterMissingException(this, HASH_METHOD_PARAMETER);
        }
        /* FIN ESUP*/
        /* ESUP 
         rm =
            new FileResourceManager(
                storeDir,
                workDir,
                urlEncodePath,
                new TxLogger(getLogger(), FileResourceManager.class.getName()), 
                debug);
                */
        ResourceIdToPathMapper HashMapper =  new HashIdMapper();
        ((HashIdMapper) HashMapper).init(hashMethod,storeRoot,getLogger());
        rm =
            new FileResourceManager(
                storeDir,
                workDir,               
                HashMapper,
                new TxLogger(getLogger(), FileResourceManager.class.getName()), 
                debug);
        /* FIN ESUP */
        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);

    }

    // TODO if error is caused by lock that could not be acquired
    // we should try deadlock detection instead of simply rolling back
    // if no deadlock is detected, retrying for lock would be preferred method    
    public synchronized void throwInternalError(Throwable cause, String uri) throws ServiceAccessException {
        Object txId = getActiveTxId();

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

            // XXX strictly speaking, this is incorrect, as we actually did not chck for deadlock,
            // but silently assume a deadlock must have been the cause for the failed 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));

        } 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();
}
