/*!
* Copyright 2010 - 2016 Pentaho Corporation. All rights reserved.
*
* Licensed 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.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.codehaus.jackson.map.util.LRUMap;
import org.pentaho.platform.api.engine.ICacheManager;
import org.pentaho.platform.api.engine.ILogoutListener;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.RepositoryException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* <code>CachingEntryCollector</code> extends <code>PentahoEntryCollector</code> by keeping a cache of ACEs per access
* controlled nodeId.
* <p/>
* This class is a copy of the one in trunk of Jackrabbit. Backported here for performance reasons.
*/
public class CachingPentahoEntryCollector extends PentahoEntryCollector {
/**
* logger instance
*/
private static final Logger log = LoggerFactory.getLogger( CachingEntryCollector.class );
public static final String ENTRY_COLLECTOR = "ENTRY_COLLECTOR";
private final ICacheManager cacheManager;
private final Map<IPentahoSession, ConcurrentMap<NodeId, FutureEntries>> futuresBySession = Collections
.synchronizedMap( new LRUMap<IPentahoSession, ConcurrentMap<NodeId, FutureEntries>>( 128, 512 ) );
/**
* Create a new instance.
*
* @param systemSession A system session.
* @param rootID The id of the root node.
* @throws RepositoryException If an error occurs.
*/
public CachingPentahoEntryCollector( SessionImpl systemSession, NodeId rootID, final Map configuration )
throws RepositoryException {
super( systemSession, rootID, configuration );
// Flush caches of session on logout
PentahoSystem.addLogoutListener( new ILogoutListener() {
@Override
public void onLogout( IPentahoSession iPentahoSession ) {
log.debug( "Flushing ACL Entries due to logout for session: " + iPentahoSession.getName() );
flushCachesOfSession( iPentahoSession );
}
} );
cacheManager = PentahoSystem.getCacheManager( null ); // not session instanced
}
private void flushCachesOfSession( IPentahoSession iPentahoSession ) {
cacheManager.removeFromSessionCache( iPentahoSession, ENTRY_COLLECTOR );
synchronized ( futuresBySession ) {
if ( futuresBySession.containsKey( iPentahoSession ) ) {
futuresBySession.get( iPentahoSession ).clear();
futuresBySession.remove( iPentahoSession );
}
}
}
private EntryCache getCache() {
IPentahoSession session = PentahoSessionHolder.getSession();
EntryCache cache = (EntryCache) cacheManager.getFromSessionCache( session, ENTRY_COLLECTOR );
if ( cache == null ) {
cache = new EntryCache();
cacheManager.putInSessionCache( session, ENTRY_COLLECTOR, cache );
}
return cache;
}
private ConcurrentMap<NodeId, FutureEntries> getFutures() {
IPentahoSession session = PentahoSessionHolder.getSession();
if ( futuresBySession.containsKey( session ) ) {
return futuresBySession.get( session );
}
ConcurrentMap<NodeId, FutureEntries> futures = new ConcurrentHashMap<NodeId, FutureEntries>();
futuresBySession.put( session, futures );
return futures;
}
@Override
protected void close() {
super.close();
performAgainstAllInCache( new CacheCallable() {
@Override public void call( EntryCache cache ) {
cache.clear();
}
} );
synchronized ( futuresBySession ) {
for ( Map.Entry<IPentahoSession, ConcurrentMap<NodeId, FutureEntries>> entry : this.futuresBySession
.entrySet() ) {
entry.getValue().clear();
}
futuresBySession.clear();
}
}
// -----------------------------------------------------< EntryCollector >---
/**
* @see EntryCollector#getEntries(org.apache.jackrabbit.core.NodeImpl)
*/
@Override
protected PentahoEntries getEntries( NodeImpl node ) throws RepositoryException {
NodeId nodeId = node.getNodeId();
Entries entries = getCache().get( nodeId );
if ( entries == null ) {
// fetch entries and update the cache
entries = updateCache( node );
}
return entries instanceof PentahoEntries ? ( PentahoEntries ) entries : new PentahoEntries( entries );
}
/**
* @see EntryCollector#getEntries(org.apache.jackrabbit.core.id.NodeId)
*/
@Override
protected Entries getEntries( NodeId nodeId ) throws RepositoryException {
Entries entries = getCache().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 ( ( isRootId( node.getNodeId() ) && getCache().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));
getCache().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 {
return throttledUpdateCache( 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 = getFutures().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 );
getFutures().remove( id );
fe.setResult( e );
return e;
} catch ( Throwable problem ) {
getFutures().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 ( getCache().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;
}
/**
* Utility SMI
*/
private interface CacheCallable {
void call( EntryCache cache );
}
private static final Pattern SESSION_KEY_PATTERN = Pattern.compile( "[^\\t]*\\t(.*)" );
private void performAgainstAllInCache( CacheCallable callable ) {
Set allKeysFromRegionCache = cacheManager.getAllKeysFromRegionCache( ICacheManager.SESSION );
for ( Object compositeKey : allKeysFromRegionCache ) {
Matcher matcher = SESSION_KEY_PATTERN.matcher( compositeKey.toString() );
if ( matcher.matches() ) {
String key = matcher.toMatchResult().group( 1 );
if ( ENTRY_COLLECTOR.equals( key ) ) {
Object fromRegionCache = cacheManager.getFromRegionCache( ICacheManager.SESSION, compositeKey );
if ( EntryCache.class.isAssignableFrom( fromRegionCache.getClass() ) ) {
callable.call( (EntryCache) fromRegionCache );
}
}
}
}
}
/**
* @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;
}
final 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" );
performAgainstAllInCache( new CacheCallable() {
@Override public void call( EntryCache 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.
performAgainstAllInCache( new CacheCallable() {
@Override public void call( EntryCache cache ) {
cache.remove( nodeId, true );
}
} );
} else if ( ( type & POLICY_MODIFIED ) == POLICY_MODIFIED ) {
// simply clear the cache entry -> reload upon next access.
performAgainstAllInCache( new CacheCallable() {
@Override public void call( EntryCache cache ) {
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" );
performAgainstAllInCache( new CacheCallable() {
@Override public void call( EntryCache 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;
public synchronized Entries get() throws RepositoryException {
while ( !ready ) {
try {
wait();
} catch ( InterruptedException e ) {
// CHECKSTYLES IGNORE
}
}
if ( problem != null ) {
if ( problem instanceof RepositoryException ) {
throw new RepositoryException( problem );
} else {
throw new RuntimeException( problem );
}
}
return result;
}
public synchronized void setResult( Entries e ) {
result = e;
ready = true;
notifyAll();
}
public synchronized 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 );
}
}
}
}
}
}
}