/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.jackrabbit.webdav.jcr;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.version.VersionIterator;
import javax.jcr.version.Version;
import javax.jcr.lock.Lock;
import javax.jcr.nodetype.NodeType;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.commons.webdav.JcrValueType;
import org.apache.jackrabbit.server.io.IOUtil;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.webdav.DavCompliance;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.MultiStatusResponse;
import org.apache.jackrabbit.webdav.jcr.property.JcrDavPropertyNameSet;
import org.apache.jackrabbit.webdav.util.HttpDateFormat;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.jcr.lock.JcrActiveLock;
import org.apache.jackrabbit.webdav.jcr.lock.SessionScopedLockEntry;
import org.apache.jackrabbit.webdav.jcr.nodetype.NodeTypeProperty;
import org.apache.jackrabbit.webdav.jcr.property.ValuesProperty;
import org.apache.jackrabbit.webdav.jcr.version.report.ExportViewReport;
import org.apache.jackrabbit.webdav.jcr.version.report.LocateCorrespondingNodeReport;
import org.apache.jackrabbit.webdav.lock.ActiveLock;
import org.apache.jackrabbit.webdav.lock.LockInfo;
import org.apache.jackrabbit.webdav.lock.Scope;
import org.apache.jackrabbit.webdav.lock.Type;
import org.apache.jackrabbit.webdav.ordering.OrderPatch;
import org.apache.jackrabbit.webdav.ordering.OrderingConstants;
import org.apache.jackrabbit.webdav.ordering.OrderingResource;
import org.apache.jackrabbit.webdav.ordering.OrderingType;
import org.apache.jackrabbit.webdav.ordering.Position;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.HrefProperty;
import org.apache.jackrabbit.webdav.property.PropEntry;
import org.apache.jackrabbit.webdav.xml.DomUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * <code>DefaultItemCollection</code> represents a JCR node item.
 */
public class DefaultItemCollection extends AbstractItemResource
        implements OrderingResource {

    private static Logger log = LoggerFactory.getLogger(DefaultItemCollection.class);
    private static final String TMP_PREFIX = "_tmp_";

    /**
     * Create a new <code>DefaultItemCollection</code>.
     *
     * @param locator
     * @param session
     * @param factory
     * @param item
     */
    protected DefaultItemCollection(DavResourceLocator locator,
                                    JcrDavSession session,
                                    DavResourceFactory factory, Item item) {
        super(locator, session, factory, item);
        if (exists() && !(item instanceof Node)) {
            throw new IllegalArgumentException("A collection resource can not be constructed from a Property item.");
        }
    }

    //----------------------------------------------< DavResource interface >---
    /**
     * @see org.apache.jackrabbit.webdav.DavResource#getComplianceClass()
     */
    @Override
    public String getComplianceClass() {
        String cc = super.getComplianceClass();
        if (isOrderable()) {
            return DavCompliance.concatComplianceClasses(
                new String[] {
                    cc,
                    DavCompliance.ORDERED_COLLECTIONS,
                }
            );
        } else {
            return cc;
        }
    }

    public long getModificationTime() {
        // retrieve mod-time from jcr:lastmodified property if existing
        if (exists()) {
            try {
                if (((Node)item).hasProperty(JcrConstants.JCR_LASTMODIFIED)) {
                    return ((Node)item).getProperty(JcrConstants.JCR_LASTMODIFIED).getLong();
                }
            } catch (RepositoryException e) {
                log.warn("Error while accessing jcr:lastModified property");
            }
        }
        // fallback: return 'now'
        return new Date().getTime();
    }

    /**
     * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods()
     */
    @Override
    public String getSupportedMethods() {
        String ms = super.getSupportedMethods();
        if (isOrderable()) {
            StringBuffer sb = new StringBuffer(ms);
            sb.append(", ").append(OrderingResource.METHODS);
            return sb.toString();
        } else {
            return ms;
        }
    }

    /**
     * Always returns true
     *
     * @return true
     * @see org.apache.jackrabbit.webdav.DavResource#isCollection()
     */
    public boolean isCollection() {
        return true;
    }

    /**
     * If this resource represents an existing <code>Node</code> the system
     * view is spooled as resource content.
     *
     * @param outputContext
     * @throws IOException
     * @see Session#exportSystemView(String, OutputStream, boolean, boolean)
     */
    @Override
    public void spool(OutputContext outputContext) throws IOException {
        // spool properties
        super.spool(outputContext);
        // spool data
        try {
            OutputStream out = outputContext.getOutputStream();
            if (out != null && exists()) {
                getRepositorySession().exportSystemView(item.getPath(), out, false, true);
            }
        } catch (PathNotFoundException e) {
            log.error("Error while spooling resource content: " + e.getMessage());
        } catch (RepositoryException e) {
            log.error("Error while spooling resource content: " + e.getMessage());
        }
    }

    @Override
    public DavProperty<?> getProperty(DavPropertyName name) {
        DavProperty prop = super.getProperty(name);

        if (prop == null && exists()) {
            Node n = (Node) item;

            // add node-specific resource properties
            try {
                if (JCR_INDEX.equals(name)) {
                    prop = new DefaultDavProperty<Integer>(JCR_INDEX, n.getIndex(), true);
                } else if (JCR_REFERENCES.equals(name)) {
                    prop = getHrefProperty(JCR_REFERENCES, n.getReferences(), true);
                } else if (JCR_WEAK_REFERENCES.equals(name)) {
                    prop = getHrefProperty(JCR_WEAK_REFERENCES, n.getWeakReferences(), true);
                } else if (JCR_UUID.equals(name)) {
                    if (isReferenceable()) {
                        prop = new DefaultDavProperty<String>(JCR_UUID, n.getUUID(), true);
                    }
                } else if (JCR_PRIMARYITEM.equals(name)) {
                    if (hasPrimaryItem()) {
                        Item primaryItem = n.getPrimaryItem();
                        prop = getHrefProperty(JCR_PRIMARYITEM, new Item[] {primaryItem}, true);
                    }
                } else if (OrderingConstants.ORDERING_TYPE.equals(name) && isOrderable()) {
                    // property defined by RFC 3648: this resource always has custom ordering!                    
                    prop = new OrderingType(OrderingConstants.ORDERING_TYPE_CUSTOM);
                }
            } catch (RepositoryException e) {
                log.error("Failed to retrieve node-specific property: " + e);
            }          
        }

        return prop;
    }

    /**
     * This implementation of the <code>DavResource</code> does only allow
     * to set the mixinnodetypes property. Please note that the existing list of
     * mixin nodetypes will be completely replaced.<br>
     * In order to add / set any other repository property on the underlying
     * {@link javax.jcr.Node} use <code>addMember(DavResource)</code> or
     * <code>addMember(DavResource, InputStream)</code> or modify the value
     * of the corresponding resource.
     *
     * @param property
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.DavResource#setProperty(org.apache.jackrabbit.webdav.property.DavProperty)
     * @see #JCR_MIXINNODETYPES
     */
    @Override
    public void setProperty(DavProperty<?> property) throws DavException {
        internalSetProperty(property);
        complete();
    }

    /**
     * Internal method used to set or add the given property
     *
     * @param property
     * @throws DavException
     * @see #setProperty(DavProperty)
     */
    private void internalSetProperty(DavProperty<?> property) throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }
        DavPropertyName propName = property.getName();
        if (JCR_MIXINNODETYPES.equals(propName)) {
            Node n = (Node) item;
            try {
                NodeTypeProperty mix = new NodeTypeProperty(property);
                Set<String> mixins = mix.getNodeTypeNames();

                for (NodeType existingMixin : n.getMixinNodeTypes()) {
                    String name = existingMixin.getName();
                    if (mixins.contains(name)){
                        // do not add existing mixins
                        mixins.remove(name);
                    } else {
                        // remove mixin that are not contained in the new list
                        n.removeMixin(name);
                    }
                }

                // add the remaining mixing types that are not yet set
                for (String mixin : mixins) {
                    n.addMixin(mixin);
                }
            } catch (RepositoryException e) {
                throw new JcrDavException(e);
            }
        } else if (JCR_PRIMARYNODETYPE.equals(propName)) {
            Node n = (Node) item;
            try {
                NodeTypeProperty ntProp = new NodeTypeProperty(property);
                Set<String> names = ntProp.getNodeTypeNames();
                if (names.size() == 1) {
                    String ntName = names.iterator().next();
                    n.setPrimaryType(ntName);
                } else {
                    // only a single node type can be primary node type.
                    throw new DavException(DavServletResponse.SC_BAD_REQUEST);
                }
            } catch (RepositoryException e) {
                throw new JcrDavException(e);
            }
        } else {
            // all props except for mixin node types and primaryType are read-only
            throw new DavException(DavServletResponse.SC_CONFLICT);
        }
    }

    /**
     * This implementation of the <code>DavResource</code> does only allow
     * to remove the mixinnodetypes property.
     *
     * @param propertyName
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.DavResource#removeProperty(org.apache.jackrabbit.webdav.property.DavPropertyName)
     * @see #JCR_MIXINNODETYPES
     */
    @Override
    public void removeProperty(DavPropertyName propertyName) throws DavException {
        internalRemoveProperty(propertyName);
        complete();
    }

    /**
     * Internal method used to remove the property with the given name.
     *
     * @param propertyName
     * @throws DavException
     * @see #removeProperty(DavPropertyName)
     */
    private void internalRemoveProperty(DavPropertyName propertyName) throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }
        if (JCR_MIXINNODETYPES.equals(propertyName)) {
            // remove all mixin nodetypes
            try {
                Node n = (Node)item;
                for (NodeType mixin : n.getMixinNodeTypes()) {
                    n.removeMixin(mixin.getName());
                }
            } catch (RepositoryException e) {
                // NoSuchNodeTypeException, ConstraintViolationException should never occur...
                throw new JcrDavException(e);
            }
        } else {
            // all props except for mixin node types are read-only
            throw new DavException(DavServletResponse.SC_CONFLICT);
        }
    }

    /**
     * Loops over the given <code>List</code>s and alters the properties accordingly.
     * Changes are persisted at the end according to the rules defined with
     * the {@link AbstractItemResource#complete()} method.<p>
     * Please note: since there is only a single property
     * ({@link ItemResourceConstants#JCR_MIXINNODETYPES}
     * that can be set or removed with PROPPATCH, this method either succeeds
     * or throws an exception, even if this violates RFC 2518. Thus no property
     * specific multistatus will be created in case of an error.
     *
     * @param changeList
     * @return
     * @throws DavException
     */
    @Override
    public MultiStatusResponse alterProperties(List<? extends PropEntry> changeList) throws DavException {
        for (PropEntry propEntry : changeList) {
            if (propEntry instanceof DavPropertyName) {
                // use the internal remove method in order to prevent premature 'save'
                DavPropertyName propName = (DavPropertyName) propEntry;
                internalRemoveProperty(propName);
            } else if (propEntry instanceof DavProperty) {
                // use the internal set method in order to prevent premature 'save'
                DavProperty<?> prop = (DavProperty<?>) propEntry;
                internalSetProperty(prop);
            } else {
                throw new IllegalArgumentException("unknown object in change list: " + propEntry.getClass().getName());
            }
        }
        // TODO: missing undo of successful set/remove if subsequent operation fails
        // NOTE, that this is relevant with transactions only.

        // success: save all changes together if no error occurred
        complete();
        return new MultiStatusResponse(getHref(), DavServletResponse.SC_OK);
    }

    /**
     * If the specified resource represents a collection, a new node is {@link Node#addNode(String)
     * added} to the item represented by this resource. If an input stream is specified
     * together with a collection resource {@link Session#importXML(String, java.io.InputStream, int)}
     * is called instead and this resource path is used as <code>parentAbsPath</code> argument.
     * <p/>
     * However, if the specified resource is not of resource type collection a
     * new {@link Property} is set or an existing one is changed by modifying its
     * value.<br>
     * NOTE: with the current implementation it is not possible to create or
     * modify multivalue JCR properties.<br>
     * NOTE: if the JCR property represented by the specified resource has an
     * {@link PropertyType#UNDEFINED undefined} resource type, its value will be
     * changed/set to type {@link PropertyType#BINARY binary}.
     *
     * @param resource
     * @param inputContext
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.DavResource#addMember(org.apache.jackrabbit.webdav.DavResource, InputContext)
     * @see Node#addNode(String)
     * @see Node#setProperty(String, java.io.InputStream)
     */
    public void addMember(DavResource resource, InputContext inputContext)
            throws DavException {

        /* RFC 2815 states that all 'parents' must exist in order all addition of members */
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_CONFLICT);
        }

        File tmpFile = null;
        try {
            Node n = (Node) item;
            InputStream in = (inputContext != null) ? inputContext.getInputStream() : null;
            String itemPath = getLocator().getRepositoryPath();
            String memberName = getItemName(resource.getLocator().getRepositoryPath());
            if (resource.isCollection()) {
                if (in == null) {
                    // MKCOL without a request body, try if a default-primary-type is defined.
                    n.addNode(memberName);
                } else {
                    // MKCOL, which is not allowed for existing resources
                    int uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW;
                    String str = inputContext.getProperty(IMPORT_UUID_BEHAVIOR);
                    if (str != null) {
                        try {
                            uuidBehavior = Integer.parseInt(str);
                        } catch (NumberFormatException e) {
                            throw new DavException(DavServletResponse.SC_BAD_REQUEST);
                        }
                    }
                    if (getTransactionId() == null) {
                        // if not part of a transaction directly import on workspace
                        // since changes would be explicitly saved in the
                        // complete-call.
                        getRepositorySession().getWorkspace().importXML(itemPath, in, uuidBehavior);
                    } else {
                        // changes will not be persisted unless the tx is completed.
                        getRepositorySession().importXML(itemPath, in, uuidBehavior);
                    }
                }
            } else {
                if (in == null) {
                    // PUT: not possible without request body
                    throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Cannot create a new non-collection resource without request body.");
                }
                // PUT : create new or overwrite existing property.
                String ct = inputContext.getContentType();
                int type = JcrValueType.typeFromContentType(ct);
                if (type != PropertyType.UNDEFINED) {
                    // no need to create value/values property. instead
                    // prop-value can be retrieved directly:
                    int pos = ct.indexOf(';');
                    String charSet = (pos > -1) ? ct.substring(pos) : "UTF-8";
                    if (type == PropertyType.BINARY) {
                        n.setProperty(memberName, inputContext.getInputStream());
                    } else {
                        BufferedReader r = new BufferedReader(new InputStreamReader(inputContext.getInputStream(), charSet));
                        String line;
                        StringBuffer value = new StringBuffer();
                        while ((line = r.readLine()) != null) {
                            value.append(line);
                        }
                        n.setProperty(memberName, value.toString(), type);
                    }
                } else {
                    // try to parse the request body into a 'values' property.
                    tmpFile = File.createTempFile(TMP_PREFIX + Text.escape(memberName), null, null);
                    FileOutputStream out = new FileOutputStream(tmpFile);
                    IOUtil.spool(in, out);
                    out.close();
                    // try to parse the request body into a 'values' property.
                    ValuesProperty vp = buildValuesProperty(new FileInputStream(tmpFile));
                    if (vp != null) {
                        if (JCR_VALUE.equals(vp.getName())) {
                            n.setProperty(memberName, vp.getJcrValue());
                        } else {
                            n.setProperty(memberName, vp.getJcrValues());
                        }
                    } else {
                        // request body cannot be parsed into a 'values' property.
                        // fallback: try to import as single value from stream.
                        n.setProperty(memberName, new FileInputStream(tmpFile));
                    }
                }
            }
            if (resource.exists() && resource instanceof AbstractItemResource) {
                // PUT may modify value of existing jcr property. thus, this
                // node is not modified by the 'addMember' call.
                ((AbstractItemResource)resource).complete();
            } else {
                complete();
            }
        } catch (ItemExistsException e) {
            // according to RFC 2518: MKCOL only possible on non-existing/deleted resource
            throw new JcrDavException(e, DavServletResponse.SC_METHOD_NOT_ALLOWED);
        } catch (RepositoryException e) {
            throw new JcrDavException(e);
        } catch (IOException e) {
            throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, e.getMessage());
        } finally {
            if (tmpFile != null) {
                tmpFile.delete();
            }
        }
    }

    /**
     * @see org.apache.jackrabbit.webdav.DavResource#getMembers()
     */
    public DavResourceIterator getMembers() {
        ArrayList<DavResource> memberList = new ArrayList<DavResource>();
        if (exists()) {
            try {
                Node n = (Node)item;
                // add all node members
                NodeIterator it = n.getNodes();
                while (it.hasNext()) {
                    Node node = it.nextNode();
                    DavResourceLocator loc = getLocatorFromItem(node);
                    memberList.add(createResourceFromLocator(loc));
                }
                // add all property members
                PropertyIterator propIt = n.getProperties();
                while (propIt.hasNext()) {
                    Property prop = propIt.nextProperty();
                    DavResourceLocator loc = getLocatorFromItem(prop);
                    memberList.add(createResourceFromLocator(loc));
                }
            } catch (RepositoryException e) {
                // ignore
                log.error(e.getMessage());
            } catch (DavException e) {
                // should never occur.
                log.error(e.getMessage());
            }
        }
        return new DavResourceIteratorImpl(memberList);
    }

    /**
     * Removes the repository item represented by the specified member
     * resource.
     *
     * @throws DavException if this resource does not exist or if an error occurs
     * while deleting the underlying item.
     * @see DavResource#removeMember(DavResource)
     * @see javax.jcr.Item#remove()
     */
    public void removeMember(DavResource member) throws DavException {
        Session session = getRepositorySession();
        try {
            String itemPath = member.getLocator().getRepositoryPath();
            if (!exists() || !session.itemExists(itemPath)) {
                throw new DavException(DavServletResponse.SC_NOT_FOUND);
            }
            if (!getResourcePath().equals(Text.getRelativeParent(member.getResourcePath(), 1))) {
                throw new DavException(DavServletResponse.SC_CONFLICT, member.getResourcePath() + "is not member of this resource (" + getResourcePath() + ")");
            }
            getRepositorySession().getItem(itemPath).remove();
            complete();
        } catch (RepositoryException e) {
            log.error("Unexpected error: " + e.getMessage());
            throw new JcrDavException(e);
        }
    }

    /**
     * @param type
     * @param scope
     * @return true if a lock with the specified type and scope is present on
     * this resource, false otherwise. If retrieving the corresponding information
     * fails, false is returned.
     * @see org.apache.jackrabbit.webdav.DavResource#hasLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope)
     */
    @Override
    public boolean hasLock(Type type, Scope scope) {
        if (isLockable(type, scope)) {
            if (Type.WRITE.equals(type)) {
                try {
                    return ((Node) item).isLocked();
                } catch (RepositoryException e) {
                    log.error(e.getMessage());
                }
            } else {
                return super.hasLock(type, scope);
            }
        }
        return false;
    }

    /**
     * Retrieve the lock with the specified type and scope.
     *
     * @param type
     * @param scope
     * @return lock with the specified type and scope is present on this
     * resource or <code>null</code>. NOTE: If retrieving the write lock present
     * on the underlying repository item fails, <code>null</code> is return.
     * @see org.apache.jackrabbit.webdav.DavResource#getLock(org.apache.jackrabbit.webdav.lock.Type, org.apache.jackrabbit.webdav.lock.Scope)
     * @see javax.jcr.Node#getLock() for the write locks.
     */
    @Override
    public ActiveLock getLock(Type type, Scope scope) {
        ActiveLock lock = null;
        if (Type.WRITE.equals(type)) {
            try {
                if (!exists()) {
                    log.warn("Unable to retrieve lock: no item found at '" + getResourcePath() + "'");
                } else if (((Node) item).isLocked()) {
                    Lock jcrLock = ((Node) item).getLock();
                    lock = new JcrActiveLock(jcrLock);
                    DavResourceLocator locator = super.getLocator();
                    String lockroot = locator
                            .getFactory()
                            .createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), jcrLock.getNode().getPath(),
                                    false).getHref(false);
                    lock.setLockroot(lockroot);
                }
            } catch (AccessDeniedException e) {
                log.error("Error while accessing resource lock: "+e.getMessage());
            } catch (UnsupportedRepositoryOperationException e) {
                log.error("Error while accessing resource lock: "+e.getMessage());
            } catch (RepositoryException e) {
                log.error("Error while accessing resource lock: "+e.getMessage());
            }
        } else {
            lock = super.getLock(type, scope);
        }
        return lock;
    }

    /**
     * Creates a lock on this resource by locking the underlying
     * {@link javax.jcr.Node node}. Except for the {@link org.apache.jackrabbit.webdav.lock.LockInfo#isDeep()} }
     * all information included in the <code>LockInfo</code> object is ignored.
     * Lock timeout is defined by JCR implementation.
     *
     * @param reqLockInfo
     * @return lock object representing the lock created on this resource.
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.DavResource#lock(org.apache.jackrabbit.webdav.lock.LockInfo)
     * @see Node#lock(boolean, boolean)
     */
    @Override
    public ActiveLock lock(LockInfo reqLockInfo) throws DavException {

        if (!isLockable(reqLockInfo.getType(), reqLockInfo.getScope())) {
            throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED);
        }

        if (Type.WRITE.equals(reqLockInfo.getType())) {
            if (!exists()) {
                log.warn("Cannot create a write lock for non-existing JCR node (" + getResourcePath() + ")");
                throw new DavException(DavServletResponse.SC_NOT_FOUND);
            }
            try {
                boolean sessionScoped = EXCLUSIVE_SESSION.equals(reqLockInfo.getScope());
                long timeout = reqLockInfo.getTimeout();
                if (timeout == LockInfo.INFINITE_TIMEOUT) {
                    timeout = Long.MAX_VALUE;
                } else {
                    timeout = timeout/1000;
                }
                javax.jcr.lock.LockManager lockMgr = getRepositorySession().getWorkspace().getLockManager();
                Lock jcrLock = lockMgr.lock((item).getPath(), reqLockInfo.isDeep(),
                        sessionScoped, timeout, reqLockInfo.getOwner());
                ActiveLock lock = new JcrActiveLock(jcrLock);
                 // add reference to DAVSession for this lock
                getSession().addReference(lock.getToken());
                return lock;
            } catch (RepositoryException e) {
                // UnsupportedRepositoryOperationException should not occur...
                throw new JcrDavException(e);
            }
        } else {
            return super.lock(reqLockInfo);
        }
    }

    /**
     * Refreshes the lock on this resource. With this implementation the
     * {@link javax.jcr.lock lock} present on the underlying {@link javax.jcr.Node node}
     * is refreshed. The timeout indicated by the <code>LockInfo</code>
     * object is ignored.
     *
     * @param reqLockInfo LockInfo as build from the request.
     * @param lockToken
     * @return the updated lock info object.
     * @throws org.apache.jackrabbit.webdav.DavException in case the lock could not be refreshed.
     * @see org.apache.jackrabbit.webdav.DavResource#refreshLock(org.apache.jackrabbit.webdav.lock.LockInfo, String)
     * @see javax.jcr.lock.Lock#refresh()
     */
    @Override
    public ActiveLock refreshLock(LockInfo reqLockInfo, String lockToken)
            throws DavException {

        if (lockToken == null) {
            throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED);
        }

        ActiveLock lock = getLock(reqLockInfo.getType(), reqLockInfo.getScope());
        if (lock == null) {
            throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "No lock with the given scope/type present on this resource.");
        }

        if (Type.WRITE.equals(lock.getType())) {
            try {
                Lock jcrLock = ((Node) item).getLock();
                jcrLock.refresh();
                return new JcrActiveLock(jcrLock);
            } catch (RepositoryException e) {
                /*
                  NOTE: LockException is only thrown by Lock.refresh()
                        the lock exception thrown by Node.getLock() was circumvented
                        by the init test if there is a lock applied...
                  NOTE: UnsupportedRepositoryOperationException should not occur
                */
                throw new JcrDavException(e);
            }
        } else {
            return super.refreshLock(reqLockInfo, lockToken);
        }
    }

    /**
     * Remove the write lock from this resource by unlocking the underlying
     * {@link javax.jcr.Node node}.
     *
     * @param lockToken
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.DavResource#unlock(String)
     * @see javax.jcr.Node#unlock()
     */
    @Override
    public void unlock(String lockToken) throws DavException {
        ActiveLock lock = getWriteLock();
        if (lock != null && lockToken.equals(lock.getToken())) {
            try {
                ((Node) item).unlock();
                getSession().removeReference(lock.getToken());                
            } catch (RepositoryException e) {
                throw new JcrDavException(e);
            }
        } else {
            super.unlock(lockToken);
        }
    }

    /**
     * Returns the write lock present on this resource or <code>null</code> if
     * no write lock exists. NOTE: that the scope of a write lock may either
     * be {@link org.apache.jackrabbit.webdav.lock.Scope#EXCLUSIVE} or
     * {@link ItemResourceConstants#EXCLUSIVE_SESSION}.
     *
     * @return write lock or <code>null</code>
     * @throws DavException if this resource does not represent a repository item.
     */
    private ActiveLock getWriteLock() throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND, "Unable to retrieve write lock for non existing repository item (" + getResourcePath() + ")");
        }
        ActiveLock writeLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
        if (writeLock == null) {
            writeLock = getLock(Type.WRITE, EXCLUSIVE_SESSION);
        }
        return writeLock;
    }

    //-----------------------------------------< OrderingResource interface >---
    /**
     * Returns true if this resource exists and the nodetype defining the
     * underlying repository node allow to reorder this nodes children.
     *
     * @return true if {@link DefaultItemCollection#orderMembers(OrderPatch)}
     * can be called on this resource.
     * @see org.apache.jackrabbit.webdav.ordering.OrderingResource#isOrderable()
     * @see javax.jcr.nodetype.NodeType#hasOrderableChildNodes()
     */
    public boolean isOrderable() {
        boolean orderable = false;
        if (exists()) {
            try {
                orderable = ((Node) item).getPrimaryNodeType().hasOrderableChildNodes();
            } catch (RepositoryException e) {
                log.warn(e.getMessage());
            }
        }
        return orderable;
    }

    /**
     * Reorder the child nodes of the repository item represented by this
     * resource as indicated by the specified {@link OrderPatch} object.
     *
     * @param orderPatch
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.ordering.OrderingResource#orderMembers(org.apache.jackrabbit.webdav.ordering.OrderPatch)
     * @see Node#orderBefore(String, String)
     */
    public void orderMembers(OrderPatch orderPatch) throws DavException {
        if (!isOrderable()) {
            throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED);
        }
        // only custom ordering is allowed
        if (!OrderingConstants.ORDERING_TYPE_CUSTOM.equalsIgnoreCase(orderPatch.getOrderingType())) {
            throw new DavException(DavServletResponse.SC_UNPROCESSABLE_ENTITY, "Only DAV:custom ordering type supported.");
        }

        Node n = (Node)item;
        try {
            for (OrderPatch.Member instruction : orderPatch.getOrderInstructions()) {
                String srcRelPath = Text.unescape(instruction.getMemberHandle());
                Position pos = instruction.getPosition();
                String destRelPath = getRelDestinationPath(pos, n.getNodes());
                // preform the reordering
                n.orderBefore(srcRelPath, destRelPath);
            }
            complete();
        } catch (RepositoryException e) {
            // UnsupportedRepositoryException should not occur
            throw new JcrDavException(e);
        }
    }

    /**
     * Retrieve the relative path of the child node that acts as destination.
     * A <code>null</code> destination path is used to place the child node indicated
     * by the source path at the end of the list.
     *
     * @param position
     * @param childNodes
     * @return the relative path of the child node used as destination or <code>null</code>
     * if the source node should be placed at the last position.
     * @throws javax.jcr.RepositoryException
     */
    private String getRelDestinationPath(Position position, NodeIterator childNodes)
            throws RepositoryException {

        String destRelPath = null;
        if (OrderingConstants.XML_FIRST.equals(position.getType())) {
            if (childNodes.hasNext()) {
                Node firstChild = childNodes.nextNode();
                // use last segment of node-path instead of name.
                destRelPath = Text.getName(firstChild.getPath());
            }
            // no child nodes available > reordering to 'first' position fails.
            if (destRelPath == null) {
                throw new ItemNotFoundException("No 'first' item found for reordering.");
            }
        } else if (OrderingConstants.XML_AFTER.equals(position.getType())) {
            String afterRelPath = position.getSegment();
            boolean found = false;
            // jcr only knows order-before > retrieve the node that follows the
            // one indicated by the 'afterRelPath'.
            while (childNodes.hasNext() && destRelPath == null) {
                // compare to last segment of node-path instead of name.
                String childRelPath = Text.getName(childNodes.nextNode().getPath());
                if (found) {
                    destRelPath = childRelPath;
                } else {
                    found = afterRelPath.equals(childRelPath);
                }
            }
        } else {
            // before or last. in the latter case the segment is 'null'
            destRelPath = position.getSegment();
        }
        if (destRelPath != null) {
            destRelPath = Text.unescape(destRelPath);
        }
        return destRelPath;
    }

    //--------------------------------------------------------------------------
    /**
     * Extend the general {@link AbstractResource#supportedLock} field by
     * lock entries specific for this resource: write locks (exclusive or
     * exclusive session-scoped) in case the underlying node has the node
     * type mix:lockable.
     *
     * @see org.apache.jackrabbit.JcrConstants#MIX_LOCKABLE
     */
    @Override
    protected void initLockSupport() {
        super.initLockSupport();
        // add exclusive write lock if allowed for the given node
        try {
            if (exists() && ((Node)item).isNodeType(JcrConstants.MIX_LOCKABLE)) {
                supportedLock.addEntry(Type.WRITE, Scope.EXCLUSIVE);
                supportedLock.addEntry(new SessionScopedLockEntry());
            }
        } catch (RepositoryException e) {
            log.warn(e.getMessage());
        }
    }

    /**
     * Defines the additional reports supported by this resource (reports
     * specific for resources representing a repository {@link Node node}):
     * <ul>
     * <li>{@link ExportViewReport export view report}</li>
     * <li>{@link LocateCorrespondingNodeReport locate corresponding node report}</li>
     * </ul>
     *
     * @see org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty
     */
    @Override
    protected void initSupportedReports() {
        super.initSupportedReports();
        if (exists()) {
            supportedReports.addReportType(ExportViewReport.EXPORTVIEW_REPORT);
            supportedReports.addReportType(LocateCorrespondingNodeReport.LOCATE_CORRESPONDING_NODE_REPORT);
        }
    }

    @Override
    protected void initPropertyNames() {
        super.initPropertyNames();

        if (exists()) {
            names.addAll(JcrDavPropertyNameSet.NODE_SET);
            
            if (isReferenceable()) {
                names.add(JCR_UUID);
            }
            if (hasPrimaryItem()) {
                names.add(JCR_PRIMARYITEM);
            }
            if (isOrderable()) {
                names.add(OrderingConstants.ORDERING_TYPE);
            }
        }
    }
                                              
    /**
     * Fill the property set for this resource.
     */
    @Override
    protected void initProperties() {
        super.initProperties();
        if (exists()) {
            // resource is serialized as system-view (xml)
            properties.add(new DefaultDavProperty<String>(DavPropertyName.GETCONTENTTYPE, "text/xml"));
            Node n = (Node)item;

            // add node-specific resource properties
            try {
                properties.add(new NodeTypeProperty(JCR_PRIMARYNODETYPE, n.getPrimaryNodeType(), false));
                properties.add(new NodeTypeProperty(JCR_MIXINNODETYPES, n.getMixinNodeTypes(), false));
            } catch (RepositoryException e) {
                log.error("Failed to retrieve node-specific property: " + e);
            }
        }
    }  

    @Override
    protected String getCreatorDisplayName() {
        // overwrite the default creation date and creator-displayname if possible
        try {
            // DAV:creator-displayname -> use jcr:createBy if present.
            if (exists() && ((Node) item).hasProperty(Property.JCR_CREATED_BY)) {
                return ((Node) item).getProperty(Property.JCR_CREATED_BY).getString();
            }
        } catch (RepositoryException e) {
            log.warn("Error while accessing jcr:createdBy property");
        }

        // fallback
        return super.getCreatorDisplayName();
    }

    @Override
    protected String getCreationDate() {
        // overwrite the default creation date and creator-displayname if possible
        try {
            if (exists() && ((Node) item).hasProperty(JcrConstants.JCR_CREATED)) {
                long creationTime = ((Node) item).getProperty(JcrConstants.JCR_CREATED).getValue().getLong();
                return HttpDateFormat.creationDateFormat().format(new Date(creationTime));
            }
        } catch (RepositoryException e) {
            log.warn("Error while accessing jcr:created property");
        }

        // fallback
        return super.getCreationDate();
    }

    /**
     * Creates a new HrefProperty with the specified name using the given
     * array of items as value.
     * 
     * @param name
     * @param values
     * @param isProtected
     * @return
     */
    protected HrefProperty getHrefProperty(DavPropertyName name, Item[] values, boolean isProtected) {
        String[] pHref = new String[values.length];
        for (int i = 0; i < values.length; i++) {
            pHref[i] = getLocatorFromItem(values[i]).getHref(true);
        }
        return new HrefProperty(name, pHref, isProtected);
    }

    /**
     * Add a {@link org.apache.jackrabbit.webdav.property.HrefProperty} with the
     * specified property name and values. Each item present in the specified
     * values array is referenced in the resulting property.
     *
     * @param name
     * @param values
     * @param isProtected
     */
    protected void addHrefProperty(DavPropertyName name, Item[] values, boolean isProtected) {
        properties.add(getHrefProperty(name, values, isProtected));
    }

    /**
     * Creates a new {@link HrefProperty href property} to the property set, where
     * all properties present in the specified iterator are referenced in the
     * resulting property.
     * 
     * @param name
     * @param itemIterator
     * @param isProtected
     * @return
     */
    protected HrefProperty getHrefProperty(DavPropertyName name, PropertyIterator itemIterator,
                                           boolean isProtected) {
        ArrayList<Property> l = new ArrayList<Property>();
        while (itemIterator.hasNext()) {
            l.add(itemIterator.nextProperty());
        }
        return getHrefProperty(name, l.toArray(new Property[l.size()]), isProtected);
    }

    /**
     * Add a new {@link HrefProperty href property} to the property set, where
     * all properties present in the specified iterator are referenced in the
     * resulting property.
     *
     * @param name
     * @param itemIterator
     * @param isProtected
     * @see #addHrefProperty(DavPropertyName, Item[], boolean)
     */
    protected void addHrefProperty(DavPropertyName name, PropertyIterator itemIterator,
                                   boolean isProtected) {
        properties.add(getHrefProperty(name, itemIterator, isProtected));
    }

    /**
     * Add a new {@link HrefProperty href property} to the property set, where
     * all versions present in the specified iterator are referenced in the
     * resulting property.
     *
     * @param name
     * @param itemIterator
     * @param isProtected
     */
    protected HrefProperty getHrefProperty(DavPropertyName name, VersionIterator itemIterator,
                                   boolean isProtected) {
        ArrayList<Version> l = new ArrayList<Version>();
        while (itemIterator.hasNext()) {
            l.add(itemIterator.nextVersion());
        }
        return getHrefProperty(name, l.toArray(new Version[l.size()]), isProtected);
    }

    /**
     * Add a new {@link HrefProperty href property} to the property set, where
     * all versions present in the specified iterator are referenced in the
     * resulting property.
     *
     * @param name
     * @param itemIterator
     * @param isProtected
     */
    protected void addHrefProperty(DavPropertyName name, VersionIterator itemIterator,
                                   boolean isProtected) {
        properties.add(getHrefProperty(name, itemIterator, isProtected));
    }

    /**
     * Tries to parse the given input stream as xml document and build a
     * {@link ValuesProperty} out of it.
     *
     * @param in
     * @return values property or 'null' if the given stream cannot be parsed
     * into an XML document or if build the property fails.
     */
    private ValuesProperty buildValuesProperty(InputStream in) {
        String errorMsg = "Cannot parse stream into a 'ValuesProperty'.";
        try {
            Document reqBody = DomUtil.parseDocument(in);
            DavProperty<?> defaultProp = DefaultDavProperty.createFromXml(reqBody.getDocumentElement());
            ValuesProperty vp = new ValuesProperty(defaultProp, PropertyType.STRING, getRepositorySession().getValueFactory());
            return vp;
        } catch (IOException e) {
            log.debug(errorMsg, e);
        } catch (ParserConfigurationException e) {
            log.debug(errorMsg, e);
        } catch (SAXException e) {
            log.debug(errorMsg, e);
        } catch (DavException e) {
            log.debug(errorMsg, e);
        } catch (RepositoryException e) {
            log.debug(errorMsg, e);
        }
        // cannot parse request body into a 'values' property
        return null;
    }

    private boolean hasPrimaryItem() {
        try {
            return exists() && ((Node) item).getPrimaryNodeType().getPrimaryItemName() != null;
        } catch (RepositoryException e) {
            log.warn(e.getMessage());
        }
        return false;
    }
    
    private boolean isReferenceable() {
        try {
            return exists() && ((Node) item).isNodeType(JcrConstants.MIX_REFERENCEABLE);
        } catch (RepositoryException e) {
            log.warn(e.getMessage());
        }
        return false;
    }
}
