/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/method/PropFindMethod.java,v 1.115 2005/02/25 17:03:52 luetzkendorf Exp $
 * $Revision: 1.115 $
 * $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 java.util.List;
import java.util.Stack;

import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.PropertyParseException;
import org.apache.slide.common.RequestedProperties;
import org.apache.slide.common.RequestedPropertiesImpl;
import org.apache.slide.common.RequestedProperty;
import org.apache.slide.common.ServiceAccessException;
import org.apache.slide.common.SlideException;
import org.apache.slide.common.SlideToken;
import org.apache.slide.common.SlideTokenWrapper;
import org.apache.slide.common.UriPath;
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.security.AccessDeniedException;
import org.apache.slide.structure.ObjectNode;
import org.apache.slide.structure.StructureException;
import org.apache.slide.util.Configuration;
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.AclConstants;
import org.apache.slide.webdav.util.DeltavConstants;
import org.apache.slide.webdav.util.LabeledRevisionNotFoundException;
import org.apache.slide.webdav.util.PropertyRetrieverImpl;
import org.apache.slide.webdav.util.UnlockListenerImpl;
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;

/**
 * PROPFIND method.
 *
 */
public class PropFindMethod extends AbstractWebdavMethod implements DeltavConstants, AclConstants,
        ReadMethod, FineGrainedLockingMethod {
    
    /**
     * Invalid propfind type.
     */
    protected static final int FIND_ERROR = -1;
    
    /**
     * Specify a property mask.
     */
    protected static final int FIND_BY_PROPERTY = 0;
    
    
    /**
     * Display all properties.
     */
    protected static final int FIND_ALL_PROP = 1;
    
    
    /**
     * Return property names.
     */
    protected static final int FIND_PROPERTY_NAMES = 2;
    
    /**
     * Constant for "DAV:" namespace uri
     */
    protected static final String DAV_NAMESPACE_URI = "DAV:";
    
    // ----------------------------------------------------- Instance Variables
    
    
    /**
     * Depth.
     */
    protected int depth;

    
    /**
     * Special workarounds based on "User Agent" detection.
     */
    protected boolean agentWorkaround;
    
    
    /**
     * Type of the PROPFIND method.
     */
    protected int propFindType;
    
    
    /** if true, an ALL_PROP request will include computed props */
    protected boolean extendedAllprop = false;
    
    
    
    /**
     ** The SAXBuilder used to create JDOM Documents.
     **/
    protected static SAXBuilder saxBuilder = null;
    
    /**
     * The list of requested properties.
     */
    protected RequestedProperties requestedProperties = null;
    
    
    /**
     * Resource to be retrieved.
     */
    protected String resourcePath;
    
    /**
     * The value of the <code>Label</code> header.
     */
    protected String labelHeader = null;
    
    /**
     * The value of the <code>Apply-To-Redirect-Ref</code> header.
     */
    protected boolean applyToRedirectRef;
    
    /**
     * If set <code>true</code>, instead of creating the complete response document
     * in memory and then sending it to the client, available parts of the response
     * are send immediatly in order to reduce the memory footprint.
     */
    protected boolean outputOptimized = true;
    
    // ----------------------------------------------------------- Constructors
    
    
    /**
     * Constructor.
     *
     * @param token     the token for accessing the namespace
     * @param config    configuration of the WebDAV servlet
     */
    public PropFindMethod(NamespaceAccessToken token,
                          WebdavServletConfig config) {
        super(token, config);
    }
    
    /**
     * @see org.apache.slide.webdav.method.FineGrainedLockingMethod#acquireFineGrainLocks()
     */
    public void acquireFineGrainLocks() {
        acquireStandardLocks(resourcePath);
        // XXX need to read lock descendants at low cost if depth is greater than 0
        // do this with a write lock (which always is recursive)
        if (depth > 0) {
            acquireLock(resourcePath, WRITE_LOCK);
        }
    }

    // ------------------------------------------------------ Protected Methods
    
    
    /**
     * Parse the request.
     *
     * @exception WebdavException Bad request
     */
    protected void parseRequest() throws WebdavException {
        
        depth = INFINITY;
        propFindType = FIND_ERROR;
        extendedAllprop = getBooleanInitParameter( "extendedAllprop" );
        outputOptimized = getBooleanInitParameter( "optimizePropfindOutput" );
        
        resourcePath = requestUri;
        if (resourcePath == null) {
            resourcePath = "/";
        }
        
        labelHeader = WebdavUtils.fixTomcatHeader(requestHeaders.getLabel(), "UTF-8");
        
        applyToRedirectRef = requestHeaders.getApplyToRedirectRef(false);
        
        retrieveDepth();
        
        agentWorkaround = "Microsoft Data Access Internet Publishing Provider DAV".equals(req.getHeader("User-Agent"));
        
        if (req.getContentLength() == 0) {
            requestedProperties = new RequestedPropertiesImpl();
            requestedProperties.setIsAllProp(true);
            propFindType = FIND_ALL_PROP;
        }
        else {
            
            try {
                
                Element element = parseRequestContent(E_PROPFIND);
                Iterator elementsIt = element.getChildren().iterator();
                if (!elementsIt.hasNext()) {
                    int statusCode = WebdavStatus.SC_BAD_REQUEST;
                    sendError(statusCode, getClass().getName()+".missingRootElementChildren", new Object[]{"DAV:"+E_PROPFIND});
                    throw new WebdavException(statusCode);
                } else {
                    // Loop through child elements looking for a valid propfind type
                    while (elementsIt.hasNext()) {
                        element = (Element)elementsIt.next();
                        if (element.getNamespaceURI().equals(DAV_NAMESPACE_URI)) {
                    
                            if (element.getName().equalsIgnoreCase(E_PROPNAME)){
                                propFindType = FIND_PROPERTY_NAMES;
                            }
                            else if (element.getName().equalsIgnoreCase(E_PROP)) {
                                requestedProperties = new RequestedPropertiesImpl(element);
                                propFindType = FIND_BY_PROPERTY;
                            }
                            else if (element.getName().equalsIgnoreCase(E_ALLPROP)) {
                                requestedProperties = new RequestedPropertiesImpl(element);
                                propFindType = FIND_ALL_PROP;
                            }
                        }
                    }
                    // If no valid values were found, throw an error
                    if (propFindType == FIND_ERROR) {
                        int statusCode = WebdavStatus.SC_BAD_REQUEST;
                        sendError(statusCode, getClass().getName()+".invalidChildOfRootElement", new Object[]{element.getNamespace()+":"+element.getName(),"DAV:"+E_PROPFIND});
                        throw new WebdavException(statusCode);
                    }
                }
            }
            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 );
            }
        }
    }
    
    /**
     * Retrieves the <code>Depth</code> header from the request.
     */
    private void retrieveDepth() throws WebdavException {
        depth = requestHeaders.getDepth(INFINITY);
        
        // limit tree browsing a bit
        if (depth > getConfig().getDepthLimit()) {
            depth = getConfig().getDepthLimit();
        }
    }
    
    
    
    /**
     * Execute the request.
     *
     * @exception WebdavException
     */
    protected void executeRequest() throws IOException, WebdavException {
        
        resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
        
        // Loads the associated object from the store.
        
        // Get the object from Data.
        ObjectNode resource = null;
        
        try {
            if ( WebdavEvent.PROPFIND.isEnabled() ) EventDispatcher.getInstance().fireVetoableEvent(WebdavEvent.PROPFIND, new WebdavEvent(this));
            resource = structure.retrieve(slideToken, resourcePath);
        } catch (StructureException e) {
            int statusCode = WebdavStatus.SC_NOT_FOUND;
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        } catch (Exception e) {
            int statusCode = getErrorCode( e );
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }
        
        resp.setContentType(TEXT_XML_UTF_8);
        
        // Create multistatus object
        Element multistatusElement = new Element(E_MULTISTATUS, DNSP);
        // We shouldn't pretty print (indent) the output, since this may
        // potentially corrupt the property values.
//        org.jdom.output.Format format = org.jdom.output.Format.getPrettyFormat();
//        format.setIndent(XML_RESPONSE_INDENT);
        org.jdom.output.Format format = org.jdom.output.Format.getRawFormat();
        XMLOutputter xmlOutputter = new XMLOutputter(format);
        
        if (resource != null) {
            if (depth == 0) {
                multistatusElement.addContent(getPropertiesOfObject(resource.getUri()));
                xmlOutputter.output(new Document(multistatusElement), resp.getWriter());
            } else {
                // The stack always contains the object of the current level
                Stack stack = new Stack();
                stack.push(resource);
                
                // Stack of the objects one level below
                Stack stackBelow = new Stack();
                
                StringBuffer buffer = new StringBuffer();
                if (outputOptimized) {
                    
                    resp.getWriter().write("<?xml version=\"1.0\" encoding=\""+Configuration.realUrlEncoding()+"\"?>");
                    resp.getWriter().write("\n");
                    String namespacePrefix = multistatusElement.getNamespacePrefix();
                    if ( (namespacePrefix != null) && (namespacePrefix.length() == 0) ) {
                        namespacePrefix = null;
                    }
                    String namespaceUri = multistatusElement.getNamespaceURI();
                    if ( (namespaceUri != null) && (namespaceUri.length() == 0) ) {
                        namespaceUri = null;
                    }
                    buffer.append("<");
                    buffer.append(multistatusElement.getQualifiedName());
                    if (namespaceUri != null) {
                        buffer.append(" xmlns");
                        if (namespacePrefix != null) {
                            buffer.append(":");
                            buffer.append(namespacePrefix);
                        }
                        buffer.append("=\"");
                        buffer.append(namespaceUri);
                        buffer.append("\"");
                    }
                    buffer.append(">");
                    resp.getWriter().write(buffer.toString());
                    resp.getWriter().write("\n");
                }
                
                while ((!stack.isEmpty()) && (depth >= 0)) {
                    
                    ObjectNode cur = (ObjectNode) stack.pop();
                    Element response = getPropertiesOfObject(cur.getUri());
                    if (outputOptimized) {
                        xmlOutputter.output(response, resp.getWriter());
                    }
                    else {
                        multistatusElement.addContent(response);
                    }
                    
                    if (depth > 0) {
                        
                        Enumeration children = null;
                        
                        try {
                            children = structure.getChildren(slideToken, cur);
                        } catch (Exception e) {
                            int statusCode = getErrorCode( e );
                            sendError( statusCode, e );
                            throw new WebdavException( statusCode );
                        }
                        
                        while (children.hasMoreElements()) {
                            // PROPFIND must not list Lock-Null resources that
                            // are timed out
                            ObjectNode node = (ObjectNode)children.nextElement();
                            UnlockListenerImpl listener = new UnlockListenerImpl(
                                    slideToken, token, versioningHelper);
                            try {
                                lock.clearExpiredLocks(slideToken, node.getUri(), listener);
                                if (!listener.isRemovedLockResource(node.getUri())) {
                                    stackBelow.push(node);
                                }
                            } catch (SlideException e) { /* ignore */}
                        }
                        
                    }
                    
                    if (stack.isEmpty()) {
                        depth--;
                        stack = stackBelow;
                        stackBelow = new Stack();
                    }
                    
                }
                
                if (outputOptimized) {
                    resp.getWriter().write("\n");
                    buffer.setLength(0);
                    buffer.append("</");
                    buffer.append(multistatusElement.getQualifiedName());
                    buffer.append(">");
                    resp.getWriter().write(buffer.toString());
                    resp.getWriter().write("\n");
                }
                else {
                    xmlOutputter.output(new Document(multistatusElement), resp.getWriter());
                }
            }
        }
        
    }
    
    
    /**
     * Return the properties of an object as a <code>&lt;response&gt;</code>.
     *
     * @param   resourceUri  the slide URI of the resource.
     *
     * @return  the <code>&lt;response&gt;</code> Element.
     *
     * @exception WebdavException
     */
    protected Element getPropertiesOfObject(String resourceUri)
        throws WebdavException {
        
        SlideToken lightSToken = new SlideTokenWrapper(slideToken);
        lightSToken.setForceLock(false);
        
        // evaluate "Label" header
        if (Configuration.useVersionControl()) {
            try {
                resourceUri = versioningHelper.getLabeledResourceUri(resourceUri, labelHeader);
            }
            catch (LabeledRevisionNotFoundException e) {
                return getErrorResponse(resourceUri,
                                        WebdavStatus.SC_CONFLICT,
                                        DeltavConstants.C_MUST_SELECT_VERSION_IN_HISTORY);
            }
            catch( RevisionDescriptorNotFoundException x ) {
            }
            catch (SlideException e) {
                return getErrorResponse(resourceUri, getErrorCode(e), null);
            }
        }
        
        ObjectNode object = null;
        try {
            object = structure.retrieve(lightSToken, resourceUri);
        }
        catch (SlideException e) {
            return getErrorResponse(resourceUri, getErrorCode(e), null);
        }
        
        Element responseElement = new Element(E_RESPONSE, DNSP);
        
        String status = HTTP_VERSION + " " + WebdavStatus.SC_OK + " "
                      + WebdavStatus.getStatusText(WebdavStatus.SC_OK);
        NodeRevisionDescriptors revisionDescriptors = null;
        NodeRevisionDescriptor revisionDescriptor = null;
        
        //boolean isCollection = false;
        
//        NodeLock objectLockToken = null;
        
        try {
            Element hrefElement = new Element(E_HREF, DNSP);
            
            //VersioningHelper vHelp =  VersioningHelper.getVersioningHelper(
            //    lightSToken, token, req, resp, getConfig() );
            String resourcePath = object.getUri();
            
            revisionDescriptors =
                content.retrieve(lightSToken, resourcePath);
            
            try {
                revisionDescriptor = content.retrieve(lightSToken,
                                                      revisionDescriptors);
                
                //isCollection = WebdavUtils.isCollection(revisionDescriptor);
                
                hrefElement.setText(
                    WebdavUtils.getAbsolutePath(object.getUri(), req,
                                                getConfig()));
                
            } catch (RevisionDescriptorNotFoundException e) {
                
                // The object doesn't have any revision, we create a dummy
                // NodeRevisionDescriptor object
                //isCollection = true;
                revisionDescriptor = new NodeRevisionDescriptor(0);
                
                if (!Configuration.useBinding(token.getUri(lightSToken, object.getUri()).getStore())) {
                    revisionDescriptor.setName(UriPath.getLastSegment(object.getUri()));
                }
                
                hrefElement.setText(
                    WebdavUtils.getAbsolutePath(object.getUri(), req,
                                                getConfig()));
            }
            
            responseElement.addContent(hrefElement);
            
//            Enumeration lockTokens = lock.enumerateLocks(lightSToken, object.getUri(), true);
            
//            if (lockTokens.hasMoreElements()) {
//                objectLockToken = (NodeLock) lockTokens.nextElement();
//            }
            
        } catch (AccessDeniedException e) {
            if (revisionDescriptor == null) {
                revisionDescriptor = new NodeRevisionDescriptor(0);
            }
        } catch (Exception e) {
            int statusCode = getErrorCode( e );
            sendError( statusCode, e );
            throw new WebdavException( statusCode );
        }

        if (WebdavUtils.isRedirectref(revisionDescriptor) && !applyToRedirectRef && !agentWorkaround) {
            int statusCode;
            if (revisionDescriptor.propertyValueContains(
                  NodeRevisionDescriptor.RESOURCE_TYPE, "permanent"))
               statusCode = WebdavStatus.SC_MOVED_PERMANENTLY;
            else
               statusCode = WebdavStatus.SC_MOVED_TEMPORARILY;
            status = new String("HTTP/1.1 " + statusCode + " "
                     + WebdavStatus.getStatusText(statusCode));
            Element statusElement = new Element(E_STATUS, DNSP);
            statusElement.setText(status);
            responseElement.addContent(statusElement);
            // @todo What if reftarget is undefined? (an invalid state)
            // @todo What if reftarget is relative?
            Element hrefElement = new Element(E_HREF, DNSP);
            Object refTarget = revisionDescriptor.getProperty(PN_REFTARGET)
                    .getValue();
            hrefElement.setText(refTarget.toString());
            Element locationElement = new Element(E_LOCATION, DNSP);
            locationElement.addContent(hrefElement);
            responseElement.addContent(locationElement);
        }
        else {
            PropertyRetrieverImpl propertyRetriever = new PropertyRetrieverImpl(token, lightSToken, getConfig());
    
            switch (propFindType) {
                case FIND_ALL_PROP :
                case FIND_BY_PROPERTY :
                    try {
                        List propstatList;
                        if (WebdavUtils.isRedirectref(revisionDescriptor)
                               && agentWorkaround)
                            propstatList = propertyRetriever.getWorkaroundPropertiesOfObject(requestedProperties, revisionDescriptors, revisionDescriptor, getSlideContextPath(), extendedAllprop);
                        else
                            propstatList = propertyRetriever.getPropertiesOfObject(requestedProperties, revisionDescriptors, revisionDescriptor, getSlideContextPath(), extendedAllprop);
                        Iterator iterator = propstatList.iterator();
                        while (iterator.hasNext()) {
                            responseElement.addContent((Element)iterator.next());
                        }
                    } catch (ServiceAccessException e) {
                        int statusCode = WebdavStatus.SC_FORBIDDEN;
                        sendError( statusCode, e );
                        throw new WebdavException( statusCode );
                    } catch (Exception e) {
                        int statusCode = getErrorCode( e );
                        sendError( statusCode, e );
                        throw new WebdavException( statusCode );
                    }
                    break;
                case FIND_PROPERTY_NAMES :
    
                    try {
                        status = HTTP_VERSION + " " + WebdavStatus.SC_OK + " " 
                            + WebdavStatus.getStatusText(WebdavStatus.SC_OK);
    
                        Element propstatElement = new Element(E_PROPSTAT, DNSP);
                        Element propElement = new Element(E_PROP, DNSP);
                        RequestedProperties propnames =
                            propertyRetriever.getAllPropertyNames(object.getUri(), true);
                        Iterator iterator = propnames.getRequestedProperties();
                        while (iterator.hasNext()) {
                            RequestedProperty p = (RequestedProperty)iterator.next();
                            Namespace nsp = DNSP.getURI().equals(p.getNamespace())
                                ? DNSP
                                : Namespace.getNamespace(p.getNamespace());
                            propElement.addContent(new Element(p.getName(), nsp));
                        }
                        Element statusElement = new Element(E_STATUS, DNSP);
                        statusElement.setText(status);
                        propstatElement.addContent(propElement);
                        propstatElement.addContent(statusElement);
                        responseElement.addContent(propstatElement);
                    }
                    catch (ServiceAccessException e) {
                        int statusCode = WebdavStatus.SC_FORBIDDEN;
                        sendError( statusCode, e );
                        throw new WebdavException( statusCode );
                    }
                    catch (Exception e) {
                        int statusCode = getErrorCode( e );
                        sendError( statusCode, e );
                        throw new WebdavException( statusCode );
                    }
                    break;
            }
        }

        return responseElement;
    }

    /**
     * Returns the appropriate <code>&lt;response&gt;</code> due to the given
     * <code>exception</code> to the <code>generatedXML</code>
     *
     * @param      resourcePath     the URI of the request to display in the
     *                            <code>&lt;href&gt;</code> element.
     * @param      errorCode      the HTTP error code.
     * @param      condition      the condition that has been violated.
     */
    private Element getErrorResponse(String resourcePath, int errorCode, String condition) {
        
        Element response = new Element(E_RESPONSE, DNSP);
        
        Element href = new Element(E_HREF, DNSP);
        href.setText(HTTP_PROTOCOL +
                         req.getServerName()+ ":" +
                         req.getServerPort() +
                         getSlideContextPath() +
                         resourcePath);
        response.addContent(href);
        Element propStat = new Element(E_PROPSTAT, DNSP);
        response.addContent(propStat);
        
        Element status = new Element(E_STATUS, DNSP);
        status.setText(HTTP_VERSION + " " + errorCode  + " " + WebdavStatus.getStatusText(errorCode));
        propStat.addContent(status);
        
        if (condition != null) {
            Element responseDescriptiont = new Element(E_RESPONSEDESCRIPTION, DNSP);
            Element errorElement = new Element(E_ERROR, DNSP);
            responseDescriptiont.addContent(errorElement);
            Element conditionElement = new Element(condition, DNSP);
            errorElement.addContent(conditionElement);
            propStat.addContent(responseDescriptiont);
        }
        return response;
    }
}









