/*
 * 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.core.security.authorization.acl;

import org.apache.jackrabbit.api.JackrabbitWorkspace;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy;
import org.apache.jackrabbit.api.security.authorization.PrivilegeManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.ProtectedItemModifier;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.security.authorization.AccessControlConstants;
import org.apache.jackrabbit.core.security.authorization.AccessControlEditor;
import org.apache.jackrabbit.core.security.authorization.AccessControlEntryImpl;
import org.apache.jackrabbit.core.security.authorization.AccessControlUtils;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.conversion.NameException;
import org.apache.jackrabbit.spi.commons.conversion.NameParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.AccessDeniedException;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.ValueFormatException;
import javax.jcr.NodeIterator;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlException;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.Privilege;
import java.security.Principal;
import java.util.Set;

/**
 * <code>ACLEditor</code>...
 */
public class ACLEditor extends ProtectedItemModifier implements AccessControlEditor, AccessControlConstants {

    /**
     * the default logger
     */
    private static final Logger log = LoggerFactory.getLogger(ACLEditor.class);
    /**
     * Default name for ace nodes
     */
    private static final String DEFAULT_ACE_NAME = "ace";
    /**
     * the editing session
     */
    private final SessionImpl session;
    private final AccessControlUtils utils;
    private final boolean allowUnknownPrincipals;

    ACLEditor(Session editingSession, AccessControlUtils utils, boolean allowUnknownPrincipals) {
        super(Permission.MODIFY_AC);
        if (editingSession instanceof SessionImpl) {
            session = ((SessionImpl) editingSession);
        } else {
            throw new IllegalArgumentException("org.apache.jackrabbit.core.SessionImpl expected. Found " + editingSession.getClass());
        }
        this.utils = utils;
        this.allowUnknownPrincipals = allowUnknownPrincipals;
    }

    /**
     *
     * @param aclNode the node
     * @param path
     * @return the control list
     * @throws RepositoryException if an error occurs
     */
    ACLTemplate getACL(NodeImpl aclNode, String path) throws RepositoryException {
        return new ACLTemplate(aclNode, path, allowUnknownPrincipals);
    }

    //------------------------------------------------< AccessControlEditor >---
    /**
     * @see AccessControlEditor#getPolicies(String)
     */
    public AccessControlPolicy[] getPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException {
        checkProtectsNode(nodePath);

        NodeImpl aclNode = getAclNode(nodePath);
        if (aclNode == null) {
            return new AccessControlPolicy[0];
        } else {
            return new AccessControlPolicy[] {getACL(aclNode, nodePath)};
        }
    }

    /**
     * Always returns an empty array as no applicable policies are exposed.
     * 
     * @see AccessControlEditor#getPolicies(Principal)
     */
    public JackrabbitAccessControlPolicy[] getPolicies(Principal principal) throws AccessControlException, RepositoryException {
        if (!session.getPrincipalManager().hasPrincipal(principal.getName())) {
            throw new AccessControlException("Unknown principal.");
        }
        // TODO: impl. missing
        return new JackrabbitAccessControlPolicy[0];
    }

    /**
     * @see AccessControlEditor#editAccessControlPolicies(String)
     */
    public AccessControlPolicy[] editAccessControlPolicies(String nodePath) throws AccessControlException, PathNotFoundException, RepositoryException {
        checkProtectsNode(nodePath);

        String mixin;
        Name aclName;
        NodeImpl controlledNode;

        if (nodePath == null) {
            controlledNode = (NodeImpl) session.getRootNode();
            mixin = session.getJCRName(NT_REP_REPO_ACCESS_CONTROLLABLE);
            aclName = N_REPO_POLICY;
        } else {
            controlledNode = getNode(nodePath);
            mixin = session.getJCRName(NT_REP_ACCESS_CONTROLLABLE);
            aclName = N_POLICY;
        }

        AccessControlPolicy acl = null;
        NodeImpl aclNode = getAclNode(controlledNode, nodePath);
        if (aclNode == null) {
            // create an empty acl unless the node is protected or cannot have
            // mixin set (e.g. due to a lock) or
            // has colliding rep:policy or rep:repoPolicy child node set.
            if (controlledNode.hasNode(aclName)) {
                // policy child node without node being access controlled
                log.warn("Colliding policy child without node being access controllable ({}).", nodePath);
            } else {
                PrivilegeManager privMgr = ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager();
                if (controlledNode.isNodeType(mixin) || controlledNode.canAddMixin(mixin)) {
                    acl = new ACLTemplate(nodePath, session.getPrincipalManager(),
                            privMgr, session.getValueFactory(), session, allowUnknownPrincipals);
                } else {
                    log.warn("Node {} cannot be made access controllable.", nodePath);
                }
            }
        } // else: acl already present -> getPolicies must be used.

        return (acl != null) ? new AccessControlPolicy[] {acl} : new AccessControlPolicy[0];
    }

    /**
     * @see AccessControlEditor#editAccessControlPolicies(Principal)
     */
    public JackrabbitAccessControlPolicy[] editAccessControlPolicies(Principal principal) throws AccessDeniedException, AccessControlException, RepositoryException {
        if (!session.getPrincipalManager().hasPrincipal(principal.getName())) {
            throw new AccessControlException("Unknown principal.");
        }
        // TODO: impl. missing
        return new JackrabbitAccessControlPolicy[0];
    }

    /**
     * @see AccessControlEditor#setPolicy(String,AccessControlPolicy)
     */
    public void setPolicy(String nodePath, AccessControlPolicy policy) throws RepositoryException {
        checkProtectsNode(nodePath);
        checkValidPolicy(nodePath, policy);

        NodeImpl aclNode = getAclNode(nodePath);
        if (aclNode != null) {
            // remove all existing aces
            for (NodeIterator aceNodes = aclNode.getNodes(); aceNodes.hasNext();) {
                NodeImpl aceNode = (NodeImpl) aceNodes.nextNode();
                removeItem(aceNode);
            }
        } else {
            // create the acl node
            aclNode = (nodePath == null) ? createRepoAclNode() : createAclNode(nodePath);
        }
        
        AccessControlEntry[] entries = ((ACLTemplate) policy).getAccessControlEntries();
        for (AccessControlEntry entry : entries) {
            AccessControlEntryImpl ace = (AccessControlEntryImpl) entry;

            Name nodeName = getUniqueNodeName(aclNode, ace.isAllow() ? "allow" : "deny");
            Name ntName = (ace.isAllow()) ? NT_REP_GRANT_ACE : NT_REP_DENY_ACE;
            ValueFactory vf = session.getValueFactory();

            // create the ACE node
            NodeImpl aceNode = addNode(aclNode, nodeName, ntName);

            // write the rep:principalName property
            String principalName = ace.getPrincipal().getName();
            setProperty(aceNode, P_PRINCIPAL_NAME, vf.createValue(principalName));

            // ... and the rep:privileges property
            Privilege[] pvlgs = ace.getPrivileges();
            Value[] names = getPrivilegeNames(pvlgs, vf);
            setProperty(aceNode, P_PRIVILEGES, names);

            // store the restrictions:
            Set<Name> restrNames = ace.getRestrictions().keySet();
            for (Name restrName : restrNames) {
                Value value = ace.getRestriction(restrName);
                setProperty(aceNode, restrName, value);
            }
        }

        // mark the parent modified.
        markModified(((NodeImpl)aclNode.getParent()));
    }

    /**
     * @see AccessControlEditor#removePolicy(String,AccessControlPolicy)
     */
    public synchronized void removePolicy(String nodePath, AccessControlPolicy policy) throws AccessControlException, RepositoryException {
        checkProtectsNode(nodePath);
        checkValidPolicy(nodePath, policy);

        NodeImpl aclNode = getAclNode(nodePath);
        if (aclNode != null) {
            removeItem(aclNode);
        } else {
            throw new AccessControlException("No policy to remove at " + nodePath);
        }
    }

    //--------------------------------------------------------------------------
    /**
     * Check if the Node identified by <code>nodePath</code> is itself part of ACL
     * defining content. It this case setting or modifying an AC-policy is
     * obviously not possible.
     *
     * @param nodePath the node path
     * @throws AccessControlException If the given nodePath identifies a Node that
     * represents a ACL or ACE item.
     * @throws RepositoryException
     */
    private void checkProtectsNode(String nodePath) throws RepositoryException {
        if (nodePath != null) {
            NodeImpl node = getNode(nodePath);
            if (utils.isAcItem(node)) {
                throw new AccessControlException("Node " + nodePath + " defines ACL or ACE itself.");
            }
        }
    }

    /**
     * Check if the specified policy can be set/removed from this editor.
     *
     * @param nodePath the node path
     * @param policy the policy
     * @throws AccessControlException if not allowed
     */
    private static void checkValidPolicy(String nodePath, AccessControlPolicy policy) throws AccessControlException {
        if (policy == null || !(policy instanceof ACLTemplate)) {
            throw new AccessControlException("Attempt to set/remove invalid policy " + policy);
        }
        ACLTemplate acl = (ACLTemplate) policy;
        boolean matchingPath = (nodePath == null) ? acl.getPath() == null : nodePath.equals(acl.getPath());
        if (!matchingPath) {
            throw new AccessControlException("Policy " + policy + " cannot be applied/removed from the node at " + nodePath);
        }
    }

    /**
     *
     * @param path the path
     * @return the node
     * @throws PathNotFoundException if not found
     * @throws RepositoryException if an error occurs
     */
    private NodeImpl getNode(String path) throws PathNotFoundException, RepositoryException {
        return (NodeImpl) session.getNode(path);
    }

    /**
     * Returns the rep:Policy node below the Node identified at the given
     * path or <code>null</code> if the node is not mix:AccessControllable
     * or if no policy node exists.
     *
     * @param nodePath the node path
     * @return node or <code>null</code>
     * @throws PathNotFoundException if not found
     * @throws RepositoryException if an error occurs
     */
    private NodeImpl getAclNode(String nodePath) throws PathNotFoundException, RepositoryException {
        NodeImpl controlledNode;
        if (nodePath == null) {
            controlledNode = (NodeImpl) session.getRootNode();
        } else {
            controlledNode = getNode(nodePath);
        }
        return getAclNode(controlledNode, nodePath);
    }

    /**
     * Returns the rep:Policy node below the given Node or <code>null</code>
     * if the node is not mix:AccessControllable or if no policy node exists.
     *
     * @param controlledNode the controlled node
     * @param nodePath
     * @return node or <code>null</code>
     * @throws RepositoryException if an error occurs
     */
    private NodeImpl getAclNode(NodeImpl controlledNode, String nodePath) throws RepositoryException {
        NodeImpl aclNode = null;
        if (nodePath == null) {
            if (ACLProvider.isRepoAccessControlled(controlledNode)) {
                aclNode = controlledNode.getNode(N_REPO_POLICY);
            }
        } else {
            if (ACLProvider.isAccessControlled(controlledNode)) {
                aclNode = controlledNode.getNode(N_POLICY);
            }
        }
        return aclNode;
    }

    /**
     *
     * @param nodePath the node path
     * @return the new node
     * @throws RepositoryException if an error occurs
     */
    private NodeImpl createAclNode(String nodePath) throws RepositoryException {
        NodeImpl protectedNode = getNode(nodePath);
        if (!protectedNode.isNodeType(NT_REP_ACCESS_CONTROLLABLE)) {
            protectedNode.addMixin(NT_REP_ACCESS_CONTROLLABLE);
        }
        return addNode(protectedNode, N_POLICY, NT_REP_ACL);
    }

    /**
     *
     * @return the new acl node used to store repository level privileges.
     * @throws RepositoryException if an error occurs
     */
    private NodeImpl createRepoAclNode() throws RepositoryException {
        NodeImpl root = (NodeImpl) session.getRootNode();
        if (!root.isNodeType(NT_REP_REPO_ACCESS_CONTROLLABLE)) {
            root.addMixin(NT_REP_REPO_ACCESS_CONTROLLABLE);
        }
        return addNode(root, N_REPO_POLICY, NT_REP_ACL);
    }

    /**
     * Create a unique valid name for the Permission nodes to be save.
     *
     * @param node a name for the child is resolved
     * @param name if missing the {@link #DEFAULT_ACE_NAME} is taken
     * @return the name
     * @throws RepositoryException if an error occurs
     */
    protected static Name getUniqueNodeName(Node node, String name) throws RepositoryException {
        if (name == null) {
            name = DEFAULT_ACE_NAME;
        } else {
            try {
                NameParser.checkFormat(name);
            } catch (NameException e) {
                name = DEFAULT_ACE_NAME;
                log.debug("Invalid path name for Permission: " + name + ".");
            }
        }
        int i = 0;
        String check = name;
        while (node.hasNode(check)) {
            check = name + i;
            i++;
        }
        return ((SessionImpl) node.getSession()).getQName(check);
    }

    /**
     * Build an array of Value from the specified <code>privileges</code> using
     * the given <code>valueFactory</code>.
     *
     * @param privileges the privileges
     * @param valueFactory the value factory
     * @return an array of Value.
     * @throws ValueFormatException if an error occurs
     */
    private static Value[] getPrivilegeNames(Privilege[] privileges, ValueFactory valueFactory)
            throws ValueFormatException {
        Value[] names = new Value[privileges.length];
        for (int i = 0; i < privileges.length; i++) {
            names[i] = valueFactory.createValue(privileges[i].getName(), PropertyType.NAME);
        }
        return names;
    }
}
