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