/*
 * 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.jcr2spi.state;

import org.apache.jackrabbit.jcr2spi.ManagerProvider;
import org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntry;
import org.apache.jackrabbit.jcr2spi.hierarchy.PropertyEntry;
import org.apache.jackrabbit.jcr2spi.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.jcr2spi.security.AccessManager;
import org.apache.jackrabbit.jcr2spi.util.LogUtil;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.QItemDefinition;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.QNodeTypeDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.AccessDeniedException;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.PathNotFoundException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.lock.LockException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.version.VersionException;

/**
 * Utility class for validating an item state against constraints
 * specified by its definition.
 */
public class ItemStateValidator {

    /**
     * Logger instance for this class
     */
    private static Logger log = LoggerFactory.getLogger(ItemStateValidator.class);

    /**
     * option for <code>{@link #checkAddNode}</code> and
     * <code>{@link #checkRemoveItem}</code> methods:<p/>
     * check access rights
     */
    public static final int CHECK_ACCESS = 1;
    /**
     * option for <code>{@link #checkAddNode}</code> and
     * <code>{@link #checkRemoveItem}</code> methods:<p/>
     * check lock status
     */
    public static final int CHECK_LOCK = 2;
    /**
     * option for <code>{@link #checkAddNode}</code> and
     * <code>{@link #checkRemoveItem}</code> methods:<p/>
     * check checked-out status
     */
    public static final int CHECK_VERSIONING = 4;
    /**
     * option for <code>{@link #checkAddNode}</code> and
     * <code>{@link #checkRemoveItem}</code> methods:<p/>
     * check constraints defined in node type
     */
    public static final int CHECK_CONSTRAINTS = 8;

    /**
     * option for <code>{@link #checkRemoveItem}</code> method:<p/>
     * check that target node is not being referenced
     */
    public static final int CHECK_COLLISION = 32;

    public static final int CHECK_NONE = 0;
    public static final int CHECK_ALL = CHECK_ACCESS | CHECK_LOCK | CHECK_VERSIONING | CHECK_CONSTRAINTS | CHECK_COLLISION;

    /**
     * manager provider
     */
    private final ManagerProvider mgrProvider;
    private final PathFactory pathFactory;

    /**
     * Creates a new <code>ItemStateValidator</code> instance.
     *
     * @param mgrProvider manager provider
     */
    public ItemStateValidator(ManagerProvider mgrProvider, PathFactory pathFactory) {
        this.mgrProvider = mgrProvider;
        this.pathFactory = pathFactory;
    }

    /**
     * Checks whether the given node state satisfies the constraints specified
     * by its primary and mixin node types. The following validations/checks are
     * performed:
     * <ul>
     * <li>check if its node type satisfies the 'required node types' constraint
     * specified in its definition</li>
     * <li>check if all 'mandatory' child items exist</li>
     * <li>for every property: check if the property value satisfies the
     * value constraints specified in the property's definition</li>
     * </ul>
     *
     * @param nodeState state of node to be validated
     * @throws ConstraintViolationException if any of the validations fail
     * @throws RepositoryException          if another error occurs
     */
    public void validate(NodeState nodeState) throws ConstraintViolationException,
        RepositoryException {
        // effective primary node type
        EffectiveNodeType entPrimary = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(nodeState.getNodeTypeName());
        QNodeDefinition def = nodeState.getDefinition();

        // check if primary type satisfies the 'required node types' constraint
        Name[] requiredPrimaryTypes = def.getRequiredPrimaryTypes();
        for (int i = 0; i < requiredPrimaryTypes.length; i++) {
            if (!entPrimary.includesNodeType(requiredPrimaryTypes[i])) {
                String msg = safeGetJCRPath(nodeState)
                        + ": missing required primary type "
                        + requiredPrimaryTypes[i];
                log.debug(msg);
                throw new ConstraintViolationException(msg);
            }
        }
        // mandatory properties
        // effective node type (primary type incl. mixins)
        Name[] ntNames = nodeState.getAllNodeTypeNames();
        EffectiveNodeType entPrimaryAndMixins = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(ntNames);
        QPropertyDefinition[] pda = entPrimaryAndMixins.getMandatoryQPropertyDefinitions();
        for (int i = 0; i < pda.length; i++) {
            QPropertyDefinition pd = pda[i];
            if (!nodeState.hasPropertyName(pd.getName())) {
                String msg = safeGetJCRPath(nodeState)
                        + ": mandatory property " + pd.getName()
                        + " does not exist";
                log.debug(msg);
                throw new ConstraintViolationException(msg);
            }
        }
        // mandatory child nodes
        QNodeDefinition[] cnda = entPrimaryAndMixins.getMandatoryQNodeDefinitions();
        for (int i = 0; i < cnda.length; i++) {
            QNodeDefinition cnd = cnda[i];
            if (!nodeState.getNodeEntry().hasNodeEntry(cnd.getName())) {
                String msg = safeGetJCRPath(nodeState)
                        + ": mandatory child node " + cnd.getName()
                        + " does not exist";
                log.debug(msg);
                throw new ConstraintViolationException(msg);
            }
        }
    }

    //-------------------------------------------------< misc. helper methods >
    /**
     * Failsafe translation of internal <code>ItemState</code> to JCR path for use
     * in error messages etc.
     *
     * @param itemState
     * @return JCR path
     * @see LogUtil#safeGetJCRPath(ItemState,org.apache.jackrabbit.spi.commons.conversion.PathResolver)
     */
    private String safeGetJCRPath(ItemState itemState) {
        return LogUtil.safeGetJCRPath(itemState, mgrProvider.getPathResolver());
    }

    //------------------------------------------------------< check methods >---
    /**
     *
     * @param parentState
     * @param options
     * @throws VersionException
     * @throws LockException
     * @throws ItemNotFoundException
     * @throws ItemExistsException
     * @throws PathNotFoundException
     * @throws RepositoryException
     */
    public void checkIsWritable(NodeState parentState, int options) throws VersionException,
        LockException, ItemNotFoundException, ItemExistsException, PathNotFoundException, RepositoryException {

        if ((options & CHECK_ACCESS) == CHECK_ACCESS) {
            // make sure current session is granted read access on parent node
            if (!mgrProvider.getAccessManager().canRead(parentState)) {
                throw new ItemNotFoundException(safeGetJCRPath(parentState));
            }
        }
        // make sure there's no foreign lock on parent node
        if ((options & CHECK_LOCK) == CHECK_LOCK) {
            checkLock(parentState);
        }
        // make sure parent node is checked-out
        if ((options & CHECK_VERSIONING) == CHECK_VERSIONING) {
            checkIsCheckedOut(parentState);
        }
        // constraints
        if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) {
            // make sure parent node is not protected
            checkProtection(parentState);
        }
    }

    /**
     *
     * @param propState
     * @param options bit-wise OR'ed flags specifying the checks that should be
     * performed; any combination of the following constants:
     * <ul>
     * <li><code>{@link #CHECK_ACCESS}</code>: make sure current session is
     * granted read access on parent node and can add a child node with the
     * given name.</li>
     * <li><code>{@link #CHECK_LOCK}</code>: make sure there's no foreign lock
     * on parent node</li>
     * <li><code>{@link #CHECK_VERSIONING}</code>: make sure parent node is
     * checked-out</li>
     * <li><code>{@link #CHECK_CONSTRAINTS}</code>: make sure no node type
     * constraints would be violated</li>
     * <li><code>{@link #CHECK_COLLISION}</code>: check for collision with
     * existing properties or nodes</li>
     * </ul>
     *
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws VersionException
     * @throws LockException
     * @throws ItemNotFoundException
     * @throws ItemExistsException
     * @throws PathNotFoundException
     * @throws RepositoryException
     */
    public void checkSetProperty(PropertyState propState, int options)
        throws ConstraintViolationException, AccessDeniedException,
        VersionException, LockException, ItemNotFoundException,
        ItemExistsException, PathNotFoundException, RepositoryException {

        NodeState parent = propState.getParent();
        QPropertyDefinition def = propState.getDefinition();
        checkWriteProperty(parent, propState.getName(), def, options);
    }

    /**
     *
     * @param parentState
     * @param propertyName
     * @param options bit-wise OR'ed flags specifying the checks that should be
     * performed; any combination of the following constants:
     * <ul>
     * <li><code>{@link #CHECK_ACCESS}</code>: make sure current session is
     * granted read access on parent node and can add a child node with the
     * given name.</li>
     * <li><code>{@link #CHECK_LOCK}</code>: make sure there's no foreign lock
     * on parent node</li>
     * <li><code>{@link #CHECK_VERSIONING}</code>: make sure parent node is
     * checked-out</li>
     * <li><code>{@link #CHECK_CONSTRAINTS}</code>: make sure no node type
     * constraints would be violated</li>
     * <li><code>{@link #CHECK_COLLISION}</code>: check for collision with
     * existing properties or nodes</li>
     * </ul>
     *
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws VersionException
     * @throws LockException
     * @throws ItemNotFoundException
     * @throws ItemExistsException
     * @throws PathNotFoundException
     * @throws RepositoryException
     */
    public void checkAddProperty(NodeState parentState, Name propertyName, QPropertyDefinition definition, int options)
        throws ConstraintViolationException, AccessDeniedException,
        VersionException, LockException, ItemNotFoundException,
        ItemExistsException, PathNotFoundException, RepositoryException {

        checkWriteProperty(parentState, propertyName, definition, options);
    }

    /**
     *
     * @param parentState
     * @param propertyName
     * @param definition
     * @param options
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws VersionException
     * @throws LockException
     * @throws ItemNotFoundException
     * @throws ItemExistsException
     * @throws PathNotFoundException
     * @throws RepositoryException
     */
    private void checkWriteProperty(NodeState parentState, Name propertyName, QPropertyDefinition definition, int options)
        throws ConstraintViolationException, AccessDeniedException,
        VersionException, LockException, ItemNotFoundException,
        ItemExistsException, PathNotFoundException, RepositoryException {

        checkIsWritable(parentState, options);

        // access restriction on prop.
        if ((options & CHECK_ACCESS) == CHECK_ACCESS) {
            // make sure current session is granted write access on new prop
            Path relPath = pathFactory.create(propertyName);
            if (!mgrProvider.getAccessManager().isGranted(parentState, relPath, new String[] {AccessManager.SET_PROPERTY_ACTION})) {
                throw new AccessDeniedException(safeGetJCRPath(parentState) + ": not allowed to create property with name " + propertyName);
            }
        }
        // constraints on property
        if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) {
            // if definition is available check if prop-def is not protected either.
            checkProtection(definition);
        }
        // collisions
        if ((options & CHECK_COLLISION) == CHECK_COLLISION) {
            checkCollision(parentState, propertyName);
        }
    }

    /**
     * Checks if adding a child node called <code>nodeName</code> of node type
     * <code>nodeTypeName</code> to the given parent node is allowed in the
     * current context.
     *
     * @param parentState
     * @param nodeName
     * @param nodeTypeName
     * @param options      bit-wise OR'ed flags specifying the checks that should be
     *                     performed; any combination of the following constants:
     * <ul>
     * <li><code>{@link #CHECK_ACCESS}</code>: make sure current session is
     * granted read access on parent node and can add a child node with the
     * given name.</li>
     * <li><code>{@link #CHECK_LOCK}</code>: make sure there's no foreign lock
     * on parent node</li>
     * <li><code>{@link #CHECK_VERSIONING}</code>: make sure parent node is
     * checked-out</li>
     * <li><code>{@link #CHECK_CONSTRAINTS}</code>: make sure no node type
     * constraints would be violated</li>
     * <li><code>{@link #CHECK_COLLISION}</code>: check for collision with
     * existing properties or nodes</li>
     * </ul>
     *
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws VersionException
     * @throws LockException
     * @throws ItemNotFoundException
     * @throws ItemExistsException
     * @throws RepositoryException
     */
    public void checkAddNode(NodeState parentState, Name nodeName,
                             Name nodeTypeName, int options)
            throws ConstraintViolationException, AccessDeniedException,
            VersionException, LockException, ItemNotFoundException,
            ItemExistsException, RepositoryException {

        checkIsWritable(parentState, options);

        // access restrictions on new node
        if ((options & CHECK_ACCESS) == CHECK_ACCESS) {
            // make sure current session is granted write access on parent node
            Path relPath = pathFactory.create(nodeName);
            if (!mgrProvider.getAccessManager().isGranted(parentState, relPath, new String[] {AccessManager.ADD_NODE_ACTION})) {
                throw new AccessDeniedException(safeGetJCRPath(parentState) + ": not allowed to add child node '" + nodeName +"'");
            }
        }
        // node type constraints
        if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) {
            // make sure there's an applicable definition for new child node
            Name[] ntNames = parentState.getAllNodeTypeNames();
            EffectiveNodeType entParent = mgrProvider.getEffectiveNodeTypeProvider().getEffectiveNodeType(ntNames);
            QNodeTypeDefinition def = mgrProvider.getNodeTypeDefinitionProvider().getNodeTypeDefinition(nodeTypeName);
            entParent.checkAddNodeConstraints(nodeName, def, mgrProvider.getItemDefinitionProvider());
        }
        // collisions
        if ((options & CHECK_COLLISION) == CHECK_COLLISION) {
            checkCollision(parentState, nodeName, nodeTypeName);
        }
    }

    /**
     * Checks if removing the given target state is allowed in the current context.
     *
     * @param targetState
     * @param options     bit-wise OR'ed flags specifying the checks that should be
     *                    performed; any combination of the following constants:
     *                    <ul>
     *                    <li><code>{@link #CHECK_ACCESS}</code>: make sure
     *                    current session is granted read access on parent
     *                    and remove privilege on target node</li>
     *                    <li><code>{@link #CHECK_LOCK}</code>: make sure
     *                    there's no foreign lock on parent node</li>
     *                    <li><code>{@link #CHECK_VERSIONING}</code>: make sure
     *                    parent node is checked-out</li>
     *                    <li><code>{@link #CHECK_CONSTRAINTS}</code>:
     *                    make sure no node type constraints would be violated</li>
     *                    </ul>
     * @throws ConstraintViolationException
     * @throws AccessDeniedException
     * @throws VersionException
     * @throws LockException
     * @throws ItemNotFoundException
     * @throws ReferentialIntegrityException
     * @throws RepositoryException
     */
    public void checkRemoveItem(ItemState targetState, int options)
            throws ConstraintViolationException, AccessDeniedException,
            VersionException, LockException, ItemNotFoundException,
            ReferentialIntegrityException, RepositoryException {

        if (targetState.isNode() && ((NodeState)targetState).isRoot()) {
            // root node
            throw new ConstraintViolationException("Cannot remove root node.");
        }
        // check parent
        checkIsWritable(targetState.getParent(), options);

        // access rights
        if ((options & CHECK_ACCESS) == CHECK_ACCESS) {
            try {
                // make sure current session is allowed to remove target node
                if (!mgrProvider.getAccessManager().canRemove(targetState)) {
                    throw new AccessDeniedException(safeGetJCRPath(targetState) + ": not allowed to remove node");
                }
            } catch (ItemNotFoundException e) {
                String msg = "internal error: failed to check access rights for " + safeGetJCRPath(targetState);
                log.debug(msg);
                throw new RepositoryException(msg, e);
            }
        }

        // constraints given from the target
        if ((options & CHECK_CONSTRAINTS) == CHECK_CONSTRAINTS) {
            // check if target not protected and not mandatory
            checkRemoveConstraints(targetState);
        }
    }

    /**
     * Verifies that the item represented by the given state is checked-out;
     * throws a <code>VersionException</code> if that's not the case.
     * <p>
     * A node is considered <i>checked-out</i> if it is versionable and
     * checked-out, or is non-versionable but its nearest versionable ancestor
     * is checked-out, or is non-versionable and there are no versionable
     * ancestors.
     *
     * @param itemState state to check
     * @throws PathNotFoundException
     * @throws VersionException
     * @throws RepositoryException
     */
    private void checkIsCheckedOut(ItemState itemState)
            throws PathNotFoundException, VersionException, RepositoryException {

        NodeState nodeState = (itemState.isNode()) ? (NodeState)itemState : itemState.getParent();
        mgrProvider.getVersionStateManager().checkIsCheckedOut(nodeState);
    }

    /**
     * Verifies that the given item state is not locked by
     * somebody else than the current session.
     *
     * @param itemState state to be checked
     * @throws PathNotFoundException
     * @throws LockException         if write access to the specified path is not allowed
     * @throws RepositoryException   if another error occurs
     */
    private void checkLock(ItemState itemState) throws LockException, RepositoryException {
        // make sure there's no foreign lock present the node (or the parent node
        // in case the state represents a PropertyState).
        NodeState nodeState = (itemState.isNode()) ? ((NodeState)itemState) : itemState.getParent();
        mgrProvider.getLockStateManager().checkLock(nodeState);
    }

    /**
     * Checks if the definition of the given item state indicates a protected
     * status.
     *
     * @param itemState
     * @throws ConstraintViolationException If the definition of the given
     * item state indicates that the state is protected.
     * @see QItemDefinition#isProtected()
     */
    private void checkProtection(ItemState itemState)
        throws ConstraintViolationException, RepositoryException {
        QItemDefinition def;
        if (itemState.isNode()) {
            def = ((NodeState)itemState).getDefinition();
        } else {
            def = ((PropertyState)itemState).getDefinition();
        }
        checkProtection(def);
    }

    /**
     * Checks if the given {@link QItemDefinition#isProtected()} is true.
     *
     * @param definition
     * @throws ConstraintViolationException If {@link QItemDefinition#isProtected()}
     * returns true.
     */
    private void checkProtection(QItemDefinition definition) throws ConstraintViolationException {
        if (definition.isProtected()) {
            throw new ConstraintViolationException("Item is protected");
        }
    }

    /**
     * An item state cannot be removed if it is protected.
     *
     * @param itemState
     * @throws ConstraintViolationException
     * @see #checkProtection(ItemState)
     */
    private void checkRemoveConstraints(ItemState itemState)
        throws ConstraintViolationException, RepositoryException {
        QItemDefinition definition;
        if (itemState.isNode()) {
            definition = ((NodeState)itemState).getDefinition();
        } else {
            definition = ((PropertyState)itemState).getDefinition();
        }
        checkProtection(definition);
    }

    /**
     *
     * @param parentState
     * @param propertyName
     * @throws ItemExistsException
     * @throws RepositoryException
     */
    private void checkCollision(NodeState parentState, Name propertyName) throws ItemExistsException, RepositoryException {
        NodeEntry parentEntry = (NodeEntry) parentState.getHierarchyEntry();
         // NOTE: check for name collisions with existing child node has been
         // removed as with JSR 283 having same-named node and property can be
         // allowed. thus delegate the corresponding validation to the underlying
         // SPI implementation.

        // check for name collisions with an existing property
        PropertyEntry pe = parentEntry.getPropertyEntry(propertyName);
        if (pe != null) {
            try {
                pe.getPropertyState();
                throw new ItemExistsException("Property '" + pe.getName() + "' already exists.");
            } catch (ItemNotFoundException e) {
                // apparently conflicting entry does not exist any more
                // ignore and return
            }
        }
    }

    /**
     *
     * @param parentState
     * @param nodeName
     * @param nodeTypeName
     * @throws RepositoryException
     * @throws ConstraintViolationException
     * @throws NoSuchNodeTypeException
     */
    private void checkCollision(NodeState parentState, Name nodeName, Name nodeTypeName) throws RepositoryException, ConstraintViolationException, NoSuchNodeTypeException {
         // NOTE: check for name collisions with existing child property has been
         // removed as with JSR 283 having same-named node and property may be
         // allowed. thus delegate the corresponding validation to the underlying
         // SPI implementation.

         // check for conflict with existing same-name sibling node.
         if (parentState.hasChildNodeEntry(nodeName, Path.INDEX_DEFAULT)) {
            // retrieve the existing node state that ev. conflicts with the new one.
            try {
                NodeState conflictingState = parentState.getChildNodeState(nodeName, Path.INDEX_DEFAULT);
                QNodeDefinition conflictDef = conflictingState.getDefinition();
                QNodeDefinition newDef = mgrProvider.getItemDefinitionProvider().getQNodeDefinition(parentState.getAllNodeTypeNames(), nodeName, nodeTypeName);

                // check same-name sibling setting of both target and existing node
                if (!(conflictDef.allowsSameNameSiblings() && newDef.allowsSameNameSiblings())) {
                    throw new ItemExistsException("Cannot add child node '"
                            + nodeName.getLocalName() + "' to "
                            + safeGetJCRPath(parentState)
                            + ": colliding with same-named existing node.");
                }
            } catch (ItemNotFoundException e) {
                // ignore: conflicting doesn't exist any more
            }
        }
    }
}
