/*
* 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 java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import javax.jcr.RepositoryException;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.cache.GrowingLRUMap;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.security.authorization.AccessControlModifications;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <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 EntryCache cache;
private ConcurrentMap<NodeId, FutureEntries> futures = new ConcurrentHashMap<NodeId, FutureEntries>();
private final String strategy;
private final boolean cacheNoAcl;
/**
* Create a new instance.
*
* @param systemSession A system session.
* @param rootID The id of the root node.
* @throws RepositoryException If an error occurs.
*/
CachingEntryCollector(SessionImpl systemSession, NodeId rootID) throws RepositoryException {
super(systemSession, rootID);
cache = new EntryCache();
// for testing purposes, see JCR-2950
String propname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.strategy";
strategy = System.getProperty(propname, "T");
if (!("S".equals(strategy) || "T".equals(strategy) || "P".equals(strategy))) {
throw new RepositoryException("Invalid value " + strategy + " specified for system property " + propname);
}
log.info("Cache Update Strategy: " + strategy);
propname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.cacheNoACL";
cacheNoAcl = Boolean.parseBoolean(System.getProperty(propname, "false"));
log.info("Caching entries with no ACLs: " + cacheNoAcl);
}
@Override
protected void close() {
super.close();
cache.clear();
}
//-----------------------------------------------------< EntryCollector >---
/**
* @see EntryCollector#getEntries(org.apache.jackrabbit.core.NodeImpl)
*/
@Override
protected Entries getEntries(NodeImpl node) throws RepositoryException {
NodeId nodeId = node.getNodeId();
Entries entries = cache.get(nodeId);
if (entries == null) {
// fetch entries and update the cache
entries = updateCache(node);
}
return entries;
}
/**
* @see EntryCollector#getEntries(org.apache.jackrabbit.core.id.NodeId)
*/
@Override
protected Entries getEntries(NodeId nodeId) throws RepositoryException {
Entries entries = cache.get(nodeId);
if (entries == null) {
// fetch entries and update the cache
NodeImpl n = getNodeById(nodeId);
entries = updateCache(n);
}
return entries;
}
/**
* Read the entries defined for the specified node and update the cache
* accordingly.
*
* @param node The target node
* @return The list of entries present on the specified node or an empty list.
* @throws RepositoryException If an error occurs.
*/
private Entries internalUpdateCache(NodeImpl node) throws RepositoryException {
Entries entries = super.getEntries(node);
if (cacheNoAcl || (isRootId(node.getNodeId()) && cache.specialCasesRoot()) || !entries.isEmpty()) {
// adjust the 'nextId' to point to the next access controlled
// ancestor node instead of the parent and remember the entries.
entries.setNextId(getNextID(node));
cache.put(node.getNodeId(), entries);
} // else: not access controlled -> ignore.
return entries;
}
/**
* Update cache for the given node id
* @param node The target node
* @return The list of entries present on the specified node or an empty list.
* @throws RepositoryException
*/
private Entries updateCache(NodeImpl node) throws RepositoryException {
if ("T".equals(strategy)) {
return throttledUpdateCache(node);
} else if ("S".equals(strategy)) {
return synchronizedUpdateCache(node);
} else if ("P".equals(strategy)) {
return parallelUpdateCache(node);
} else {
// panic
throw new RuntimeException("invalid value for updateCacheStrategy: " + strategy);
}
}
/**
* See {@link CachingEntryCollector#updateCache(NodeImpl)} ; this variant runs fully synchronized
*/
synchronized private Entries synchronizedUpdateCache(NodeImpl node) throws RepositoryException {
return internalUpdateCache(node);
}
/**
* See {@link CachingEntryCollector#updateCache(NodeImpl)} ; this variant runs fully parallel
*/
private Entries parallelUpdateCache(NodeImpl node) throws RepositoryException {
return internalUpdateCache(node);
}
/**
* See {@link CachingEntryCollector#updateCache(NodeImpl)} ; this variant blocks the current
* thread if a concurrent update for the same node id takes place
*/
private Entries throttledUpdateCache(NodeImpl node) throws RepositoryException {
NodeId id = node.getNodeId();
FutureEntries fe = null;
FutureEntries nfe = new FutureEntries();
boolean found = true;
fe = futures.putIfAbsent(id, nfe);
if (fe == null) {
found = false;
fe = nfe;
}
if (found) {
// we have found a previous FutureEntries object, so use it
return fe.get();
} else {
// otherwise obtain result and when done notify waiting FutureEntries
try {
Entries e = internalUpdateCache(node);
futures.remove(id);
fe.setResult(e);
return e;
} catch (Throwable problem) {
futures.remove(id);
fe.setProblem(problem);
if (problem instanceof RepositoryException) {
throw (RepositoryException)problem;
} else {
throw new RuntimeException(problem);
}
}
}
}
/**
* Find the next access control ancestor in the hierarchy 'null' indicates
* that there is no ac-controlled ancestor.
*
* @param node The target node for which the cache needs to be updated.
* @return The NodeId of the next access controlled ancestor in the hierarchy
* or null
*/
private NodeId getNextID(NodeImpl node) throws RepositoryException {
NodeImpl n = node;
NodeId nextId = null;
while (nextId == null && !isRootId(n.getNodeId())) {
NodeId parentId = n.getParentId();
if (cache.containsKey(parentId)) {
nextId = parentId;
} else {
NodeImpl parent = (NodeImpl) n.getParent();
if (hasEntries(parent)) {
nextId = parentId;
} else {
// try next ancestor
n = parent;
}
}
}
return nextId;
}
/**
* Returns {@code true} if the specified {@code nodeId} is the ID of the
* root node; false otherwise.
*
* @param nodeId The identifier of the node to be tested.
* @return {@code true} if the given id is the identifier of the root node.
*/
private boolean isRootId(NodeId nodeId) {
return rootID.equals(nodeId);
}
/**
* Evaluates if the given node is access controlled and holds a non-empty
* rep:policy child node.
*
* @param n The node to test.
* @return true if the specified node is access controlled and holds a
* non-empty policy child node.
* @throws RepositoryException If an error occurs.
*/
private static boolean hasEntries(NodeImpl n) throws RepositoryException {
if (ACLProvider.isAccessControlled(n)) {
NodeImpl aclNode = n.getNode(N_POLICY);
return aclNode.hasNodes();
}
// no ACL defined here
return false;
}
/**
* @see EntryCollector#notifyListeners(org.apache.jackrabbit.core.security.authorization.AccessControlModifications)
*/
@Override
@SuppressWarnings("unchecked")
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);
if ((type & POLICY_ADDED) == POLICY_ADDED) {
// clear the complete cache since the nextAcNodeId may
// have changed due to the added ACL.
log.debug("Policy added, clearing the cache");
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.
cache.remove(nodeId, true);
} else if ((type & POLICY_MODIFIED) == POLICY_MODIFIED) {
// simply clear the cache entry -> reload upon next access.
cache.remove(nodeId, false);
} else if ((type & MOVE) == MOVE) {
// some sort of move operation that may affect the cache
log.debug("Move operation, clearing the cache");
cache.clear();
break; // no need for further processing.
}
}
super.notifyListeners(modifications);
}
/**
* A place holder for a yet to be computed {@link Entries} result
*/
private class FutureEntries {
private boolean ready = false;
private Entries result = null;
private Throwable problem = null;
synchronized public Entries get() throws RepositoryException {
while (!ready) {
try {
wait();
} catch (InterruptedException e) {
}
}
if (problem != null) {
if (problem instanceof RepositoryException) {
throw new RepositoryException(problem);
} else {
throw new RuntimeException(problem);
}
}
return result;
}
synchronized public void setResult(Entries e) {
result = e;
ready = true;
notifyAll();
}
synchronized public void setProblem(Throwable t) {
problem = t;
ready = true;
notifyAll();
}
}
/**
* A cache to lookup the ACEs defined on a given (access controlled)
* node. The internal map uses the ID of the node as key while the value
* consists of {@Entries} objects that not only provide the ACEs defined
* for that node but also the ID of the next access controlled parent node.
*/
private class EntryCache {
private final Map<NodeId, Entries> cache;
private Entries rootEntries;
private boolean specialCaseRoot = true;
@SuppressWarnings("unchecked")
public EntryCache() {
int maxsize = 5000;
String propname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.maxsize";
try {
maxsize = Integer.parseInt(System.getProperty(propname, Integer.toString(maxsize)));
} catch (NumberFormatException ex) {
log.debug("Parsing system property " + propname + " with value: " + System.getProperty(propname), ex);
}
log.info("Creating cache with max size of: " + maxsize);
cache = new GrowingLRUMap(1024, maxsize);
String propsrname = "org.apache.jackrabbit.core.security.authorization.acl.CachingEntryCollector.scroot";
specialCaseRoot = Boolean.parseBoolean(System.getProperty(propsrname, "true"));
log.info("Root is special-cased: " + specialCaseRoot);
}
public boolean specialCasesRoot() {
return specialCaseRoot;
}
public boolean containsKey(NodeId id) {
if (specialCaseRoot && isRootId(id)) {
return rootEntries != null;
} else {
synchronized (cache) {
return cache.containsKey(id);
}
}
}
public void clear() {
rootEntries = null;
synchronized (cache) {
cache.clear();
}
}
public Entries get(NodeId id) {
Entries result;
if (specialCaseRoot && isRootId(id)) {
result = rootEntries;
} else {
synchronized (cache) {
result = cache.get(id);
}
}
if (result != null) {
log.debug("Cache hit for nodeId {}", id);
} else {
log.debug("Cache miss for nodeId {}", id);
}
return result;
}
public void put(NodeId id, Entries entries) {
log.debug("Updating cache for nodeId {}", id);
// fail early on potential cache corruption
if (id.equals(entries.getNextId())) {
throw new IllegalArgumentException("Trying to update cache entry for " + id + " with a circular reference");
}
if (specialCaseRoot && isRootId(id)) {
rootEntries = entries;
} else {
synchronized (cache) {
cache.put(id, entries);
}
}
}
public void remove(NodeId id, boolean adjustNextIds) {
log.debug("Removing nodeId {} from cache", id);
Entries result;
synchronized (cache) {
if (specialCaseRoot && isRootId(id)) {
result = rootEntries;
rootEntries = null;
} else {
result = cache.remove(id);
}
if (adjustNextIds && result != null) {
NodeId nextId = result.getNextId();
for (Entries entry : cache.values()) {
if (id.equals(entry.getNextId())) {
// fail early on potential cache corruption
if (id.equals(nextId)) {
throw new IllegalArgumentException("Trying to update cache entry for " + id + " with a circular reference");
}
entry.setNextId(nextId);
}
}
}
}
}
}
}