/*
 * $Header: /home/cvspublic/jakarta-slide/src/stores/org/apache/slide/store/txfile/TxXMLFileDescriptorsStore.java,v 1.21 2004/12/07 17:55:19 luetzkendorf Exp $
 * $Revision: 1.21 $
 * $Date: 2004/12/07 17:55:19 $
 *
 * ====================================================================
 *
 * 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.ResourceManager;
import org.apache.commons.transaction.file.ResourceManagerException;
import org.apache.slide.common.*;
import org.apache.slide.store.*;
import org.apache.slide.structure.*;

import org.apache.slide.security.*;
import org.apache.slide.util.logger.Logger;
import org.apache.slide.lock.*;
import org.apache.slide.content.*;

import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;

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

/**
 * Transactional descriptors file store. 
 * Represents descriptor data as XML using {@link XMLResourceDescriptor}.
 *
 * <br><br>  
 * <em>Note</em>: To achieve search performance on properties comparable to a RDBMS some sort of index will be needed.  
 * 
 * @see XMLResourceDescriptor
 */
public class TxXMLFileDescriptorsStore
    extends AbstractTxFileStoreService
    implements NodeStore, LockStore, RevisionDescriptorsStore, RevisionDescriptorStore, SecurityStore {

    protected static final String LOG_CHANNEL = "file-meta-store";
    
    protected static final int DEBUG_LEVEL = Logger.DEBUG;

    protected static final String ENCODING_PARAMETER = "encoding";

    protected static final String DEFER_SAVING_PARAMETER = "defer-saving";

    protected String characterEncoding = "UTF-8"; // a save choice
    protected boolean deferSaving = false; // the save choice

    protected Map suspendedContexts = new HashMap();
    protected Map activeContexts = new HashMap();

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

        super.setParameters(parameters);

        String encoding = (String) parameters.get(ENCODING_PARAMETER);
        if (encoding != null) {
            characterEncoding = encoding;
        }
        
        String deferSavingString = (String) parameters.get(DEFER_SAVING_PARAMETER);
        if (deferSavingString != null) {
            deferSaving = Boolean.valueOf(deferSavingString).booleanValue();
            if (deferSaving) {
                getLogger().log(
                    "Enabling deferred saving",
                    LOG_CHANNEL,
                    Logger.INFO);
            }
        }
    }

    /*
     * --- public methods of interface NodeStore ---
     *
     *  
     */

    public ObjectNode retrieveObject(Uri uri) throws ServiceAccessException, ObjectNotFoundException {
        XMLResourceDescriptor xfd = getFileDescriptor(uri);
        ObjectNode object = xfd.retrieveObject();
        return object;
    }

    public void storeObject(Uri uri, ObjectNode object) throws ServiceAccessException, ObjectNotFoundException {
        XMLResourceDescriptor xfd = getFileDescriptor(uri);
        xfd.storeObject(object);
        if (deferSaving) {
            xfd.registerForSaving();
        } else {
            xfd.save();
        }
    }

    public void createObject(Uri uri, ObjectNode object) throws ServiceAccessException, ObjectAlreadyExistsException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.create();
            xfd.storeObject(object);
            if (deferSaving) {
                xfd.registerForSaving();
                TxContext txContext = getActiveTxContext();
                if (txContext != null) {
                    txContext.register(uri, xfd);
                }
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            // should not happen, if it does, it is an error inside this store:
            throwInternalError("Newly created file vanished");
        }
    }

    public void removeObject(Uri uri, ObjectNode object) throws ServiceAccessException, ObjectNotFoundException {
        XMLResourceDescriptor xfd = getFileDescriptor(uri);
        xfd.delete();
        if (deferSaving) {
            TxContext txContext = getActiveTxContext();
            if (txContext != null) {
                txContext.deregister(uri);
            }
        }
    }

    /*
     * --- public methods of interface SecurityStore ---
     *
     *  
     */

    public void grantPermission(Uri uri, NodePermission permission) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.grantPermission(permission);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public void revokePermission(Uri uri, NodePermission permission) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.revokePermission(permission);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public void revokePermissions(Uri uri) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.revokePermissions();
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public Enumeration enumeratePermissions(Uri uri) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            return xfd.enumeratePermissions();
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
            return null; // XXX fake (is never called)
        }
    }

    /*
     * --- public methods of interface LockStore ---
     *
     *  
     */

    public void putLock(Uri uri, NodeLock lock) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.putLock(lock);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public void renewLock(Uri uri, NodeLock lock) throws ServiceAccessException, LockTokenNotFoundException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.renewLock(lock);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public void removeLock(Uri uri, NodeLock lock) throws ServiceAccessException, LockTokenNotFoundException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.removeLock(lock);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throw new LockTokenNotFoundException(lock);
        }
    }

    public void killLock(Uri uri, NodeLock lock) throws ServiceAccessException, LockTokenNotFoundException {
        removeLock(uri, lock);
    }

    public Enumeration enumerateLocks(Uri uri) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            return xfd.enumerateLocks();
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
            return null; // XXX fake (is never called)
        }
    }

    /*
     * --- public methods of interface RevisionDescriptorsStore ---
     *
     *  
     */

    public NodeRevisionDescriptors retrieveRevisionDescriptors(Uri uri)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            return xfd.retrieveRevisionDescriptors();
        } catch (ObjectNotFoundException e) {
            throw new RevisionDescriptorNotFoundException(uri.toString());
        }
    }

    public void createRevisionDescriptors(Uri uri, NodeRevisionDescriptors revisionDescriptors)
        throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.createRevisionDescriptors(revisionDescriptors);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public void storeRevisionDescriptors(Uri uri, NodeRevisionDescriptors revisionDescriptors)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.storeRevisionDescriptors(revisionDescriptors);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throw new RevisionDescriptorNotFoundException(uri.toString());
        }
    }

    public void removeRevisionDescriptors(Uri uri) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.removeRevisionDescriptors();
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    /*
     * --- public methods of interface RevisionDescriptorStore ---
     *
     *  
     */

    public NodeRevisionDescriptor retrieveRevisionDescriptor(Uri uri, NodeRevisionNumber revisionNumber)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            return xfd.retrieveRevisionDescriptor(revisionNumber);
        } catch (ObjectNotFoundException e) {
            throw new RevisionDescriptorNotFoundException(uri.toString());
        }
    }

    public void createRevisionDescriptor(Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.createRevisionDescriptor(revisionDescriptor);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

    public void storeRevisionDescriptor(Uri uri, NodeRevisionDescriptor revisionDescriptor)
        throws ServiceAccessException, RevisionDescriptorNotFoundException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.storeRevisionDescriptor(revisionDescriptor);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throw new RevisionDescriptorNotFoundException(uri.toString());
        }
    }

    public void removeRevisionDescriptor(Uri uri, NodeRevisionNumber number) throws ServiceAccessException {
        try {
            XMLResourceDescriptor xfd = getFileDescriptor(uri);
            xfd.removeRevisionDescriptor(number);
            if (deferSaving) {
                xfd.registerForSaving();
            } else {
                xfd.save();
            }
        } catch (ObjectNotFoundException e) {
            throwInternalError(e);
        }
    }

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

    public synchronized void commit(Xid xid, boolean onePhase) throws XAException {
        try {
            super.commit(xid, onePhase);
        } finally {
            Object txId = wrap(xid);
            activeContexts.remove(txId);
            suspendedContexts.remove(txId);
            activeTransactionBranch.set(null);
        }
    }

    public synchronized void rollback(Xid xid) throws XAException {
        try {
            super.rollback(xid);
        } finally {
            Object txId = wrap(xid);
            activeContexts.remove(txId);
            suspendedContexts.remove(txId);
            activeTransactionBranch.set(null);
        }
    }
    
    public synchronized int prepare(Xid xid) throws XAException {
        Object txId = wrap(xid);
        if (getLogger().isEnabled(LOG_CHANNEL, DEBUG_LEVEL)) {
            getLogger().log(
                "Thread " + Thread.currentThread() + " prepares transaction branch " + txId,
                LOG_CHANNEL,
                Logger.DEBUG);
        }
        try {
            if (deferSaving) {
                // save all descriptors registered for saving
                TxContext txContext = (TxContext) activeContexts.get(txId);
                if (txContext == null) txContext = (TxContext) suspendedContexts.get(txId);
                // really should not, but only to be sure...
                if (txContext != null) {
                    try {
                        txContext.saveDescriptors();
                    } catch (ObjectNotFoundException onfe) {
                        getLogger().log(
                            "Thread " + Thread.currentThread() + " failed to prepare transaction branch " + txId,
                            onfe,
                            LOG_CHANNEL,
                            Logger.CRITICAL);
                        throw new XAException(onfe.toString());
                    } catch (ServiceAccessException sae) {
                        getLogger().log(
                            "Thread " + Thread.currentThread() + " failed to prepare transaction branch " + txId,
                            sae,
                            LOG_CHANNEL,
                            Logger.CRITICAL);
                        throw new XAException(sae.toString());
                    }
                } else {
                    getLogger().log(
                        "Thread " + Thread.currentThread() + " could prepare *unknown* transaction branch " + txId,
                        LOG_CHANNEL,
                        Logger.WARNING);
                }
            }
            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 " + Thread.currentThread() + " failed to prepare transaction branch " + txId,
                e,
                LOG_CHANNEL,
                Logger.CRITICAL);
            throw createXAException(e);
        }
    }

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

        switch (flags) {
            case TMSUSPEND :
                suspendedContexts.put(txId, getActiveTxContext());
                activeContexts.remove(txId);
                activeTransactionBranch.set(null);
                break;
            case TMFAIL :
                try {
                    rm.markTransactionForRollback(wrap(xid));
                } catch (ResourceManagerException e) {
                    throw createXAException(e);
                }
                activeTransactionBranch.set(null);
                break;
            case TMSUCCESS :
                activeTransactionBranch.set(null);
                break;
        }
    }

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

        switch (flags) {
            // a new transaction
            case TMNOFLAGS :
                if (getActiveTxId() != null) {
                    throw new XAException(XAException.XAER_INVAL);
                }
                try {
                    rm.startTransaction(txId);
                    TxContext txContext = new TxContext(txId); 
                    activeTransactionBranch.set(txContext);
                    activeContexts.put(txId, txContext);
                } 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);
                }
                TxContext txContext = new TxContext(txId);
                activeTransactionBranch.set(txContext);
                activeContexts.put(txId, txContext);
                break;
            case TMRESUME :
                if (getActiveTxId() != null) {
                    throw new XAException(XAException.XAER_INVAL);
                }
                txContext = (TxContext) suspendedContexts.remove(txId);
                if (txContext == null) {
                    throw new XAException(XAException.XAER_NOTA);
                }
                activeTransactionBranch.set(txContext);
                break;
        }
    }

    /*
     * --- XMLFileDescriptor access and caching methods ---
     *
     *  
     */

    /**
     * Either returns a cached file descriptor or loads it from DB 
     */
    protected XMLResourceDescriptor getFileDescriptor(Uri uri) throws ServiceAccessException, ObjectNotFoundException {
        TxContext txContext = getActiveTxContext();
        XMLResourceDescriptor xfd;
        if (txContext != null) {
            xfd = txContext.lookup(uri);
            if (xfd == null) {
                Object txId = txContext.xid;
                xfd = new XMLResourceDescriptor(uri, this, rm, txId, characterEncoding);
                xfd.load();
                if (txId != null) {
                    txContext.register(uri, xfd);
                }
            }
        } else {
            xfd = new XMLResourceDescriptor(uri, this, rm, null, characterEncoding);
            xfd.load();
        }
        if (!xfd.getUri().equals(uri.toString())) {
           // this may happen with files systems that don't operate case sensitive
           // e.g. requested uri /files/test.doc but found /files/TEST.DOC
           throw new ObjectNotFoundException(uri);
        }
        return xfd;
    }
    
    protected TxContext getActiveTxContext() {
        TxContext context = (TxContext)activeTransactionBranch.get();
        return context;
    }

    protected Object getActiveTxId() {
        TxContext context = (TxContext) activeTransactionBranch.get();
        return (context == null ? null : context.xid);
    }

    protected String getLogChannel() {
        return LOG_CHANNEL;
    }

    private static class TxContext {
        public Object xid;
        public Map descriptors = new HashMap();
        
        public TxContext(Object xid) {
            this.xid = xid;
        }
        
        public void register(Uri uri, XMLResourceDescriptor xfd) {
            descriptors.put(uri.toString(), xfd);
        }
        
        public void deregister(Uri uri) {
            descriptors.remove(uri.toString());
        }
        
        public XMLResourceDescriptor lookup(Uri uri) {
            return (XMLResourceDescriptor) descriptors.get(uri.toString());
        }

        public Collection list() {
            return descriptors.values();
        }

        public void saveDescriptors() throws ObjectNotFoundException, ServiceAccessException {
            for (Iterator it = list().iterator(); it.hasNext();) {
                XMLResourceDescriptor xfd = (XMLResourceDescriptor) it.next();
                if (xfd.isRegisteredForSaving()) xfd.save();
            }
        }

    }
}
