/*! * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * Copyright (c) 2002-2016 Pentaho Corporation.. All rights reserved. */ package org.pentaho.platform.plugin.services.metadata; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.metadata.model.Domain; import org.pentaho.metadata.model.LogicalModel; import org.pentaho.metadata.model.concept.IConcept; import org.pentaho.metadata.repository.DomainAlreadyExistsException; import org.pentaho.metadata.repository.DomainIdNullException; import org.pentaho.metadata.repository.DomainStorageException; import org.pentaho.metadata.repository.IMetadataDomainRepository; import org.pentaho.metadata.util.SecurityHelper; 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.api.engine.ISystemConfig; import org.pentaho.platform.api.repository2.unified.RepositoryFileAcl; import org.pentaho.platform.engine.core.system.PentahoSessionHolder; import org.pentaho.platform.engine.core.system.PentahoSystem; import java.io.InputStream; import java.io.Serializable; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * This is the platform implementation which provides session-based caching for an existing {@link * IMetadataDomainRepository}. * * @author Jordan Ganoff (jganoff@pentaho.com) */ public class SessionCachingMetadataDomainRepository implements IMetadataDomainRepository, org.pentaho.platform.plugin.services.metadata.IPentahoMetadataDomainRepositoryExporter, ILogoutListener, IAclAwarePentahoMetadataDomainRepositoryImporter, IModelAnnotationsAwareMetadataDomainRepositoryImporter { private static final Log logger = LogFactory.getLog( SessionCachingMetadataDomainRepository.class ); /** * Public so others know where we're caching our objects */ public static String CACHE_REGION = "metadata-domain-repository"; //$NON-NLS-1$ ICacheManager cacheManager; boolean domainIdsCacheEnabled = true; private final IMetadataDomainRepository delegate; private static final String DOMAIN_CACHE_KEY_PREDICATE = "domain-id-cache-for-session:"; /** * this as a public class so that if necessary someone can get access to a session key and clear the cache in their * own way via javascript rule / etc */ public static class CacheKey implements Serializable { private static final long serialVersionUID = 1737869846319540136L; public String sessionId; public String domainId; protected CacheKey() { } public CacheKey( String sessionId, String domainId ) { this.sessionId = sessionId; this.domainId = domainId; } @Override public boolean equals( final Object o ) { if ( this == o ) { return true; } if ( o == null || getClass() != o.getClass() ) { return false; } CacheKey cacheKey = (CacheKey) o; if ( domainId != null ? !domainId.equals( cacheKey.domainId ) : cacheKey.domainId != null ) { return false; } if ( sessionId != null ? !sessionId.equals( cacheKey.sessionId ) : cacheKey.sessionId != null ) { return false; } return true; } @Override public int hashCode() { int result = sessionId != null ? sessionId.hashCode() : 0; result = 31 * result + ( domainId != null ? domainId.hashCode() : 0 ); return result; } @Override public String toString() { return "(" + sessionId + "," + domainId + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } /** * Wraps the provided domain repository to provide session-based caching of domains. */ public SessionCachingMetadataDomainRepository( final IMetadataDomainRepository delegate ) { if ( delegate == null ) { throw new NullPointerException(); } this.delegate = delegate; cacheManager = PentahoSystem.getCacheManager( null ); // cache manager gets loaded just once... if ( cacheManager != null ) { if ( !cacheManager.cacheEnabled( CACHE_REGION ) ) { if ( !cacheManager.addCacheRegion( CACHE_REGION ) ) { cacheManager = null; } } } if ( cacheManager == null ) { throw new IllegalStateException( getClass().getSimpleName() + " (" + CACHE_REGION + ") cannot be initialized" ); //$NON-NLS-1$ //$NON-NLS-2$ } PentahoSystem.addLogoutListener( this ); // So you can remove a users' region when their session disappears ISystemConfig systemConfig = PentahoSystem.get( ISystemConfig.class ); if ( systemConfig != null ) { String enableDomainIdCache = systemConfig.getProperty( "system.enableDomainIdCache" ); domainIdsCacheEnabled = ( enableDomainIdCache == null ) || Boolean.valueOf( enableDomainIdCache ); } } /** * Simple callback interface to facilitate iterating over cache keys */ protected static interface CacheIteratorCallback { /** * Will be called for each cache key found * * @param cacheManager The cache manager we're iterating through * @param key Key from cache manager * @return Returning false will cause the look that is calling this callback to break */ public Boolean call( final ICacheManager cacheManager, final CacheKey key ); } /** * Removes all keys it encounters from the known cache region */ protected final CacheIteratorCallback REMOVE_ALL_CALLBACK = new CacheIteratorCallback() { @Override public Boolean call( final ICacheManager cacheManager, final CacheKey key ) { if ( logger.isDebugEnabled() ) { logger.debug( "Removing domain from cache: " + key ); //$NON-NLS-1$ } cacheManager.removeFromRegionCache( CACHE_REGION, key ); return true; // continue } }; /** * Calls the callback for every key in the cache region * * @param callback {@see CacheCallback} */ protected void forAllKeys( final CacheIteratorCallback callback ) { try { Set<?> cachedObjects = cacheManager.getAllKeysFromRegionCache( CACHE_REGION ); if ( cachedObjects != null ) { for ( Object k : cachedObjects ) { if ( k instanceof CacheKey ) { CacheKey key = (CacheKey) k; if ( Boolean.FALSE.equals( callback.call( cacheManager, key ) ) ) { break; } } } } } catch ( Throwable e ) { // due to a known issue in hibernate cache // the getAll* methods of ICacheManager throw a NullPointerException if // cache values are null (this can happen due to cache object timeouts) // please see: http://opensource.atlassian.com/projects/hibernate/browse/HHH-3248 if ( logger.isDebugEnabled() ) { logger.debug( "", e ); //$NON-NLS-1$ } } } /** * Calls the callback for every key in the cache region whose session id matches the provided session's id. * * @param session Session to use for matching keys * @param callback {@see CacheCallback} */ protected void forAllKeysInSession( final IPentahoSession session, final CacheIteratorCallback callback ) { forAllKeys( new CacheIteratorCallback() { @Override public Boolean call( final ICacheManager cacheManager, final CacheKey key ) { if ( session.getId() == null ? key.sessionId == null : session.getId().equals( key.sessionId ) ) { if ( Boolean.FALSE.equals( callback.call( cacheManager, key ) ) ) { return false; // break } } return true; // continue } } ); } @Override public Domain getDomain( final String id ) { final IPentahoSession session = PentahoSessionHolder.getSession(); final CacheKey key = new CacheKey( session.getId(), id ); Domain domain = (Domain) cacheManager.getFromRegionCache( CACHE_REGION, key ); if ( domain != null ) { if ( logger.isDebugEnabled() ) { logger.debug( "Found domain in cache: " + key ); //$NON-NLS-1$ } if ( delegate instanceof IAclAwarePentahoMetadataDomainRepositoryImporter && !( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).hasAccessFor( id ) ) { if ( logger.isDebugEnabled() ) { logger.debug( "User no longer has access to Domain, purging from session cache: " + key ); } purgeDomain( domain.getId() ); domain = null; } return domain; } domain = delegate.getDomain( id ); if ( domain != null ) { if ( logger.isDebugEnabled() ) { logger.debug( "Requested Domain wasn't in Session Cache, but was found in the delegating repository: " + id ); } SecurityHelper helper = new SecurityHelper(); domain = helper.createSecureDomain( this, domain ); // cache domain with the key we used to look it up, not whatever new id it might have now if ( logger.isDebugEnabled() ) { logger.debug( "Caching domain by session: " + key ); //$NON-NLS-1$ } cacheManager.putInRegionCache( CACHE_REGION, key, domain ); } return domain; } /** * Remove all cache entries whose domain's id is equal to {@code domainId}. * * @param domainId Domain id to remove from cache */ private void purgeDomain( final String domainId ) { forAllKeys( new CacheIteratorCallback() { @Override public Boolean call( ICacheManager cacheManager, CacheKey key ) { if ( domainId == null ? key.domainId == null : domainId.equals( key.domainId ) ) { cacheManager.removeFromRegionCache( CACHE_REGION, key ); } return true; // continue } } ); removeDomainFromIDCache( domainId ); } @Override public void reloadDomains() { forAllKeys( REMOVE_ALL_CALLBACK ); clearDomainIdsFromCache(); delegate.reloadDomains(); } @Override public void flushDomains() { forAllKeys( REMOVE_ALL_CALLBACK ); clearDomainIdsFromCache(); delegate.flushDomains(); } protected void flushDomains( final IPentahoSession session ) { forAllKeysInSession( session, REMOVE_ALL_CALLBACK ); clearDomainIdsFromCache( session ); } /** * Remove domain ID cache for all sessions */ protected void clearDomainIdsFromCache() { try { Set<?> cachedObjects = cacheManager.getAllKeysFromRegionCache( CACHE_REGION ); if ( cachedObjects != null ) { for ( Object k : cachedObjects ) { if ( k instanceof String ) { String key = (String) k; if ( key != null && key.startsWith( DOMAIN_CACHE_KEY_PREDICATE ) ) { cacheManager.removeFromRegionCache( CACHE_REGION, key ); } } } } } catch ( Throwable e ) { // due to a known issue in hibernate cache // the getAll* methods of ICacheManager throw a NullPointerException if // cache values are null (this can happen due to cache object timeouts) // please see: http://opensource.atlassian.com/projects/hibernate/browse/HHH-3248 if ( logger.isDebugEnabled() ) { logger.debug( "", e ); //$NON-NLS-1$ } } } /** * Remove domain ID cache for a given session * * @param session */ protected void clearDomainIdsFromCache( IPentahoSession session ) { final String key = generateDomainIdCacheKeyForSession( session ); if ( cacheManager.getFromRegionCache( CACHE_REGION, key ) != null ) { cacheManager.removeFromRegionCache( CACHE_REGION, key ); } } /** * Remove a single domain ID from all session domain ID caches * * @param domainId */ private void removeDomainFromIDCache( String domainId ) { try { Set<?> cachedObjects = cacheManager.getAllKeysFromRegionCache( CACHE_REGION ); if ( cachedObjects != null ) { for ( Object k : cachedObjects ) { if ( k instanceof String ) { String key = (String) k; if ( key != null && key.startsWith( DOMAIN_CACHE_KEY_PREDICATE ) ) { Set<String> domainIds = (Set<String>) cacheManager.getFromRegionCache( CACHE_REGION, key ); domainIds.remove( domainId ); cacheManager.putInRegionCache( CACHE_REGION, key, domainIds ); } } } } } catch ( Throwable e ) { // due to a known issue in hibernate cache // the getAll* methods of ICacheManager throw a NullPointerException if // cache values are null (this can happen due to cache object timeouts) // please see: http://opensource.atlassian.com/projects/hibernate/browse/HHH-3248 if ( logger.isDebugEnabled() ) { logger.debug( "", e ); //$NON-NLS-1$ } } } protected String generateDomainIdCacheKeyForSession( IPentahoSession session ) { return DOMAIN_CACHE_KEY_PREDICATE + session.getId(); } @Override public void removeDomain( final String domainId ) { delegate.removeDomain( domainId ); purgeDomain( domainId ); removeDomainFromIDCache( domainId ); } @Override public void storeDomain( final Domain domain, final boolean overwrite ) throws DomainIdNullException, DomainAlreadyExistsException, DomainStorageException { delegate.storeDomain( domain, overwrite ); purgeDomain( domain.getId() ); clearDomainIdsFromCache(); } @Override public void removeModel( final String domainId, final String modelId ) throws DomainIdNullException, DomainStorageException { delegate.removeModel( domainId, modelId ); purgeDomain( domainId ); } @Override public Set<String> getDomainIds() { final IPentahoSession session = PentahoSessionHolder.getSession(); final String domainKey = generateDomainIdCacheKeyForSession( session ); Set<String> domainIds; if ( domainIdsCacheEnabled ) { domainIds = (Set<String>) cacheManager.getFromRegionCache( CACHE_REGION, domainKey ); if ( domainIds != null ) { boolean dirtyCache = false; for ( String domain : domainIds ) { if ( delegate instanceof IAclAwarePentahoMetadataDomainRepositoryImporter && !( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).hasAccessFor( domain ) ) { domainIds.remove( domain ); removeDomainFromIDCache( domain ); dirtyCache = true; } } if ( dirtyCache ) { cacheManager.putInRegionCache( CACHE_REGION, domainKey, new HashSet<String>( domainIds ) ); } // We've previously cached domainIds available for this session return domainIds; } } else { delegate.reloadDomains(); } // Domains are accessible by anyone. What they contain may be different so rely on the lookup to be // session-specific. domainIds = delegate.getDomainIds(); if ( domainIdsCacheEnabled ) { cacheManager.putInRegionCache( CACHE_REGION, domainKey, new HashSet<String>( domainIds ) ); } return domainIds; } @Override public String generateRowLevelSecurityConstraint( final LogicalModel model ) { return delegate.generateRowLevelSecurityConstraint( model ); } @Override public boolean hasAccess( final int accessType, final IConcept aclHolder ) { return delegate.hasAccess( accessType, aclHolder ); } @Override public void onLogout( final IPentahoSession session ) { flushDomains( session ); } public Map<String, InputStream> getDomainFilesData( final String domainId ) { if ( delegate instanceof org.pentaho.platform.plugin.services.metadata.IPentahoMetadataDomainRepositoryExporter ) { return ( (org.pentaho.platform.plugin.services.metadata.IPentahoMetadataDomainRepositoryExporter) delegate ) .getDomainFilesData( domainId ); } else { throw new UnsupportedOperationException( "Exporting is not supported by this Metadata Domain Repository" ); } } @Override public void storeDomain( InputStream inputStream, String domainId, boolean overwrite, RepositoryFileAcl acl ) throws DomainIdNullException, DomainAlreadyExistsException, DomainStorageException { if ( delegate instanceof IAclAwarePentahoMetadataDomainRepositoryImporter ) { ( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).storeDomain( inputStream, domainId, overwrite, acl ); } } @Override public void setAclFor( String domainId, RepositoryFileAcl acl ) { if ( delegate instanceof IAclAwarePentahoMetadataDomainRepositoryImporter ) { ( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).setAclFor( domainId, acl ); } } @Override public RepositoryFileAcl getAclFor( String domainId ) { if ( delegate instanceof IAclAwarePentahoMetadataDomainRepositoryImporter ) { return ( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).getAclFor( domainId ); } else { return null; } } @Override public boolean hasAccessFor( String domainId ) { return !( delegate instanceof IAclAwarePentahoMetadataDomainRepositoryImporter ) || ( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).hasAccessFor( domainId ); } @Override public void storeDomain( InputStream inputStream, String domainId, boolean overwrite ) throws DomainIdNullException, DomainAlreadyExistsException, DomainStorageException { if ( delegate instanceof IPentahoMetadataDomainRepositoryImporter ) { ( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).storeDomain( inputStream, domainId, overwrite ); } } @Override public void addLocalizationFile( String domainId, String locale, InputStream inputStream, boolean overwrite ) throws DomainStorageException { if ( delegate instanceof IPentahoMetadataDomainRepositoryImporter ) { ( (IAclAwarePentahoMetadataDomainRepositoryImporter) delegate ).addLocalizationFile( domainId, locale, inputStream, overwrite ); } } @Override public String loadAnnotationsXml( String domainId ) { if ( delegate instanceof IModelAnnotationsAwareMetadataDomainRepositoryImporter ) { return ( (IModelAnnotationsAwareMetadataDomainRepositoryImporter) delegate ).loadAnnotationsXml( domainId ); } return null; } @Override public void storeAnnotationsXml( String domainId, String annotationsXml ) { if ( delegate instanceof IModelAnnotationsAwareMetadataDomainRepositoryImporter ) { ( (IModelAnnotationsAwareMetadataDomainRepositoryImporter) delegate ) .storeAnnotationsXml( domainId, annotationsXml ); } } }