/* * 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) 2006 - 2013 Pentaho Corporation and Contributors. All rights reserved. */ package org.pentaho.reporting.libraries.resourceloader; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.libraries.base.boot.ObjectFactory; import org.pentaho.reporting.libraries.base.util.IOUtils; import org.pentaho.reporting.libraries.base.util.StringUtils; import org.pentaho.reporting.libraries.resourceloader.cache.BundleCacheResourceWrapper; import org.pentaho.reporting.libraries.resourceloader.cache.NullResourceBundleDataCache; import org.pentaho.reporting.libraries.resourceloader.cache.NullResourceDataCache; import org.pentaho.reporting.libraries.resourceloader.cache.NullResourceFactoryCache; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceBundleDataCache; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceBundleDataCacheEntry; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceBundleDataCacheProvider; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceDataCache; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceDataCacheEntry; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceDataCacheProvider; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceFactoryCache; import org.pentaho.reporting.libraries.resourceloader.cache.ResourceFactoryCacheProvider; import org.pentaho.reporting.libraries.resourceloader.modules.cache.ehcache.EHCacheModule; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.sql.Blob; import java.sql.SQLException; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * The resource manager takes care about the loaded resources, performs caching, if needed and is the central instance * when dealing with resources. Resource loading is a two-step process. In the first step, the {@link ResourceLoader} * accesses the physical storage or network connection to read in the binary data. The loaded {@link ResourceData} * carries versioning information with it an can be cached indendently from the produced result. Once the loading is * complete, a {@link ResourceFactory} interprets the binary data and produces a Java-Object from it. * <p/> * Resources are identified by an Resource-Key and some optional loader parameters (which can be used to parametrize the * resource-factories). * * @author Thomas Morgner * @see ResourceData * @see ResourceLoader * @see ResourceFactory */ public final class ResourceManager { private static final Log logger = LogFactory.getLog( ResourceManager.class ); private ResourceManagerBackend backend; public static final String BUNDLE_LOADER_PREFIX = "org.pentaho.reporting.libraries.resourceloader.bundle.loader."; public static final String LOADER_PREFIX = "org.pentaho.reporting.libraries.resourceloader.loader."; public static final String FACTORY_TYPE_PREFIX = "org.pentaho.reporting.libraries.resourceloader.factory.type."; private ResourceDataCache dataCache; private ResourceBundleDataCache bundleCache; private ResourceFactoryCache factoryCache; /** * A set that contains the class-names of all cache-modules, which could not be instantiated correctly. This set is * used to limit the number of warnings in the log to exactly one per class. */ private static final Set<Class> failedModules = new HashSet<Class>(); /** * Default Constructor. */ public ResourceManager() { this( new DefaultResourceManagerBackend() ); } public ResourceManager( final ResourceManagerBackend resourceManagerBackend ) { if ( resourceManagerBackend == null ) { throw new NullPointerException(); } this.backend = resourceManagerBackend; this.bundleCache = new NullResourceBundleDataCache(); this.dataCache = new NullResourceDataCache(); this.factoryCache = new NullResourceFactoryCache(); registerDefaults(); } public ResourceManager( final ResourceManager parent, final ResourceManagerBackend backend ) { if ( backend == null ) { throw new NullPointerException(); } if ( parent == null ) { throw new NullPointerException(); } this.backend = backend; this.bundleCache = parent.getBundleCache(); this.dataCache = parent.getDataCache(); this.factoryCache = parent.getFactoryCache(); registerDefaults(); } public ResourceManagerBackend getBackend() { return backend; } /** * Creates a ResourceKey that carries no Loader-Parameters from the given object. * * @param data the key-data * @return the generated resource-key, never null. * @throws ResourceKeyCreationException if the key-creation failed. */ public ResourceKey createKey( final Object data ) throws ResourceKeyCreationException { return createKey( data, null ); } /** * Creates a ResourceKey that carries the given Loader-Parameters contained in the optional map. * * @param data the key-data * @param parameters an optional map of parameters. * @return the generated resource-key, never null. * @throws ResourceKeyCreationException if the key-creation failed. */ public ResourceKey createKey( final Object data, final Map<? extends ParameterKey, ? extends Object> parameters ) throws ResourceKeyCreationException { return backend.createKey( data, parameters ); } /** * Derives a new key from the given resource-key. Only keys for a hierarchical storage system (like file-systems or * URLs) can have derived keys. Since LibLoader 0.3.0 only hierarchical keys can be derived. For that, the deriving * path must be given as String. * <p/> * Before trying to derive the key, the system tries to interpret the path as absolute key-value. * * @param parent the parent key, must never be null * @param path the relative path, that is used to derive the key. * @return the derived key. * @throws ResourceKeyCreationException if deriving the key failed. */ public ResourceKey deriveKey( final ResourceKey parent, final String path ) throws ResourceKeyCreationException { return deriveKey( parent, path, null ); } /** * Derives a new key from the given resource-key. Only keys for a hierarchical storage system (like file-systems or * URLs) can have derived keys. Since LibLoader 0.3.0 only hierarchical keys can be derived. For that, the deriving * path must be given as String. * <p/> * The optional parameter-map will be applied to the derived key after the parent's parameters have been copied to the * new key. * <p/> * Before trying to derive the key, the system tries to interpret the path as absolute key-value. * * @param parent the parent key, or null to interpret the path as absolute key. * @param path the relative path, that is used to derive the key. * @param parameters a optional map containing resource-key parameters. * @return the derived key. * @throws ResourceKeyCreationException if deriving the key failed. */ public ResourceKey deriveKey( final ResourceKey parent, final String path, final Map<? extends ParameterKey, ? extends Object> parameters ) throws ResourceKeyCreationException { return backend.deriveKey( parent, path, parameters ); } /** * Tries to convert the resource-key into an URL. Not all resource-keys have an URL representation. This method exists * to make it easier to connect LibLoader to other resource-loading frameworks. * * @param key the resource-key * @return the URL for the key, or null if there is no such key. */ public URL toURL( final ResourceKey key ) { return backend.toURL( key ); } public Resource createDirectly( final Object keyValue, final Class target ) throws ResourceLoadingException, ResourceCreationException, ResourceKeyCreationException { final ResourceKey key = createKey( keyValue ); return create( key, null, target ); } /** * Tries to find the first resource-bundle-loader that would be able to process the key. * * @param key the resource-key. * @return the resourceloader for that key, or null, if no resource-loader is able to process the key. * @throws ResourceLoadingException if an error occured. */ public synchronized ResourceBundleData loadResourceBundle( final ResourceKey key ) throws ResourceLoadingException { final ResourceBundleDataCache bundleCache = getBundleCache(); final ResourceBundleDataCacheEntry cached = bundleCache.get( key ); if ( cached != null ) { final ResourceBundleData data = cached.getData(); // check, whether it is valid. final long version = data.getVersion( this ); if ( ( cached.getStoredVersion() < 0 ) || ( version >= 0 && cached.getStoredVersion() == version ) ) { // now also make sure that the underlying data has not changed. // This may look a bit superfluous, but the repository may not provide // sensible cacheable information. // // As condition of satisfaction, try to find the first piece of data that // is in the cache and see whether it has changed. ResourceKey bundleKey = data.getBundleKey(); int counter = 1; while ( bundleKey != null ) { final ResourceDataCacheEntry bundleRawDataCacheEntry = getDataCache().get( bundleKey ); if ( bundleRawDataCacheEntry != null ) { final ResourceData bundleRawData = bundleRawDataCacheEntry.getData(); if ( bundleRawData != null ) { if ( isValidData( bundleRawDataCacheEntry, bundleRawData ) ) { logger.debug( "Returning cached entry [" + counter + "]" ); return data; } getDataCache().remove( bundleRawData ); } } bundleKey = bundleKey.getParent(); counter += 1; } } bundleCache.remove( data ); } final ResourceBundleData data = backend.loadResourceBundle( this, key ); if ( data != null && isResourceDataCacheable( data ) ) { bundleCache.put( this, data ); } return data; } private boolean isResourceDataCacheable( final ResourceData data ) { try { return data.getVersion( this ) != -1; } catch ( ResourceLoadingException e ) { return false; } } public ResourceData load( final ResourceKey key ) throws ResourceLoadingException { final ResourceBundleData bundle = loadResourceBundle( key ); if ( bundle != null ) { logger.debug( "Loaded bundle for key " + key ); return bundle; } final ResourceKey parent = key.getParent(); if ( parent != null ) { // try to load the bundle data of the parent final ResourceBundleData parentData = loadResourceBundle( parent ); if ( parentData != null ) { logger.debug( "Loaded bundle for key (derivate) " + key ); return parentData.deriveData( key ); } } return loadRawData( key ); } private boolean isValidData( final ResourceDataCacheEntry cached, final ResourceData data ) throws ResourceLoadingException { // check, whether it is valid. if ( cached.getStoredVersion() < 0 ) { // a non versioned entry is always valid. (Maybe this is from a Jar-URL?) return true; } final long version = data.getVersion( this ); if ( version < 0 ) { // the system is no longer able to retrieve the version information? // (but versioning information must have been available in the past) // oh, that's bad. Assume the worst and re-read the data. return false; } if ( cached.getStoredVersion() == version ) { return true; } else { return false; } } public synchronized ResourceData loadRawData( final ResourceKey key ) throws UnrecognizedLoaderException, ResourceLoadingException { final ResourceDataCache dataCache = getDataCache(); // Alternative 3: This is a plain resource and not contained in a bundle. Load as binary data final ResourceDataCacheEntry cached = dataCache.get( key ); if ( cached != null ) { final ResourceData data = cached.getData(); if ( data != null ) { if ( isValidData( cached, data ) ) { return data; } dataCache.remove( data ); } } final ResourceData data = backend.loadRawData( this, key ); if ( data != null && isResourceDataCacheable( data ) ) { dataCache.put( this, data ); } return data; } public Resource create( final ResourceKey key, final ResourceKey context, final Class target ) throws ResourceLoadingException, ResourceCreationException { if ( target == null ) { throw new NullPointerException( "Target must not be null" ); } if ( key == null ) { throw new NullPointerException( "Key must not be null." ); } return create( key, context, new Class[] { target } ); } public Resource create( final ResourceKey key, final ResourceKey context ) throws ResourceLoadingException, ResourceCreationException { return create( key, context, (Class[]) null ); } public Resource create( final ResourceKey key, final ResourceKey context, final Class[] target ) throws ResourceLoadingException, ResourceCreationException { if ( key == null ) { throw new NullPointerException(); } final ResourceFactoryCache factoryCache = getFactoryCache(); // ok, we have a handle to the data, and the data is current. // Lets check whether we also have a cached result. final Resource resource = factoryCache.get( key, target ); if ( resource != null ) { if ( backend.isResourceUnchanged( this, resource ) ) { // mama, look i am a good cache manager ... return resource; } else { // someone evil changed one of the dependent resources ... factoryCache.remove( resource ); } } final ResourceData loadedData = load( key ); final Resource newResource; if ( loadedData instanceof ResourceBundleData ) { final ResourceBundleData resourceBundleData = (ResourceBundleData) loadedData; final ResourceManager derivedManager = resourceBundleData.deriveManager( this ); newResource = backend.create( derivedManager, resourceBundleData, context, target ); if ( isResourceCacheable( newResource ) ) { if ( EHCacheModule.CACHE_MONITOR.isDebugEnabled() ) { EHCacheModule.CACHE_MONITOR.debug( "Storing created bundle-resource for key: " + key ); } factoryCache.put( newResource ); if ( key != newResource.getSource() ) { factoryCache.put( new BundleCacheResourceWrapper( newResource, key ) ); } } else { if ( EHCacheModule.CACHE_MONITOR.isDebugEnabled() ) { EHCacheModule.CACHE_MONITOR.debug( "Created bundle-resource is not cacheable for " + key ); } } } else { newResource = backend.create( this, loadedData, context, target ); if ( isResourceCacheable( newResource ) ) { if ( EHCacheModule.CACHE_MONITOR.isDebugEnabled() ) { EHCacheModule.CACHE_MONITOR.debug( "Storing created resource for key: " + key ); } factoryCache.put( newResource ); } else { if ( EHCacheModule.CACHE_MONITOR.isDebugEnabled() ) { EHCacheModule.CACHE_MONITOR.debug( "Created resource is not cacheable for " + key ); } } } return newResource; } private boolean isResourceCacheable( final Resource newResource ) { final ResourceKey source = newResource.getSource(); if ( newResource.isTemporaryResult() ) { return false; } if ( newResource.getVersion( source ) == -1 ) { return false; } final ResourceKey[] keys = newResource.getDependencies(); for ( int i = 0; i < keys.length; i++ ) { if ( newResource.getVersion( keys[ i ] ) == -1 ) { return false; } } return true; } public ResourceDataCache getDataCache() { return dataCache; } public void setDataCache( final ResourceDataCache dataCache ) { if ( dataCache == null ) { throw new NullPointerException(); } this.dataCache = dataCache; } public ResourceFactoryCache getFactoryCache() { return factoryCache; } public void setFactoryCache( final ResourceFactoryCache factoryCache ) { if ( factoryCache == null ) { throw new NullPointerException(); } this.factoryCache = factoryCache; } public ResourceBundleDataCache getBundleCache() { return bundleCache; } public void setBundleCache( final ResourceBundleDataCache bundleCache ) { if ( bundleCache == null ) { throw new NullPointerException(); } this.bundleCache = bundleCache; } public void registerDefaults() { // Create all known resource loaders ... registerDefaultLoaders(); // Register all known factories ... registerDefaultFactories(); // add the caches .. registerDataCache(); registerBundleDataCache(); registerFactoryCache(); } public void registerDefaultFactories() { backend.registerDefaultFactories(); } public void registerBundleDataCache() { try { final ObjectFactory objectFactory = LibLoaderBoot.getInstance().getObjectFactory(); final ResourceBundleDataCacheProvider maybeDataCacheProvider = objectFactory.get( ResourceBundleDataCacheProvider.class ); final ResourceBundleDataCache cache = maybeDataCacheProvider.createBundleDataCache(); if ( cache != null ) { setBundleCache( cache ); } } catch ( Throwable e ) { // ok, did not work ... synchronized( failedModules ) { if ( failedModules.contains( ResourceBundleDataCacheProvider.class ) == false ) { logger.warn( "Failed to create data cache: " + e.getLocalizedMessage() ); failedModules.add( ResourceBundleDataCacheProvider.class ); } } } } public void registerDataCache() { try { final ObjectFactory objectFactory = LibLoaderBoot.getInstance().getObjectFactory(); final ResourceDataCacheProvider maybeDataCacheProvider = objectFactory.get( ResourceDataCacheProvider.class ); final ResourceDataCache cache = maybeDataCacheProvider.createDataCache(); if ( cache != null ) { setDataCache( cache ); } } catch ( Throwable e ) { // ok, did not work ... synchronized( failedModules ) { if ( failedModules.contains( ResourceDataCacheProvider.class ) == false ) { logger.warn( "Failed to create data cache: " + e.getLocalizedMessage() ); failedModules.add( ResourceDataCacheProvider.class ); } } } } public void registerFactoryCache() { try { final ObjectFactory objectFactory = LibLoaderBoot.getInstance().getObjectFactory(); final ResourceFactoryCacheProvider maybeDataCacheProvider = objectFactory.get( ResourceFactoryCacheProvider.class ); final ResourceFactoryCache cache = maybeDataCacheProvider.createFactoryCache(); if ( cache != null ) { setFactoryCache( cache ); } } catch ( Throwable e ) { synchronized( failedModules ) { if ( failedModules.contains( ResourceFactoryCacheProvider.class ) == false ) { logger.warn( "Failed to create factory cache: " + e.getLocalizedMessage() ); failedModules.add( ResourceFactoryCacheProvider.class ); } } } } public void registerDefaultLoaders() { backend.registerDefaultLoaders(); } public void registerBundleLoader( final ResourceBundleLoader loader ) { if ( loader == null ) { throw new NullPointerException(); } backend.registerBundleLoader( loader ); } public void registerLoader( final ResourceLoader loader ) { if ( loader == null ) { throw new NullPointerException(); } backend.registerLoader( loader ); } public void registerFactory( final ResourceFactory factory ) { if ( factory == null ) { throw new NullPointerException(); } backend.registerFactory( factory ); } public void shutDown() { factoryCache.shutdown(); dataCache.shutdown(); } /** * Creates a String version of the <code>ResourceKey</code> that can be deserialized with the * <code>deserialize()</code> method. * * @param bundleKey the key to the bundle containing the resource, or null if no bundle exists. * @param key the key to be serialized * @throws ResourceException indicates an error trying to serialize the key * @throws NullPointerException indicates the supplied key is <code>null</code> */ public String serialize( final ResourceKey bundleKey, final ResourceKey key ) throws ResourceException { return backend.serialize( bundleKey, key ); } /** * Converts a serialized version of a <code>ResourceKey</code> into an actual <code>ResourceKey</code> by locating the * proper <code>ResourceLoader</code> that can perform the deserialization. * * @param serializedKey the String serialized key to be deserialized * @return the <code>ResourceKey</code> that has been deserialized * @throws ResourceKeyCreationException indicates an error trying to create the <code>ResourceKey</code> from the * deserialized version */ public ResourceKey deserialize( final ResourceKey bundleKey, final String serializedKey ) throws ResourceKeyCreationException { return backend.deserialize( bundleKey, serializedKey ); } public ResourceKey createOrDeriveKey( final ResourceKey context, final Object value, final Object baseURL ) throws ResourceKeyCreationException { if ( value == null ) { throw new ResourceKeyCreationException( "Empty key is invalid" ); } final ResourceKey key; if ( value instanceof ResourceKey ) { key = (ResourceKey) value; } else if ( value instanceof Blob ) { try { final Blob b = (Blob) value; final byte[] data = IOUtils.getInstance().readBlob( b ); key = createKey( data ); } catch ( IOException ioe ) { throw new ResourceKeyCreationException( "Failed to load data from blob", ioe ); } catch ( SQLException e ) { throw new ResourceKeyCreationException( "Failed to load data from blob", e ); } } else if ( value instanceof String ) { final String source = (String) value; if ( StringUtils.isEmpty( source ) ) { throw new ResourceKeyCreationException( "Empty key is invalid" ); } try { if ( baseURL instanceof String ) { final ResourceKey baseKey = createKeyFromString( null, (String) baseURL ); return createKeyFromString( baseKey, source ); } else if ( baseURL instanceof ResourceKey ) { final ResourceKey baseKey = (ResourceKey) baseURL; return createKeyFromString( baseKey, source ); } else if ( baseURL != null ) { // if a base-url object is given, we assume that it is indeed valid. final ResourceKey baseKey = createKey( baseURL ); return createKeyFromString( baseKey, source ); } } catch ( ResourceException rke ) { logger .debug( "Failed to resolve key via given base-url. Try to treat resource as absolute resource instead", rke ); } key = createKeyFromString( context, source ); } else { // URLs, Files, byte-arrays etc are treated as absolute objects key = createKey( value ); } return key; } private ResourceKey createKeyFromString( final ResourceKey contextKey, final String file ) throws ResourceKeyCreationException { try { if ( contextKey != null ) { return deriveKey( contextKey, file ); } } catch ( ResourceException re ) { // failed to load from context logger.debug( "Failed to load datasource as derived path: ", re ); } try { return createKey( new URL( file ) ); } catch ( ResourceException re ) { logger.debug( "Failed to load datasource as URL: ", re ); } catch ( MalformedURLException e ) { // } try { return createKey( new File( file ) ); } catch ( ResourceException re ) { // failed to load from context logger.debug( "Failed to load datasource as file: ", re ); } return createKey( file ); } }