/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  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.simple;

import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.version.Version;
import javax.jcr.version.VersionHistory;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.MultiStatus;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.jcr.JcrDavException;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.HrefProperty;
import org.apache.jackrabbit.webdav.version.LabelInfo;
import org.apache.jackrabbit.webdav.version.MergeInfo;
import org.apache.jackrabbit.webdav.version.UpdateInfo;
import org.apache.jackrabbit.webdav.version.VersionControlledResource;
import org.apache.jackrabbit.webdav.version.VersionHistoryResource;
import org.apache.jackrabbit.webdav.version.VersionResource;
import org.apache.jackrabbit.webdav.version.VersionableResource;
import org.apache.jackrabbit.webdav.version.report.ReportType;
import org.apache.jackrabbit.webdav.version.report.SupportedReportSetProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



/**
 * <code>VersionControlledResourceImpl</code> represents a JCR node item and
 * covers all functionality related to versioning of {@link Node}s.
 *
 * @see Node
 */
public class VersionControlledResourceImpl extends DeltaVResourceImpl
        implements VersionControlledResource {

    private static final Logger log = LoggerFactory.getLogger(VersionControlledResourceImpl.class);

    /**
     * Create a new {@link org.apache.jackrabbit.webdav.DavResource}.
     *
     * @param locator
     * @param factory
     * @param session
     * @param config
     * @param item
     * @throws DavException
     */
    public VersionControlledResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, Item item) throws DavException {
        super(locator, factory, session, config, item);
        initSupportedReports();
    }

    /**
     * Create a new {@link org.apache.jackrabbit.webdav.DavResource}.
     *
     * @param locator
     * @param factory
     * @param session
     * @param config
     * @param isCollection
     * @throws DavException
     */
    public VersionControlledResourceImpl(DavResourceLocator locator, DavResourceFactory factory, DavSession session, ResourceConfig config, boolean isCollection) throws DavException {
        super(locator, factory, session, config, isCollection);
        initSupportedReports();
    }

    //--------------------------------------------------------< DavResource >---
    /**
     * Return a comma separated string listing the supported method names.
     *
     * @return the supported method names.
     * @see org.apache.jackrabbit.webdav.DavResource#getSupportedMethods()
     */
    @Override
    public String getSupportedMethods() {
        StringBuffer sb = new StringBuffer(super.getSupportedMethods());
        // Versioning support
        sb.append(", ").append(VersionableResource.METHODS);
        if (isVersionControlled()) {
            try {
                if (getNode().isCheckedOut()) {
                    sb.append(", ").append(DavMethods.METHOD_CHECKIN);
                } else {
                    sb.append(", ").append(DavMethods.METHOD_CHECKOUT);
                    sb.append(", ").append(DavMethods.METHOD_LABEL);
                }
            } catch (RepositoryException e) {
                // should not occur.
                log.error(e.getMessage());
            }
        }
        return sb.toString();
    }

    //------------------------------------------< VersionControlledResource >---
    /**
     * Adds version control to this resource. If the resource is already under
     * version control, this method has no effect. If this resource is a Collection
     * resource this method fails with {@link DavServletResponse#SC_METHOD_NOT_ALLOWED}.
     *
     * @throws org.apache.jackrabbit.webdav.DavException if this resource does not
     * exist yet, is a collection or if an error occurs while making the
     * underlying node versionable.
     * @see org.apache.jackrabbit.webdav.version.VersionableResource#addVersionControl()
     */
    public void addVersionControl() throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }
        if (isCollection()) {
            // since the version-controlled-collection feature is not supported
            // collections may not be put under dav version control even if
            // the underlying node was / could be made jcr versionable.
            throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED);
        }
        if (!isVersionControlled()) {
            Node item = getNode();
            try {
                item.addMixin(JcrConstants.MIX_VERSIONABLE);
                item.save();
            } catch (RepositoryException e) {
                throw new JcrDavException(e);
            }
        } // else: is already version controlled -> ignore
    }

    /**
     * Calls {@link javax.jcr.Node#checkin()} on the underlying repository node.
     *
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkin()
     */
    public String checkin() throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }
        if (!isVersionControlled()) {
            throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED);
        }
        try {
            Version v = getNode().checkin();
            String versionHref = getLocatorFromNode(v).getHref(false);
            return versionHref;
        } catch (RepositoryException e) {
            // UnsupportedRepositoryException should not occur
            throw new JcrDavException(e);
        }
    }

    /**
     * Calls {@link javax.jcr.Node#checkout()} on the underlying repository node.
     *
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#checkout()
     */
    public void checkout() throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }
        if (!isVersionControlled()) {
            throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED);
        }
        try {
            getNode().checkout();
        } catch (RepositoryException e) {
            // UnsupportedRepositoryException should not occur
            throw new JcrDavException(e);
        }
    }


    /**
     * UNCHECKOUT cannot be implemented on top of JSR 170 repository.
     * Therefore this methods always throws a <code>DavException</code> with error code
     * {@link org.apache.jackrabbit.webdav.DavServletResponse#SC_NOT_IMPLEMENTED}.
     *
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#uncheckout()
     */
    public void uncheckout() throws DavException {
        throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED);
    }

    /**
     * UPDATE feature is not (yet) supported. This method allows fails with
     * {@link DavServletResponse#SC_NOT_IMPLEMENTED}.
     *
     * @param updateInfo
     * @return
     * @throws DavException
     * @see VersionControlledResource#update(UpdateInfo)
     */
    public MultiStatus update(UpdateInfo updateInfo) throws DavException {
        throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED);
    }

    /**
     * MERGE feature is not (yet) supported. This method allows fails with
     * {@link DavServletResponse#SC_NOT_IMPLEMENTED}.
     *
     * @param mergeInfo
     * @return
     * @throws DavException
     * @see VersionControlledResource#merge(MergeInfo)
     */
    public MultiStatus merge(MergeInfo mergeInfo) throws DavException {
        throw new DavException(DavServletResponse.SC_NOT_IMPLEMENTED);
    }

    /**
     * Modify the labels present with the versions of this resource.
     *
     * @param labelInfo
     * @throws DavException
     * @see VersionControlledResource#label(LabelInfo)
     * @see javax.jcr.version.VersionHistory#addVersionLabel(String, String, boolean)
     * @see javax.jcr.version.VersionHistory#removeVersionLabel(String)
     */
    public void label(LabelInfo labelInfo) throws DavException {
        if (labelInfo == null) {
            throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Valid label request body required.");
        }
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }

        try {
            if (!isVersionControlled() || getNode().isCheckedOut()) {
                throw new DavException(DavServletResponse.SC_PRECONDITION_FAILED, "A LABEL request may only be applied to a version-controlled, checked-in resource.");
            }
            DavResource[] resArr = this.getReferenceResources(CHECKED_IN);
            if (resArr.length == 1 && resArr[0] instanceof VersionResource) {
                ((VersionResource)resArr[0]).label(labelInfo);
            } else {
                throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "DAV:checked-in property on '" + getHref() + "' did not point to a single VersionResource.");
            }
        } catch (RepositoryException e) {
            throw new JcrDavException(e);
        }
    }

    /**
     * Returns the {@link javax.jcr.version.VersionHistory} associated with the repository node.
     * If the node is not versionable an exception is thrown.
     *
     * @return the {@link VersionHistoryResource} associated with this resource.
     * @throws org.apache.jackrabbit.webdav.DavException
     * @see org.apache.jackrabbit.webdav.version.VersionControlledResource#getVersionHistory()
     * @see javax.jcr.Node#getVersionHistory()
     */
    public VersionHistoryResource getVersionHistory() throws DavException {
        if (!exists()) {
            throw new DavException(DavServletResponse.SC_NOT_FOUND);
        }
        if (!isVersionControlled()) {
            throw new DavException(DavServletResponse.SC_FORBIDDEN);
        }
        try {
            VersionHistory vh = getNode().getVersionHistory();
            DavResourceLocator loc = getLocatorFromNode(vh);
            DavResource vhr =  createResourceFromLocator(loc);
            if (vhr instanceof VersionHistoryResource) {
                return (VersionHistoryResource)vhr;
            } else {
                // severe error since resource factory doesn't behave correctly.
                throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        } catch (RepositoryException e) {
            throw new JcrDavException(e);
        }
    }

    //--------------------------------------------------------------------------
    /**
     * Define the set of reports supported by this resource.
     *
     * @see SupportedReportSetProperty
     * @see DeltaVResourceImpl#initSupportedReports()
     */
    @Override
    protected void initSupportedReports() {
        super.initSupportedReports();
        if (exists()) {
            supportedReports.addReportType(ReportType.LOCATE_BY_HISTORY);
            if (isVersionControlled()) {
                supportedReports.addReportType(ReportType.VERSION_TREE);
            }
        }
    }

    /**
     * Fill the property set for this resource.
     * @see DavResourceImpl#initProperties()
     */
    @Override
    protected void initProperties() {
        if (!propsInitialized) {
            super.initProperties();
            if (isVersionControlled()) {
                Node n = getNode();
                // properties defined by RFC 3253 for version-controlled resources
                try {
                    // DAV:version-history (computed)
                    String vhHref = getLocatorFromNode(n.getVersionHistory()).getHref(true);
                    properties.add(new HrefProperty(VERSION_HISTORY, vhHref, true));

                    // DAV:auto-version property: there is no auto version, explicit CHECKOUT is required.
                    properties.add(new DefaultDavProperty(AUTO_VERSION, null, false));

                    // baseVersion -> used for DAV:checked-out or DAV:checked-in
                    String baseVHref = getLocatorFromNode(n.getBaseVersion()).getHref(false);
                    if (n.isCheckedOut()) {
                        // DAV:predecessors property
                        if (n.hasProperty(JcrConstants.JCR_PREDECESSORS)) {
                            Value[] pv = n.getProperty(JcrConstants.JCR_PREDECESSORS).getValues();
                            Node[] predecessors = new Node[pv.length];
                            for (int i = 0; i < pv.length; i++) {
                                predecessors[i] = n.getSession().getNodeByIdentifier(pv[i].getString());
                            }
                            properties.add(getHrefProperty(VersionResource.PREDECESSOR_SET, predecessors, false, false));
                        }
                        // DAV:checked-out property (protected)
                        properties.add(new HrefProperty(CHECKED_OUT, baseVHref, true));
                    } else {
                        // DAV:checked-in property (protected)
                        properties.add(new HrefProperty(CHECKED_IN, baseVHref, true));
                    }
                } catch (RepositoryException e) {
                    log.error(e.getMessage());
                }
            }
        }
    }

    /**
     * @return true, if this resource is a non-collection resource and represents
     * an existing repository node that has the mixin nodetype 'mix:versionable' set.
     */
    private boolean isVersionControlled() {
        boolean vc = false;
        // since the version-controlled-collection feature is not supported
        // all collection are excluded from version-control even if the
        // underlying node was JCR versionable.
        if (exists() && !isCollection()) {
            Node item = getNode();
            try {
                vc = item.isNodeType(JcrConstants.MIX_VERSIONABLE);
            } catch (RepositoryException e) {
                log.warn(e.getMessage());
            }
        }
        return vc;
    }
}
