/*
 * $Header: /home/cvspublic/jakarta-slide/src/webdav/server/org/apache/slide/webdav/util/PropertyHelper.java,v 1.92 2005/02/02 11:33:36 luetzkendorf Exp $
 * $Revision: 1.92 $
 * $Date: 2005/02/02 11:33:36 $
 *
 * ====================================================================
 *
 * 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.util;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.slide.common.NamespaceAccessToken;
import org.apache.slide.common.PropertyName;
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.NodeRevisionDescriptor;
import org.apache.slide.content.NodeRevisionDescriptors;
import org.apache.slide.content.NodeRevisionNumber;
import org.apache.slide.content.NodeProperty.NamespaceCache;
import org.apache.slide.lock.NodeLock;
import org.apache.slide.util.XMLValue;
import org.apache.slide.webdav.WebdavServletConfig;
import org.apache.slide.webdav.util.properties.LockDiscoveryProperty;
import org.apache.slide.webdav.util.properties.PropertyComputer;
import org.apache.slide.webdav.util.properties.PropertyDefaultProvider;
import org.apache.slide.webdav.util.resourcekind.AbstractResourceKind;
import org.apache.slide.webdav.util.resourcekind.ResourceKind;
import org.apache.slide.webdav.util.resourcekind.ResourceKindManager;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;



/**
 * Helper class for handling WebDAV properties.
 *
 */

public class PropertyHelper extends AbstractWebdavHelper implements DeltavConstants, AclConstants, DaslConstants, BindConstants {
    
    public final static String LOCKDISCOVERY_INCL_PRINCIPAL =
        "lockdiscoveryIncludesPrincipalURL";
    
    public final static String PRIVILEGE_NAMESPACE = "privilege-namespace";
    
    private WebdavServletConfig sConf = null;
    
    private WebdavContext webdavContext = null;
    
    /**
     * Factory method.
     * @deprecated
     */
    public static PropertyHelper getPropertyHelper( SlideToken sToken, NamespaceAccessToken nsaToken) {
        return new PropertyHelper( sToken, nsaToken, (WebdavServletConfig)null );
    }
    
    /**
     * Factory method.
     */
    public static PropertyHelper getPropertyHelper( SlideToken sToken, NamespaceAccessToken nsaToken, WebdavServletConfig sConf ) {
        return new PropertyHelper( sToken, nsaToken, sConf );
    }
    
    public static PropertyHelper getPropertyHelper( SlideToken sToken, NamespaceAccessToken nsaToken, WebdavContext webdavContext ) {
        return new PropertyHelper( sToken, nsaToken, webdavContext );
    }
    
    /**
     * Protected contructor
     */
    protected PropertyHelper( SlideToken sToken, NamespaceAccessToken nsaToken, WebdavServletConfig sConf ) {
        super( sToken, nsaToken );
        this.sConf = sConf;
    }
    
    protected PropertyHelper( SlideToken sToken, NamespaceAccessToken nsaToken, WebdavContext webdavContext) {
        super( sToken, nsaToken );
        this.webdavContext = webdavContext;
        this.sConf = webdavContext.getServletConfig();
        
    }
    
    /**
     * Create initial default properties to be stored for the specified
     * resource kind.
     * The result set does not contain transient properties.
     * @post result != null
     * @return a list of initial non-transient properties (empty list if none)
     */
    public List createInitialProperties(ResourceKind resourceKind, String resourcePath) {
        
        List result = new ArrayList();
        
        for(Iterator i = resourceKind.getDefaultProviders().values().iterator(); i.hasNext();) {
            PropertyDefaultProvider provider = (PropertyDefaultProvider)i.next();
            Object defaultValue = provider.createDefaultValue(this.nsaToken, 
                    this.sToken, resourceKind, resourcePath);
            
            if (defaultValue != null) {
                result.add(new NodeProperty(provider.getPropertyName(), 
                        defaultValue));
            }
        }
        
        return result;
    }

    /**
     * Create CDATA value.
     */
    public Object createCdataValue( String text ) {
        StringBuffer b = new StringBuffer();
        if( text == null )
            text = "";
        b.append( "![CDATA[" ).append( text ).append( "]]" );
        return b.toString();
    }
    
    /**
     * Create href value.
     */
    public Object createHrefValue( String uri ) {
        String result = "";
        
        Element href = new Element( E_HREF, DNSP );
        href.addContent( uri );
        try {
            result = xmlOut.outputString( href );
        }
        catch( Exception x ) {
            x.printStackTrace();
        }
        
        return result;
    }
    
    /**
     * Create href set value.
     */
    public Object createHrefSetValue( List uriList ) {
        StringBuffer b = new StringBuffer();
        Iterator i = uriList.iterator();
        while( i.hasNext() )
            b.append( createHrefValue((String)i.next()) );
        return b.toString();
    }
    
    /**
     * Create href set value.
     */
    public Object createHrefSetValue( String rootElement, List uriList ) {
        String result = "";
        
        Element root = new Element( rootElement, DNSP );
        Iterator i = uriList.iterator();
        
        while( i.hasNext() ) {
            Element href = new Element( E_HREF, DNSP );
            href.addContent( (String)i.next() );
            root.addContent( href );
        }
        try {
            result = xmlOut.outputString( root );
        }
        catch( Exception x ) {
            x.printStackTrace();
        }
        
        return result;
    }
    
    /**
     * Parse an XML-Valued property value.
     */
    public Element parsePropertyValue( String propValue ) throws JDOMException, IOException {
        Document d = xmlBuilder.build( new StringReader(propValue) );
        return d.getRootElement();
    }
    

    /**
     * Returns the property of a resource described by the resourcePath.
     * @deprecated use {@link #getProperty(PropertyName, String, String)}
     */
    public NodeProperty getProperty(String propertyName, String resourcePath, 
            String slideContextPath) 
    throws SlideException, JDOMException {
        return getProperty(PropertyName.getPropertyName(propertyName), 
                resourcePath, slideContextPath);
    }
    
    /**
     * Returns the property of a resource described by the resourcePath.
     *
     * @param    propertyName     the name of the property.
     * @param    resourcePath     the path that identifies the resource.
     * @param    slideContextPath a  String, path representing the full context of the WebDAV-Servlet.
     * @return   the property.
     * @throws   SlideException
     * @throws   JDOMException
     */
    public NodeProperty getProperty(PropertyName propertyName, 
            String resourcePath, String slideContextPath) 
    throws SlideException, JDOMException {

        UriHandler uriHandler = UriHandler.getUriHandler(resourcePath);
        String uri = null;
        NodeRevisionDescriptors revisionDescriptors = null;
        NodeRevisionDescriptor revisionDescriptor = null;
        Content contentHelper = nsaToken.getContentHelper();
        
        if (uriHandler.isVersionUri()) {
            uri = uriHandler.getAssociatedHistoryUri();
            NodeRevisionNumber revisionNumber = new NodeRevisionNumber(uriHandler.getVersionName());
            revisionDescriptors = contentHelper.retrieve(sToken, uri);
            revisionDescriptor = contentHelper.retrieve(sToken, revisionDescriptors, revisionNumber);
        }
        else if (uriHandler.isHistoryUri()) {
            uri = uriHandler.getAssociatedHistoryUri();
            NodeRevisionNumber revisionNumber = new NodeRevisionNumber("0.0");
            revisionDescriptors = contentHelper.retrieve(sToken, uri);
            revisionDescriptor = contentHelper.retrieve(sToken, revisionDescriptors, revisionNumber);
        }
        else {
            uri = resourcePath;
            revisionDescriptors = contentHelper.retrieve(sToken, uri);
            revisionDescriptor = contentHelper.retrieve(sToken, revisionDescriptors);
        }
        return getProperty(propertyName, revisionDescriptors, revisionDescriptor, slideContextPath);
    }
    
    /**
     * Returns the property of the resource described by
     * the resourcePath.
     *
     * @param    propertyName         the name of the property.
     * @param    revisionDescriptors  the NodeRevisionDescriptors of the resource.
     * @param    revisionDescriptor   the NodeRevisionDescriptor of the resource.
     *
     * @return   the property.
     *
     * @throws   SlideException
     * @throws   JDOMException
     * @deprecated use {@link #getProperty(String, NodeRevisionDescriptors, NodeRevisionDescriptor, String)}
     */
    public NodeProperty getProperty(String propertyName, 
            NodeRevisionDescriptors revisionDescriptors, 
            NodeRevisionDescriptor revisionDescriptor) 
    throws SlideException, JDOMException {
        return getProperty(propertyName, revisionDescriptors, revisionDescriptor, null);
    }
    
    
    /**
     * Returns the property of a resource described by its descriptors.
     * @deprecated use {@link #getProperty(PropertyName, NodeRevisionDescriptors, NodeRevisionDescriptor, String)}.
     */
    public NodeProperty getProperty(String propertyName, 
            NodeRevisionDescriptors revisionDescriptors, 
            NodeRevisionDescriptor revisionDescriptor, 
            String slideContextPath) throws SlideException, JDOMException {
        return getProperty(PropertyName.getPropertyName(propertyName),
                revisionDescriptors, revisionDescriptor, slideContextPath);
    }
    
    /**
     * Returns the property of a resource described by its descriptors.
     *
     * @param    propertyName         the name of the property.
     * @param    revisionDescriptors  the NodeRevisionDescriptors of the resource.
     * @param    revisionDescriptor   the NodeRevisionDescriptor of the resource.
     * @param    slideContextPath     a  String, representing the full context of the webdav servlet.
     *
     * @return   the property.
     *
     * @throws   SlideException
     * @throws   JDOMException
     */
    public NodeProperty getProperty(PropertyName propertyName, 
            NodeRevisionDescriptors revisionDescriptors, 
            NodeRevisionDescriptor revisionDescriptor, 
            String slideContextPath) 
        throws SlideException, JDOMException 
    {
        
        NodeProperty property = revisionDescriptor.getProperty(propertyName);
        ResourceKind resourceKind = AbstractResourceKind.determineResourceKind(
                nsaToken, revisionDescriptors, revisionDescriptor);
        if (resourceKind.isSupportedLiveProperty(propertyName)) {
            if (resourceKind.isComputedProperty(propertyName)) {
                property = computeProperty(propertyName, revisionDescriptors, revisionDescriptor, slideContextPath);
            }
        }
        
        return property;
    }
    
    /**
     * Returns the computed property of the resource.
     *
     * @param    propertyName         the name of the property.
     * @param    revisionDescriptors  the NodeRevisionDescriptors of the resource.
     * @param    revisionDescriptor   the NodeRevisionDescriptor of the resource.
     * @param    contextPath         a  String , the result of HttpRequest.getContextPath()
     * @param    servletPath         a String, the result of HttpRequest.getServletPath()
     *
     * @return   the property.
     *
     * @throws   SlideException
     * @throws   JDOMException
     */
    public NodeProperty computeProperty(String propertyName, 
            NodeRevisionDescriptors revisionDescriptors, 
            NodeRevisionDescriptor revisionDescriptor, 
            String slideContextPath) throws SlideException, JDOMException 
    {
        return computeProperty(PropertyName.getPropertyName(propertyName),
                revisionDescriptors, revisionDescriptor, slideContextPath);
        
    }
    
    public NodeProperty computeProperty(PropertyName propertyName, 
            NodeRevisionDescriptors revisionDescriptors, 
            NodeRevisionDescriptor revisionDescriptor, 
            String slideContextPath) throws SlideException, JDOMException 
    {
        
        ResourceKind resourceKind = ResourceKindManager.determineResourceKind(
                this.nsaToken, revisionDescriptors, revisionDescriptor);
        
        PropertyComputer definition = resourceKind.getPropertyComputer(propertyName);

        if (this.webdavContext == null) {
            this.webdavContext = WebdavContextImpl.create(this.sToken, 
                    sConf, slideContextPath);
        }
        
        if (definition != null) {
            Object value = definition.computeValue(this.nsaToken, 
                    revisionDescriptors, 
                    revisionDescriptor, 
                    resourceKind, 
                    this.webdavContext);
        
            if (value != null) {
                return new NodeProperty(definition.getPropertyName(), value);
            } else {
                return null; // TODO
            }
        }
        
        throw new SlideException("Unknown property" + propertyName);
        
    }
    
    /**
     ** Returns a List of <code>&lt;exclude&gt;</code> elements containing the
     ** paths to exclude from the search since they does not contain
     ** version-controlled resources (e.g. the users path and the history paths) .
     **
     ** @return     a List of <code>&lt;exclude&gt;</code> elements.
     **/
    public List getNonVcrPathExcludeList() {
        return getNonVcrPathExcludeList(this.nsaToken);
    }
    public static List getNonVcrPathExcludeList(NamespaceAccessToken nsaToken) {
        
        List excludeList = new ArrayList();
        
        String usersPath = truncateLeadingSlash(nsaToken.getNamespaceConfig().getUsersPath());
        addExcludeElement(usersPath, excludeList);
        
        String groupsPath = truncateLeadingSlash(nsaToken.getNamespaceConfig().getGroupsPath());
        addExcludeElement(groupsPath, excludeList);
        
        String rolesPath = truncateLeadingSlash(nsaToken.getNamespaceConfig().getRolesPath());
        addExcludeElement(rolesPath, excludeList);
        
        Iterator historyPathIterator = HistoryPathHandler.getHistoryPathHandler().getResolvedHistoryPaths().iterator();
        while (historyPathIterator.hasNext()) {
            String path = truncateLeadingSlash(historyPathIterator.next().toString());
            addExcludeElement(path, excludeList);
        }
        
        return excludeList;
    }
    
    private static void addExcludeElement(String path, List excludeList) {
        if (path != null && path.length() > 0) {
            Element excludeElement = new Element(DaslConstants.E_EXCLUDE, NamespaceCache.SLIDE_NAMESPACE);
            excludeElement.setText(path);
            excludeList.add(excludeElement);
        }
    }
    
    /**
     * Returns an XMLValue containing the value of the <code>&lt;lockdiscovery&gt;</code>
     * property.
     *
     * @param    objectLockToken     the NodeLock for which to compute the value
     * @param    servletPath         a String, the result of HttpRequest.getServletPath()
     * @param    contextPath         a  String , the result of HttpRequest.getContextPath()
     *
     * @return   the value of the <code>&lt;lockdiscovery&gt;</code> property.
     *
     */
    public XMLValue computeLockDiscovery(NodeLock objectLockToken, String slideContextPath) {
        
        XMLValue xmlValue = new XMLValue();
        //Element activelock = createActiveLockElement(objectLockToken, slideContextPath);
        Element activelock = LockDiscoveryProperty.createActiveLockElement(
                objectLockToken, nsaToken, sConf, slideContextPath); 
        if (activelock != null) {
            xmlValue.add(activelock);
        }
        return xmlValue;
    }

    /**
     * Returns the concatenation of <code>serverURL</code>, <code>contextPath</code>
     * and <code>uri</code> and inserts all needed slashes.
     *
     * This method is deprecated, as it takes neither scope nor the servlet
     * path into account (when the servlet is not the default servlet). Please
     * use WebdavUtils.getAbsolutePath() instead.
     *
     * @deprecated
     * @param      serverURL    the URL of the server
     * @param      contextPath  the context path.
     * @param      uri          the (slide-) URI of the resource.
     *
     * @return     the concatenated URL.
     */
    public static String getAbsoluteURL(String serverURL, String contextPath, String uri) {
        // FIXME servletContext??
        StringBuffer buffer = new StringBuffer();
        String lastAppended = null;
        //        if (serverURL != null) {
        //            buffer.append(serverURL);
        //            lastAppended = buffer.toString();
        //        }
        if (contextPath != null) {
            if ( (lastAppended != null) && !lastAppended.endsWith("/") && !contextPath.startsWith("/") ) {
                buffer.append("/");
            }
            buffer.append(contextPath);
            lastAppended = buffer.toString();
        }
        if (uri != null) {
            if ( (lastAppended != null) && !lastAppended.endsWith("/") && !uri.startsWith("/") ) {
                buffer.append("/");
            }
            buffer.append(uri);
        }
        
        return WebdavUtils.encodeURL(buffer.toString());
    }
    
    /**
     * Returns <code>true</code> if the given <code>uri</code> is an absolute URL.
     *
     * @param      contextPath  the context path.
     * @param      uri          the (slide-) URI of the resource.
     *
     * @return     <code>true</code> if the given <code>uri</code> is an absolute URL.
     */
    public static boolean isAbsoluteURL(String servletContextPath, String uri) {
        if (uri.startsWith(S_RESOURCE_ID)) {
            return true;
        }
        if (uri.startsWith(S_LOCK_TOKEN)) {
            return true;
        }
        if (!uri.startsWith("/")) {
            uri = "/" + uri;
        }
        return uri.startsWith(servletContextPath);
    }
    
    /**
     * Adds the given <code>uri</code> as a <code>&lt;href&gt;</code> element
     * to the value of the property (specified by the <code>propertyName</code>)
     * of the given NodeRevisionDescriptor (if not already contained).
     *
     * @param      revisionDescriptor  the NodeRevisionDescriptor for which to
     *                                 update the property.
     * @param      propertyName        the name of the property to add the uri to.
     * @param      uri                 the uri to add as a <code>&lt;href&gt;</code> element.
     * @return     true, if href was added; false, if already contained
     * @throws   JDOMException
     *
     */
    public static boolean addHrefToProperty(NodeRevisionDescriptor revisionDescriptor, String propertyName, String uri) throws JDOMException {
        return addElementToProperty(revisionDescriptor, propertyName, E_HREF, uri);
    }
    
    /**
     * Adds the given <code>elementValue</code> as a <code>&lt;elementName&gt;</code> element
     * to the value of the property (specified by the <code>propertyName</code>)
     * of the given NodeRevisionDescriptor (if not already contained).
     *
     * @param      revisionDescriptor  the NodeRevisionDescriptor for which to
     *                                 update the property.
     * @param      propertyName        the name of the property to add the element to.
     * @param      elementName         the name of the element to add.
     * @param      elementValue        the value to add as a <code>&lt;elementName&gt;</code> element.
     * @return     true, if element was added; false, if already contained
     * @throws   JDOMException
     *
     */
    public static boolean addElementToProperty(NodeRevisionDescriptor revisionDescriptor, String propertyName, String elementName, String elementValue) throws JDOMException {
        
        NodeProperty property = revisionDescriptor.getProperty(propertyName);
        if (property == null) {
            property = new NodeProperty(propertyName, "");
        }
        XMLValue xmlValue = new XMLValue((String)property.getValue());
        Iterator iterator = xmlValue.iterator();
        boolean alreadyContained = false;
        Element element = null;
        while (iterator.hasNext() && !alreadyContained) {
            element = (Element)iterator.next();
            if (element.getName().equals(elementName) && element.getText().equals(elementValue)) {
                alreadyContained = true;
            }
        }
        if (!alreadyContained) {
            element = new Element(elementName);
            element.setText(elementValue);
            xmlValue.add(element);
        }
        revisionDescriptor.setProperty(propertyName, xmlValue.toString());
        return !alreadyContained;
    }
    
    /**
     * Removes the <code>&lt;href&gt;</code> element with the given <code>uri</code>
     * from the value of the property (specified by the <code>propertyName</code>)
     * of the given NodeRevisionDescriptor (if contained).
     *
     * @param      revisionDescriptor  the NodeRevisionDescriptor for which to
     *                                 update the property.
     * @param      propertyName        the name of the property to remove the uri from.
     * @param      uri                 the uri of the <code>&lt;href&gt;</code> element
     *                                 to remove.
     * @return     true, if href was removed; false, if not found
     * @throws   JDOMException
     *
     */
    public static boolean removeHrefFromProperty(NodeRevisionDescriptor revisionDescriptor, String propertyName, String uri) throws JDOMException {
        return removeElementFromProperty(revisionDescriptor, propertyName, E_HREF, uri);
    }
    
    /**
     * Removes the <code>&lt;elementName&gt;</code> element with the given <code>elementValue</code>
     * from the value of the property (specified by the <code>propertyName</code>)
     * of the given NodeRevisionDescriptor (if contained).
     *
     * @param      revisionDescriptor  the NodeRevisionDescriptor for which to
     *                                 update the property.
     * @param      propertyName        the name of the property to add the element to.
     * @param      elementName         the name of the element to add.
     * @param      elementValue        the value to add as a <code>&lt;elementName&gt;</code> element.
     * @return     true, if element was removed; false, if not found
     *
     * @throws   JDOMException
     *
     */
    public static boolean removeElementFromProperty(NodeRevisionDescriptor revisionDescriptor, String propertyName, String elementName, String elementValue) throws JDOMException {
        
        NodeProperty property = revisionDescriptor.getProperty(propertyName);
        boolean found = false;
        if (property != null) {
            
            XMLValue xmlValue = new XMLValue((String)property.getValue());
            Iterator iterator = xmlValue.iterator();
            Element element = null;
            while (iterator.hasNext() && !found) {
                element = (Element)iterator.next();
                if (element.getName().equals(elementName) && element.getText().equals(elementValue)) {
                    found = true;
                    iterator.remove();
                }
            }
            if (found) {
                revisionDescriptor.setProperty(propertyName, xmlValue.toString());
            }
        }
        return found;
    }
    
    public static boolean replaceElementAtProperty(
            NodeRevisionDescriptor revisionDescriptor, 
            PropertyName propertyName, String elementName, 
            String oldElementValue, String newElementValue) throws JDOMException {
        
        NodeProperty property = revisionDescriptor.getProperty(propertyName);

        XMLValue xmlValue = new XMLValue((String)property.getValue());
        Iterator iterator = xmlValue.iterator();
        
        boolean anythingReplaced = false;

        while (iterator.hasNext()) {
            Element element = (Element)iterator.next();
            if (element.getName().equals(elementName) && element.getText().equals(oldElementValue)) {
                anythingReplaced = true;
                element.setText(newElementValue);
            }
        }
        revisionDescriptor.setProperty(propertyName, xmlValue.toString());
        return !anythingReplaced;
    }
    
    /**
     * Any leading <code>/</code> in the given <code>uri</code> will be removed.
     *
     * @param      uri  the Uri to remove leading slashes from.
     *
     * @return     the Uri without leading slashes.
     */
    public static String truncateLeadingSlash(String uri) {
        
        if (uri == null) {
            return uri;
        }
        
        while (uri.startsWith("/")) {
            uri = uri.substring(1);
        }
        return uri;
    }
    
    /**
     * Any leading <code>/</code> in the given <code>uri</code> will be removed.
     *
     * @param      uri  the Uri to remove leading slashes from.
     *
     * @return     the Uri without leading slashes.
     */
    public static String computeEtag(String uri, NodeRevisionDescriptor nrd) {
        StringBuffer sb = new StringBuffer();
        sb.append(System.currentTimeMillis()).append('_')
          .append(uri.hashCode()).append('_')
          .append(nrd.getLastModified()).append('_')
          .append(nrd.getContentLength());
        return DigestUtils.md5Hex(sb.toString());
    }
    
    
}



