001    /**
002     * ================================================
003     * LibLoader : a free Java resource loading library
004     * ================================================
005     *
006     * Project Info:  http://reporting.pentaho.org/libloader/
007     *
008     * (C) Copyright 2006, by Pentaho Corporation and Contributors.
009     *
010     * This library is free software; you can redistribute it and/or modify it under the terms
011     * of the GNU Lesser General Public License as published by the Free Software Foundation;
012     * either version 2.1 of the License, or (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
015     * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016     * See the GNU Lesser General Public License for more details.
017     *
018     * You should have received a copy of the GNU Lesser General Public License along with this
019     * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
020     * Boston, MA 02111-1307, USA.
021     *
022     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
023     * in the United States and other countries.]
024     *
025     *
026     * ------------
027     * $Id: ResourceManager.java 2746 2007-04-04 11:12:36Z taqua $
028     * ------------
029     * (C) Copyright 2006, by Pentaho Corporation.
030     */
031    package org.jfree.resourceloader;
032    
033    import java.net.URL;
034    import java.util.ArrayList;
035    import java.util.HashSet;
036    import java.util.Iterator;
037    import java.util.Map;
038    import java.util.Set;
039    
040    import org.jfree.resourceloader.cache.NullResourceDataCache;
041    import org.jfree.resourceloader.cache.NullResourceFactoryCache;
042    import org.jfree.resourceloader.cache.ResourceDataCache;
043    import org.jfree.resourceloader.cache.ResourceDataCacheEntry;
044    import org.jfree.resourceloader.cache.ResourceDataCacheProvider;
045    import org.jfree.resourceloader.cache.ResourceFactoryCache;
046    import org.jfree.resourceloader.cache.ResourceFactoryCacheProvider;
047    import org.jfree.util.Configuration;
048    import org.jfree.util.Log;
049    import org.jfree.util.ObjectUtilities;
050    
051    /**
052     * The resource manager takes care about the loaded resources, performs caching, if needed and is the central instance
053     * when dealing with resources. Resource loading is a two-step process. In the first step, the {@link ResourceLoader}
054     * accesses the physical storage or network connection to read in the binary data. The loaded {@link ResourceData}
055     * carries versioning information with it an can be cached indendently from the produced result. Once the loading is
056     * complete, a {@link ResourceFactory} interprets the binary data and produces a Java-Object from it.
057     * <p/>
058     * Resources are identified by an Resource-Key and some optional loader parameters (which can be used to parametrize the
059     * resource-factories).
060     *
061     * @author Thomas Morgner
062     * @see ResourceData
063     * @see ResourceLoader
064     * @see ResourceFactory
065     */
066    public class ResourceManager
067    {
068      /**
069       * A set that contains the class-names of all cache-modules, which could not be instantiated correctly.
070       * This set is used to limit the number of warnings in the log to exactly one per class.
071       */
072      private static final Set failedModules = new HashSet();
073    
074      private ArrayList resourceLoaders;
075      private ArrayList resourceFactories;
076      private ResourceDataCache dataCache;
077      private ResourceFactoryCache factoryCache;
078    
079      private static final String LOADER_PREFIX = "org.jfree.resourceloader.loader.";
080      private static final String FACTORY_TYPE_PREFIX = "org.jfree.resourceloader.factory.type.";
081      public static final String DATA_CACHE_PROVIDER_KEY = "org.jfree.resourceloader.cache.DataCacheProvider";
082      public static final String FACTORY_CACHE_PROVIDER_KEY = "org.jfree.resourceloader.cache.FactoryCacheProvider";
083    
084      public ResourceManager()
085      {
086        resourceLoaders = new ArrayList();
087        resourceFactories = new ArrayList();
088        dataCache = new NullResourceDataCache();
089        factoryCache = new NullResourceFactoryCache();
090      }
091    
092      /**
093       * Creates a ResourceKey that carries no Loader-Parameters from the given object.
094       *
095       * @param data the key-data
096       * @return the generated resource-key, never null.
097       * @throws ResourceKeyCreationException if the key-creation failed.
098       */
099      public synchronized ResourceKey createKey(final Object data)
100          throws ResourceKeyCreationException
101      {
102        return createKey(data, null);
103      }
104    
105      /**
106       * Creates a ResourceKey that carries the given Loader-Parameters contained in the optional map.
107       *
108       * @param data       the key-data
109       * @param parameters an optional map of parameters.
110       * @return the generated resource-key, never null.
111       * @throws ResourceKeyCreationException if the key-creation failed.
112       */
113      public synchronized ResourceKey createKey(final Object data, final Map parameters)
114          throws ResourceKeyCreationException
115      {
116        if (data == null)
117        {
118          throw new NullPointerException("Key data must not be null.");
119        }
120    
121        final Iterator values = resourceLoaders.iterator();
122        while (values.hasNext())
123        {
124          final ResourceLoader loader = (ResourceLoader) values.next();
125          try
126          {
127            final ResourceKey key = loader.createKey(data, parameters);
128            if (key != null)
129            {
130              return key;
131            }
132          }
133          catch (ResourceKeyCreationException rkce)
134          {
135            // ignore it.
136          }
137        }
138    
139        throw new ResourceKeyCreationException
140            ("Unable to create key: No loader was able " +
141                "to handle the given key data: " + data);
142      }
143    
144      /**
145       * Derives a new key from the given resource-key. Only keys for a hierarchical storage system (like file-systems or
146       * URLs) can have derived keys. Since LibLoader 0.3.0 only hierarchical keys can be derived. For that, the deriving
147       * path must be given as String.
148       * <p/>
149       * Before trying to derive the key, the system tries to interpret the path as absolute key-value.
150       *
151       * @param parent the parent key, must never be null
152       * @param path   the relative path, that is used to derive the key.
153       * @return the derived key.
154       */
155      public ResourceKey deriveKey(final ResourceKey parent, final String path)
156          throws ResourceKeyCreationException
157      {
158        return deriveKey(parent, path, null);
159      }
160    
161      /**
162       * Derives a new key from the given resource-key. Only keys for a hierarchical storage system (like file-systems or
163       * URLs) can have derived keys. Since LibLoader 0.3.0 only hierarchical keys can be derived. For that, the deriving
164       * path must be given as String.
165       * <p/>
166       * The optional parameter-map will be applied to the derived key after the parent's parameters have been copied to
167       * the new key.
168       * <p/>
169       * Before trying to derive the key, the system tries to interpret the path as absolute key-value.
170       *
171       * @param parent the parent key, or null to interpret the path as absolute key.
172       * @param path   the relative path, that is used to derive the key.
173       * @return the derived key.
174       */
175      public ResourceKey deriveKey(final ResourceKey parent, final String path, final Map parameters)
176          throws ResourceKeyCreationException
177      {
178        if (path == null)
179        {
180          throw new NullPointerException("Key data must not be null.");
181        }
182        if (parent == null)
183        {
184          return createKey(path, parameters);
185        }
186    
187        // First, try to derive the resource directly. This makes sure, that we preserve the parent's context.
188        // If a file is derived, we assume that the result will be a file; and only if that fails we'll try to
189        // query the other contexts. If the parent is an URL-context, the result is assumed to be an URL as well.
190        ResourceKeyCreationException rce = null;
191        for (int i = 0; i < resourceLoaders.size(); i++)
192        {
193          final ResourceLoader loader = (ResourceLoader) resourceLoaders.get(i);
194          if (loader.isSupportedKey(parent) == false)
195          {
196            continue;
197          }
198          try
199          {
200            final ResourceKey key = loader.deriveKey(parent, path, parameters);
201            if (key != null)
202            {
203              return key;
204            }
205          }
206          catch (ResourceKeyCreationException rcke)
207          {
208            rce = rcke;
209          }
210        }
211    
212        // First, try to load the key as absolute value.
213        // This assumes, that we have no catch-all implementation.
214        for (int i = 0; i < resourceLoaders.size(); i++)
215        {
216          final ResourceLoader loader = (ResourceLoader) resourceLoaders.get(i);
217          final ResourceKey key = loader.createKey(path, parameters);
218          if (key != null)
219          {
220            return key;
221          }
222        }
223    
224        if (rce != null)
225        {
226          throw rce;
227        }
228        throw new ResourceKeyCreationException
229            ("Unable to create key: No such schema or the key was not recognized.");
230      }
231    
232      /**
233       * Tries to find the first resource-loader that would be able to process the key.
234       *
235       * @param key the resource-key.
236       * @return the resourceloader for that key, or null, if no resource-loader is able to process the key.
237       */
238      private ResourceLoader findBySchema(final ResourceKey key)
239      {
240        for (int i = 0; i < resourceLoaders.size(); i++)
241        {
242          final ResourceLoader loader = (ResourceLoader) resourceLoaders.get(i);
243          if (loader.isSupportedKey(key))
244          {
245            return loader;
246          }
247        }
248        return null;
249      }
250    
251      /**
252       * Tries to convert the resource-key into an URL. Not all resource-keys have an URL representation. This method
253       * exists to make it easier to connect LibLoader to other resource-loading frameworks.
254       *
255       * @param key the resource-key
256       * @return the URL for the key, or null if there is no such key.
257       */
258      public URL toURL(final ResourceKey key)
259      {
260        final ResourceLoader loader = findBySchema(key);
261        if (loader == null)
262        {
263          return null;
264        }
265        return loader.toURL(key);
266      }
267    
268      public ResourceData load(final ResourceKey key) throws ResourceLoadingException
269      {
270        final ResourceLoader loader = findBySchema(key);
271        if (loader == null)
272        {
273          throw new ResourceLoadingException
274              ("Invalid key: No resource-loader registered for schema: " + key.getSchema());
275        }
276    
277        final ResourceDataCacheEntry cached = dataCache.get(key);
278        if (cached != null)
279        {
280          final ResourceData data = cached.getData();
281          // check, whether it is valid.
282          if (cached.getStoredVersion() < 0)
283          {
284            // a non versioned entry is always valid. (Maybe this is from a Jar-URL?)
285            return data;
286          }
287    
288          final long version = data.getVersion(this);
289          if (version < 0)
290          {
291            // the system is no longer able to retrieve the version information?
292            // (but versioning information must have been available in the past)
293            // oh, that's bad. Assume the worst and re-read the data.
294            dataCache.remove(data);
295          }
296          else if (cached.getStoredVersion() == version)
297          {
298            return data;
299          }
300          else
301          {
302            dataCache.remove(data);
303          }
304        }
305        final ResourceData data = loader.load(key);
306        return dataCache.put(this, data);
307      }
308    
309      public Resource createDirectly(final Object keyValue, final Class target)
310          throws ResourceLoadingException,
311          ResourceCreationException,
312          ResourceKeyCreationException
313      {
314        final ResourceKey key = createKey(keyValue);
315        return create(key, null, target);
316      }
317    
318      public Resource create(final ResourceKey key, final ResourceKey context, final Class target)
319          throws ResourceLoadingException, ResourceCreationException
320      {
321        if (target == null)
322        {
323          throw new NullPointerException("Target must not be null");
324        }
325        if (key == null)
326        {
327          throw new NullPointerException("Key must not be null.");
328        }
329        return create(key, context, new Class[]{target});
330      }
331    
332      public Resource create(final ResourceKey key, final ResourceKey context)
333          throws ResourceLoadingException, ResourceCreationException
334      {
335        return create(key, context, (Class[]) null);
336      }
337    
338      public Resource create(final ResourceKey key, final ResourceKey context, final Class[] target)
339          throws ResourceLoadingException, ResourceCreationException
340      {
341        if (key == null)
342        {
343          throw new NullPointerException("Key must not be null.");
344        }
345    
346        // ok, we have a handle to the data, and the data is current.
347        // Lets check whether we also have a cached result.
348        final Resource resource = factoryCache.get(key);
349        if (resource != null)
350        {
351          if (isResourceUnchanged(resource))
352          {
353            // mama, look i am a good cache manager ...
354            return resource;
355          }
356          else
357          {
358            // someone evil changed one of the dependent resources ...
359            factoryCache.remove(resource);
360          }
361        }
362    
363        // AutoMode ..
364        if (target == null)
365        {
366          return autoCreateResource(key, context);
367        }
368    
369        ResourceCreationException exception = null;
370        final ResourceData data = load(key);
371        for (int i = 0; i < resourceFactories.size(); i++)
372        {
373          final ResourceFactory fact =
374              (ResourceFactory) resourceFactories.get(i);
375          if (isSupportedTarget(target, fact) == false)
376          {
377            // Unsupported keys: Try the next factory ..
378            continue;
379          }
380    
381          try
382          {
383            return performCreate(data, fact, context);
384          }
385          catch (ContentNotRecognizedException ce)
386          {
387            // Ignore it, unless it is the last one.
388          }
389          catch (ResourceCreationException rex)
390          {
391            // ignore it, try the next factory ...
392            exception = rex;
393            if (Log.isDebugEnabled())
394            {
395              Log.debug("Failed at " + fact.getClass() + ": ", rex);
396            }
397          }
398    
399        }
400    
401        if (exception != null)
402        {
403          throw exception;
404        }
405        throw new ContentNotRecognizedException
406            ("None of the selected factories was able to handle the given data: " + key);
407      }
408    
409      private boolean isSupportedTarget(final Class[] target, final ResourceFactory fact)
410      {
411        final Class factoryType = fact.getFactoryType();
412        for (int j = 0; j < target.length; j++)
413        {
414          final Class aClass = target[j];
415          if (aClass != null && aClass.isAssignableFrom(factoryType))
416          {
417            return true;
418          }
419        }
420        return false;
421      }
422    
423      private Resource autoCreateResource(final ResourceKey key,
424                                          final ResourceKey context)
425          throws ResourceLoadingException, ResourceCreationException
426      {
427        final ResourceData data = load(key);
428    
429        final Iterator it = resourceFactories.iterator();
430        while (it.hasNext())
431        {
432          final ResourceFactory fact = (ResourceFactory) it.next();
433          try
434          {
435            final Resource res = performCreate(data, fact, context);
436            if (res != null)
437            {
438              return res;
439            }
440          }
441          catch (ResourceCreationException rex)
442          {
443            // ignore it, try the next factory ...
444          }
445        }
446        throw new ResourceCreationException
447            ("No known factory was able to handle the given data.");
448      }
449    
450      private Resource performCreate(final ResourceData data,
451                                     final ResourceFactory fact,
452                                     final ResourceKey context)
453          throws ResourceLoadingException, ResourceCreationException
454      {
455        final Resource created = fact.create(this, data, context);
456        factoryCache.put(created);
457        return created;
458      }
459    
460      private boolean isResourceUnchanged(final Resource resource)
461          throws ResourceLoadingException
462      {
463        final ResourceKey[] deps = resource.getDependencies();
464        for (int i = 0; i < deps.length; i++)
465        {
466          final ResourceKey dep = deps[i];
467          final long version = resource.getVersion(dep);
468          if (version == -1)
469          {
470            // non-versioning key, ignore it.
471            continue;
472          }
473    
474          final ResourceData data = load(dep);
475          if (data.getVersion(this) != version)
476          {
477            // oh, my bad, an outdated or changed entry.
478            // We have to re-read the whole thing.
479            return false;
480          }
481        }
482        // all versions have been confirmed to be valid. Nice, we can use the
483        // cached product.
484        return true;
485      }
486    
487      public ResourceDataCache getDataCache()
488      {
489        return dataCache;
490      }
491    
492      public void setDataCache(final ResourceDataCache dataCache)
493      {
494        if (dataCache == null)
495        {
496          throw new NullPointerException();
497        }
498        this.dataCache = dataCache;
499      }
500    
501      public ResourceFactoryCache getFactoryCache()
502      {
503        return factoryCache;
504      }
505    
506      public void setFactoryCache(final ResourceFactoryCache factoryCache)
507      {
508        if (factoryCache == null)
509        {
510          throw new NullPointerException();
511        }
512        this.factoryCache = factoryCache;
513      }
514    
515      public void registerDefaults()
516      {
517        // Create all known resource loaders ...
518        registerDefaultLoaders();
519    
520        // Register all known factories ...
521        registerDefaultFactories();
522    
523        // add the caches ..
524        registerDataCache();
525        registerFactoryCache();
526      }
527    
528      public void registerDefaultFactories()
529      {
530        final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
531        final Iterator itType = config.findPropertyKeys(FACTORY_TYPE_PREFIX);
532        while (itType.hasNext())
533        {
534          final String key = (String) itType.next();
535          final String factoryClass = config.getConfigProperty(key);
536    
537          final Object maybeFactory = ObjectUtilities.loadAndInstantiate
538              (factoryClass, ResourceManager.class, ResourceFactory.class);
539          if (maybeFactory instanceof ResourceFactory == false)
540          {
541            continue;
542          }
543    
544          final ResourceFactory factory = (ResourceFactory) maybeFactory;
545          factory.initializeDefaults();
546          registerFactory(factory);
547        }
548      }
549    
550      public void registerDataCache()
551      {
552        final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
553        final String dataCacheProviderClass =
554            config.getConfigProperty(DATA_CACHE_PROVIDER_KEY);
555        if (dataCacheProviderClass == null)
556        {
557          return;
558        }
559        final Object maybeDataCacheProvider =
560            ObjectUtilities.loadAndInstantiate
561                (dataCacheProviderClass, ResourceManager.class, ResourceDataCacheProvider.class);
562        if (maybeDataCacheProvider instanceof ResourceDataCacheProvider)
563        {
564          final ResourceDataCacheProvider provider = (ResourceDataCacheProvider) maybeDataCacheProvider;
565          try
566          {
567            final ResourceDataCache cache = provider.createDataCache();
568            if (cache != null)
569            {
570              setDataCache(cache);
571            }
572          }
573          catch (Throwable e)
574          {
575            // ok, did not work ...
576            synchronized (failedModules)
577            {
578              if (failedModules.contains(dataCacheProviderClass) == false)
579              {
580                Log.warn("Failed to create data cache: " + e.getLocalizedMessage());
581                failedModules.add(dataCacheProviderClass);
582              }
583            }
584          }
585        }
586      }
587    
588      public void registerFactoryCache()
589      {
590        final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
591        final String cacheProviderClass = config.getConfigProperty
592            (FACTORY_CACHE_PROVIDER_KEY);
593        if (cacheProviderClass == null)
594        {
595          return;
596        }
597        final Object maybeCacheProvider = ObjectUtilities.loadAndInstantiate
598            (cacheProviderClass, ResourceManager.class, ResourceFactoryCacheProvider.class);
599    
600        if (maybeCacheProvider != null)
601        {
602          final ResourceFactoryCacheProvider provider = (ResourceFactoryCacheProvider) maybeCacheProvider;
603          try
604          {
605            final ResourceFactoryCache cache = provider.createFactoryCache();
606            if (cache != null)
607            {
608              setFactoryCache(cache);
609            }
610          }
611          catch (Throwable e)
612          {
613            synchronized (failedModules)
614            {
615              if (failedModules.contains(cacheProviderClass) == false)
616              {
617                Log.warn("Failed to create factory cache: " + e.getLocalizedMessage());
618                failedModules.add(cacheProviderClass);
619              }
620            }
621          }
622        }
623      }
624    
625      public void registerDefaultLoaders()
626      {
627        final Configuration config = LibLoaderBoot.getInstance().getGlobalConfig();
628        final Iterator it = config.findPropertyKeys(LOADER_PREFIX);
629        while (it.hasNext())
630        {
631          final String key = (String) it.next();
632          final String value = config.getConfigProperty(key);
633          final Object o = ObjectUtilities.loadAndInstantiate(value, ResourceManager.class, ResourceLoader.class);
634          if (o != null)
635          {
636            final ResourceLoader loader = (ResourceLoader) o;
637            //Log.debug("Registering loader for " + loader.getSchema());
638            registerLoader(loader);
639          }
640        }
641      }
642    
643      public void registerLoader(final ResourceLoader loader)
644      {
645        if (loader == null)
646        {
647          throw new NullPointerException("ResourceLoader must not be null.");
648        }
649        loader.setResourceManager(this);
650        resourceLoaders.add(loader);
651      }
652    
653      public void registerFactory(final ResourceFactory factory)
654      {
655        if (factory == null)
656        {
657          throw new NullPointerException("ResourceFactory must not be null.");
658        }
659        resourceFactories.add(factory);
660      }
661    
662    }