/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU General Public License, version 2 as published by the Free Software * Foundation. * * You should have received a copy of the GNU General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/gpl-2.0.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 General Public License for more details. * * * Copyright 2014 - 2016 Pentaho Corporation. All rights reserved. */ package org.pentaho.platform.repository2.unified.jcr.sejcr; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import org.pentaho.platform.api.engine.ISystemConfig; import org.pentaho.platform.engine.core.system.PentahoSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.extensions.jcr.SessionFactoryUtils; import javax.jcr.Credentials; import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.SimpleCredentials; import java.io.IOException; import java.util.concurrent.TimeUnit; /** * JCR Session Factory which caches Sessions by Credentials per Thread. The size of the cache and TTL of the entries can * be configured with repository.spring.properties * <p> * Created by nbaker on 6/9/14. */ class GuavaCachePoolPentahoJcrSessionFactory extends NoCachePentahoJcrSessionFactory implements PentahoJcrSessionFactory { private CredentialsStrategySessionFactory credentialsStrategySessionFactory; private int cacheDuration = 300; private int cacheSize = 100; private Logger logger = LoggerFactory.getLogger( getClass() ); private PentahoTransactionManager transactionManager; public GuavaCachePoolPentahoJcrSessionFactory( Repository repository, String workspace ) { this( repository, workspace, null ); } public GuavaCachePoolPentahoJcrSessionFactory( Repository repository, String workspace, PentahoTransactionManager transactionManager ) { super( repository, workspace ); this.transactionManager = transactionManager; ISystemConfig systemConfig = PentahoSystem.get( ISystemConfig.class ); if ( systemConfig != null && systemConfig.getConfiguration( "repository" ) != null ) { try { this.cacheDuration = Integer.parseInt( systemConfig.getConfiguration( "repository" ).getProperties().getProperty( "cache-ttl", "300" ) ); this.cacheSize = Integer.parseInt( systemConfig.getConfiguration( "repository" ).getProperties().getProperty( "cache-size", "100" ) ); } catch ( IOException e ) { logger.info( "Could not find repository.cache-duration" ); } } } /** * Session cache by credentials, partitioned by thread. Two threads obtaining sessions for the same credentials cannot * use the same Session. */ private LoadingCache<CacheKey, Session> sessionCache = CacheBuilder.newBuilder().expireAfterAccess( cacheDuration, TimeUnit.SECONDS ).maximumSize( cacheSize ).removalListener( new RemovalListener<CacheKey, Session>() { @Override public void onRemoval( RemovalNotification<CacheKey, Session> objectObjectRemovalNotification ) { // We're not logging out on cache purge as someone may have obtained it from the cache already. // TODO: implement reference tracking (checkin/checkout) in order to condition the logout. // Session value = objectObjectRemovalNotification.getValue(); // if ( value != null && value.isLive() ) { // value.logout(); // } } } ).recordStats().build( new CacheLoader<CacheKey, Session>() { @Override public Session load( CacheKey credKey ) throws Exception { return GuavaCachePoolPentahoJcrSessionFactory.super.getSession( credKey.creds ); } } ); @Override public Session getSession( Credentials creds ) throws RepositoryException { // Aquire from cache Session session; if ( transactionManager == null || !transactionManager.isCreatingTransaction() ) { if ( logger.isDebugEnabled() ) { logger.debug( "Thread is not transacted, checking cache for session: " + creds ); } try { CacheKey key = new CacheKey( creds ); // find or create session = sessionCache.get( key ); if ( !session.isLive() ) { if ( logger.isDebugEnabled() ) { logger.debug( "Cached session is not longer alive. disposing: " + creds ); } sessionCache.invalidate( key ); session = sessionCache.get( key ); } if ( SessionFactoryUtils.isSessionThreadBound( session, credentialsStrategySessionFactory ) ) { if ( logger.isDebugEnabled() ) { logger.debug( "Session is bound to a transaction. This should never happen, ignoring this session and creating a new " + "session: " + creds ); } sessionCache.invalidate( key ); session = sessionCache.get( key ); } session.refresh( false ); } catch ( Exception e ) { logger.error( "Error obtaining session from cache. Creating one directly instead: " + creds, e ); session = super.getSession( creds ); } } else { if ( logger.isDebugEnabled() ) { logger.debug( "Thread is transacted, obtaining session directly, not cached: " + creds ); } session = super.getSession( creds ); } return session; } /** * Used by the sessionCache as a key for Jcr Sessions. */ private class CacheKey { SimpleCredentials creds; Long threadId; private CacheKey( Credentials creds ) { this.creds = (SimpleCredentials) creds; this.threadId = Thread.currentThread().getId(); } @Override public boolean equals( Object o ) { if ( this == o ) { return true; } if ( o == null || getClass() != o.getClass() ) { return false; } CacheKey cacheKey = (CacheKey) o; if ( creds != null ? !creds.getUserID().equals( cacheKey.creds.getUserID() ) : cacheKey.creds != null ) { return false; } if ( threadId != null ? !threadId.equals( cacheKey.threadId ) : cacheKey.threadId != null ) { return false; } return true; } @Override public int hashCode() { int result = creds != null ? creds.getUserID().hashCode() : 0; result = 31 * result + ( threadId != null ? threadId.hashCode() : 0 ); return result; } } }