/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/wvcm/src/org/apache/wvcm/ResourceImpl.java,v 1.14 2004/07/30 06:52:25 ozeigermann Exp $
 * $Revision: 1.14 $
 * $Date: 2004/07/30 06:52:25 $
 *
 * ====================================================================
 *
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Slide", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 * [Additional notices, if required by prior licensing conditions]
 *
 */

package org.apache.wvcm;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.wvcm.AccessControlElement.Privilege;
import javax.wvcm.Folder;
import javax.wvcm.Location;
import javax.wvcm.LockToken;
import javax.wvcm.Principal;
import javax.wvcm.PropertyNameList;
import javax.wvcm.PropertyNameList.AttributeName;
import javax.wvcm.PropertyNameList.PropertyName;
import javax.wvcm.Resource;
import javax.wvcm.SearchToken;
import javax.wvcm.WvcmException;
import javax.wvcm.WvcmException.ReasonCode;
import org.apache.wvcm.store.AccessorFactory;
import org.apache.wvcm.store.FolderAccessor;
import org.apache.wvcm.store.ResourceAccessor;
import org.apache.wvcm.util.XPathWrapper;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.Namespace;
import org.jdom.input.SAXBuilder;

/**
 * Implementation of Resource.
 *
 * @author <a href="mailto:peter.nevermann@softwareag.com">Peter Nevermann</a>
 * @version $Revision: 1.14 $
 */
public class ResourceImpl implements Resource {
    
    protected static Namespace dnsp = Namespace.getNamespace("d", "DAV:");
    public static String MISSING_PROPERTY_VALUE = "*** Missing property value ***";
    
    private ResourceAccessor accessor = null;
    private FolderAccessor folderAccessor = null;
    private Location location = null;
    private LoadedProperties loadedProperties;  // container for the loaded property values
    private Set lockTokens = new HashSet();
    
    /**
     * Constructor
     */
    public ResourceImpl( Location location ) {
        this.location = location;
        this.accessor = AccessorFactory.createAccessor( this );
        this.folderAccessor = AccessorFactory.createFolderAccessor( this );
        this.loadedProperties = new LoadedProperties( new HashMap() );
    }
    
    /**
     * Return an implementation-defined <code>String</code>
     * that identifies the persistent state of the resource.
     * The semantics of a ContentIdentifier is similar to that
     * of an HTTP ETag (see RFC-2616).
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.CONTENT_IDENTIFIER</code> as a wanted property.
     */
    public String getContentIdentifier() throws WvcmException {
        return (String)loadedProperties().get( PropertyName.CONTENT_IDENTIFIER );
    }
    
    /**
     * Sets the content language of the resource.
     * @param locale The content language for the resource.
     */
    public void setContentLanguage(Locale contentLanguage) {
        loadedProperties().set( PropertyName.CONTENT_LANGUAGE, contentLanguage );
    }
    
    /**
     * Return an attribute of this {@link Resource}.
     * @param name The name of the attribute.
     * @throws WvcmException if this {@link Resource} was not created with
     * either <code>PropertyName.ALL_ATTRIBUTES</code>
     * or the specified attribute as a wanted property.
     */
    public Object getAttribute(AttributeName name) throws WvcmException {
        return loadedProperties().get( name );
    }
    
    /**
     * Create a copy of the resource identified by this {@link Resource}
     * at the location identified by the <code>destination</code>.
     * The content of the copy is the same as the content of the
     * resource identified by this {@link Resource}, but the properties of the
     * copy are the default properties for a new resource.
     * @param destination The location of the new resource created by doCopy.
     * @param overwrite If <code>false</code> the existence of a resource
     * at the destination will cause the copy to fail; otherwise,
     * doCopy will replace the destination resource.
     */
    public void doCopy(String destination, boolean overwrite) throws WvcmException {
        try {
            accessor().doCopy(destination, overwrite);
        }
        catch (WvcmException e) {
            if (e.getReasonCode() == ReasonCode.LOCKED) {
                // retry with lock-tokens from destination and destination folder
                Resource destResource =
                    ((LocationImpl)location()).provider().location(destination).resource();
                Folder destFolder = destResource.location().parent().folder();
                Iterator i;
                i = destFolder.doReadProperties(null).getLockTokens().iterator();
                while (i.hasNext()) {
                    addLockToken((LockToken)i.next());
                }
                try {
                    i = destResource.doReadProperties(null).getLockTokens().iterator();
                    while (i.hasNext()) {
                        addLockToken((LockToken)i.next());
                    }
                } catch (WvcmException x) {} // ignore silently, resource may not exist
                accessor().doCopy(destination, overwrite);
            }
            else if (e.getReasonCode() == ReasonCode.CONFLICT) {
                if (getNestedLockTokens(e)) {
                    // retry with nested lock-tokens
                    accessor().doCopy(destination, overwrite);
                }
                else {
                    throw e;
                }
            }
            else {
                throw e;
            }
        }
    }
    
    /**
     * Return the date that the resource was originally created.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.CREATION_DATE </code> as a wanted property
     */
    public Date getCreationDate() throws WvcmException {
        return (Date)loadedProperties().get( PropertyName.CREATION_DATE );
    }
    
    /**
     * NOT YET STANDARD
     * Return the date that the resource was modified.
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#MODIFICATION_DATE </code> MODIFICATION_DATE </code>} as a wanted property
     */
    public Date getModificationDate() throws WvcmException {
        return (Date)loadedProperties().get( PropertyName.MODIFICATION_DATE );
    }
    
    /**
     * NOT YET STANDARD
     * Return the principal that originally created the resource.
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#CREATION_USER </code> CREATION_USER </code>} as a wanted property
     */
    public Principal getCreationUser() throws WvcmException {
        return (Principal)loadedProperties().get( PropertyName.CREATION_USER );
    }
    
    /**
     * NOT YET STANDARD
     * Return the principal that modified the resource.
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#MODIFICATION_USER </code> MODIFICATION_USER </code>} as a wanted property
     */
    public Principal getModificationUser() throws WvcmException {
        return (Principal)loadedProperties().get( PropertyName.MODIFICATION_USER );
    }
    
    /**
     * Return the location of the persistent resource for which
     * this {@link Resource} is a proxy.
     * <p>
     * The format of the location string is specific to the
     * repository that stores the persistent resource.
     * A URL, a UNC filename, and an NFS filename are examples
     * of possible formats for a location string.</p>
     */
    public Location location() {
        return location;
    }
    
    /**
     * Return the content length as an integer number of bytes.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.CONTENT_LENGTH</code> as a wanted property
     */
    public long getContentLength() throws WvcmException {
        Long cl = (Long)loadedProperties().get( PropertyName.CONTENT_LENGTH );
        return cl.longValue();
    }
    
    /**
     * Return a {@link Resource} containing the wanted properties.
     * If the property state is being maintained on both the
     * client and the server, the client value is returned.
     * A requested property named XXX can be retrieved from
     * the resource with the <code>getXxx</code> method.
     */
    public Resource doReadProperties(PropertyNameList wantedPropertyList) throws WvcmException {
        return accessor().doReadProperties( wantedPropertyList );
    }
    
    /**
     * Return a description of the user that created the resource,
     * in a format that is suitable for display to an end user.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.CREATOR_DISPLAY_NAME</code> as a wanted property.
     */
    public String getCreatorDisplayName() throws WvcmException {
        return (String)loadedProperties().get( PropertyName.CREATOR_DISPLAY_NAME );
    }
    
    /**
     * Return a description of the media-type of the resource content.
     * The format is a MIME type string (see RFC1590).
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.CONTENT_TYPE</code> as a wanted property.
     */
    public String getContentType() throws WvcmException {
        return (String)loadedProperties().get( PropertyName.CONTENT_TYPE );
    }
    
    /**
     * Adds or replaces the value of the specified attribute of this {@link Resource}.
     * @param name The name of the attribute.
     * @param value The new value of the specified attribute.
     */
    public void setAttribute(AttributeName name, Object value) {
        if (value == null) {
            value = "";
        }
        loadedProperties().set( name, value );
    }
    
    /**
     * Return the names of properties that have been updated in
     * the proxy, but the updates have not yet been successfully
     * applied to the resource.
     */
    public PropertyNameList getUpdatedPropertyList() throws WvcmException {
        return new PropertyNameList((PropertyName[])loadedProperties().listOfSetProperties().toArray(new PropertyName[0]));
    }
    
    /**
     * Return a comment describing this {@link Resource}
     * that is suitable for display to a user.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.COMMENT</code> as a wanted property.
     */
    public String getComment() throws WvcmException {
        return (String)loadedProperties().get( PropertyName.COMMENT );
    }
    
    /**
     * Sets the content type of the resource.
     * @param contentType The content type for the resource.
     */
    public void setContentType(String contentType) {
        loadedProperties().set( PropertyName.CONTENT_TYPE, contentType );
    }
    
    /**
     * Removes the specified attribute of this {@link Resource}.
     * @param name The name of the attribute.
     */
    public void removeAttribute(AttributeName name) {
        loadedProperties().remove( name );
    }
    
    /**
     * Adds or replaces the comment string of this {@link Resource}.
     * @param comment The new comment to apply to this {@link Resource}.
     */
    public void setComment(String comment) {
        loadedProperties().set( PropertyName.COMMENT, comment );
    }
    
    /**
     * Persists property changes to this {@link Resource}.
     */
    public void doWriteProperties() throws WvcmException {
        accessor().doWriteProperties();
        commit();
    }
    
    /**
     * Adds or replaces the creator display name string of the resource.
     * @param comment The new creator display name to apply to the resource.
     */
    public void setCreatorDisplayName(String val) {
        loadedProperties().set( PropertyName.CREATOR_DISPLAY_NAME, val );
    }
    
    /**
     * Return a short description of the resource, in a format
     * that is suitable for display to an end user in a tree display.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.DISPLAY_NAME</code> as a wanted property.
     */
    public String getDisplayName() throws WvcmException {
        return (String)loadedProperties().get( PropertyName.DISPLAY_NAME );
    }
    
    /**
     * Return a description of the character set of the resource content (see RFC2278).
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.CONTENT_CHARACTER_SET</code> as a wanted property.
     */
    public String getContentCharacterSet() throws WvcmException {
        return (String)loadedProperties().get( PropertyName.CONTENT_CHARACTER_SET );
    }
    
    /**
     * Return a resource containing the wanted properties.
     * A requested property named XXX can be retrieved from
     * the resource with the <code>getXxx</code> method.
     * The resource content is written to <code>content</code>
     * and <code>content</code> is closed.
     * If state is being maintained on both the
     * client and the server, the client state is retrieved.
     */
    public Resource doReadContent(PropertyNameList wantedPropertyList, OutputStream content) throws WvcmException {
        return accessor().doReadContent( wantedPropertyList, content );
    }
    
    /**
     * Adds or replaces the display name string of the resource.
     * @param comment The new display name to apply to the resource.
     */
    public void setDisplayName(String val) {
        loadedProperties().set( PropertyName.DISPLAY_NAME, val );
    }
    
    /**
     * Return a description of the natural language used in the
     * resource content.
     * The format of the description is an ISO 3316 language string
     * followed by an optional underscore and ISO 639 country code
     * (see RFC1766).
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.COMMENT</code> as a wanted property.
     */
    public Locale getContentLanguage() throws WvcmException {
        return (Locale)loadedProperties().get( PropertyName.CONTENT_LANGUAGE );
    }
    
    /**
     * Persists content changes to a resource.
     * <p>
     * If content for a resource is being maintained persistently on
     * both the client and the server, only the client copy of the content
     * is updated.</p>
     * <p>
     * If <code>contentIdentifier</code> matches the current
     * state identifier of the persistent resource,
     * the content of the resource is replaced with the
     * bytes read from <code>content</code>, and <code>content</code>
     * is then closed.</p>
     * <p>
     * If reading from the stream throws a <code>java.io.IOException</code>,
     * then no further data will be read from the stream,
     * and after attempting to close the stream, a <code>WvcmException</code>
     * wrapping the <code> IOException</code> will be thrown,
     * possibly leading to incomplete data being stored on the resource.</p>
     * @throws WvcmException if the resource identified by this {@link Resource}
     * does not exist.
     */
    public void doWriteContent(InputStream content, String contentIdentifier) throws WvcmException {
        accessor().doWriteContent( content, contentIdentifier );
        loadedProperties().commit( PropertyName.CONTENT_TYPE );
        loadedProperties().commit( PropertyName.CONTENT_CHARACTER_SET );
    }
    
    /**
     * Sets the content character set of the resource.
     * @param contentCharacterSet The content character set for the resource.
     */
    public void setContentCharacterSet(String contentCharacterSet) {
        loadedProperties().set( PropertyName.CONTENT_CHARACTER_SET, contentCharacterSet );
    }
    
    /**
     * Return the date the content of the resource was last modified.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.LAST_MODIFIED</code> as a wanted property.
     */
    public Date getLastModified() throws WvcmException {
        return (Date)loadedProperties().get( PropertyName.LAST_MODIFIED );
    }
    
    /**
     * Return a list of {@link Resource} objects containing the wanted properties
     * according to the conditions of the specified searct token from the scope
     * defined by this {@link Resource}.
     * A requested property named XXX can be retrieved from
     * the resource with the <code>getXxx</code> method.
     *
     * @param    wantedPropertyList  the wanted properties
     * @param    searchToken         a  SearchToken
     * @return   the result list
     * @throws   WvcmException
     */
    public List doSearch(PropertyNameList wantedPropertyList, SearchToken searchToken) throws WvcmException {
        return accessor().doSearch( wantedPropertyList, searchToken );
    }
    
    /**
     * Method getWorkspaceFolderList
     *
     * @return   a List
     *
     * @throws   WvcmException
     *
     */
    public List getWorkspaceFolderList() throws WvcmException {
        return (List)loadedProperties().get( PropertyName.WORKSPACE_FOLDER_LIST );
    }
    
    /**
     * Return a list of String objects that identify
     * the names of providers for this resource, with the preferred
     * providers specified earlier in the list.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.PROVIDER_LIST</code> as a wanted property.
     */
    public List getProviderList() throws WvcmException {
        // TODO: prio=h, effort=0.5, descr=(get provider list)
        return null;
    }
    
    /**
     * NOT YET STANDARD !!!
     * Return the resource identifier. The resource identifier enables clients to determine
     * whether two bindings are to the same resource. The value of this property
     * is a URI, and may use any registered URI scheme that guarantees the
     * uniqueness of the value across all resources for all time.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.RESOURCE_ID</code> as a wanted property.
     */
    public String getResourceIdentifier() throws WvcmException {
        String result = null;
        XmlPropertyValue v =
            (XmlPropertyValue)loadedProperties().get( PropertyName.RESOURCE_IDENTIFIER );
        if (v != null ) {
            result = (String)v.selectSingleNode( "string(d:href)", dnsp );
        }
        return result;
    }
    
    /**
     * NOT YET STANDARD !!!
     * Return the list of parents of this resource. The parent-list property enables
     * clients to discover what folders contain a binding to this resource
     * (i.e. what folders have this resource as an internal member).
     * The returned list contains javax.wvcm.Resource#ParentBinding instances.
     * @throws WvcmException if this {@link Resource} was not created with
     * <code>PropertyName.PARENT_LIST</code> as a wanted property.
     */
    public List getParentBindingList() throws WvcmException {
        return (List)loadedProperties().get( PropertyName.PARENT_BINDING_LIST );
    }
    
    
    // -------------------------------------------------------------------------
    // Non-API methods
    // -------------------------------------------------------------------------
    
    /**
     * Get the accessor.
     *
     * @return   a ResourceAccessor
     */
    protected ResourceAccessor accessor() {
        return accessor;
    }
    
    /**
     * Get the folder accessor for this resource.
     * @return   a FolderAccessor
     */
    protected FolderAccessor folderAccessor() {
        return folderAccessor;
    }
    
    /**
     * Get a folder accessor for the specified resource.
     * @return   a FolderAccessor
     */
    protected FolderAccessor folderAccessor( Resource r ) {
        return AccessorFactory.createFolderAccessor(r);
    }
    
    /**
     * Get the stateContainer
     *
     * @return   the loaded properties
     */
    protected LoadedProperties loadedProperties() {
        return loadedProperties;
    }
    
    /**
     * Get the specified property value.
     *
     * @param    pname the PropertyName
     * @return   the value
     * @throws   WvcmException
     */
    public Object getProperty( PropertyName pname ) throws WvcmException {
        return loadedProperties().get( pname );
    }
    
    /**
     * Adds or replaces the value of the specified property of this {@link Resource}.
     *
     * @param    name                a  PropertyName
     * @param    value               an Object
     */
    public void setProperty( PropertyName name, Object value ) {
        loadedProperties().set( name, value );
    }
    
    /**
     * Get the container of loaded properties
     *
     * @return    the container of loaded properties
     *
     */
    public Map getPropertyContainer() {
        return loadedProperties().getContainer();
    }
    
    /**
     * Set the map of wanted properties
     *
     * @param    wantedProperties the wanted properties
     *
     */
    public void setPropertyContainer( Map wantedProperties ) {
        loadedProperties().setContainer( wantedProperties );
    }
    
    /**
     * Get list of created or modified properties
     *
     * @return   a List
     */
    public List listOfSetProperties() {
        return loadedProperties().listOfSetProperties();
    }
    
    /**
     * Get list of removed attributes
     *
     * @return   a List
     */
    public List listOfRemovedAttributes() {
        return loadedProperties().listOfRemovedAttributes();
    }
    
    /**
     * Called after successful doWriteProperties to reset the caches.
     */
    public void commit() {
        loadedProperties().commit();
    }
    
    /**
     * Returns a string representation of the object.
     *
     * @return  a string representation of the object.
     */
    public String toString() {
        return location().string();
    }
    
    /**
     * Binds the resource identified by this {@link Resource}
     * to the location identified by the <code>destination</code>.
     * The content and properties of a resource are not modified
     * by doBind.
     * @param destination The location of the new binding to the resource.
     * @param overwrite If <code>false</code> the existence of a resource
     * at the destination will cause doBind to fail; otherwise,
     * doBind will first unbind the existing resource at the destination.
     *
     * @throws WvcmException Preconditions:
     * <br>(cannot-modify-destination-checked-in-parent): If this Resource
     *  is a controlled resource, the request MUST fail when the folder containing
     *  the destination location is a checked-in controlled folder.
     * <br>(binding-allowed): This Resource supports multiple bindings to it.
     * <br>(cross-server-binding): If this Resource is on another server from
     *  the folder that contains the destination, the destination server
     *  MUST support cross-server bindings.
     * <br>(can-overwrite): If there already is a resource at the destination,
     *  <code>overwrite</code> MUST be <code>true</code>.
     * <br>(cycle-allowed): If this Resource is a folder, and the folder that contains
     *  the destination is a member of this Resource, then the server MUST support cycles
     *  in the location namespace.
     *
     * @throws WvcmException Postconditions:
     * <br>(preserve-properties): The property values of the resource identified by this Resource
     *  MUST NOT have been modified by the doBind request unless this specification states otherwise.
     * <br>(new-binding): The destination MUST identify the resource identified by this Resource.
     */
    public void doBind(Location destination, boolean overwrite) throws WvcmException {
        Folder destFolder = destination.parent().folder();
        String bindingName = destination.lastSegment();
        try {
            folderAccessor(destFolder).doBind(bindingName, this, overwrite);
        }
        catch (WvcmException e) {
            if (e.getReasonCode() == ReasonCode.LOCKED) {
                // retry with lock-tokens from destination and destination folder
                Resource destResource = destination.resource();
                Iterator i;
                i = destFolder.doReadProperties(null).getLockTokens().iterator();
                while (i.hasNext()) {
                    addLockToken((LockToken)i.next());
                }
                try {
                    i = destResource.doReadProperties(null).getLockTokens().iterator();
                    while (i.hasNext()) {
                        addLockToken((LockToken)i.next());
                    }
                } catch (WvcmException x) {} // ignore silently, resource may not exist
                folderAccessor(destFolder).doBind(bindingName, this, overwrite);
            }
            else if (e.getReasonCode() == ReasonCode.CONFLICT) {
                if (getNestedLockTokens(e)) {
                    // retry with nested lock-tokens
                    folderAccessor(destFolder).doBind(bindingName, this, overwrite);
                }
                else {
                    throw e;
                }
            }
            else {
                throw e;
            }
        }
    }
    
    /**
     * Return the list of names of properties available on this {@link Resource}.
     * @param onlyAttributes Only return the names of attributes.
     */
    public PropertyNameList getPropertyNameList(boolean onlyAttributes) throws WvcmException {
        if (onlyAttributes) {
            return loadedProperties().getAttributeNameList();
        }
        else {
            return loadedProperties().getPropertyNameList();
        }
    }
    
    /**
     * Unbinds the resource identified by the locator of this {@link Resource}.
     * The deletion of a resource only guarantees that the resource
     * is no longer accessible at the specified location; it does
     * not affect its accessibility at other locations.
     * If a folder is unbound, no resource is accessible at any
     * location that has the location of the unbound folder as its prefix.
     *
     * @throws WvcmException Preconditions:
     * <br>(cannot-modify-checked-in-parent): If this Resource identifies a controlled resource,
     *  the doUnbind MUST fail when the folder containing the controlled resource
     *  is a checked-in controlled folder.
     * <br>(no-version-unbind): A server MAY fail an attempt to apply doUnbind to a version.
     *
     * @throws WvcmException Postconditions:
     * <br>(resource-unbound): There is no resource at the location identified by this Resource.
     * <br>(unbind-activity-reference): If an activity is unbound, any reference to that activity
     *  in an ActivityList, SubactivityList, or CurrentActivityList MUST be removed.
     * <br>(update-predecessor-list): If a version was unbound, the server MUST have replaced
     *  any reference to that version in a PredecessorList by a copy of the PredecessorList of the unbound version.
     * <br>(version-history-has-root): If the request unbound the root version of a version history,
     *  the request MUST have updated the RootVersion of the version history to refer to
     *  another version that is an ancestor of all other remaining versions in that version history.
     *  A result of this postcondition is that every version history will have at least one version,
     *  and the only way to delete all versions is to unbind the version history resource.
     * <br>(delete-version-reference): If a version is unbound, any reference to that version in a MergeList
     * or AutoMergeList property MUST be removed.
     * <br>(delete-version-set): If the request unbound a version history,
     *  the request MUST have unbound all versions in the VersionList of that version history,
     *  and MUST have satisfied the postconditions for version deletion.
     */
    public void doUnbind() throws WvcmException {
        try {
            accessor().doDelete();
            loadedProperties().resetContainer();
        }
        catch (WvcmException e) {
            if (e.getReasonCode() == ReasonCode.LOCKED) {
                // retry with lock-tokens from parent
                Folder parentFolder =
                    (Folder)location().parent().folder().doReadProperties(null);
                Iterator i = parentFolder.getLockTokens().iterator();
                while (i.hasNext()) {
                    addLockToken((LockToken)i.next());
                }
                accessor().doDelete();
                loadedProperties().resetContainer();
            }
            else if (e.getReasonCode() == ReasonCode.CANNOT_UNBIND_RESOURCE) {
                if (getNestedLockTokens(e)) {
                    // retry with nested lock-tokens
                    accessor().doDelete();
                    loadedProperties().resetContainer();
                }
                else {
                    throw e;
                }
            }
            else {
                throw e;
            }
        }
    }
    
    private boolean getNestedLockTokens(WvcmException e) throws WvcmException {
        if (e.getNestedExceptions() == null) {
            throw e;
        }
        boolean nestedLocked = false;
        for (int i = 0; i < e.getNestedExceptions().length; i++) {
            WvcmException nested = (WvcmException)e.getNestedExceptions()[i];
            if (nested.getReasonCode() == ReasonCode.LOCKED) {
                nestedLocked = true;
                Resource r = ((LocationImpl)location).provider()
                    .location(nested.getLocation()).resource().doReadProperties(null);
                Iterator locktokens = r.getLockTokens().iterator();
                while (locktokens.hasNext()) {
                    addLockToken((LockToken)locktokens.next());
                }
            }
        }
        return nestedLocked;
    }
    
    /**
     * Unbinds the resource identified by this {@link Resource}
     * from its current location and binds it
     * to the location identified by the <code>destination</code>.
     * The content and properties of a resource are not modified
     * by doRebind, except for the properties that are location dependent.
     * @param destination The new location of the resource.
     * @param overwrite If <code>false</code> the existence of a resource
     * at the destination will cause doRebind to fail; otherwise,
     * doRebind will replace the destination resource.
     *
     * @throws WvcmException Preconditions:
     * <br>(can-overwrite): If there already is a resource at the destination,
     *  <code>overwrite</code> MUST be <code>true</code>.
     * <br>(cannot-modify-checked-in-parent): If this ControllableResource is a controlled resource,
     *  the request MUST fail when the folder containing this ControllableResource
     *  is a checked-in controlled folder.
     * <br>(cannot-modify-destination-checked-in-parent): If this ControllableResource
     *  is a controlled resource, the request MUST fail when the folder containing
     *  the destination location is a checked-in controlled folder.
     * <br>(cannot-rename-history): This proxy MUST NOT identify a version history.
     * <br>(cannot-rename-version): If this proxy identifies a version, the request MUST fail.
     *
     * @throws WvcmException Postconditions:
     * <br>(preserve-properties): The property values of the resource identified by this Resource
     *  MUST NOT have been modified by the doRebind request unless this specification states otherwise.
     * <br>(workspace-member-moved): If this Resource did not identify a workspace,
     *  the Workspace property MUST have been updated to have the same value as
     *  the Workspace of its parent folder at the destination location.
     * <br>(workspace-moved): If this proxy identified a workspace,
     *  any reference to that workspace in a Workspace property MUST have been updated
     *  to refer to the new location of that workspace.
     * <br>(update-checked-out-reference): If a checked-out resource is moved, any reference
     *  to that resource in a ActivityCheckoutList property MUST be updated
     *  to refer to the new location of that resource.
     * <br>(update-workspace-reference): If this proxy identifies a workspace,
     *  any reference to that workspace in a CurrentWorkspaceList property
     *  MUST be updated to refer to the new location of that workspace.
     * <br>(update-activity-reference): If this proxy identifies an activity, any reference to that activity
     *  in a ActivityList, SubactivityList, or CurrentActivityList MUST be updated to refer to
     *  the new location of that activity.
     */
    public void doRebind(Location destination, boolean overwrite) throws WvcmException {
        Folder destFolder = destination.parent().folder();
        try {
            accessor().doMove(destination.string(), overwrite);
            location = ((LocationImpl)location).provider().location(destination.string());
            loadedProperties().resetContainer();
        }
        catch (WvcmException e) {
            if (e.getReasonCode() != ReasonCode.LOCKED) {
                // retry with lock-tokens from destination and destination folder
                Resource destResource = destination.resource();
                Iterator i;
                i = destFolder.doReadProperties(null).getLockTokens().iterator();
                while (i.hasNext()) {
                    addLockToken((LockToken)i.next());
                }
                try {
                    i = destResource.doReadProperties(null).getLockTokens().iterator();
                    while (i.hasNext()) {
                        addLockToken((LockToken)i.next());
                    }
                } catch (WvcmException x) {} // ignore silently, resource may not exist
                accessor().doMove(destination.string(), overwrite);
                location = ((LocationImpl)location).provider().location(destination.string());
                loadedProperties().resetContainer();
            }
            else if (e.getReasonCode() == ReasonCode.CONFLICT) {
                if (getNestedLockTokens(e)) {
                    // retry with nested lock-tokens
                    accessor().doDelete();
                    loadedProperties().resetContainer();
                }
                else {
                    throw e;
                }
            }
            else {
                throw e;
            }
        }
    }
    
    /**
     * NOT YET STANDARD
     * Return a list of {@link Privilege} objects defined for the resource.
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#SUPPORTED_PRIVILEGE_LIST SUPPORTED_PRIVILEGE_LIST} as a wanted property.
     */
    public List getSupportedPrivilegeList() throws WvcmException {
        return (List)loadedProperties().get( PropertyName.SUPPORTED_PRIVILEGE_LIST );
    }
    
    /**
     * NOT YET STANDARD
     * Get the owner of this resource
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#OWNER OWNER} as a wanted property.
     */
    public Principal getOwner() throws WvcmException {
        return (Principal)loadedProperties().get( PropertyName.OWNER );
    }
    
    /**
     * NOT YET STANDARD
     * Return a list of {@link Folder} objects that identify folders
     * that contain privileges.
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#PRIVILEGE_FOLDER_LIST PRIVILEGE_FOLDER_LIST} as a wanted property.
     */
    public List getPrivilegeFolderList() throws WvcmException {
        return (List)loadedProperties().get( PropertyName.PRIVILEGE_FOLDER_LIST );
    }
    
    /**
     * NOT YET STANDARD
     * Return a list of {@link Folder} objects that identify folders
     * that contain principals.
     * @throws WvcmException if this {@link Resource} was not created with
     * {@link PropertyNameList.PropertyName#PRINCIPAL_FOLDER_LIST PRINCIPAL_FOLDER_LIST} as a wanted property.
     */
    public List getPrincipalFolderList() throws WvcmException {
        return (List)loadedProperties().get( PropertyName.PRINCIPAL_FOLDER_LIST );
    }
    
    /**
     * NOT YET STANDARD
     * Modifies the access control list (ACL) of this resource. Specifically, this method only
     * permits modification to ACEs that are not inherited, and are not protected.
     */
    public void doWriteAccessControlList(List acl) throws WvcmException {
        accessor().doWriteAccessControlList( acl );
    }
    
    /**
     * NOT YET STANDARD
     * Return the ACL of this resource.
     * The ACL specifies the list of access control elements (ACEs), which define what principals
     * are to get what privileges for this resource.
     * Each ACE specifies the set of privileges to be either granted or denied to a single principal.
     * If the ACL is empty, no principal is granted any privilege.
     *
     * @param    includeInherited    if false, only ACEs defined for the resource are returned;
     *                               otherwise, the ACL includes all inherited ACEs
     * @return   a list of ACEs
     * @throws   WvcmException
     */
    public List doReadAccessControlList(boolean includeInherited) throws WvcmException {
        return accessor().doReadAccessControlList(includeInherited);
    }
    
    public boolean equals(Object o) {
        if (!(o instanceof Resource)) {
            return false;
        }
        else {
            return ((Resource)o).location().equals(location());
        }
    }
    
    public int hashCode() {
        return location().hashCode();
    }
    
    /**
     * Method addLockToken
     *
     * @param    lockToken           a  LockToken
     *
     */
    public void addLockToken(LockToken lockToken) {
        if (lockTokens.contains(lockToken)) {
            lockTokens.remove(lockToken);
        }
        this.lockTokens.add(lockToken);
    }
    
    public void removeLockToken(LockToken lockToken) {
        this.lockTokens.remove(lockToken);
    }
    
    /**
     * NOT YET STANDARD
     * Returns the list of lock tokens available at this resource.
     *
     * @return   a List
     * @throws   WvcmException
     */
    public List getLockTokens() throws WvcmException {
        return new ArrayList(lockTokens);
    }
    
    public List getActiveLockTokens() throws WvcmException {
        List result = new ArrayList();
        Iterator i = lockTokens.iterator();
        while (i.hasNext()) {
            LockToken lt = (LockToken)i.next();
            if (lt.isActive()) {
                result.add(lt);
            }
        }
        return result;
    }
    
    /**
     * NOT YET STANDARD
     * Locks this resource.
     *
     * @param    timeout             a  Timeout
     * @param    deep                a  boolean
     * @throws   WvcmException
     */
    public void doLock(javax.wvcm.LockToken.Timeout timeout, boolean deep) throws WvcmException {
        doLock(timeout, deep, true, "wvcm");
    }
    
    /**
     * NOT YET STANDARD
     * Locks this resource.
     *
     * @param    timeout             a  Timeout
     * @param    deep                a  boolean
     * @param    exclusive           a  boolean
     * @param    owner               a  String
     * @throws   WvcmException
     */
    public void doLock(javax.wvcm.LockToken.Timeout timeout, boolean deep, boolean exclusive, String owner) throws WvcmException {
        LockToken lockToken = accessor().doLock(timeout, deep, exclusive, owner);
        if (lockToken != null) {
            addLockToken(lockToken);
        }
    }
    
    /**
     * NOT YET STANDARD
     * Releases the active lock of this resource.
     *
     * @throws   WvcmException
     */
    public void doUnlock() throws WvcmException {
        Iterator i = lockTokens.iterator();
        if (!i.hasNext()) {
            return;
        }
        
        boolean found = false;
        while (!found && i.hasNext()) {
            LockToken lt = (LockToken)i.next();
            if (lt.isActive() || lt.getLockScope() == LockToken.Scope.EXCLUSIVE) {
                found = true;
                doUnlock(lt);
                break;
            }
        }
        
        if (!found) {
            throw new WvcmException("Could not unlock: no active lock-token found",
                                    location().string(),
                                    ReasonCode.CANNOT_UNLOCK,
                                    null);
        }
    }
    
    /**
     * NOT YET STANDARD
     * Releases the specified lock of this resource.
     *
     * @param    lockToken           a  LockToken
     * @throws   WvcmException
     */
    public void doUnlock(LockToken lockToken) throws WvcmException {
        accessor().doUnlock(lockToken);
        if (lockToken != null) {
            removeLockToken(lockToken);
        }
    }
    
    /**
     * Loaded properties container.
     */
    protected class LoadedProperties {
        
        private Map container;
        private Set setProperties;
        private Set removedAttributes;
        
        /**
         * Constructor
         *
         * @param    m a container HashMap
         */
        LoadedProperties( Map m ) {
            this.container = Collections.synchronizedMap(m);
            this.setProperties = Collections.synchronizedSet( new HashSet() );
            this.removedAttributes = Collections.synchronizedSet( new HashSet() );
        }
        
        /**
         * Get a property value
         *
         * @param    pname the PropertyName
         * @return   the value
         * @throws   WvcmException if property is not available (because it was not requested)
         */
        protected synchronized Object get( PropertyName pname ) throws WvcmException {
            if (!container.containsKey(pname)) {
                throw new WvcmException(
                    "Property value "+pname.getString()+" not available", location.string(), ReasonCode.VALUE_UNAVAILABLE, null );
            }
            if (container.get(pname) == MISSING_PROPERTY_VALUE) {
                throw new WvcmException(
                    "Property value "+pname.getString()+" missing", location.string(), ReasonCode.PROPERTY_MISSING, null );
            }
            return container.get( pname );
        }
        
        /**
         * Set a property value
         *
         * @param    pname the PropertyName
         * @param    value the value
         */
        protected synchronized void set( PropertyName pname, Object value ) {
            container.put( pname, value );
            setProperties.add( pname );
        }
        
        /**
         * Remove an attribute
         *
         * @param    aname the AttributeName
         */
        protected synchronized void remove( AttributeName aname ) {
            container.remove( aname );
            removedAttributes.add( aname );
        }
        
        /**
         * Get list of removed attributes
         *
         * @return   a List
         */
        protected synchronized List listOfRemovedAttributes() {
            return new ArrayList( removedAttributes );
        }
        
        /**
         * Get list of created or modified properties
         *
         * @return   a List
         */
        protected synchronized List listOfSetProperties() {
            return new ArrayList( setProperties );
        }
        
        /**
         * Get list of available PropertyName's
         *
         * @return   a List
         */
        protected synchronized PropertyNameList getPropertyNameList() {
            List l = new ArrayList(container.keySet());
            return new PropertyNameList((PropertyName[])l.toArray(new PropertyName[0]));
        }
        
        /**
         * Get list of available AttributeName's
         *
         * @return   a List
         */
        protected synchronized PropertyNameList getAttributeNameList() {
            List l = new ArrayList();
            Iterator i = container.keySet().iterator();
            while( i.hasNext() ) {
                PropertyName key = (PropertyName)i.next();
                if( key instanceof AttributeName )
                    l.add( key );
            }
            return new PropertyNameList((PropertyName[])l.toArray(new PropertyName[0]));
        }
        
        /**
         * Called after successful doWriteProperties to reset the caches.
         */
        protected synchronized void commit() {
            this.setProperties = Collections.synchronizedSet( new HashSet() );
            this.removedAttributes = Collections.synchronizedSet( new HashSet() );
        }
        
        /**
         * Called after successful doWriteContent to reset CONTENT_TYPE and CONTENT_CHARACTER_SET.
         */
        protected synchronized void commit( PropertyName pname ) {
            this.setProperties.remove( pname );
        }
        
        /**
         * Reset the properties container.
         * Called e.g. after doUncheckout to reset the container and caches.
         */
        protected synchronized void resetContainer() {
            this.container = Collections.synchronizedMap( new HashMap() );
            this.setProperties = Collections.synchronizedSet( new HashSet() );
            this.removedAttributes = Collections.synchronizedSet( new HashSet() );
        }
        
        /**
         * Set the properties container.
         */
        protected synchronized void setContainer( Map container ) {
            this.container = Collections.synchronizedMap( container );
            this.setProperties = Collections.synchronizedSet( new HashSet() );
            this.removedAttributes = Collections.synchronizedSet( new HashSet() );
        }
        
        /**
         * Update the properties container.
         */
        protected synchronized void addAllMissingAttributes( Map missingProperties ) {
            Iterator i = missingProperties.entrySet().iterator();
            while( i.hasNext() ) {
                Map.Entry e = (Map.Entry)i.next();
                if( (e.getKey() instanceof AttributeName) && !this.container.containsKey(e.getKey()) )
                    this.container.put( e.getKey(), e.getValue() );
            }
            this.setProperties = Collections.synchronizedSet( new HashSet() );
            this.removedAttributes = Collections.synchronizedSet( new HashSet() );
        }
        
        /**
         * Get the properties container.
         */
        protected synchronized Map getContainer() {
            return new HashMap( container );
        }
    }
    
    /**
     * An XML property value
     */
    public static class XmlPropertyValue {
        
        private Document d;
        
        /**
         * Constructor
         *
         * @param    xmlString           a  String
         *
         */
        public XmlPropertyValue(Resource requestResource, String xmlString) throws WvcmException {
            try {
                Reader in = new StringReader(xmlString);
                this.d = ProviderImpl.getSAXBuilder().build(in);
            }
            catch (JDOMException e) {
                e.printStackTrace();
                throw new WvcmException(
                    "Could not parse XML property value "+xmlString,
                    requestResource.location().string(),
                    ReasonCode.READ_FAILED,
                    new Exception[]{e}
                );
            }
            catch (IOException e) {
                // TODO: throw new XAssertionFailed("unexpected IOException reading from memory");
                e.printStackTrace();
                throw new WvcmException(
                    "Could not parse XML property value "+xmlString,
                    requestResource.location().string(),
                    ReasonCode.READ_FAILED,
                    new Exception[]{e}
                );
            }
        }
        
        /**
         * Method getRootElm
         *
         * @return   an Element
         *
         */
        public Element getRootElement() {
            return d != null
                ? d.getRootElement()
                : null;
        }
        
        /**
         * Evaluate an XPATH expression
         *
         * @param    xpath               a  String
         * @param    nsp                 a  Namespace
         * @param    context             an Object
         *
         * @return   an Object
         *
         * @throws   WvcmException
         *
         */
        public Object selectSingleNode( String xpath, Namespace nsp ) throws WvcmException {
            XPathWrapper xp = new XPathWrapper( "string(d:href)", nsp );
            return xp.selectSingleNode( getRootElement() );
        }
    }
}

