/*
 * 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.commons.collections.map.LRUMap;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.AccessControlModifications;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import javax.jcr.security.AccessControlEntry;
import java.util.List;
import java.util.Map;

/**
 * <code>CachingEntryCollector</code> extends <code>EntryCollector</code> by
 * keeping a cache of ACEs per access controlled nodeId.
 */
class CachingEntryCollector extends EntryCollector {

    /**
     * logger instance
     */
    private static final Logger log = LoggerFactory.getLogger(CachingEntryCollector.class);

    /**
     * Cache to look up the list of access control entries defined at a given
     * nodeID (key). The map only contains an entry if the corresponding Node
     * is access controlled.
     */
    //private final Map<NodeId, List<AccessControlEntry>> cache;
    private final Map<NodeId, CacheEntry> cache;
    private final Object monitor = new Object();

    /**
     * 
     * @param systemSession
     * @param rootID
     * @throws RepositoryException
     */
    @SuppressWarnings("unchecked")    
    CachingEntryCollector(SessionImpl systemSession, NodeId rootID) throws RepositoryException {
        super(systemSession, rootID);
        
        cache = new LRUMap(1000);
    }

    @Override
    protected void close() {
        super.close();
        synchronized (monitor) {
            cache.clear();
        }
    }

    //-----------------------------------------------------< EntryCollector >---
    /**
     * @see EntryCollector#getEntries(org.apache.jackrabbit.core.NodeImpl)
     */
    @Override    
    protected List<AccessControlEntry> getEntries(NodeImpl node) throws RepositoryException {
        List<AccessControlEntry> entries;
        NodeId nodeId = node.getNodeId();
        synchronized (monitor) {
            CacheEntry ce = cache.get(nodeId);
            if (ce != null) {
                entries = ce.entries;
            } else {
                // fetch entries and update the cache
                entries = updateCache(node);
            }
        }
        return entries;
    }
    
    /**
     * @see EntryCollector#getEntries(org.apache.jackrabbit.core.id.NodeId)
     */
    @Override
    protected List<AccessControlEntry> getEntries(NodeId nodeId) throws RepositoryException {
        List<AccessControlEntry> entries;
        synchronized (monitor) {
            CacheEntry ce = cache.get(nodeId);
            if (ce != null) {
                entries = ce.entries;
            } else {
                // fetch entries and update the cache
                NodeImpl n = getNodeById(nodeId);
                entries = updateCache(n);
            }
        }
        return entries;
    }

    /**
     *
     * @param node
     * @return
     * @throws RepositoryException
     */
    private List<AccessControlEntry> updateCache(NodeImpl node) throws RepositoryException {
        List<AccessControlEntry> entries = super.getEntries(node);
        if (!entries.isEmpty()) {
            // find the next access control ancestor in the hierarchy
            // 'null' indicates that there is no ac-controlled ancestor.
            NodeId nextId = null;
            NodeImpl n = node;            
            while (nextId == null && !rootID.equals(n.getNodeId())) {
                if (cache.containsKey(n.getNodeId())) {
                    nextId = n.getNodeId();
                } else if (cache.containsKey(n.getParentId())) {
                    nextId = n.getParentId();
                } else {
                    n = (NodeImpl) n.getParent();
                    if (hasEntries(n)) {
                        nextId = n.getNodeId();
                    } // else: not access controlled -> test next ancestors
                }
            }

            // build a new cacheEntry and add it to the cache
            CacheEntry ce = new CacheEntry(entries, nextId);
            cache.put(node.getNodeId(), ce);
            
            log.debug("Update cache for node with ID {0}: {1}", node, ce);
        } // else: not access controlled -> ignore.
        return entries;
    }

    /**
     * Evaluates if the given node is access controlled and holds a non-empty
     * rep:policy child node.
     * 
     * @param n
     * @return
     * @throws RepositoryException
     */
    private static boolean hasEntries(NodeImpl n) throws RepositoryException {
        if (ACLProvider.isAccessControlled(n)) {
            // collect the aces of that node.
            NodeImpl aclNode = n.getNode(N_POLICY);
            return aclNode.hasNodes();
        }

        // no acl defined here
        return false;
    }

    /**
     * Returns the id of the next access-controlled ancestor if the specified
     * is contained in the cache. Otherwise the method of the super-class is called.
     *
     * @param nodeId
     * @return
     * @throws RepositoryException
     * @see EntryCollector#getParentId(org.apache.jackrabbit.core.id.NodeId)
     */
    @Override
    protected NodeId getParentId(NodeId nodeId) throws RepositoryException {
        synchronized (monitor) {
            CacheEntry ce = cache.get(nodeId);
            if (ce != null) {
                return ce.nextAcNodeId;
            } else {
                // no cache entry
                return super.getParentId(nodeId);
            }
        }
    }

    /**
     * @see EntryCollector#notifyListeners(org.apache.jackrabbit.core.security.authorization.AccessControlModifications)
     */
    @Override
    public void notifyListeners(AccessControlModifications modifications) {
        /* Update cache for all affected access controlled nodes */
        for (Object key : modifications.getNodeIdentifiers()) {
            if (!(key instanceof NodeId)) {
                log.warn("Cannot process AC modificationMap entry. Keys must be NodeId.");
                continue;
            }
            NodeId nodeId = (NodeId) key;
            int type = modifications.getType(nodeId);
            synchronized (monitor) {
                if ((type & POLICY_ADDED) == POLICY_ADDED) {
                    // clear the complete cache since the nextAcNodeId may
                    // have changed due to the added acl.
                    cache.clear();
                    break; // no need for further processing.
                } else if ((type & POLICY_REMOVED) == POLICY_REMOVED) {
                    // clear the entry and change the entries having a nextID
                    // pointing to this node.
                    CacheEntry ce = cache.remove(nodeId);
                    if (ce != null) {
                        NodeId nextId = ce.nextAcNodeId;
                        for (CacheEntry entry : cache.values()) {
                            if (nodeId.equals(entry.nextAcNodeId)) {
                                entry.nextAcNodeId = nextId;
                            }
                        }
                    }
                } else if ((type & POLICY_MODIFIED) == POLICY_MODIFIED) {
                    // simply clear the cache entry -> reload upon next access.
                    cache.remove(nodeId);
                }
            }
        }
        super.notifyListeners(modifications);
    }

    //--------------------------------------------------------------------------
    /**
     *
     */
    private class CacheEntry {

        private final List<AccessControlEntry> entries;
        private NodeId nextAcNodeId;

        private CacheEntry(List<AccessControlEntry> entries, NodeId nextAcNodeId) {
            this.entries = entries;
            this.nextAcNodeId = nextAcNodeId;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("size = ").append(entries.size()).append(", ");
            sb.append("nextAcNodeId = ").append(nextAcNodeId);
            return sb.toString();
        }
    }
}
@@ -5523,54 +5523,8 @@
 ) %7B%0A
-            // collect the aces of that node.%0A
     

fix comment