View Javadoc

1   package org.apache.velocity.runtime.resource;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.util.ArrayList;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Vector;
26  
27  import org.apache.commons.collections.ExtendedProperties;
28  import org.apache.velocity.exception.ParseErrorException;
29  import org.apache.velocity.exception.ResourceNotFoundException;
30  import org.apache.velocity.exception.VelocityException;
31  import org.apache.velocity.runtime.RuntimeConstants;
32  import org.apache.velocity.runtime.RuntimeServices;
33  import org.apache.velocity.runtime.log.Log;
34  import org.apache.velocity.runtime.resource.loader.ResourceLoader;
35  import org.apache.velocity.runtime.resource.loader.ResourceLoaderFactory;
36  import org.apache.velocity.util.ClassUtils;
37  import org.apache.velocity.util.StringUtils;
38  
39  
40  /**
41   * Class to manage the text resource for the Velocity Runtime.
42   *
43   * @author  <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
44   * @author  <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
45   * @author  <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
46   * @author  <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
47   * @author  <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
48   * @version  $Id: ResourceManagerImpl.java 1043077 2010-12-07 15:01:25Z apetrelli $
49   */
50  public class ResourceManagerImpl
51      implements ResourceManager
52  {
53  
54      /** A template resources. */
55      public static final int RESOURCE_TEMPLATE = 1;
56  
57      /** A static content resource. */
58      public static final int RESOURCE_CONTENT = 2;
59  
60      /** token used to identify the loader internally. */
61      private static final String RESOURCE_LOADER_IDENTIFIER = "_RESOURCE_LOADER_IDENTIFIER_";
62  
63      /** Object implementing ResourceCache to be our resource manager's Resource cache. */
64      protected ResourceCache globalCache = null;
65  
66      /** The List of templateLoaders that the Runtime will use to locate the InputStream source of a template. */
67      protected final List resourceLoaders = new ArrayList();
68  
69      /**
70       * This is a list of the template input stream source initializers, basically properties for a particular template stream
71       * source. The order in this list reflects numbering of the properties i.e.
72       *
73       * <p>&lt;loader-id&gt;.resource.loader.&lt;property&gt; = &lt;value&gt;</p>
74       */
75      private final List sourceInitializerList = new ArrayList();
76  
77      /**
78       * Has this Manager been initialized?
79       */
80      private boolean isInit = false;
81  
82      /** switch to turn off log notice when a resource is found for the first time. */
83      private boolean logWhenFound = true;
84  
85      /** The internal RuntimeServices object. */
86      protected RuntimeServices rsvc = null;
87  
88      /** Logging. */
89      protected Log log = null;
90  
91      /**
92       * Initialize the ResourceManager.
93       *
94       * @param  rsvc  The Runtime Services object which is associated with this Resource Manager.
95       */
96      public synchronized void initialize(final RuntimeServices rsvc)
97      {
98          if (isInit)
99          {
100             log.debug("Re-initialization of ResourceLoader attempted and ignored.");
101             return;
102         }
103 
104         ResourceLoader resourceLoader = null;
105 
106         this.rsvc = rsvc;
107         log = rsvc.getLog();
108 
109         log.trace("ResourceManager initializing: %s", this.getClass());
110 
111         assembleResourceLoaderInitializers();
112 
113         for (Iterator it = sourceInitializerList.iterator(); it.hasNext();)
114         {
115             /**
116              * Resource loader can be loaded either via class name or be passed
117              * in as an instance.
118              */
119             ExtendedProperties configuration = (ExtendedProperties) it.next();
120 
121             String loaderClass = StringUtils.nullTrim(configuration.getString("class"));
122             ResourceLoader loaderInstance = (ResourceLoader) configuration.get("instance");
123 
124             if (loaderInstance != null)
125             {
126                 resourceLoader = loaderInstance;
127             }
128             else if (loaderClass != null)
129             {
130                 resourceLoader = ResourceLoaderFactory.getLoader(rsvc, loaderClass);
131             }
132             else
133             {
134                 String msg = "Unable to find '" +
135                           configuration.getString(RESOURCE_LOADER_IDENTIFIER) +
136                           ".resource.loader.class' specification in configuration." +
137                           " This is a critical value.  Please adjust configuration.";
138                 log.error(msg);
139                 throw new VelocityException(msg);
140             }
141 
142             resourceLoader.commonInit(rsvc, configuration);
143             resourceLoader.init(configuration);
144             resourceLoaders.add(resourceLoader);
145         }
146 
147         /*
148          * now see if this is overridden by configuration
149          */
150 
151         logWhenFound = rsvc.getBoolean(RuntimeConstants.RESOURCE_MANAGER_LOGWHENFOUND, true);
152 
153         /*
154          *  now, is a global cache specified?
155          */
156 
157         String cacheClassName = rsvc.getString(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS);
158 
159         Object cacheObject = null;
160 
161         if (org.apache.commons.lang.StringUtils.isNotEmpty(cacheClassName))
162         {
163             try
164             {
165                 cacheObject = ClassUtils.getNewInstance(cacheClassName);
166             }
167             catch (ClassNotFoundException cnfe)
168             {
169                 String msg = "The specified class for ResourceCache (" + cacheClassName +
170                           ") does not exist or is not accessible to the current classloader.";
171                 log.error(msg, cnfe);
172                 throw new VelocityException(msg, cnfe);
173             }
174             catch (IllegalAccessException ae)
175             {
176                 throw new VelocityException("Could not access class '"
177                     + cacheClassName + "'", ae);
178             }
179             catch (InstantiationException ie)
180             {
181                 throw new VelocityException("Could not instantiate class '"
182                     + cacheClassName + "'", ie);
183             }
184 
185             if (!(cacheObject instanceof ResourceCache))
186             {
187                 String msg = "The specified resource cache class (" + cacheClassName +
188                           ") must implement " + ResourceCache.class.getName();
189                 log.error(msg);
190                 throw new RuntimeException(msg);
191             }
192         }
193 
194         /*
195          *  if we didn't get through that, just use the default.
196          */
197         if (cacheObject == null)
198         {
199             cacheObject = new ResourceCacheImpl();
200         }
201 
202         globalCache = (ResourceCache) cacheObject;
203 
204         globalCache.initialize(rsvc);
205 
206         log.trace("Default ResourceManager initialization complete.");
207     }
208 
209     /**
210      * This will produce a List of Hashtables, each hashtable contains the intialization info for a particular resource loader. This
211      * Hashtable will be passed in when initializing the the template loader.
212      */
213     private void assembleResourceLoaderInitializers()
214     {
215         Vector resourceLoaderNames = rsvc.getConfiguration().getVector(RuntimeConstants.RESOURCE_LOADER);
216         StringUtils.trimStrings(resourceLoaderNames);
217 
218         for (Iterator it = resourceLoaderNames.iterator(); it.hasNext(); )
219         {
220 
221             /*
222              * The loader id might look something like the following:
223              *
224              * file.resource.loader
225              *
226              * The loader id is the prefix used for all properties
227              * pertaining to a particular loader.
228              */
229             String loaderName = (String) it.next();
230             StringBuffer loaderID = new StringBuffer(loaderName);
231             loaderID.append(".").append(RuntimeConstants.RESOURCE_LOADER);
232 
233             ExtendedProperties loaderConfiguration =
234         		rsvc.getConfiguration().subset(loaderID.toString());
235 
236             /*
237              *  we can't really count on ExtendedProperties to give us an empty set
238              */
239             if (loaderConfiguration == null)
240             {
241                 log.debug("ResourceManager : No configuration information found "+
242                           "for resource loader named '%s' (id is %s). Skipping it...",
243                           loaderName, loaderID);
244                 continue;
245             }
246 
247             /*
248              *  add the loader name token to the initializer if we need it
249              *  for reference later. We can't count on the user to fill
250              *  in the 'name' field
251              */
252 
253             loaderConfiguration.setProperty(RESOURCE_LOADER_IDENTIFIER, loaderName);
254 
255             /*
256              * Add resources to the list of resource loader
257              * initializers.
258              */
259             sourceInitializerList.add(loaderConfiguration);
260         }
261     }
262 
263     /**
264      * Gets the named resource. Returned class type corresponds to specified type (i.e. <code>Template</code> to <code>
265      * RESOURCE_TEMPLATE</code>).
266      *
267      * This method is now unsynchronized which requires that ResourceCache
268      * implementations be thread safe (as the default is).
269      *
270      * @param  resourceName  The name of the resource to retrieve.
271      * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
272      * @param  encoding  The character encoding to use.
273      *
274      * @return  Resource with the template parsed and ready.
275      *
276      * @throws  ResourceNotFoundException  if template not found from any available source.
277      * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
278      */
279     public Resource getResource(final String resourceName, final int resourceType, final String encoding)
280         throws ResourceNotFoundException,
281             ParseErrorException
282     {
283         /*
284          * Check to see if the resource was placed in the cache.
285          * If it was placed in the cache then we will use
286          * the cached version of the resource. If not we
287          * will load it.
288          *
289          * Note: the type is included in the key to differentiate ContentResource
290          * (static content from #include) with a Template.
291          */
292 
293         String resourceKey = resourceType + resourceName;
294         Resource resource = globalCache.get(resourceKey);
295 
296         if (resource != null)
297         {
298             try
299             {
300                 // avoids additional method call to refreshResource
301                 if (resource.requiresChecking())
302                 {
303                     /*
304                      * both loadResource() and refreshResource() now return
305                      * a new Resource instance when they are called
306                      * (put in the cache when appropriate) in order to allow
307                      * several threads to parse the same template simultaneously.
308                      * It is redundant work and will cause more garbage collection but the
309                      * benefit is that it allows concurrent parsing and processing
310                      * without race conditions when multiple requests try to
311                      * refresh/load the same template at the same time.
312                      *
313                      * Another alternative is to limit template parsing/retrieval
314                      * so that only one thread can parse each template at a time
315                      * but that creates a scalability bottleneck.
316                      *
317                      * See VELOCITY-606, VELOCITY-595 and VELOCITY-24
318                      */
319                     resource = refreshResource(resource, encoding);
320                 }
321             }
322             catch (ResourceNotFoundException rnfe)
323             {
324                 /*
325                  *  something exceptional happened to that resource
326                  *  this could be on purpose,
327                  *  so clear the cache and try again
328                  */
329 
330                 globalCache.remove(resourceKey);
331 
332                 return getResource(resourceName, resourceType, encoding);
333             }
334             catch (ParseErrorException pee)
335             {
336                 log.error("ResourceManager.getResource() exception", pee);
337                 throw pee;
338             }
339             catch (RuntimeException re)
340             {
341                 log.error("ResourceManager.getResource() exception", re);
342         	    throw re;
343             }
344         }
345         else
346         {
347             try
348             {
349                 /*
350                  *  it's not in the cache, so load it.
351                  */
352                 resource = loadResource(resourceName, resourceType, encoding);
353 
354                 if (resource.getResourceLoader().isCachingOn())
355                 {
356                     globalCache.put(resourceKey, resource);
357                 }
358             }
359             catch (ResourceNotFoundException rnfe)
360             {
361                 log.error("ResourceManager : unable to find resource '" +
362                           resourceName + "' in any resource loader.");
363                 throw rnfe;
364             }
365             catch (ParseErrorException pee)
366             {
367                 log.error("ResourceManager.getResource() parse exception", pee);
368                 throw pee;
369             }
370             catch (RuntimeException re)
371             {
372                 log.error("ResourceManager.getResource() load exception", re);
373                 throw re;
374             }
375         }
376 
377         return resource;
378     }
379 
380     /**
381      * Create a new Resource of the specified type.
382      *
383      * @param  resourceName  The name of the resource to retrieve.
384      * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
385      * @return  new instance of appropriate resource type
386      * @since 1.6
387      */
388     protected Resource createResource(String resourceName, int resourceType)
389     {
390         return ResourceFactory.getResource(resourceName, resourceType);
391     }
392 
393     /**
394      * Loads a resource from the current set of resource loaders.
395      *
396      * @param  resourceName  The name of the resource to retrieve.
397      * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
398      * @param  encoding  The character encoding to use.
399      *
400      * @return  Resource with the template parsed and ready.
401      *
402      * @throws  ResourceNotFoundException  if template not found from any available source.
403      * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
404      */
405     protected Resource loadResource(String resourceName, int resourceType, String encoding)
406         throws ResourceNotFoundException,
407             ParseErrorException
408     {
409         Resource resource = createResource(resourceName, resourceType);
410         resource.setRuntimeServices(rsvc);
411         resource.setName(resourceName);
412         resource.setEncoding(encoding);
413 
414         /*
415          * Now we have to try to find the appropriate
416          * loader for this resource. We have to cycle through
417          * the list of available resource loaders and see
418          * which one gives us a stream that we can use to
419          * make a resource with.
420          */
421 
422         long howOldItWas = 0;
423 
424         for (Iterator it = resourceLoaders.iterator(); it.hasNext();)
425         {
426             ResourceLoader resourceLoader = (ResourceLoader) it.next();
427             resource.setResourceLoader(resourceLoader);
428 
429             /*
430              *  catch the ResourceNotFound exception
431              *  as that is ok in our new multi-loader environment
432              */
433 
434             try
435             {
436 
437                 if (resource.process())
438                 {
439                     /*
440                      *  FIXME  (gmj)
441                      *  moved in here - technically still
442                      *  a problem - but the resource needs to be
443                      *  processed before the loader can figure
444                      *  it out due to to the new
445                      *  multi-path support - will revisit and fix
446                      */
447 
448                     if (logWhenFound && log.isDebugEnabled())
449                     {
450                         log.debug("ResourceManager: found %s with loader ",
451                                   resourceName, resourceLoader.getClassName());
452                     }
453 
454                     howOldItWas = resourceLoader.getLastModified(resource);
455 
456                     break;
457                 }
458             }
459             catch (ResourceNotFoundException rnfe)
460             {
461                 /*
462                  *  that's ok - it's possible to fail in
463                  *  multi-loader environment
464                  */
465             }
466         }
467 
468         /*
469          * Return null if we can't find a resource.
470          */
471         if (resource.getData() == null)
472         {
473             throw new ResourceNotFoundException("Unable to find resource '" + resourceName + "'");
474         }
475 
476         /*
477          *  some final cleanup
478          */
479 
480         resource.setLastModified(howOldItWas);
481         resource.setModificationCheckInterval(resource.getResourceLoader().getModificationCheckInterval());
482 
483         resource.touch();
484 
485         return resource;
486     }
487 
488     /**
489      * Takes an existing resource, and 'refreshes' it. This generally means that the source of the resource is checked for changes
490      * according to some cache/check algorithm and if the resource changed, then the resource data is reloaded and re-parsed.
491      *
492      * @param  resource  resource to refresh
493      * @param  encoding  character encoding of the resource to refresh.
494      *
495      * @throws  ResourceNotFoundException  if template not found from current source for this Resource
496      * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
497      */
498     protected Resource refreshResource(Resource resource, final String encoding)
499         throws ResourceNotFoundException, ParseErrorException
500     {
501         /*
502          * The resource knows whether it needs to be checked
503          * or not, and the resource's loader can check to
504          * see if the source has been modified. If both
505          * these conditions are true then we must reload
506          * the input stream and parse it to make a new
507          * AST for the resource.
508          */
509 
510         /*
511          *  touch() the resource to reset the counters
512          */
513         resource.touch();
514 
515         /* check whether this can now be found in a higher priority
516          * resource loader.  if so, pass the request off to loadResource.
517          */
518         ResourceLoader loader = resource.getResourceLoader();
519         if (resourceLoaders.size() > 0 && resourceLoaders.indexOf(loader) > 0)
520         {
521             String name = resource.getName();
522             if (loader != getLoaderForResource(name))
523             {
524                 return loadResource(name, resource.getType(), encoding);
525             }
526         }
527 
528         if (resource.isSourceModified())
529         {
530             /*
531              *  now check encoding info.  It's possible that the newly declared
532              *  encoding is different than the encoding already in the resource
533              *  this strikes me as bad...
534              */
535 
536             if (!org.apache.commons.lang.StringUtils.equals(resource.getEncoding(), encoding))
537             {
538                 log.warn("Declared encoding for template '%s' is different on reload. Old = '%s' New = '%s'",
539                          resource.getName(), resource.getEncoding(), encoding);
540                 resource.setEncoding(encoding);
541             }
542 
543             /*
544              *  read how old the resource is _before_
545              *  processing (=>reading) it
546              */
547             long howOldItWas = loader.getLastModified(resource);
548 
549             String resourceKey = resource.getType() + resource.getName();
550 
551             /*
552              * we create a copy to avoid partially overwriting a
553              * template which may be in use in another thread
554              */
555 
556             Resource newResource =
557                 ResourceFactory.getResource(resource.getName(), resource.getType());
558 
559             newResource.setRuntimeServices(rsvc);
560             newResource.setName(resource.getName());
561             newResource.setEncoding(resource.getEncoding());
562             newResource.setResourceLoader(loader);
563             newResource.setModificationCheckInterval(loader.getModificationCheckInterval());
564 
565             newResource.process();
566             newResource.setLastModified(howOldItWas);
567             resource = newResource;
568 
569             globalCache.put(resourceKey, newResource);
570         }
571         return resource;
572     }
573 
574     /**
575      * Determines if a template exists, and returns name of the loader that provides it. This is a slightly less hokey way to
576      * support the Velocity.templateExists() utility method, which was broken when per-template encoding was introduced. We can
577      * revisit this.
578      *
579      * @param  resourceName  Name of template or content resource
580      *
581      * @return  class name of loader than can provide it
582      */
583     public String getLoaderNameForResource(String resourceName)
584     {
585         ResourceLoader loader = getLoaderForResource(resourceName);
586         if (loader == null)
587         {
588             return null;
589         }
590         return loader.getClass().toString();
591     }
592 
593     /**
594      * Returns the first {@link ResourceLoader} in which the specified
595      * resource exists.
596      */
597     private ResourceLoader getLoaderForResource(String resourceName)
598     {
599         for (Iterator i = resourceLoaders.iterator(); i.hasNext(); )
600         {
601             ResourceLoader loader = (ResourceLoader)i.next();
602             if (loader.resourceExists(resourceName))
603             {
604                 return loader;
605             }
606         }
607         return null;
608     }
609 
610 }