/*
 * 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.core.cache.GrowingLRUMap;
import org.apache.jackrabbit.core.ItemImpl;
import org.apache.jackrabbit.core.ItemManager;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.security.authorization.AbstractCompiledPermissions;
import org.apache.jackrabbit.core.security.authorization.AccessControlListener;
import org.apache.jackrabbit.core.security.authorization.AccessControlModifications;
import org.apache.jackrabbit.core.security.authorization.AccessControlUtils;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.security.authorization.PrivilegeBits;
import org.apache.jackrabbit.core.security.authorization.PrivilegeManagerImpl;
import org.apache.jackrabbit.core.security.authorization.PrivilegeRegistry;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.ItemNotFoundException;
import javax.jcr.RepositoryException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * <code>CompiledPermissionsImpl</code>...
 */
class CompiledPermissionsImpl extends AbstractCompiledPermissions implements AccessControlListener {

    private static final Logger LOG = LoggerFactory.getLogger(CompiledPermissionsImpl.class);

    public static final int DEFAULT_MAX_CACHE_SIZE = 5000;

    public static final int MAX_CACHE_SIZE = Integer.getInteger(
            "org.apache.jackrabbit.core.security.authorization.acl.CompiledPermissionsImpl.cacheSize",
            DEFAULT_MAX_CACHE_SIZE
    );

    private final List<String> principalNames;
    private final SessionImpl session;
    private final EntryCollector entryCollector;
    private final AccessControlUtils util;

    /*
     * Start with initial map size of 1024 and grow up to 5000 before
     * removing LRU items.
     */
    @SuppressWarnings("unchecked")
    private final Map<ItemId, Boolean> readCache = new GrowingLRUMap(1024, MAX_CACHE_SIZE);

    private final Object monitor = new Object();

    CompiledPermissionsImpl(Set<Principal> principals, SessionImpl session,
                            EntryCollector entryCollector, AccessControlUtils util,
                            boolean listenToEvents) throws RepositoryException {
        this.session = session;
        this.entryCollector = entryCollector;
        this.util = util;

        principalNames = new ArrayList<String>(principals.size());
        for (Principal princ : principals) {
            principalNames.add(princ.getName());
        }

        if (listenToEvents) {
            /*
            Make sure this AclPermission recalculates the permissions if
            any ACL concerning it is modified.
            */
            entryCollector.addListener(this);
        }

        LOG.debug("Read permission cache size = {}", MAX_CACHE_SIZE);
    }

    private Result buildResult(NodeImpl node, boolean isExistingNode,
                               boolean isAcItem, EntryFilterImpl filter) throws RepositoryException {
        // retrieve all ACEs at path or at the direct ancestor of path that
        // apply for the principal names.
        NodeImpl n = ACLProvider.getNode(node, isAcItem);
        Iterator<Entry> entries = entryCollector.collectEntries(n, filter).iterator();

        /*
        Calculate privileges and permissions:
        Since the ACEs only define privileges on a node and do not allow
        to add additional restrictions, the permissions can be determined
        without taking the given target name or target item into account.
        */
        int allows = Permission.NONE;
        int denies = Permission.NONE;

        PrivilegeBits allowBits = PrivilegeBits.getInstance();
        PrivilegeBits denyBits = PrivilegeBits.getInstance();
        PrivilegeBits parentAllowBits = PrivilegeBits.getInstance();
        PrivilegeBits parentDenyBits = PrivilegeBits.getInstance();

        String parentPath = Text.getRelativeParent(filter.getPath(), 1);
        NodeId nodeId = (node == null) ? null : node.getNodeId();

        while (entries.hasNext()) {
            Entry ace = entries.next();
            /*
            Determine if the ACE also takes effect on the parent:
            Some permissions (e.g. add-node or removal) must be determined
            from privileges defined for the parent.
            A 'local' entry defined on the target node never effects the
            parent. For inherited ACEs determine if the ACE matches the
            parent path.
            */
            PrivilegeBits entryBits = ace.getPrivilegeBits();
            boolean isLocal = isExistingNode && ace.isLocal(nodeId);
            boolean matchesParent = (!isLocal && ace.matches(parentPath));
            if (matchesParent) {
                if (ace.isAllow()) {
                    parentAllowBits.addDifference(entryBits, parentDenyBits);
                } else {
                    parentDenyBits.addDifference(entryBits, parentAllowBits);
                }
            }
            if (ace.isAllow()) {
                allowBits.addDifference(entryBits, denyBits);
                int permissions = PrivilegeRegistry.calculatePermissions(allowBits, parentAllowBits, true, isAcItem);
                allows |= Permission.diff(permissions, denies);
            } else {
                denyBits.addDifference(entryBits, allowBits);
                int permissions = PrivilegeRegistry.calculatePermissions(denyBits, parentDenyBits, false, isAcItem);
                denies |= Permission.diff(permissions, allows);
            }
        }

        return new Result(allows, denies, allowBits, denyBits);
    }

    //------------------------------------< AbstractCompiledPermissions >---
    /**
     * @see AbstractCompiledPermissions#buildResult(org.apache.jackrabbit.spi.Path)
     */
    @Override
    protected Result buildResult(Path absPath) throws RepositoryException {
        boolean existingNode = false;
        NodeImpl node;

        ItemManager itemMgr = session.getItemManager();
        try {
            ItemImpl item = itemMgr.getItem(absPath);
            if (item.isNode()) {
                node = (NodeImpl) item;
                existingNode = true;
            } else {
                node = (NodeImpl) item.getParent();
            }
        } catch (RepositoryException e) {
            // path points to a non-persisted item.
            // -> find the nearest persisted node starting from the root.
            Path.Element[] elems = absPath.getElements();
            NodeImpl parent = (NodeImpl) session.getRootNode();
            for (int i = 1; i < elems.length - 1; i++) {
                Name name = elems[i].getName();
                int index = elems[i].getIndex();
                if (!parent.hasNode(name, index)) {
                    // last persisted node reached
                    break;
                }
                parent = parent.getNode(name, index);

            }
            node = parent;
        }

        if (node == null) {
            // should never get here
            throw new ItemNotFoundException("Item out of hierarchy.");
        }

        boolean isAcItem = util.isAcItem(absPath);
        return buildResult(node, existingNode, isAcItem, new EntryFilterImpl(principalNames, absPath, session));
    }

    @Override
    protected Result buildRepositoryResult() throws RepositoryException {
        return buildResult(null, true, false, new EntryFilterImpl(principalNames, session.getQPath("/"), session));
    }

    /**
     * @see AbstractCompiledPermissions#getPrivilegeManagerImpl()
     */
    @Override
    protected PrivilegeManagerImpl getPrivilegeManagerImpl() throws RepositoryException {
        return (PrivilegeManagerImpl) ((JackrabbitWorkspace) session.getWorkspace()).getPrivilegeManager();
    }

    /**
     * @see AbstractCompiledPermissions#clearCache()
     */
    @Override
    protected void clearCache() {
        synchronized (monitor) {
            readCache.clear();
        }
        super.clearCache();
    }

    //--------------------------------------------< CompiledPermissions >---
    /**
     * @see org.apache.jackrabbit.core.security.authorization.CompiledPermissions#close()
     */
    @Override
    public void close() {
        entryCollector.removeListener(this);
        // NOTE: do not logout shared session.
        super.close();
    }

    /**
     * @see org.apache.jackrabbit.core.security.authorization.CompiledPermissions#canRead(Path, ItemId)
     */
    public boolean canRead(Path path, ItemId itemId) throws RepositoryException {
        ItemId id = (itemId == null) ? session.getHierarchyManager().resolvePath(path) : itemId;
        // no extra check for existence as method may only be called for existing items.
        boolean isExistingNode = id.denotesNode();
        boolean canRead = false;
        synchronized (monitor) {
            if (readCache.containsKey(id)) {
                canRead = readCache.get(id);
            } else {
                ItemManager itemMgr = session.getItemManager();
                NodeId nodeId = (isExistingNode) ? (NodeId) id : ((PropertyId) id).getParentId();
                NodeImpl node = (NodeImpl) itemMgr.getItem(nodeId);

                boolean isAcItem = util.isAcItem(node);
                EntryFilterImpl filter;
                if (path == null) {
                    filter = new EntryFilterImpl(principalNames, id, session);
                } else {
                    filter = new EntryFilterImpl(principalNames, path, session);
                }

                if (isAcItem) {
                    /* item defines ac content -> regular evaluation */
                    Result result = buildResult(node, isExistingNode, isAcItem, filter);
                    canRead = result.grants(Permission.READ);
                } else {
                    /*
                     simplified evaluation focusing on READ permission. this allows
                     to omit evaluation of parent node permissions that are
                     required when calculating the complete set of permissions
                     (see special treatment of remove, create or ac-specific
                      permissions).
                     */
                    for (Entry ace : entryCollector.collectEntries(node, filter)) {
                        if (ace.getPrivilegeBits().includesRead()) {
                            canRead = ace.isAllow();
                            break;
                        }
                    }
                }
                readCache.put(id, canRead);
            }
        }
        return canRead;
    }

    //----------------------------------------< ACLModificationListener >---
    /**
     * @see org.apache.jackrabbit.core.security.authorization.AccessControlListener#acModified(org.apache.jackrabbit.core.security.authorization.AccessControlModifications)
     */
    public void acModified(AccessControlModifications modifications) {
        // ignore the details of the modifications and clear all caches.
        clearCache();
    }
}
