/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/AbstractWebdavMethod.java,v 1.80 2005/06/03 15:00:54 ozeigermann Exp $
 * $Revision: 1.80 $
 * $Date: 2005/06/03 15:00:54 $
 *
 * ====================================================================
 *
 * 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.webdav.method;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.transaction.HeuristicMixedException;
import javax.transaction.HeuristicRollbackException;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

import org.apache.commons.transaction.locking.GenericLock;
import org.apache.commons.transaction.locking.GenericLockManager;
import org.apache.commons.transaction.locking.LockManager;
import org.apache.commons.transaction.locking.MultiLevelLock;
import org.apache.commons.transaction.util.PrintWriterLogger;
import org.apache.slide.authenticate.CredentialsToken;
import org.apache.slide.common.Domain;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.NestedSlideException;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.common.SlideToken;
import org.apache.slide.content.Content;
import org.apache.slide.content.NodeProperty;
import org.apache.slide.content.NodeRevisionContent;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.content.NodeProperty.NamespaceCache;
import org.apache.slide.lock.Lock;
import org.apache.slide.lock.NodeLock;
import org.apache.slide.lock.ObjectLockedException;
import org.apache.slide.macro.ConflictException;
import org.apache.slide.macro.Macro;
import org.apache.slide.search.Search;
import org.apache.slide.security.Security;
import org.apache.slide.store.ConcurrencyConflictError;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.ObjectNotFoundException;
import org.apache.slide.structure.Structure;
import org.apache.slide.transaction.ExternalTransactionContext;
import org.apache.slide.util.Configuration;
import org.apache.slide.util.Messages;
import org.apache.slide.util.XMLValue;
import org.apache.slide.util.logger.Logger;
import org.apache.slide.webdav.WebdavException;
import org.apache.slide.webdav.WebdavMethod;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.util.BindConstants;
import org.apache.slide.webdav.util.DeltavConstants;
import org.apache.slide.webdav.util.NotificationConstants;
import org.apache.slide.webdav.util.PreconditionViolationException;
import org.apache.slide.webdav.util.TransactionConstants;
import org.apache.slide.webdav.util.UnlockListenerImpl;
import org.apache.slide.webdav.util.UriHandler;
import org.apache.slide.webdav.util.VersioningHelper;
import org.apache.slide.webdav.util.ViolatedPrecondition;
import org.apache.slide.webdav.util.WebdavConstants;
import org.apache.slide.webdav.util.WebdavStatus;
import org.apache.slide.webdav.util.WebdavUtils;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;
import org.jdom.output.XMLOutputter;

/**
 * WebDAV method.
 *
 */
public abstract class AbstractWebdavMethod implements WebdavMethod, WebdavConstants,
        DeltavConstants, BindConstants, NotificationConstants, TransactionConstants {


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

    /**
     * String constant for <code>no-cache</code>.
     */
    protected static final String NO_CACHE = "no-cache";

    /**
     * String constant for <code>private</code>.
     */
    protected static final String PRIVATE_CACHE = "private";

    /**
     * String constant for <code>http://</code>.
     */
    public static final String HTTP_PROTOCOL = "http://";

    /**
     * String constant for <code>HTTP/1.1</code>.
     */
    public static final String HTTP_VERSION = "HTTP/1.1";

    /**
     * String constant for <code>text/xml</code>.
     */
    public static final String TEXT_XML = "text/xml";

    /**
     * String constant for <code>text/xml; charset="UTF-8"</code>.
     */
    public static final String TEXT_XML_UTF_8 = "text/xml; charset=UTF-8";

    /**
     * The indent to use in the XML response.
     */
    public static final String XML_RESPONSE_INDENT = "    ";

    private static final String LOG_CHANNEL =
        AbstractWebdavMethod.class.getName();
    private static final String LOG_CHANNEL_LOCKING = LOG_CHANNEL + ".locking";

    //  public static final String PRINCIPAL =
    //      "org.apache.slide.webdav.method.principal";

    public static final int INFINITY = Integer.MAX_VALUE;
    
    protected static final Namespace DNSP = NamespaceCache.DEFAULT_NAMESPACE;
    
    /**
     * The set of SimpleDateFormat formats to use in getDateHeader().
     */
    protected static final SimpleDateFormat formats[] = {
        new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US),
            new SimpleDateFormat("EEEEEE, dd-MMM-yy HH:mm:ss zzz", Locale.US),
            new SimpleDateFormat("EEE MMMM d HH:mm:ss yyyy", Locale.US)
    };

    private static final int LOCK_LOG_LEVEL = Logger.DEBUG;
    
    protected static final int READ_LOCK = 1;
    protected static final int WRITE_LOCK = 2;

    private static final int SHARED_LOCK = 1;
    private static final int EXCLUSIVE_LOCK = 2;
    
    // global read/write lock to allow for deadlock-free access
    private static final MultiLevelLock GLOBAL_LOCK = new GenericLock("global", WRITE_LOCK,
            new PrintWriterLogger(new PrintWriter(System.out), LOG_CHANNEL, false));

    private static final LockManager FINE_LOCK_MANAGER = new GenericLockManager(WRITE_LOCK,
            new PrintWriterLogger(new PrintWriter(System.out), LOG_CHANNEL, false));

    private static final GenericLock GLOBAL_READ_LOCK = new GenericLock("GLOBAL READ", EXCLUSIVE_LOCK,
            new PrintWriterLogger(new PrintWriter(System.out), LOG_CHANNEL, false));

    private static final GenericLock GLOBAL_WRITE_LOCK = new GenericLock("GLOBAL WRITE", EXCLUSIVE_LOCK,
            new PrintWriterLogger(new PrintWriter(System.out), LOG_CHANNEL, false));

    
    private static final int DEFAULT_MAX_RETRY_REPEATS = 1;
    
    // ----------------------------------------------------- Instance Variables


    /**
     * Requested Slide-Uri.
     */
    protected String requestUri;


    /**
     * Servlet request.
     */
    protected HttpServletRequest req;


    /**
     * Servlet response.
     */
    protected HttpServletResponse resp;


    /**
     * Configuration.
     */
    protected WebdavServletConfig config;
    
    protected String slideContextPath;


    /**
     * Request body.
     */
    protected String requestBody;


    /**
     * Namespace access token.
     */
    protected NamespaceAccessToken token;


    /**
     * Structure helper.
     */
    protected Structure structure;


    /**
     * Content helper.
     */
    protected Content content;


    /**
     * Security helper.
     */
    protected Security security;


    /**
     * Lock helper.
     */
    protected Lock lock;


    /** wam
     * Search helper.
     */
    protected Search search;


    /**
     * Macro helper.
     */
    protected Macro macro;
    
    /**
     * Versioning helper.
     */
    protected VersioningHelper versioningHelper;


    /**
     * Slide token.
     */
    protected SlideToken slideToken;

    /**
     * Fine grain locks acquired by this method.
     */
    protected Set locks = new HashSet();
    
    /**
     * The request content (XML) Document.
     */
    private Document requestContentDocument = null;

    /**
     * Indicates if the request content has already been parsed.
     */
    private boolean isRequestContentParsed = false;

    /**
     * Request headers
     */
    protected RequestHeaders requestHeaders = new RequestHeaders();

    /**
     * Indicates if the request will be repeated upon concurrency failure.
     */
    protected boolean retryUponConflict;
    
    // -------------------------------------------------- Static Initialization

    
    // ----------------------------------------------------------- Constructors
    
    
    /**
     * Constructor.
     *
     * @param token     the token for accessing the namespace
     * @param config    configuration of the WebDAV servlet
     */
    public AbstractWebdavMethod(NamespaceAccessToken token,
                                WebdavServletConfig config) {
        
        this.config = config;
        this.token = token;
        
        // initialize helpers
        structure = token.getStructureHelper();
        content = token.getContentHelper();
        security = token.getSecurityHelper();
        lock = token.getLockHelper();
        macro = token.getMacroHelper();
    }
    
    
    // -------------------------------------------- WebdavMethod Implementation
    
    
    /**
     * Exceute method.
     *
     * @exception WebdavException
     */
    public void run(HttpServletRequest req, HttpServletResponse resp)
        throws WebdavException {
        
        // XXX this is a pretty ugly spot and way to set this
        // TODO find a better solution
        UriHandler.setGloballyUseHistoryCollectionHack(useHistoryCollectionHack());
        
        this.req = req;
        this.resp = resp;
        this.slideToken = WebdavUtils.getSlideToken(req);
        String forceLowercaseLogin = token.getNamespaceConfig().getParameter("force-lowercase-login");
        if ("true".equals(forceLowercaseLogin)) {
            String name = slideToken.getCredentialsToken().getPrincipal().getName().toLowerCase();
            slideToken.setCredentialsToken(new CredentialsToken(name));
        }
        
        this.requestUri = WebdavUtils.getRelativePath(req, config);
        this.slideContextPath = req.getContextPath();
        if (!this.config.isDefaultServlet()) {
            this.slideContextPath += req.getServletPath();
        }
        
        // TODO this is a workaround to pass the slideContextPath to the search
        // implementation
        slideToken.addParameter("slideContextPath", this.slideContextPath);
        
        this.versioningHelper = VersioningHelper.getVersioningHelper(
                slideToken, token, req, resp, getConfig(), this);
        
        parseRequestHeaders();
        
        boolean transactionIsStarted = false;
        boolean globalLockObtained = false;
        String txId = null;
        try {
            parseRequest();
            
            ExternalTransactionContext externalTransaction = null;
            
            txId = requestHeaders.getTxId();
            if (txId != null) {
                externalTransaction = ExternalTransactionContext.lookupContext(txId);
                if (externalTransaction != null) {
                    Domain.log("Using external transaction " + txId, LOG_CHANNEL, Logger.INFO);
                    slideToken.setExternalTx();
                    // pure reads must be guaranteed to be inside transaction as well
                    slideToken.setForceStoreEnlistment(true);
                    Transaction tx = externalTransaction.getTransaction(); 
                    token.getTransactionManager().resume(tx);
                    transactionIsStarted = true;
                }
            }
            
            if (!slideToken.isExternalTransaction()) {
                token.begin();
                transactionIsStarted = true;
                if (txForAllRequests()) {
                    slideToken.setForceStoreEnlistment(true);
                }

                assureGlobalLocks();
                globalLockObtained = true;
            }

            boolean done = false;
            retryUponConflict = isRepeatUponConflict() && !slideToken.isExternalTransaction();
            int retries = getMaxRetryRepeats();

            ConcurrencyConflictError finalCce = null;

            while (!done && retries != 0) {
                try {
                    executeRedirect();
                    done = true;
                } catch (ConcurrencyConflictError cce) {
                    finalCce = cce;
                    if (retryUponConflict) {
                        retries--;
                        token.getLogger().log(cce.getMessage(), LOG_CHANNEL, Logger.WARNING);
                        token.rollback();
                        token.begin();
                    } else {
                        break;
                    }
                }
            }
            if (!done) {
                // rethrow it as an ordinary conflict exception
                throw new ConflictException(finalCce.getUri(), finalCce);
            }

            if (!slideToken.isExternalTransaction() && transactionIsStarted) {
                token.commit();
                transactionIsStarted = false;
            }
        } catch (WebdavException ex) {
            // Ignore the WebDav Exception and assume that the response code
            // is already set.
        } catch (SlideException ex) {
            int statusCode = getErrorCode( ex );
            if (statusCode == WebdavStatus.SC_UNAUTHORIZED) {
                // This means the user is required to authenticate (correctly),
                // so add the specific header and invalidate session to force
                // new authentication
                String authentication = config.getAuthenticationHeader();
                resp.setHeader("WWW-Authenticate", authentication);
                req.getSession().invalidate();
            }
            sendError( statusCode, ex );
            // do not throw exception as the response code has already been set, 
            // otherwise the servlet will log this as an error and issue a stack trace
//            throw new WebdavException( statusCode );
        } catch (Exception ex) {
            token.getLogger().log(ex,LOG_CHANNEL,Logger.ERROR);
            int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
            sendError( statusCode, ex );
            throw new WebdavException( statusCode );
        } finally {
            try {
                if (!slideToken.isExternalTransaction() && transactionIsStarted) {
                    // Something went wrong, we are here and the TA is still
                    // open
                    try {
                        token.rollback();
                    } catch (Exception e) {
                        token.getLogger().log(e,LOG_CHANNEL,Logger.ERROR);
                    }
                }
                if (slideToken.isExternalTransaction()) {
                    Transaction transaction;
                    try {
                        if (token.getStatus() == javax.transaction.Status.STATUS_ACTIVE) {
                            transaction = token.getTransactionManager().suspend();
                            if (transaction != null) {
                                ExternalTransactionContext.registerContext(txId, transaction);
                            }
                        }
                    } catch (SystemException e) {
                        token.getLogger().log(e,LOG_CHANNEL,Logger.ERROR);
                    }
                }
            } finally {
                if (globalLockObtained) {
                    releaseGlobalLocks();
                }
            }
        }
    }

    protected void executeRedirect() throws IOException, SlideException, IllegalStateException,
            SecurityException, HeuristicMixedException, HeuristicRollbackException,
            RollbackException, NotSupportedException, SystemException {
        
        /*
         * Check for object existence and cleanup locks but only if this is
         * not a request to finalize an external transaction. Otherwise we are
         * making calls to the store that require a transaction to be in process 
         * while we're trying to commit or abort the current transaction.
         */            
        if (!isEndofTransactionRequest()) {
            try {
                // retrive to check it exists, otherwise it can't have locks
                structure.retrieve(slideToken, requestUri);
                // clear expired lock-tokens
                UnlockListenerImpl listener = new UnlockListenerImpl(slideToken, token, versioningHelper);
                lock.clearExpiredLocks(slideToken, requestUri, listener);
                
                if (listener.getUnlockCount() > 0) {
                    // If we have have cleared any lock or any lock-null resource in   
                    // the previous step we commit this changes, otherwise they will  
                    // be lost if executeRequest() exits with an exception (e.g. 
                    // because of Not Found 404)
                    token.commit();
                    token.begin();
                }
            } catch (ObjectNotFoundException e) {
                // ignore, it can't have locks
            }
        }
        
        boolean responseIsRedirected = false;
        
        /* 
         * Check whether to apply to redirect ref but only if this is not an
         * external transaction request to commit or abort the current transaction. 
         * Otherwise db deadlock can occur.
         */          
        if (!requestHeaders.getApplyToRedirectRef(false)
                && !isEndofTransactionRequest()) {
            try {
                NodeRevisionDescriptors revisionDescriptors
                        = content.retrieve(slideToken, requestUri);
                NodeRevisionDescriptor revisionDescriptor
                        = content.retrieve(slideToken, revisionDescriptors);
                if (WebdavUtils.isRedirectref(revisionDescriptor)) {
                    NodeProperty refTarget
                            = revisionDescriptor.getProperty(PN_REFTARGET);

                    // Add the header indicating that this redirect is the
                    // result of a redirect reference.
                    resp.addHeader(H_REDIRECT_REF,
                            refTarget.getValue().toString());
                 
                    // Set the location header for the redirect.
                    resp.addHeader("Location",
                            refTarget.getValue().toString());
                           
                    // Determine the appropriate status code.
                    boolean permanent
                            = revisionDescriptor.propertyValueContains(
                            P_REDIRECT_LIFETIME, "permanent");
                    if (permanent) {
                        resp.setStatus(WebdavStatus.SC_MOVED_PERMANENTLY);
                        resp.addHeader(H_CACHE_CONTROL, PRIVATE_CACHE);
                    }
                    else {
                        resp.setStatus(WebdavStatus.SC_MOVED_TEMPORARILY);
                        resp.addHeader(H_CACHE_CONTROL, NO_CACHE);
                    }

                    // Set this flag so that we don't attempt to execute the
                    // request on the redirect reference itself.
                    responseIsRedirected = true;
                }
            }
            catch (ObjectNotFoundException notFound) {
                // TODO is this really an error?
//                token.getLogger().log("Error while trying to send redirect",
//                        notFound, LOG_CHANNEL, Logger.ERROR);
            }
            catch (RevisionDescriptorNotFoundException e) {
                // TODO not sure, but this makes VcPutVHR testcase passing
            }
            catch (ObjectLockedException e) {
                // TODO not sure, but this makes nonOwnerUsesLocktoken testcase passing 
            }
        }
        
        if (!responseIsRedirected) {
            executeRequest();
        }
    }
    
    // --------------------------------------------------------- Public Methods


    /**
     * Returns a boolean indicating whether this request is an
     * external transaction request to commit or abort the
     * current transaction. These requests must be given a
     * short path to UnlockMethod.execute() that avoids db
     * interaction -- even reads. 
     *
     * @return true if the current request is an external transaction commit or abort request.
     */
    public boolean isEndofTransactionRequest()
    {
        /*
         * if this is an UnlockMethod and has a transaction
         * command set in the request then this request is
         * trying to commit or abort an external transaction.
         */
        if (this instanceof UnlockMethod)
        {
            UnlockMethod meth = (UnlockMethod)this;
            if (meth.getCommand() != UnlockMethod.NO_TRANSACTION)
                return true;
        }
        
        return false;
    }

    /**
     * Returns the configuration of the WebdavServlet.
     *
     * @return WebdavServletConfig
     */
    public WebdavServletConfig getConfig() {
        return config;
    }


    /**
     * Return an absolute URL path (absolute in the HTTP sense) based on a Slide
     * path.
     */
    public String getFullPath(String slidePath) {
       return WebdavUtils.getAbsolutePath(slidePath, req, getConfig());
    }


    /**
     * Returns a Slide path based on an absolute URL
     * (absolute in the HTTP sense)
     */
    public String getSlidePath(String fullpath) {
        return WebdavUtils.getSlidePath(fullpath, getSlideContextPath());
    }
    
    public String getSlideContextPath() {
        return this.slideContextPath;
    }
    
    /**
     * Returns the slide uri requested.  
     */
    public String getRequestUri() {
        return this.requestUri;
    }
    
    public SlideToken getSlideToken() {
        return this.slideToken;
    }
    
    public NamespaceAccessToken getNamespaceAccessToken() {
        return this.token;
    }
    
    public Content getContent() {
        return content;
    }
    public Lock getLock() {
        return lock;
    }
    public Macro getMacro() {
        return macro;
    }
    public Search getSearch() {
        return search;
    }
    public Security getSecurity() {
        return security;
    }
    public Structure getStructure() {
        return structure;
    }
    public VersioningHelper getVersioning() {
        return versioningHelper;
    }
    public HttpServletRequest getRequest() {
        return this.req;
    }
    public HttpServletResponse getResponse() {
        return this.resp;
    }
    // ------------------------------------------------------ Protected Methods
    
    /**
     * Read request contents.
     *
     * @param req Request object handed out by the servlet container
     * @return char[] Array of char which contains the body of the request
     */
    protected void readRequestContent() {
        
        if (req.getContentLength() == 0)
            return;
        
        // TODO : Modify this and make it chunking aware
        
        try {
            requestBody = new String(NodeRevisionContent.read(req.getInputStream()),
                                     getEncodingString(req.getCharacterEncoding()));
        }
        catch (Exception e) {
            token.getLogger().log(e,LOG_CHANNEL,Logger.ERROR);
        }
    }
    
    
    
    /**
     * Translate the encoding string into a proper Java encoding String.
     */
    public static String getEncodingString(String httpEncoding) {
        String result = httpEncoding;
        if (result == null) result = System.getProperty("file.encoding");
        if (result.startsWith("\"")) result = result.substring(1, result.length());
        if (result.endsWith("\""))   result = result.substring(0, result.length()-1);
        return result;
    }
    
    
    /**
     * Method parseHeaders
     *
     */
    private void parseRequestHeaders() throws WebdavException {
        requestHeaders.parse();
    }
    
    /**
     * Test if a resource given by a path is a collection
     */
    protected boolean isCollection(String path) {
        return WebdavUtils.isCollection(token, slideToken, path);
    }
    
    /**
     * Test whether the resource given by lowerNode is a descendant of the
     * resource given by upperNode
     *
     * @param    lowerNode           an ObjectNode
     * @param    upperNode           an ObjectNode
     *
     * @return   true, if lowerNode is below upperNode in the namespace
     * @throws   ServiceAccessException
     *
     */
    protected boolean isDescendant( ObjectNode lowerNode, ObjectNode upperNode ) throws ServiceAccessException {
        if (lowerNode.getUuri().equals(upperNode.getUuri())) {
            return true;
        }
        if (upperNode.hasBinding(lowerNode)) {
            return true;
        }
        
        NodeRevisionDescriptors lowerNrds = null;
        NodeRevisionDescriptor lowerNrd = null;
        try {
            lowerNrds = content.retrieve(slideToken, lowerNode.getUri());
            lowerNrd = content.retrieve(slideToken, lowerNrds);
            
            NodeProperty psProp = lowerNrd.getProperty(PN_PARENT_SET);
            // FIXME psProp may be null
            XMLValue xv = new XMLValue( String.valueOf(psProp.getValue()) );
            Iterator i = xv.getList().iterator();
            while (i.hasNext()) {
                // iterate over parent elements
                Element pElm = (Element)i.next();
                String hrPath = pElm.getChild( E_HREF, DNSP ).getText();
                ObjectNode hrNode = structure.retrieve( slideToken, hrPath );
                return isDescendant( hrNode, upperNode );
            }
        } catch (ServiceAccessException e) {
            throw e;
        } catch (Exception e) {}
        
        return false;
    }
    
    protected boolean isRequestChunked() {
        String te = req.getHeader("Transfer-Encoding");
        if (te == null) return false;
        return te.indexOf("chunked") != -1;
    }
    
    /**
     * Parse WebDAV XML query.
     *
     * @exception WebdavException
     */
    protected abstract void parseRequest()
        throws WebdavException;
    
    
    
    /**
     * Returns the request content (XML) Document.
     *
     * @return     the request content (XML) Document.
     */
    protected Document getRequestContent() {
        return requestContentDocument;
    }
    
    //--
    
    /**
     * precondition: sourceUri != null
     */
    protected String parseUri(String uri) throws WebdavException {  // TODO: better name
        int protocolIndex = uri.indexOf("://");
        if (protocolIndex >= 0) {
            // if the Destination URL contains the protocol, we can safely
            // trim everything upto the first "/" character after "://"
            int firstSeparator =
                uri.indexOf("/", protocolIndex + 4);
            if (firstSeparator < 0) {
                uri = "/";
            } else {
                uri = uri.substring(firstSeparator);
            }
        } else {
            String hostName = req.getServerName();
            if ((hostName != null) && (uri.startsWith(hostName))) {
                uri = uri.substring(hostName.length());
            }
            
            int portIndex = uri.indexOf(":");
            if (portIndex >= 0) {
                uri = uri.substring(portIndex);
            }
            
            if (uri.startsWith(":")) {
                int firstSeparator = uri.indexOf("/");
                if (firstSeparator < 0) {
                    uri = "/";
                } else {
                    uri = uri.substring(firstSeparator);
                }
            }
        }
        
        // headers are "ISO-8859-1" encoded [not any more with TC 4.1.18
        uri = WebdavUtils.normalizeURL(WebdavUtils.fixTomcatURL(uri));
        
        String contextPath = req.getContextPath();
        if ((contextPath != null) && (uri.startsWith(contextPath))) {
            uri = uri.substring(contextPath.length());
        }
        
        String pathInfo = req.getPathInfo();
        if (pathInfo != null) {
            String servletPath = req.getServletPath();
            if ((servletPath != null) && (uri.startsWith(servletPath))) {
                uri = uri.substring(servletPath.length());
            }
        }
        uri = getConfig().getScope() + uri;
        
        return uri;
    }
    
    protected Element parseRequestContent(String rootName) throws JDOMException, IOException {
        Document document;
        Element root;
        
        document = parseRequestContent();
        if (document == null) {
            throw new JDOMException("Request content missing");
        }
        root = document.getRootElement();
        if( root == null || !root.getName().equals(rootName) ) {
            Domain.warn( "Root element must be "+rootName );
            throw new JDOMException("Root element must be <"+rootName+">");
        }
        return root;
    }
    
    /**
     * Parses the request content (XML) Document.
     *
     * @return     the request content (XML) Document.
     *
     * @throws     IOException     if an I/O error occurred.
     * @throws     JDOMException   if parsing the document failed.
     */
    protected Document parseRequestContent() throws JDOMException, IOException {
        
        if (isRequestContentParsed) {
            return requestContentDocument;
        }
                
        if (req.getContentLength() == 0 || req.getContentLength() == -1) {
            return requestContentDocument;
        }
        
        try {
            requestContentDocument = new SAXBuilder().build(req.getInputStream());
            isRequestContentParsed = true;
        }
        catch (JDOMException e) {
            if (e.getCause() instanceof IOException) {
                throw (IOException)e.getCause();
            }
            else {
                throw e;
            }
        }
        
        return requestContentDocument;
    }
    
    
    /**
     * Generate XML response.
     *
     * @exception WebdavException
     */
    protected abstract void executeRequest()
        throws WebdavException, IOException;
    
    
    /**
     * Simulate MS IIS5 ?
     *
     * @return boolean
     */
    protected boolean isMsProprietarySupport() {
        return (token.getNamespaceConfig().getParameter("ms") != null);
    }

    /**
     * Sends a precondition vilolation response.
     *
     * @param pve the ProconditionViolationException that describes the violated
     *            precondition.
     */
    protected void sendPreconditionViolation(PreconditionViolationException pve) throws IOException {
        
        if (pve != null) {
            
            ViolatedPrecondition violatedPrecondition = pve.getViolatedPrecondition();
            
            int statusCode = violatedPrecondition.getStatusCode();
            printStackTrace( pve, statusCode );
            String statusText = WebdavStatus.getStatusText(statusCode);
            if (violatedPrecondition.getExplanation() != null) {
                statusText = statusText+": "+violatedPrecondition.getExplanation();
            }
            resp.setStatus(statusCode, statusText);

            resp.setContentType(TEXT_XML_UTF_8);

            org.jdom.output.Format format = org.jdom.output.Format.getPrettyFormat();
            format.setIndent(XML_RESPONSE_INDENT);
            new XMLOutputter(format).
                output(new Document(MethodUtil.getPreconditionViolationError(pve.getViolatedPrecondition())), resp.getWriter());
        }
    }
    
    // -------------------------------------------------------- Private Methods
    
    
    /**
     * Get return status based on exception type.
     */
    protected int getErrorCode(Throwable ex) {
        return WebdavUtils.getErrorCode(ex);
    }
    
    
    /**
     * Get return status based on exception type.
     */
    protected int getErrorCode(SlideException ex) {
        return WebdavUtils.getErrorCode(ex);
    }
    
    
    
    /**
     * Get return status based on exception type.
     */
    protected int getErrorCode(ServiceAccessException ex) {
        return WebdavUtils.getErrorCode(ex);
    }
    
    
    /**
     * Returns the value of a boolean init parameter of the servlet.
     * Default value: false.
     */
    protected boolean getBooleanInitParameter( String name ) {
        return "true".equalsIgnoreCase( getConfig().getInitParameter(name) );
    }

    protected boolean isAutoCreateUsers() {
        return ("true".equalsIgnoreCase(token.getNamespaceConfig().getParameter("auto-create-users")));
    }
    
    protected String getUsersPath() {
        return token.getNamespaceConfig().getUsersPath();
    }
    
    protected String getRolesPath() {
        return token.getNamespaceConfig().getRolesPath();
    }
    
    protected String getHistoryPath() {
        return Domain.getParameter(I_HISTORYPATH, I_HISTORYPATH_DEFAULT);
    }

    /**
     * Checks whether the hack that restricts the size of collections in the
     * history collection is configured to be used.
     */
    protected boolean useHistoryCollectionHack() {
        return "true".equalsIgnoreCase(token.getNamespaceConfig().getParameter("history-collection-hack"));
    }

    /**
     * Checks whether all requests shall be done inside of transactions.
     */
    protected boolean txForAllRequests() {
        return "true".equalsIgnoreCase(token.getNamespaceConfig().getParameter("all-methods-in-transactions"));
    }

    /**
     * Checks if Slide is configured to allow at most a single write request at a time.
     * @return <code>true</code> if there can be at most one write request at a time 
     */
    protected boolean isSequentialWrite() {
        String sm = token.getNamespaceConfig().getParameter("sequential-mode");
        return ("write".equalsIgnoreCase(sm) || "full".equalsIgnoreCase(sm));
    }
    
    /**
     * Checks if Slide is configured to allow reads while write requests are being executed.
     * @return <code>true</code> if reads are disallowed during writes 
     */
    protected boolean isSequentialRead() {
        return "full".equalsIgnoreCase(token.getNamespaceConfig().getParameter("sequential-mode"));
    }
    
    /**
     * Checks if Slide is configured to restrict access using fine grained
     * locks.
     * 
     * @return <code>true</code> if Slide is configured to lock in a fain
     *         grained way
     */
    protected boolean isFineGrainSequential() {
        return "fine-grain".equalsIgnoreCase(token.getNamespaceConfig().getParameter(
                "sequential-mode"));
    }

    /**
     * Checks if Slide is configured to internally repeat a request if it conflicts with other
     * concurrent ones.
     */
    protected boolean isRepeatUponConflict() {
        return "true".equalsIgnoreCase(token.getNamespaceConfig().getParameter(
                "repeat-upon-conflict"));
    }
    
    /**
     * Checks if Slide is configured to internally repeat a request if it conflicts with other
     * concurrent ones.
     */
    protected int getMaxRetryRepeats() {
        String maxRepeat = token.getNamespaceConfig().getParameter("max-retry-repeat");
        if (maxRepeat != null) {
            try {
                int repeats = Integer.parseInt(maxRepeat);
                return repeats;
            } catch (NumberFormatException nfe) {
                token.getLogger().log("max-retry-repeat must be an integer, using default",
                        LOG_CHANNEL_LOCKING, Logger.WARNING);
            }
        }
        return DEFAULT_MAX_RETRY_REPEATS;
    }
    
    protected void assureGlobalLocks() {
        if (this instanceof FineGrainedLockingMethod && isFineGrainSequential()) {
            ((FineGrainedLockingMethod) this).acquireFineGrainLocks();
        } else if (this instanceof ReadMethod) {
            if (isSequentialRead()) {
                try {
                    GLOBAL_LOCK.acquire(this, 1, true, true, Long.MAX_VALUE);
                } catch (InterruptedException e) {
                }
            }
        } else if (this instanceof WriteMethod) {
            if (isSequentialWrite()) {
                try {
                    GLOBAL_LOCK.acquire(this, 2, true, true, Long.MAX_VALUE);
                } catch (InterruptedException e) {
                }
            }
        }
    }

    protected void releaseGlobalLocks() {
        GLOBAL_LOCK.release(this);
        if (this instanceof FineGrainedLockingMethod && isFineGrainSequential()) {
            // no need to do this atomically
            releaseLocks();
        }
    }

    protected void acquireHistoryLocks(String path) {
        // some writing request have an additional request to the history
        // folder if auto versioning is turned on
        if (Configuration.useVersionControl() && isAutoVersionControl(path)
                && !isExcludedForVersionControl(path)) {
            acquireLock(getHistoryPath(), this instanceof WriteMethod ? WRITE_LOCK : READ_LOCK);
        }

    }

    /**
     * Acquires a global read lock to disallow any writing. 
     * Should be used by methods that do not allow any writing while running.
     */
    protected void acquireGlobalReadLock() {
        // we have a shared global read lock which means we are compatible with
        // any other read
        acquireLock(GLOBAL_READ_LOCK, SHARED_LOCK);
        // this means no other write request may run in parallel, but if another global read
        // has the lock, we will *not* be blocked
        blockWithLock(GLOBAL_WRITE_LOCK, EXCLUSIVE_LOCK);
    }
    
    /**
     * Acquires a global write lock to disallow any concurrent access. 
     */
    protected void acquireGlobalWriteLock() {
        // we really want to read and write alone:
        acquireLock(GLOBAL_READ_LOCK, EXCLUSIVE_LOCK);
        acquireLock(GLOBAL_WRITE_LOCK, EXCLUSIVE_LOCK);
    }
    
    /**
     * Inhibits a global read lock. Should be called by all fine grain writing methods.
     */
    protected void inhibitGlobalReadLock() {
        // we have a shared global write lock which means other fine grained write
        // methods are compatible
        acquireLock(GLOBAL_WRITE_LOCK, SHARED_LOCK);
        // do not allow global read as long as we write to any resource, but
        // if another global write has the lock, we will *not* be blocked
        blockWithLock(GLOBAL_READ_LOCK, EXCLUSIVE_LOCK);
    }
    
    /**
     * Acquires the standard locks applicable to most methods. Those are
     * <ol>
     * <li>READ locks for the accessed path and all ancestors on all levels up
     * to the root
     * <li>recursive READ locks to the users and roles folder and an additional
     * write lock to the users folder if auto creation of users is enabled
     * <li>upgrade requests to descendants of the history folder to the full
     * history folder, effectively making all requests to the history folder
     * global
     * <li>assures no writing requests run concurrently with a search request
     * as it is unpredictable which resources a search will touch
     * </ol>
     * 
     * @param path
     *            the path to acquire standard locks for
     */
    protected void acquireStandardLocks(String path) {
        // (1) all requests need read access from uri to root
        List paths = getPathsToRoot(path);
        acquireLock(paths, READ_LOCK);
        // (2) all requests need read access to users and roles and even write
        // when users are auto created
        String usersPath = getUsersPath();
        String rolesPath = getRolesPath();
        acquireLock(usersPath, isAutoCreateUsers() ? WRITE_LOCK : READ_LOCK);
        acquireLock(rolesPath, READ_LOCK);
        // (2a) adapt explicite calls to users and roles to general lock
        // above
        // this effectively gives a *single* lock for the users and roles folder
        // each
        if (path.startsWith(getUsersPath())) {
            acquireLock(usersPath, this instanceof WriteMethod ? WRITE_LOCK : READ_LOCK);
        } else if (path.startsWith(rolesPath)) {
            acquireLock(rolesPath, this instanceof WriteMethod ? WRITE_LOCK : READ_LOCK);
        }
        // (3) adapt explicite read to history folder to general lock
        // above
        // this effectively gives a *single* lock for history folder
        if (Configuration.useVersionControl() && path.startsWith(getHistoryPath())) {
            acquireLock(getHistoryPath(), READ_LOCK);
        }
        // (4) be sure to write only when no search or other global read currently takes place
        // this is the counter part to acquireSearchLock()
        if (this instanceof WriteMethod) {
            inhibitGlobalReadLock();
        }
    }

    /**
     * Locks the parent of a path with either a read or a write lock.
     * 
     * @param path
     *            the child of the path to lock
     * @param level
     *            either {@link #READ_LOCK} or {@link #WRITE_LOCK}
     * 
     * @see #acquireLock(String, int)
     */
    protected void acquireParentLock(String path, int level) {
        int lastSlash = path.lastIndexOf('/');
        if (lastSlash > 0) {
            String parentPath = path.substring(0, lastSlash);
            acquireLock(parentPath, level);
        }
    }

    /**
     * Locks a path with either a read or a write lock. <br>
     * <br>
     * <em>Caution #1:</em> All methods need read locks to all ancestors on
     * all levels. Effectively this means a write lock is always recursive and
     * applies to all descendants on all levels.
     * 
     * <br>
     * <br>
     * <em>Caution #2:</em> A write lock implies a read lock.
     * 
     * @param path
     *            the path to lock
     * @param level
     *            either {@link #READ_LOCK}or {@link #WRITE_LOCK}
     */
    protected void acquireLock(String path, int level) {
        GenericLock lock = (GenericLock) FINE_LOCK_MANAGER.atomicGetOrCreateLock(path);
        acquireLock(lock, level);
    }

    /**
     * Acquires a lock.
     * 
     * @param lock
     *            the lock to acquire
     * @param level
     *            either {@link #READ_LOCK} or {@link #WRITE_LOCK}
     * 
     * @see #acquireLock(String, int)
     */
    protected void acquireLock(GenericLock lock, int level) {
        try {
            lock.acquire(this, level, true, true, Long.MAX_VALUE);
        } catch (InterruptedException e) {
        }
        locks.add(lock);
        token.getLogger().log(this + 
                (level == READ_LOCK ? " read" : " write") + " locking "
                        + lock.getResourceId().toString(), LOG_CHANNEL_LOCKING, LOCK_LOG_LEVEL);
    }

    /**
     * Acquires a supporting block lock.
     * 
     * @param lock
     *            the lock to acquire
     * @param level
     *            either {@link #READ_LOCK} or {@link #WRITE_LOCK}
     * 
     * @see #acquireLock(String, int)
     */
    protected void blockWithLock(GenericLock lock, int level) {
        try {
            lock.acquire(this, level, true, GenericLock.COMPATIBILITY_REENTRANT_AND_SUPPORT,
                    Long.MAX_VALUE);
        } catch (InterruptedException e) {
        }
        locks.add(lock);
        token.getLogger().log(
                this + (level == READ_LOCK ? " read" : " write") + " locking "
                        + lock.getResourceId().toString(), LOG_CHANNEL_LOCKING, LOCK_LOG_LEVEL);
    }

    /**
     * Locks a list of paths with either a read or a write lock.
     * 
     * @param paths
     *            the paths to lock
     * @param level
     *            either {@link #READ_LOCK} or {@link #WRITE_LOCK}
     * 
     * @see #acquireLock(String, int)
     */
    protected void acquireLock(List paths, int level) {
        for (Iterator i = paths.iterator(); i.hasNext();) {
            String path = (String) i.next();
            acquireLock(path, level);
        }
    }

    protected void releaseLocks() {
        GenericLock lock;
        for (Iterator i = locks.iterator(); i.hasNext();) {
            lock = (GenericLock) i.next();
            lock.release(this);
            token.getLogger().log(
                    this + " releasing " + lock.getResourceId().toString(),
                    LOG_CHANNEL_LOCKING, LOCK_LOG_LEVEL);
        }
    }

    protected List getPathsToRoot(String uri) {
        StringTokenizer tokenizer = new StringTokenizer(uri, "/");
        List paths = new ArrayList(tokenizer.countTokens() + 1);

        paths.add("/");

        String path = "";
        while (tokenizer.hasMoreTokens()) {
            path += "/" + tokenizer.nextToken();
            paths.add(path);
        }

        return paths;
    }
    
   /**
     * Returns the value of an integer init parameter of the servlet.
     * Default value: -1.
     */
    protected int getIntInitParameter( String name ) {
        int result = -1;
        try {
            result = Integer.parseInt( getConfig().getInitParameter(name) );
        }
        catch( NumberFormatException x ) {};
        return result;
    }
    
    /**
     * Error handling routine
     */
    protected void sendError( int statusCode ) {
        try { resp.sendError( statusCode ); } catch( Throwable x ) {};
    }
    
    /**
     * Error handling routine
     */
    protected void sendError( int statusCode, String message ) {
        String statusText =
            WebdavStatus.getStatusText(statusCode)+
            (message != null
                 ? ": "+Messages.format( message, (Object)null )
                 : "");
        try { resp.sendError( statusCode, statusText ); } catch( Throwable x ) {};
    }
    
    /**
     * Error handling routine
     */
    protected void sendError( int statusCode, String message, Object[] args ) {
        String statusText =
            WebdavStatus.getStatusText(statusCode)+": "+
            Messages.format( message, args );
        try { resp.sendError( statusCode, statusText ); } catch( Throwable x ) {};
    }
    
    /**
     * Error handling routine
     */
    protected void sendError( int statusCode, Throwable t ) {
        printStackTrace( t, statusCode );
        String explanation = (t == null || t.getMessage() == null || "".equals(t.getMessage())
                                  ? Messages.format(t.getClass().getName(), (Object)null)
                                  : t.getMessage()
                             );
        String statusText =
            WebdavStatus.getStatusText(statusCode)+": "+explanation;
        try { resp.sendError( statusCode, statusText ); } catch( Throwable x ) {};
    }
    
    /**
     * Prints the stack trace of the given exception if the specified status code
     * is greater-or-equal the value of the servlet init-parameter printStackTrace.
     * If the init-parameter is not specified, stack traces are printed for status
     * codes >= 500.
     */
    protected void printStackTrace( Throwable x, int statusCode ) {
        int printStackTraceFrom = getIntInitParameter( "printStackTrace" );
        if( printStackTraceFrom < 0 )
            printStackTraceFrom = 500;
        if( statusCode >= printStackTraceFrom )
            x.printStackTrace();
    }
    
    
    /**
     * Generate status text.
     *
     * @param parentElement  the parent Element to append to.
     * @param href Slide-Uri of the object
     * @param statusCode HTTP status code of the error
     */
    protected void generateStatusText(Element parentElement, String href, int statusCode) {
        
        Element hrefElement = new Element(E_HREF, DNSP);
        parentElement.addContent(hrefElement);
        hrefElement.setText(getFullPath(href));
        Element statusElement = new Element(E_STATUS, DNSP);
        parentElement.addContent(statusElement);
        statusElement.setText("HTTP/1.1 " + statusCode + " "
                                  + WebdavStatus.getStatusText(statusCode));
    }
    
    /**
     * Generate an XML error message.
     *
     * @param macroException Nested exception
     * @return String XML message
     */
    protected String generateErrorMessage
        (NestedSlideException nestedException) {
        
        Element multistatus = new Element(E_MULTISTATUS, DNSP);
        
        Enumeration nestedExceptionsList =
            nestedException.enumerateExceptions();
        while (nestedExceptionsList.hasMoreElements()) {
            
            Element response = new Element(E_RESPONSE, DNSP);
            multistatus.addContent(response);
            SlideException ex =
                (SlideException) nestedExceptionsList.nextElement();
            generateStatusText(response, MethodUtil.getErrorMessage(ex),
                               getErrorCode(ex));
            if (ex instanceof PreconditionViolationException) {
                response.addContent(MethodUtil.getPreconditionViolationResponseDescription((PreconditionViolationException)ex));
            }
            
        }
        
        StringWriter stringWriter = new StringWriter();
        try {
            new XMLOutputter().output(multistatus, stringWriter);
        }
        catch (IOException e) {
            Domain.log(e);
        }
        return stringWriter.toString();
        
    }
    
    protected boolean exists( String uriStr ) throws SlideException {
        boolean destinationExists = true;
        
        try {
            content.retrieve(slideToken, uriStr);
        }
        catch (ObjectNotFoundException x) {
            destinationExists = false;
        }
        return destinationExists;
    }
    
    protected boolean isLocked( String uriStr ) throws ServiceAccessException {
        // use a non-blocking slide token.
        boolean isLocked = false;
        try {
            Enumeration locks = lock.enumerateLocks (slideToken, uriStr, false);
            while (locks.hasMoreElements()) {
                if (lock.isLocked(slideToken,(NodeLock) locks.nextElement(),false)) {
                    isLocked = true;
                }
            }
        }
        catch (ServiceAccessException x) {
            throw x;
        }
        catch (SlideException x) {
            // ignore silently
        }
        return isLocked;
    }
    
    protected boolean isLockNull( String uriStr ) throws ServiceAccessException {
        boolean isLockNull = false;
        try {
            NodeRevisionDescriptor nrd =
                content.retrieve(slideToken, content.retrieve(slideToken, uriStr));
            isLockNull = isLockNull( nrd );
        }
        catch (ServiceAccessException x) {
            throw x;
        }
        catch (SlideException x) {
            // ignore silently
        }
        return isLockNull;
    }
    
    protected boolean isLockNull( NodeRevisionDescriptor nrd ) {
        return nrd.propertyValueContains(P_RESOURCETYPE, E_LOCKNULL);
    }
    
    protected boolean isAutoVersionControl(String resourcePath) {
        return new Boolean(Domain.getParameter(I_AUTO_VERSION_CONTROL,
                                               I_AUTO_VERSION_CONTROL_DEFAULT,
                                               token.getUri(slideToken, resourcePath).getStore()))
            .booleanValue();
    }
    
    protected boolean isExcludedForVersionControl(String resourcePath) {
        String versionControlExcludePaths =
            Domain.getParameter(I_VERSIONCONTROL_EXCLUDEPATH,
                                I_VERSIONCONTROL_EXCLUDEPATH_DEFAULT,
                                token.getUri(slideToken, resourcePath).getStore());
        if (versionControlExcludePaths != null && versionControlExcludePaths.length() > 0) {
            StringTokenizer st = new StringTokenizer(versionControlExcludePaths, ";");
            while (st.hasMoreTokens()) {
                if (isExcluded(resourcePath, st.nextToken())) {
                    return true;
                }
            }
        }
        return false;
    }
    
    private boolean isExcluded(String resourcePath, String excludePath) {
        UriHandler uh = UriHandler.getUriHandler(resourcePath);
        if (excludePath != null && excludePath.length() > 0) {
            UriHandler exUh = UriHandler.getUriHandler(excludePath);
            if (exUh.isAncestorOf(uh)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Check if the conditions specified in the optional If headers are
     * satisfied.
     *
     * @param request The servlet request we are processing
     * @param response The servlet response we are creating
     * @param resourceInfo File object
     * @return boolean true if the resource meets all the specified conditions,
     * and false if any of the conditions is not satisfied, in which case
     * request processing is stopped
     */
    protected boolean checkIfHeaders(HttpServletRequest request,
                                     HttpServletResponse response,
                                     ResourceInfo resourceInfo)
        throws IOException 
    {
        // the ETag without apostrophes ("), we use apostrophes as delimiters
        // to because some clients provide If header with out apostrophes
        String eTag = getETagValue(resourceInfo, true);
        long lastModified = resourceInfo.date;
        
        StringTokenizer commaTokenizer;
        
        String headerValue;
        
        // Checking If-Match
        headerValue = request.getHeader("If-Match");
        if (headerValue != null) {
            if (headerValue.indexOf("*") == -1) {
                
                commaTokenizer = new StringTokenizer(headerValue, ", \"");
                boolean matchingTagFound = false;
                
                while (!matchingTagFound && commaTokenizer.hasMoreTokens()) {
                    matchingTagFound = commaTokenizer.nextToken().equals(eTag);
                }
                
                // If none of the given ETags match, 412 Precodition failed is
                // sent back
                if (!matchingTagFound) {
                    response.sendError(
                            HttpServletResponse.SC_PRECONDITION_FAILED);
                    return false;
                }
            } else {
                if (!resourceInfo.exists()) {
                    response.sendError(
                            HttpServletResponse.SC_PRECONDITION_FAILED);
                    return false;
                }
            }
        }
        
        // Checking If-Modified-Since
        headerValue = request.getHeader("If-Modified-Since");
        if (headerValue != null) {
            
            // If an If-None-Match header has been specified, if modified since
            // is ignored.
            if (request.getHeader("If-None-Match") == null) {
                Date date = parseHttpDate(headerValue);
                
                if ((date != null)
                    && (lastModified <= (date.getTime() + 1000)) ) {
                    // The entity has not been modified since the date
                    // specified by the client. This is not an error case.
                    response.sendError
                        (HttpServletResponse.SC_NOT_MODIFIED);
                    return false;
                }
            }
        }
        
        // Checking If-None-Match
        headerValue = request.getHeader("If-None-Match");
        if (headerValue != null) {
            if (headerValue.indexOf("*") == -1) {
                
                commaTokenizer = new StringTokenizer(headerValue, ", \"");
                while (commaTokenizer.hasMoreTokens()) {
                    if (commaTokenizer.nextToken().equals(eTag)) {
                        // For GET and HEAD, we respond with  304 Not Modified.
                        // For every other method, 412 Precondition Failed 
                        if ( ("GET".equals(request.getMethod()))
                            || ("HEAD".equals(request.getMethod())) ) 
                        {
                            response.sendError(
                                    HttpServletResponse.SC_NOT_MODIFIED);
                            return false;
                        } else {
                            response.sendError(
                                    HttpServletResponse.SC_PRECONDITION_FAILED);
                            return false;
                        }
                    }
                }
            } else {
                if (resourceInfo.exists()) {
                    response.sendError(
                            HttpServletResponse.SC_PRECONDITION_FAILED);
                    return false;
                }
            }
        }
        
        // Checking If-Unmodified-Since
        headerValue = request.getHeader("If-Unmodified-Since");
        if (headerValue != null) {
            Date date = parseHttpDate(headerValue);
            
            if ( (date != null) && (lastModified > date.getTime()) ) {
                // The entity has not been modified since the date
                // specified by the client. This is not an error case.
                response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
                return false;
            }
        }
        return true;
    }
    
    /**
     * Parses the date string given as one of the {@link #formats}. 
     * If the date does not fit any of these formats it returns <code>null</code>.
     * 
     * @param headerValue date string from and HTTP header (e.g. If-Modified)
     * @return a Date representing the date given of <code>null</code> if 
     *         the string has no valid format.
     */
    protected Date parseHttpDate(String headerValue) {
        Date date = null;
        
        // Parsing the HTTP Date
        for (int i = 0; (date == null) && (i < formats.length); i++) {
            try {
                synchronized (formats[i]) {
                    date = formats[i].parse(headerValue);
                }
            } catch (ParseException e) {
                // ignore the invalid format and try the next
            }
        }
        return date;
    }


    /**
     * Get the ETag value associated with a file.
     *
     * @param resourceInfo File object
     * @param strong True if we want a strong ETag, in which case a checksum
     * of the file has to be calculated
     */
    protected String getETagValue(ResourceInfo resourceInfo, boolean strong) {
        // FIXME : Compute a strong ETag if requested, using an MD5 digest
        // of the file contents
        if (resourceInfo.exists()) {
            return resourceInfo.etag;
        }
        else {
            return resourceInfo.length + "-" + resourceInfo.date;
        }
    }
    
    
    /**
     * Get the ETag associated with a file.
     *
     * @param resourceInfo File object
     * @param strong True if we want a strong ETag, in which case a checksum
     * of the file has to be calculated
     */
    protected String getETag(ResourceInfo resourceInfo, boolean strong) {
        if (strong)
            return "\"" + getETagValue(resourceInfo, strong) + "\"";
        else
            return "W/\"" + getETagValue(resourceInfo, strong) + "\"";
    }
    
    protected class ResourceInfo {
        
        /**
         * Constructor.
         *
         * @param path Path name of the resource
         */
        public ResourceInfo(String path, NodeRevisionDescriptor properties) {
            
            this.path = path;
            this.exists = true;
            this.creationDate = properties.getCreationDateAsDate().getTime();
            this.date = properties.getLastModifiedAsDate().getTime();
            this.httpDate = properties.getLastModified();
            this.length = properties.getContentLength();
            this.etag = properties.getETag();
            
        }
        /**
         * Creates a ResourceInfo for a non existing resource.
         * @param path Path of the resource
         */
        public ResourceInfo(String path) {
            this.path = path;
            this.exists = false;
            this.length = 0;
            this.date = System.currentTimeMillis();
        }
        
        
        public String path;
        public long creationDate;
        public String httpDate;
        public long date;
        public long length;
        public String etag;
        //public boolean collection;
        public boolean exists;
        
        
        /**
         * Test if the associated resource exists.
         */
        public boolean exists() {
            return exists;
        }
        
        
        /**
         * String representation.
         */
        public String toString() {
            return path;
        }
        
        
    }
    
    protected class RequestHeaders {
        private static final int
            ST_UNDEFINED = 0,
            ST_INVALID = 1,
            ST_DEFINED = 2;
        
        // raw headers
        private String hIfStr;
        private String hLockTokenStr;
        private String hDepthStr;
        private String hDestinationStr;
        private String hOverwriteStr;
        private String hTimeoutStr;
        private String hLabelStr;
        private String hNotificationTypeStr;
        private String hCallbackStr;
        private String hSubscriptionIdStr;
        private String hNotificationDelayStr;
        private String hSubscriptionLifetimeStr;
        private String hTxIdStr;
        private String hTxMethodStr;
        private String hContentTypeStr;
        private String hApplyToRedirectRefStr;

        // parsed headers
        private List hIf;
        private String hLockToken;
        private int hDepth;
        private String hDestination;
        private boolean hOverwrite;
        private int hTimeout;
        private String hLabel;
        private String hNotificationType;
        private String hCallback;
        private int []hSubscriptionId;
        private int hNotificationDelay;
        private int hSubscriptionLifetime;
        private String hTxId;
        private String hTxMethod;
        private String hContentType;
        private boolean hApplyToRedirectRef;

        // states
        private int stIf = ST_UNDEFINED;
        private int stLockToken = ST_UNDEFINED;
        private int stDepth = ST_UNDEFINED;
        private int stDestination = ST_UNDEFINED;
        private int stOverwrite = ST_UNDEFINED;
        private int stTimeout = ST_UNDEFINED;
        private int stLabel = ST_UNDEFINED;
        private int stNotificationType = ST_UNDEFINED;
        private int stCallback = ST_UNDEFINED;
        private int stSubscriptionId = ST_UNDEFINED;
        private int stNotificationDelay = ST_UNDEFINED;
        private int stSubscriptionLifetime = ST_UNDEFINED;
        private int stTxId = ST_UNDEFINED;
        private int stTxMethod= ST_UNDEFINED;
        private int stContentType = ST_UNDEFINED;
        private int stApplyToRedirectRef = ST_UNDEFINED;

        protected RequestHeaders() {
        }
        
        protected boolean isDefined( String header ) {
            return req.getHeader(header) != null;
        }
        
        protected List getIf() throws WebdavException {
            if (stIf == ST_UNDEFINED) {
                return Collections.EMPTY_LIST;
            }
            else if (stIf == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header If: "+hIfStr );
                throw new WebdavException( sc );
            }
            else {
                return hIf;
            }
        }
        
        protected String getLockToken() throws WebdavException {
            if (stLockToken == ST_UNDEFINED) {
                return null;
            }
            else if (stLockToken == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header LockToken: "+hLockTokenStr );
                throw new WebdavException( sc );
            }
            else {
                return hLockToken;
            }
        }
        
        protected int getDepth( int defaultValue ) throws WebdavException {
            if (stDepth == ST_UNDEFINED) {
                return defaultValue;
            }
            else if (stDepth == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header Depth: "+hDepthStr );
                throw new WebdavException( sc );
            }
            else {
                return hDepth;
            }
        }
        
        protected String getDestination() throws WebdavException {
            if (stDestination == ST_UNDEFINED) {
                return null;
            }
            else if (stDestination == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header Destination: "+hDestinationStr );
                throw new WebdavException( sc );
            }
            else {
                return hDestination;
            }
        }
        
        protected boolean getOverwrite( boolean defaultValue ) throws WebdavException {
            if (stOverwrite == ST_UNDEFINED) {
                return defaultValue;
            }
            else if (stOverwrite == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header Overwrite: "+hOverwriteStr );
                throw new WebdavException( sc );
            }
            else {
                return hOverwrite;
            }
        }
        
        protected String getLabel() throws WebdavException {
            if (stLabel == ST_UNDEFINED) {
                return null;
            }
            else if (stLabel == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header Label: "+hLabelStr );
                throw new WebdavException( sc );
            }
            else {
                return hLabel;
            }
        }
        
        protected int getTimeout( int defaultValue) throws WebdavException {
            if (stTimeout == ST_UNDEFINED) {
                return defaultValue;
            }
            else if (stTimeout == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header Timeout: "+hTimeoutStr );
                throw new WebdavException( sc );
            }
            else {
                return hTimeout;
            }
        }

        protected String getNotificationType() throws WebdavException {
            if (stNotificationType == ST_UNDEFINED) {
                return null;
            } else if (stNotificationType == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid notification type: "+hNotificationTypeStr );
                throw new WebdavException( sc );
            }
            else {
                return hNotificationType;
            }
        }

        protected int []getSubscriptionId() throws WebdavException {
            if (stSubscriptionId == ST_UNDEFINED) {
                return new int[0];
            } else if (stSubscriptionId == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid subscription ID: "+hSubscriptionIdStr );
                throw new WebdavException( sc );
            }
            else {
                return hSubscriptionId;
            }
        }

        protected String getCallback() throws WebdavException {
            if (stCallback == ST_UNDEFINED) {
                return null;
            } else if (stCallback == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid callback: "+hCallbackStr );
                throw new WebdavException( sc );
            }
            else {
                return hCallback;
            }
        }

        protected int getNotificationDelay( int defaultValue ) throws WebdavException {
            if (stNotificationDelay == ST_UNDEFINED) {
                return defaultValue;
            }
            else if (stNotificationDelay == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid notification delay: "+hNotificationDelayStr );
                throw new WebdavException( sc );
            }
            else {
                return hNotificationDelay;
            }
        }

        protected int getSubscriptionLifetime( int defaultValue ) throws WebdavException {
            if (stSubscriptionLifetime == ST_UNDEFINED) {
                return defaultValue;
            }
            else if (stSubscriptionLifetime == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid subscription lifetime: "+hSubscriptionLifetimeStr );
                throw new WebdavException( sc );
            }
            else {
                return hSubscriptionLifetime;
            }
        }

        protected String getTxId() throws WebdavException {
            if (stTxId == ST_UNDEFINED) {
                return null;
            } else if (stTxId == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid transaction id: "+hTxIdStr );
                throw new WebdavException( sc );
            }
            else {
                return hTxId;
            }
        }

        protected String getTxMethod() throws WebdavException {
            if (stTxMethod == ST_UNDEFINED) {
                return null;
            } else if (stTxMethod == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid transaction method: "+hTxMethodStr );
                throw new WebdavException( sc );
            }
            else {
                return hTxMethod;
            }
        }
        
        protected String getContentType() {
           if (stContentType == ST_UNDEFINED) {
              return null;
           } else {
              return hContentType;
           }
        }


        protected boolean getApplyToRedirectRef( boolean defaultValue )
            throws WebdavException
        {
            if (stApplyToRedirectRef == ST_UNDEFINED) {
                return defaultValue;
            }
            else if (stApplyToRedirectRef == ST_INVALID) {
                int sc = WebdavStatus.SC_PRECONDITION_FAILED;
                sendError( sc, "Invalid header Apply-To-Redirect-Ref: " +
                hApplyToRedirectRefStr );
                throw new WebdavException( sc );
            }
            else {
                return hApplyToRedirectRef;
            }
        }


        protected void parse() {
            // TransactionId header
            hTxIdStr = req.getHeader(H_TRANSACTION);
            if (hTxIdStr != null) {
                stTxId = ST_DEFINED;
                try {
                    hTxId = hTxIdStr;
                } catch (Exception e) {
                    stTxId = ST_INVALID;
                }
            }

            // TransactionMethod header
            hTxMethodStr = req.getHeader(H_TRANSACTION_METHOD);
            if (hTxMethodStr != null) {
                stTxMethod = ST_DEFINED;
                try {
                    hTxMethod = hTxMethodStr;
                } catch (Exception e) {
                    stTxMethod = ST_INVALID;
                }
            }

            // NotificationType header
            hNotificationTypeStr = req.getHeader(H_NOTIFICATION_TYPE);
            if (hNotificationTypeStr != null) {
                stNotificationType = ST_DEFINED;
                try {
                    hNotificationType = hNotificationTypeStr;
                } catch (Exception e) {
                    stNotificationType = ST_INVALID;
                }
            }

            // NotificationDelay header
            hNotificationDelayStr = req.getHeader(H_NOTIFICATION_DELAY);
            if (hNotificationDelayStr != null) {
                stNotificationDelay = ST_DEFINED;
                try {
                    hNotificationDelay = Integer.parseInt(hNotificationDelayStr);
                } catch (Exception e) {
                    stNotificationDelay = ST_INVALID;
                }
            }

            // SubscriptionLifetime header
            hSubscriptionLifetimeStr = req.getHeader(H_SUBSCRIPTION_LIFETIME);
            if (hSubscriptionLifetimeStr != null) {
                stSubscriptionLifetime = ST_DEFINED;
                try {
                    hSubscriptionLifetime = Integer.parseInt(hSubscriptionLifetimeStr);
                } catch (Exception e) {
                    stSubscriptionLifetime = ST_INVALID;
                }
            }

            // SubscriptionID header
            hSubscriptionIdStr = req.getHeader(H_SUBSCRIPTION_ID);
            if (hSubscriptionIdStr != null) {
                stSubscriptionId = ST_DEFINED;
                try {
                    StringTokenizer tokenizer = new StringTokenizer(hSubscriptionIdStr, ",");
                    hSubscriptionId = new int[tokenizer.countTokens()];
                    int i = 0;
                    while ( tokenizer.hasMoreTokens() ) {
                        hSubscriptionId[i] = Integer.parseInt(tokenizer.nextToken().trim());
                        i++;
                    }
                } catch (Exception e) {
                    stSubscriptionId = ST_INVALID;
                }
            }

            // Call back header
            hCallbackStr = req.getHeader(H_CALL_BACK);
            if (hCallbackStr != null) {
                stCallback = ST_DEFINED;
                try {
                    hCallback = hCallbackStr;
                } catch (Exception e) {
                    stCallback = ST_INVALID;
                }
            }

            // If header
            hIfStr = req.getHeader(H_IF);
            if (hIfStr != null) {
                stIf = ST_DEFINED;
                try {
                    hIf = extractLockTokens(hIfStr);
                }
                catch (Exception e) {
                    stIf = ST_INVALID;
                }
            }
            
            // Lock-Token header
            hLockTokenStr = req.getHeader(H_LOCK_TOKEN);
            if (hLockTokenStr != null) {
                stLockToken = ST_DEFINED;
                try {
                    List tl = extractLockTokens(hLockTokenStr);
                    hLockToken = (String)tl.get(0);
                }
                catch (Exception e) {
                    stLockToken = ST_INVALID;
                }
            }
            
            // Depth header
            hDepthStr = req.getHeader(H_DEPTH);
            if (hDepthStr != null) {
                stDepth = ST_DEFINED;
                if ("0".equals(hDepthStr)) {
                    hDepth = 0;
                }
                else if ("1".equals(hDepthStr)) {
                    hDepth = 1;
                }
                else if ("infinity".equalsIgnoreCase(hDepthStr)) {
                    hDepth = INFINITY;
                }
                else {
                    stDepth = ST_INVALID;
                    hDepth = Integer.parseInt(hDepthStr);
                }
            }
            
            // Destination header
            hDestinationStr = req.getHeader(H_DESTINATION);
            if (hDestinationStr != null) {
                stDestination = ST_DEFINED;
                hDestination = hDestinationStr;
            }
            
            // Overwrite header
            hOverwriteStr = req.getHeader(H_OVERWRITE);
            if (hOverwriteStr != null) {
                stOverwrite = ST_DEFINED;
                if ("T".equalsIgnoreCase(hOverwriteStr)) {
                    hOverwrite = true;
                }
                else if ("F".equalsIgnoreCase(hOverwriteStr)) {
                    hOverwrite = false;
                }
                else {
                    stOverwrite = ST_INVALID;
                }
            }
            
            // Timeout header
            hTimeoutStr = req.getHeader(H_TIMEOUT);
            if (hTimeoutStr != null) {
                stTimeout = ST_DEFINED;
                try {
                    hTimeout = extractLockDuration( hTimeoutStr );
                }
                catch (Exception e) {
                    stTimeout = ST_INVALID;
                }
            }
            
            // Label header
            hLabelStr = req.getHeader(H_LABEL);
            if (hLabelStr != null) {
                stLabel = ST_DEFINED;
                hLabel = hLabelStr;
            }

            // Apply-To-Redirect-Ref header
            hApplyToRedirectRefStr = req.getHeader(H_OVERWRITE);
            if (hApplyToRedirectRefStr != null) {
                stApplyToRedirectRef = ST_DEFINED;
                if ("T".equalsIgnoreCase(hApplyToRedirectRefStr)) {
                    hApplyToRedirectRef = true;
                }
                else if ("F".equalsIgnoreCase(hApplyToRedirectRefStr)) {
                    hApplyToRedirectRef = false;
                }
                else {
                    stApplyToRedirectRef = ST_INVALID;
                }
            }
            
            // Content-Type header
            hContentTypeStr = req.getHeader(H_CONTENT_TYPE);
            if (hContentTypeStr != null) {
               stContentType = ST_DEFINED;
               hContentType = hContentTypeStr; 
            }

        }
        
        private List extractLockTokens(String hStr) {
            List result = new ArrayList();
            int pos = hStr.indexOf(S_LOCK_TOKEN);
            int endPos = -1;
            int offset = S_LOCK_TOKEN.length();
            String lockToken = null;
            
            while (pos != -1) {
                
                endPos = hStr.indexOf('>', pos + offset);
                if (endPos == -1) {
                    lockToken = hStr;
                    endPos = hStr.length();
                } else {
                    lockToken = hStr.substring(pos + offset, endPos);
                }
                
                //System.out.println("Lock Token found :-" + lockToken + "-");
                slideToken.addLockToken(lockToken);
                result.add( lockToken );
                pos = hStr.indexOf(S_LOCK_TOKEN, endPos);
            }
            return result;
        }
        
        private int extractLockDuration(String hStr) {
            int result;
            int firstCommaPos = hStr.indexOf(',');
            if (firstCommaPos != -1) {
                hStr = hStr.substring(0, firstCommaPos);
            }
            if (hStr.startsWith("Second-")) {
                result = Integer.parseInt(hStr.substring("Second-".length()));
            } else {
                if (hStr.equalsIgnoreCase("Infinite")) {
                    result = INFINITY;
                } else {
                    result = Integer.parseInt(hStr);
                }
            }
            return result;
        }
    }
}
