/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/UpdateMethod.java,v 1.35 2005/02/25 17:03:53 luetzkendorf Exp $
 * $Revision: 1.35 $
 * $Date: 2005/02/25 17:03:53 $
 *
 * ====================================================================
 *
 * 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 java.util.List;

import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.NestedSlideException;
import org.apache.slide.common.PropertyParseException;
import org.apache.slide.common.RequestedProperties;
import org.apache.slide.common.RequestedPropertiesImpl;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.content.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.RevisionDescriptorNotFoundException;
import org.apache.slide.event.EventDispatcher;
import org.apache.slide.structure.ObjectNode;
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.PropertyRetriever;
import org.apache.slide.webdav.util.PropertyRetrieverImpl;
import org.apache.slide.webdav.util.UriHandler;
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.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.output.XMLOutputter;

/**
 * UPDATE method.
 *
 */
public class UpdateMethod extends AbstractMultistatusResponseMethod
    implements DeltavConstants, WriteMethod, FineGrainedLockingMethod {

    /** The update target */
    private String resourcePath;

    /** The update source */
    private String updateSourcePath;

    /** The label of the update source */
    private String updateLabelName;

    /** Requested properties */
    private RequestedProperties requestedProps;

    /**
     * The URI of the server, e.g. <code>localhost:4000</code>.
     */
    protected String serverUri = null;

    /**
     * The PropertyRetriever used to retrieve any requested properties.
     */
    protected PropertyRetriever propertyRetriever = null;


    // ----------------------------------------------------------- Constructors


    /**
     * Constructor.
     *
     * @param token     the token for accessing the namespace
     * @param config    configuration of the WebDAV servlet
     */
    public UpdateMethod(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.
     *
     * @exception WebdavException
     */
    protected void parseRequest() throws WebdavException {

        serverUri = req.getServerName() + ":" + req.getServerPort();
        propertyRetriever = new PropertyRetrieverImpl(token, slideToken, getConfig());

        resourcePath = requestUri;
        if (resourcePath == null) {
            resourcePath = "/";
        }

        if( req.getContentLength() == 0 ) {
            int statusCode = WebdavStatus.SC_BAD_REQUEST;
            sendError( statusCode, getClass().getName()+".missingRequestBody" );
            throw new WebdavException( statusCode );
        }

        try{
            parseUpdateRequestContent();
        }
        catch (JDOMException  e){
            int statusCode = WebdavStatus.SC_BAD_REQUEST;
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
        catch (PropertyParseException  e){
            int statusCode = WebdavStatus.SC_BAD_REQUEST;
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
        catch( IOException e ){
            int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
    }

    /**
     * Parses the expected request content specified for the Update method.
     *
     * @throws     JDOMException  if parsing the request failed for any reason.
     * @throws     IOException
     * @throws      PropertyParseException  if parsing the property fails for any reason.
     */
    private void parseUpdateRequestContent() throws IOException, JDOMException, PropertyParseException {

        Element ve = null;
        Iterator i = parseRequestContent(E_UPDATE).getChildren().iterator();
        while( i.hasNext() ) {
            Element e = (Element)i.next();
            if( e.getName().equals(E_VERSION) ) {
                if( updateSourcePath != null ) {
                    throw new JDOMException("At most one &lt;"+E_VERSION+"&gt; element allowed" );
                }
                if (updateLabelName != null) {
                    throw new JDOMException("Either a &lt;"+E_VERSION+"&gt; OR a &lt;"+E_LABEL_NAME+"&gt; element allowed");
                }
                // get the href element
                ve = e;
                try {
                    Element hre = (Element)ve.getChildren().get(0);
                    if( hre == null || !hre.getName().equals(E_HREF) )
                        throw new Exception();
                    updateSourcePath = getSlidePath( hre.getText() );
                }
                catch( Exception x ) {
                    throw new JDOMException("&lt;"+E_VERSION+"&gt; element must contain &lt;"+E_HREF+"&gt; element" );
                }
            }
            if( e.getName().equals(E_PROP) ) {
                if( requestedProps != null ) {
                    throw new JDOMException("At most one "+E_PROP+" element allowed" );
                }
                requestedProps = new RequestedPropertiesImpl( e );
            }
            if( e.getName().equals(E_LABEL_NAME) ) {
                if (updateSourcePath != null) {
                    throw new JDOMException("Either a &lt;"+E_VERSION+"&gt; OR a &lt;"+E_LABEL_NAME+"&gt; element allowed");
                }
                if( updateLabelName != null ) {
                    throw new JDOMException("At most one &lt;"+E_LABEL_NAME+"&gt; element allowed" );
                }
                updateLabelName = e.getText();
            }
        }
    }

    /**
     * Execute the request.
     *
     * @exception WebdavException
     */
    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 );
        }

        Element multistatusElement = new Element(E_MULTISTATUS, DNSP);

        try {
            if ( WebdavEvent.UPDATE.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(WebdavEvent.UPDATE, new WebdavEvent(this));

            update(updateSourcePath, updateLabelName, resourcePath, getDepth(), multistatusElement);
        } catch (NestedSlideException nestedSlideException) {

            if (!requestHeaders.isDefined(H_DEPTH)) {
                // do not send a 207 multistatus if the depth header is not set
                SlideException exception = (SlideException)nestedSlideException.enumerateExceptions().nextElement();
                resp.setStatus(getErrorCode(exception));  // special handling needed
                if (exception instanceof PreconditionViolationException) {
                    try {
                        sendPreconditionViolation((PreconditionViolationException)exception);
                    } catch(IOException e) {
                        // Critical error ... Servlet container is dead or something
                        int statusCode = WebdavStatus.SC_INTERNAL_SERVER_ERROR;
                        sendError( statusCode, e );
                        throw new WebdavException( statusCode );
                    }
                }
                throw new WebdavException(getErrorCode(exception), false); // abort the TA
            }
        } catch (SlideException e) {
            int statusCode = getErrorCode( e );
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }


        try {
            resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
            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(multistatusElement), resp.getWriter());
        }
        catch (Exception e) {
            int statusCode = getErrorCode( e );
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
    }

    /**
     * Updates the resource identified by <code>resourcePath</code>
     * with the properties and the content either of the resource identified
     * <code>updateSourcePath</code> or the version with the label
     * <code>updateLabelName</code> (only one of these parameters is set).
     * If <code>depth</code> is > 0, the operation is applied recursivly
     * to all children of the destination resource.
     *
     * @param      updateSourcePath    the URI of update source.
     * @param      updateLabelName     the label of the version used for the update.
     * @param      resourcePath        the URI of update destination.
     * @param      depth               the depth to use. If > 0, the update is
     *                                 applied recursivly.
     * @param      multistatusElement  the <code>&lt;multistatus&gt;</code> element
     *                                 to append the <code>&lt;response&gt;</code>
     *                                 elements to.
     */
    protected void update(String updateSourcePath, String updateLabelName, String resourcePath, int depth, Element multistatusElement) throws NestedSlideException {
        NestedSlideException nestedSlideException = new NestedSlideException(null);
        update(updateSourcePath, updateLabelName, resourcePath, depth, multistatusElement, nestedSlideException);
        if ( ! nestedSlideException.isEmpty() ) {
            throw nestedSlideException;
        }
    }


    /**
     * Updates the resource identified by <code>resourcePath</code>
     * with the properties and the content either of the resource identified
     * <code>updateSourcePath</code> or the version with the label
     * <code>updateLabelName</code> (only one of these parameters is set).
     * If <code>depth</code> is > 0, the operation is applied recursivly
     * to all children of the destination resource.
     *
     * @param      updateSourcePath    the URI of update source.
     * @param      updateLabelName     the label of the version used for the update.
     * @param      resourcePath        the URI of update destination.
     * @param      depth               the depth to use. If > 0, the update is
     *                                 applied recursivly.
     * @param      multistatusElement  the <code>&lt;multistatus&gt;</code> element
     *                                 to append the <code>&lt;response&gt;</code>
     *                                 elements to.
     */
    protected void update(String updateSourcePath, String updateLabelName, String resourcePath, int depth, Element multistatusElement, NestedSlideException nestedSlideException) {

        Element responseElement = new Element(E_RESPONSE, DNSP);
        multistatusElement.addContent(responseElement);
        Element hrefElement = new Element(E_HREF, DNSP);
        
        String absUri = WebdavUtils.getAbsolutePath (resourcePath, req, getConfig());
        hrefElement.setText(absUri);
        responseElement.addContent(hrefElement);
        
        Element statusElement = new Element(E_STATUS, DNSP);
        responseElement.addContent(statusElement);

        Enumeration childrenEnum = null;
        try {
            if ( isCollection(resourcePath) && (depth > 0) ) {
                ObjectNode currentNode = structure.retrieve(slideToken, resourcePath);
                childrenEnum = structure.getChildren(slideToken, currentNode);
            }

            checkPreconditions(updateSourcePath, updateLabelName, resourcePath);

            if (updateLabelName != null) {
                updateSourcePath = versioningHelper.getLabeledResourceUri(resourcePath,
                                                                          updateLabelName);
            }

            versioningHelper.update( resourcePath, updateSourcePath );
            statusElement.setText(HTTP_VERSION + " " +
                                      WebdavStatus.SC_OK  + " " +
                                      WebdavStatus.getStatusText(WebdavStatus.SC_OK));

            appendRequestedProps(resourcePath, responseElement);
        }
        catch (SlideException e) {
            handleException(e, statusElement, responseElement, nestedSlideException);
        }
        catch (JDOMException e) {
            handleException(e, statusElement, responseElement, nestedSlideException);
        }
        // process children recursivly
        if (childrenEnum != null) {
            while (childrenEnum.hasMoreElements()) {
                update(updateSourcePath,
                       updateLabelName,
                           ((ObjectNode)childrenEnum.nextElement()).getUri(),
                       depth-1,
                       multistatusElement,
                       nestedSlideException);
            }
        }
    }

    /**
     * Appends the &lt;propstat&gt; elements for the requested properties to
     * the given <code>responseElement</code>.
     *
     * @param      resourcePath     the path of the resource for which to retrieve
     *                              the properties.
     * @param      responseElement  the &lt;reponse&gt; element to add the &lt;propstat&gt;
     *                              elements to.
     *
     * @throws     SlideException
     * @throws     JDOMException
     */
    private void appendRequestedProps(String resourcePath, Element responseElement) throws SlideException, JDOMException {

        if (requestedProps != null) {
            // TOCHECK serverURI ???
//            List propStatList = propertyRetriever.getPropertiesOfObject(requestedProps,
//                                                                        resourcePath,
//                                                                        req.getContextPath(),
//                                                                        serverUri,
//                                                                        true);
            List propStatList = propertyRetriever.getPropertiesOfObject(requestedProps,
                    resourcePath,
                    getSlideContextPath(),
                    true);
            Iterator iterator = propStatList.iterator();
            while (iterator.hasNext()) {
                responseElement.addContent((Element)iterator.next());
            }
        }
    }


    /**
     * Checks the preconditions of the Update method.
     *
     * @param      updateSourcePath    the URI of update source.
     * @param      updateLabelName     the label of the version used for the update.
     * @param      resourcePath        the URI of update destination.
     *
     * @throws     SlideException
     * @throws     PreconditionViolationException  if any precondition has been violated.
     */
    private void checkPreconditions(String updateSourcePath, String updateLabelName, String resourcePath) throws SlideException, PreconditionViolationException {

        ViolatedPrecondition violatedPrecondition = getPreconditionViolation(updateSourcePath,
                                                                             updateLabelName,
                                                                             resourcePath);
        if (violatedPrecondition != null) {
            throw new PreconditionViolationException(violatedPrecondition, resourcePath);
        }
    }


    /**
     * Sets the appropriate status text and appends a &lt;responsedescription&gt;
     * element if a precondition has been violated.
     *
     * @param      exception             the JDOMException that occurred.
     * @param      statusElement         the &lt;status&gt; element.
     * @param      responseElement       the &lt;response&gt; element.
     * @param      nestedSlideException  the NestedSlideException to add the exception to.
     */
    private void handleException(JDOMException exception, Element statusElement, Element responseElement, NestedSlideException nestedSlideException) {
        handleException(new SlideException("Nested exception: " + exception),
                        statusElement,
                        responseElement,
                        nestedSlideException);
    }

    /**
     * Sets the appropriate status text and appends a &lt;responsedescription&gt;
     * element if a precondition has been violated.
     *
     * @param      exception             the SlideException that occurred.
     * @param      statusElement         the &lt;status&gt; element.
     * @param      responseElement       the &lt;response&gt; element.
     * @param      nestedSlideException  the NestedSlideException to add the exception to.
     */
    private void handleException(SlideException exception, Element statusElement, Element responseElement, NestedSlideException nestedSlideException) {

        nestedSlideException.addException(exception);

        int errorCode = getErrorCode(exception);
        statusElement.setText(HTTP_VERSION + " " + errorCode  + " " +
                                  WebdavStatus.getStatusText(errorCode));
        if (exception instanceof PreconditionViolationException) {
            Element responseDescriptionElement = new Element(E_RESPONSEDESCRIPTION,
                                                             DNSP);
            responseElement.addContent(responseDescriptionElement);
            Element errorElement = MethodUtil.getPreconditionViolationError(((PreconditionViolationException)exception).getViolatedPrecondition());
            responseDescriptionElement.addContent(errorElement);
        }
    }


    /**
     * Checks the (DeltaV) preconditions
     * <ul>
     * <li>&lt;DAV:must-be-checked-in-version-controlled-resource&gt;</li>
     * <li>&lt;DAV:must-select-version-in-history&gt;</li>
     * </ul>
     *
     * @param      updateSourcePath  the URI of update source.
     * @param      updateLabelName   the label of the version used for the update.
     * @param      resourcePath      the URI of update destination.
     *
     * @return     the precondition that has been violated (if any).
     *
     * @throws     SlideException
     */
    protected ViolatedPrecondition getPreconditionViolation(String updateSourcePath, String updateSourceLabel, 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);
        if ( ! (resourceKind instanceof CheckedInVersionControlled) ) {
            return new ViolatedPrecondition(DeltavConstants.C_MUST_BE_CHECKED_IN_VERSION_CONTROLLED_RESOURCE,
                                            WebdavStatus.SC_CONFLICT);
        }

        if (updateSourceLabel != null) {
            try {
                updateSourcePath = versioningHelper.getLabeledResourceUri(resourcePath, updateLabelName);
            }
            catch (LabeledRevisionNotFoundException e) {
                return new ViolatedPrecondition(DeltavConstants.C_MUST_SELECT_VERSION_IN_HISTORY,
                                                WebdavStatus.SC_CONFLICT);
            }
        }

        String associatedVrUri = versioningHelper.getUriOfAssociatedVR(resourcePath);
        String vcrHistoryUri = UriHandler.getUriHandler(associatedVrUri).getAssociatedHistoryUri();
        UriHandler vrUriHandler = UriHandler.getUriHandler(updateSourcePath);
        boolean isVersionOfVcrHistory = false;
        if (vrUriHandler.isVersionUri() &&
            vcrHistoryUri.equals(vrUriHandler.getAssociatedHistoryUri()) ) {

            NodeRevisionDescriptors vrDescriptors =
                content.retrieve(slideToken, updateSourcePath);
            try {
                content.retrieve(slideToken, vrDescriptors);
                isVersionOfVcrHistory = true;
            }
            catch (RevisionDescriptorNotFoundException e) {
            }
        }

        if ( ! isVersionOfVcrHistory ) {
            return new ViolatedPrecondition(DeltavConstants.C_MUST_SELECT_VERSION_IN_HISTORY,
                                            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.
     */
    protected int getDepth() throws WebdavException {
        return requestHeaders.getDepth(0);
    }


}


