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