/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/LabelMethod.java,v 1.32 2005/02/25 17:03:52 luetzkendorf Exp $
 * $Revision: 1.32 $
 * $Date: 2005/02/25 17:03:52 $
 *
 * ====================================================================
 *
 * 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.util.Enumeration;
import java.util.Iterator;

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.content.NodeProperty;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.event.EventDispatcher;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.util.XMLValue;
import org.apache.slide.webdav.WebdavException;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.event.WebdavEvent;
import org.apache.slide.webdav.util.DeltavConstants;
import org.apache.slide.webdav.util.LabeledRevisionNotFoundException;
import org.apache.slide.webdav.util.PreconditionViolationException;
import org.apache.slide.webdav.util.PropertyHelper;
import org.apache.slide.webdav.util.ViolatedPrecondition;
import org.apache.slide.webdav.util.WebdavStatus;
import org.apache.slide.webdav.util.WebdavUtils;
import org.apache.slide.webdav.util.resourcekind.AbstractResourceKind;
import org.apache.slide.webdav.util.resourcekind.CheckedInVersionControlled;
import org.apache.slide.webdav.util.resourcekind.ResourceKind;
import org.apache.slide.webdav.util.resourcekind.Version;
import org.apache.slide.webdav.util.resourcekind.VersionControlled;
import org.jdom.Element;
import org.jdom.JDOMException;

/**
 * LABEL method.
 *
 * @version $Revision: 1.32 $
 *
 */
public class LabelMethod extends AbstractMultistatusResponseMethod implements DeltavConstants,
        WriteMethod, FineGrainedLockingMethod {
    /**
     ** String constant for <code>Label missing</code>.
     **/
    public static final String LABEL_MISSING = "Label missing";
    
    /**
     * String constant for <code>Request content &lt;label&gt; element must
     * contain either &lt;add&gt;, &lt;set&gt; or &lt;remove&gt;</code>.
     */
    public static final String LABEL_MUST_CONTAIN_EITHER_ADD_SET_OR_REMOVE =
        "Request content <label> element must contain either <" +
        E_ADD + ">, <" + E_SET + "> or <" + E_REMOVE + ">";
    
    
    
    
    /**
     * Resource to be written.
     */
    private String resourcePath;
    
    /**
     * Indicates if the resource to delete is a collection.
     */
    protected boolean isCollection = false;
    
    /**
     * Indicates which label operation to perform:
     * <code>add</code>, <code>set</code> or <code>remove</code>.
     */
    protected String operation = null;
    
    /**
     * The label to add/set/remove.
     */
    protected String label = null;
    
    /**
     * The value of the <code>Label</code> header
     */
    protected String labelHeader = null;
    
    
    // ----------------------------------------------------------- Constructors
    
    
    /**
     * Constructor.
     *
     * @param token     the token for accessing the namespace
     * @param config    configuration of the WebDAV servlet
     */
    public LabelMethod(NamespaceAccessToken token,
                       WebdavServletConfig config) {
        super(token, config);
    }
    
    /**
     * @see org.apache.slide.webdav.method.FineGrainedLockingMethod#acquireFineGrainLocks()
     */
    public void acquireFineGrainLocks() {
        // TODO a global write lock is certainly too much, but I have no better idea (OZ)
        acquireGlobalWriteLock();
    }

    /**
     * Parse WebDAV XML query.
     *
     * @throws WebdavException
     */
    protected void parseRequest() throws WebdavException {

        resourcePath = requestUri;
        if (resourcePath == null) {
            resourcePath = "/";
        }
        
        labelHeader = WebdavUtils.fixTomcatHeader(requestHeaders.getLabel(), "UTF-8");
        
        try{
            Element root = parseRequestContent(DeltavConstants.E_LABEL);
            Element current = root.getChild(DeltavConstants.E_ADD, DNSP);
            Element operationElement = null;
            if (current != null) {
                operationElement = current;
            }
            current = root.getChild(DeltavConstants.E_SET, DNSP);
            if ( current != null) {
                if (operationElement != null) {
                    throw new JDOMException(LABEL_MUST_CONTAIN_EITHER_ADD_SET_OR_REMOVE);
                }
                operationElement = current;
            }
            current = root.getChild(DeltavConstants.E_REMOVE, DNSP);
            if ( current != null) {
                if (operationElement != null) {
                    throw new JDOMException(LABEL_MUST_CONTAIN_EITHER_ADD_SET_OR_REMOVE);
                }
                operationElement = current;
            }
            if (operationElement == null) {
                throw new JDOMException(LABEL_MUST_CONTAIN_EITHER_ADD_SET_OR_REMOVE);
            }
            operation = operationElement.getName();
            
            Element labelName = operationElement.getChild(DeltavConstants.E_LABEL_NAME, DNSP);
            if ( (labelName == null) ||
                    (labelName.getText() == null) ||
                    (labelName.getText().length() == 0) ) {
                throw new JDOMException(LABEL_MISSING);
            }
            label = labelName.getText();
        }
        catch (IOException  e){
            int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
        catch (JDOMException  e){
            int statusCode = WebdavStatus.SC_BAD_REQUEST;
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
    }
    
    /**
     * Execute the request.
     *
     * @throws WebdavException
     * @throws IOException
     */
    protected void executeRequest() throws WebdavException, IOException {
        
        // Prevent dirty reads
        slideToken.setForceStoreEnlistment(true);
        
        // check lock-null resources
        try {
            if (isLockNull(resourcePath)) {
                int statusCode = WebdavStatus.SC_NOT_FOUND;
                sendError( statusCode, "lock-null resource", new Object[]{resourcePath} );
                throw new WebdavException( statusCode );
            }
        }
        catch (ServiceAccessException e) {
            int statusCode = getErrorCode((Exception)e);
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
        
        isCollection = isCollection(resourcePath);
        try {
            if ( WebdavEvent.LABEL.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(WebdavEvent.LABEL, new WebdavEvent(this));

            labelResource(resourcePath);
        } catch (NestedSlideException nestedSlideException) {
            // If it's not a collection, we don't want to give a 207,
            // because it's silly, and it confuses many clients (such as
            // MS Web Folders).
            if (generateMultiStatusResponse(isCollection, nestedSlideException, requestUri)) {
                String errorMessage = generateErrorMessage(nestedSlideException);
                // Write it on the servlet writer
                resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
                try {
                    resp.setContentType(TEXT_XML_UTF_8);
                    resp.getWriter().write(errorMessage);
                } catch(IOException ex) {
                    // Critical error ... Servlet container is dead or something
                    int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
                    sendError( statusCode, ex );
                    throw new WebdavException( statusCode );
                }
            } else {
                // Returning 207 on non-collection requests is generally
                // considered bad. So let's not do it, since this way
                // makes clients generally behave better.
                SlideException exception = (SlideException)nestedSlideException.enumerateExceptions().nextElement();
                if (exception instanceof PreconditionViolationException) {
                    try {
                        sendPreconditionViolation((PreconditionViolationException)exception);
                    } catch(IOException ex) {
                        // Critical error ... Servlet container is dead or something
                        int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
                        sendError( statusCode, ex );
                        throw new WebdavException( statusCode );
                    }
                }
                else {
                    int statusCode = getErrorCode( exception );
                    sendError( statusCode, exception );
                    throw new WebdavException( statusCode );
                }
            }
            //
            // make sure the transaction is aborted
            // throw any WebDAV exception to indicate the transaction wants to be aborted
            //
            throw new WebdavException(WebdavStatus.SC_ACCEPTED, false);
        } catch( SlideException x ) {
            int statusCode = getErrorCode(x);
            sendError( statusCode, x );
            throw new WebdavException( statusCode );
        }
        finally {
            resp.setHeader(H_CACHE_CONTROL, NO_CACHE);
        }
    }
    
    /**
     * Labels the reource identified by the given <code>resourcePath</code>.
     * If the resource is a collection and a <code>Depth</code> header is specified
     * than the request is applied recursivly. If an attempt to label a resource
     * fails, the corresponding Exception is contained in the thrown
     * NestedSlideException.
     *
     * @param      resourcePath   the path of the resource to label.
     *
     * @throws     NestedSlideException, if an attempt to label a resource fails.
     */
    protected void labelResource(String resourcePath) throws NestedSlideException {
        NestedSlideException nestedSlideException = new NestedSlideException(null);
        try {
            labelResource(resourcePath, getDepth(), nestedSlideException);
        }
        catch (WebdavException e) {
            nestedSlideException.addException(e);
        }
        if ( ! nestedSlideException.isEmpty() ) {
            throw nestedSlideException;
        }
    }
    
    /**
     * Labels the reource identified by the given <code>resourcePath</code>.
     * If the resource is a collection and the <code>depth</code> > 0
     * than the request is applied recursivly. If an attempt to label a resource
     * fails, the corresponding Exception will be added to the given
     * <code>nestedSlideException</code>.
     *
     * @param      resourcePath          the path of the resource to label.
     * @param      depth                 the depth to use if the resource is a collection.
     * @param      nestedSlideException  the NestedSlideException to add all occurring
     *                                   Exceptions to.
     */
    protected void labelResource(String resourcePath, int depth, NestedSlideException nestedSlideException) {
        
        try {
            
            if ( ! isCollection(resourcePath) ) {
                ViolatedPrecondition violatedPrecondition = getPreconditionViolation(resourcePath);
                if (violatedPrecondition != null) {
                    throw new PreconditionViolationException(violatedPrecondition, resourcePath);
                }
                
                performLabelOperation(resourcePath);
            }
            else if (depth > 0) {
                
                // process children recursivly
                ObjectNode currentNode = structure.retrieve(slideToken, resourcePath);
                Enumeration childrenEnum = structure.getChildren(slideToken, currentNode);
                if (childrenEnum != null) {
                    while (childrenEnum.hasMoreElements()) {
                        labelResource(((ObjectNode)childrenEnum.nextElement()).getUri(), depth-1, nestedSlideException);
                    }
                }
            }
        }
        catch (SlideException e) {
            nestedSlideException.addException(e);
        }
        catch (JDOMException e) {
            nestedSlideException.addException(new SlideException(e.getMessage()));
        }
    }
    
    /**
     * Perform the LABEL operation means it either adds, sets or removes the label.
     *
     * @param      resourcePath  the resource to add/set/remove the label.
     *
     * @throws     JDOMException
     * @throws     SlideException
     */
    private void performLabelOperation(String resourcePath) throws JDOMException, SlideException {
        
        String labelHeader = WebdavUtils.fixTomcatHeader(requestHeaders.getLabel(), "UTF-8");
        String labeledResourceUri = getResourceUri(resourcePath, labelHeader);
        NodeRevisionDescriptors revisionDescriptors =
            content.retrieve( slideToken, labeledResourceUri);
        NodeRevisionDescriptor revisionDescriptor =
            content.retrieve( slideToken, revisionDescriptors);
        ResourceKind resourceKind = AbstractResourceKind.determineResourceKind(token, revisionDescriptors, revisionDescriptor);
        if (resourceKind instanceof Version) {
            
            if (DeltavConstants.E_REMOVE.equals(operation)) {
                PropertyHelper.removeElementFromProperty(revisionDescriptor,
                                                         P_LABEL_NAME_SET,
                                                         E_LABEL_NAME,
                                                         label);
                content.store(slideToken, revisionDescriptors.getUri(), revisionDescriptor, null);
            }
            
            if (DeltavConstants.E_SET.equals(operation)) {
                try {
                    NodeRevisionDescriptor alreadyLabeledDescriptor =
                        versioningHelper.retrieveLabeledRevision(revisionDescriptors.getUri(),
                                                                 label);
                    PropertyHelper.removeElementFromProperty(alreadyLabeledDescriptor,
                                                             P_LABEL_NAME_SET,
                                                             E_LABEL_NAME,
                                                             label);
                    content.store(slideToken, revisionDescriptors.getUri(), alreadyLabeledDescriptor, null);
                }
                catch (LabeledRevisionNotFoundException e) {
                    // there is no version with the given label so far,
                    // so we can perform the <set> operation without any preparation
                }
            }
            
            if (DeltavConstants.E_ADD.equals(operation) ||
                DeltavConstants.E_SET.equals(operation) ) {
                PropertyHelper.addElementToProperty(revisionDescriptor,
                                                    P_LABEL_NAME_SET,
                                                    E_LABEL_NAME,
                                                    label);
                content.store(slideToken, revisionDescriptors.getUri(), revisionDescriptor, null);
            }
        }
    }
    
    /**
     * Checks the (DeltaV) preconditions
     * <ul>
     * <li>&lt;DAV:must-be-checked-in&gt;</li>
     * <li>&lt;DAV:must-select-version-in-history&gt;</li>
     * <li>&lt;DAV:must-be-new-label&gt;</li>
     * <li>&lt;DAV:label-must-exist&gt;</li>
     * </ul>
     *
     * @param      resourcePath       the URI of the resource.
     *
     * @return     the precondition that has been violated (if any).
     *
     * @throws     SlideException
     */
    protected ViolatedPrecondition getPreconditionViolation(String resourcePath) throws SlideException {
        
        ViolatedPrecondition violatedPrecondition = null;
        NodeRevisionDescriptors revisionDescriptors =
            content.retrieve( slideToken, resourcePath);
        NodeRevisionDescriptor revisionDescriptor =
            content.retrieve( slideToken, revisionDescriptors);
        ResourceKind resourceKind = AbstractResourceKind.determineResourceKind(token, revisionDescriptors, revisionDescriptor);
        
        // check <DAV:must-be-checked-in>
        if ( (resourceKind instanceof VersionControlled) &&
            !(resourceKind instanceof CheckedInVersionControlled) ) {
            return new ViolatedPrecondition(DeltavConstants.C_MUST_BE_CHECKED_IN,
                                            WebdavStatus.SC_CONFLICT);
        }
        
        // check <DAV:must-select-version-in-history>
        if ( (resourceKind instanceof VersionControlled) &&
                (labelHeader != null) ) {
            try {
                versioningHelper.getLabeledResourceUri(resourcePath, labelHeader);
            }
            catch (LabeledRevisionNotFoundException e) {
                return new ViolatedPrecondition(DeltavConstants.C_MUST_SELECT_VERSION_IN_HISTORY,
                                                WebdavStatus.SC_CONFLICT);
            }
        }
        
        try {
            String slideResourceUri = getResourceUri(resourcePath, labelHeader);
            revisionDescriptors =
                content.retrieve( slideToken, slideResourceUri);
            revisionDescriptor =
                content.retrieve( slideToken, revisionDescriptors);
            resourceKind = AbstractResourceKind.determineResourceKind(token, revisionDescriptors, revisionDescriptor);
            if (resourceKind instanceof Version) {
                
                // check <DAV:label-must-exist>
                if (DeltavConstants.E_REMOVE.equals(operation)) {
                    if ( ! hasLabel(revisionDescriptor, label) ) {
                        return new ViolatedPrecondition(DeltavConstants.C_LABEL_MUST_EXIST,
                                                        WebdavStatus.SC_CONFLICT);
                    }
                }
                
                try {
                    versioningHelper.retrieveLabeledRevision(revisionDescriptors.getUri(), label);
                    // check <DAV:must-be-new-label>
                    if (DeltavConstants.E_ADD.equals(operation)) {
                        return new ViolatedPrecondition(DeltavConstants.C_MUST_BE_NEW_LABEL,
                                                        WebdavStatus.SC_CONFLICT);
                    }
                }
                catch (LabeledRevisionNotFoundException e) {}
            }
        }
        catch (LabeledRevisionNotFoundException e) {
            // check <DAV:label-must-exist>
            if (DeltavConstants.E_REMOVE.equals(operation)) {
                return new ViolatedPrecondition(DeltavConstants.C_LABEL_MUST_EXIST,
                                                WebdavStatus.SC_CONFLICT);
            }
        }
        
        return violatedPrecondition;
    }
    
    
    /**
     * Returns the value of the <code>Depth</code> header. If not specified,
     * <code>0</code> is used as default.
     *
     * @return     the value of the <code>Depth</code> header.
     */
    private int getDepth() throws WebdavException {
        return requestHeaders.getDepth(0);
    }
    
    /**
     * Returns <code>true</code> if the given <code>revisionDescriptor</code>
     * has a <code>&lt;label-name-set&gt;</code> property that contains a
     * <code>&lt;label-name&gt;</code> element with the given <code>label</code>.
     *
     * @param      revisionDescriptor  the NodeRevisionDescriptor to check.
     * @param      label               the label to look for.
     *
     * @return     <code>true</code>, if the label was found.
     */
    protected boolean hasLabel(NodeRevisionDescriptor revisionDescriptor, String label) {
        
        boolean containsLabel = false;
        NodeProperty labelNameSetProperty = revisionDescriptor.getProperty(DeltavConstants.PN_LABEL_NAME_SET);
        if ( (labelNameSetProperty != null) && (labelNameSetProperty.getValue() != null) ) {
            try {
                XMLValue xmlValue = new XMLValue(labelNameSetProperty.getValue().toString());
                Iterator iterator = xmlValue.iterator();
                while ( !containsLabel && iterator.hasNext()) {
                    containsLabel = label.equals(((Element)iterator.next()).getText());
                }
            }
            catch (JDOMException e) {}
            catch (IllegalArgumentException e) {}
        }
        
        return containsLabel;
    }
    
    /**
     * Returns the Uri of the resource identified by the given <code>resourcePath</code>
     * and the given <code>label</code>. If the <code>label</code> is <code>null</code>
     * and the resource is a VCR, the associated VR is returned.
     *
     * @param      resourcePath  the path of the resource.
     * @param      label         the label (may be <code>null</code>).
     *
     * @return     the Uri of the resource identified by the given <code>resourcePath</code>
     *             and the given <code>label</code>.
     *
     * @throws     SlideException
     */
    protected String getResourceUri(String resourcePath, String label) throws SlideException {
        
        String labeledResourceUri = versioningHelper.getLabeledResourceUri(resourcePath,
                                                                           label);
        NodeRevisionDescriptors revisionDescriptors =
            content.retrieve( slideToken,labeledResourceUri);
        NodeRevisionDescriptor revisionDescriptor =
            content.retrieve( slideToken, revisionDescriptors);
        ResourceKind resourceKind = AbstractResourceKind.determineResourceKind(token, revisionDescriptors, revisionDescriptor);
        if (resourceKind instanceof VersionControlled) {
            labeledResourceUri = versioningHelper.getUriOfAssociatedVR(resourcePath);
        }
        
        return labeledResourceUri;
    }
    
}


