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 745757 2009-02-19 06:48:10Z nbubna $
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><loader-id>.resource.loader.<property> = <value></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("Default ResourceManager initializing. (" + 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 '" + loaderName +
243 "' (id is "+loaderID+"). Skipping it...");
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 " + resourceName +
451 " with loader " +
452 resourceLoader.getClassName());
453 }
454
455 howOldItWas = resourceLoader.getLastModified(resource);
456
457 break;
458 }
459 }
460 catch (ResourceNotFoundException rnfe)
461 {
462 /*
463 * that's ok - it's possible to fail in
464 * multi-loader environment
465 */
466 }
467 }
468
469 /*
470 * Return null if we can't find a resource.
471 */
472 if (resource.getData() == null)
473 {
474 throw new ResourceNotFoundException("Unable to find resource '" + resourceName + "'");
475 }
476
477 /*
478 * some final cleanup
479 */
480
481 resource.setLastModified(howOldItWas);
482 resource.setModificationCheckInterval(resource.getResourceLoader().getModificationCheckInterval());
483
484 resource.touch();
485
486 return resource;
487 }
488
489 /**
490 * Takes an existing resource, and 'refreshes' it. This generally means that the source of the resource is checked for changes
491 * according to some cache/check algorithm and if the resource changed, then the resource data is reloaded and re-parsed.
492 *
493 * @param resource resource to refresh
494 * @param encoding character encoding of the resource to refresh.
495 *
496 * @throws ResourceNotFoundException if template not found from current source for this Resource
497 * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
498 */
499 protected Resource refreshResource(Resource resource, final String encoding)
500 throws ResourceNotFoundException, ParseErrorException
501 {
502 /*
503 * The resource knows whether it needs to be checked
504 * or not, and the resource's loader can check to
505 * see if the source has been modified. If both
506 * these conditions are true then we must reload
507 * the input stream and parse it to make a new
508 * AST for the resource.
509 */
510
511 /*
512 * touch() the resource to reset the counters
513 */
514 resource.touch();
515
516 /* check whether this can now be found in a higher priority
517 * resource loader. if so, pass the request off to loadResource.
518 */
519 ResourceLoader loader = resource.getResourceLoader();
520 if (resourceLoaders.size() > 0 && resourceLoaders.indexOf(loader) > 0)
521 {
522 String name = resource.getName();
523 if (loader != getLoaderForResource(name))
524 {
525 return loadResource(name, resource.getType(), encoding);
526 }
527 }
528
529 if (resource.isSourceModified())
530 {
531 /*
532 * now check encoding info. It's possible that the newly declared
533 * encoding is different than the encoding already in the resource
534 * this strikes me as bad...
535 */
536
537 if (!org.apache.commons.lang.StringUtils.equals(resource.getEncoding(), encoding))
538 {
539 log.warn("Declared encoding for template '" +
540 resource.getName() +
541 "' is different on reload. Old = '" +
542 resource.getEncoding() + "' New = '" + encoding);
543
544 resource.setEncoding(encoding);
545 }
546
547 /*
548 * read how old the resource is _before_
549 * processing (=>reading) it
550 */
551 long howOldItWas = loader.getLastModified(resource);
552
553 String resourceKey = resource.getType() + resource.getName();
554
555 /*
556 * we create a copy to avoid partially overwriting a
557 * template which may be in use in another thread
558 */
559
560 Resource newResource =
561 ResourceFactory.getResource(resource.getName(), resource.getType());
562
563 newResource.setRuntimeServices(rsvc);
564 newResource.setName(resource.getName());
565 newResource.setEncoding(resource.getEncoding());
566 newResource.setResourceLoader(loader);
567 newResource.setModificationCheckInterval(loader.getModificationCheckInterval());
568
569 newResource.process();
570 newResource.setLastModified(howOldItWas);
571 resource = newResource;
572
573 globalCache.put(resourceKey, newResource);
574 }
575 return resource;
576 }
577
578 /**
579 * Gets the named resource. Returned class type corresponds to specified type (i.e. <code>Template</code> to <code>
580 * RESOURCE_TEMPLATE</code>).
581 *
582 * @param resourceName The name of the resource to retrieve.
583 * @param resourceType The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
584 *
585 * @return Resource with the template parsed and ready.
586 *
587 * @throws ResourceNotFoundException if template not found from any available source.
588 * @throws ParseErrorException if template cannot be parsed due to syntax (or other) error.
589 * @throws Exception if a problem in parse
590 *
591 * @deprecated Use {@link #getResource(String resourceName, int resourceType, String encoding )}
592 */
593 public Resource getResource(String resourceName, int resourceType)
594 throws ResourceNotFoundException,
595 ParseErrorException,
596 Exception
597 {
598 return getResource(resourceName, resourceType, RuntimeConstants.ENCODING_DEFAULT);
599 }
600
601 /**
602 * Determines if a template exists, and returns name of the loader that provides it. This is a slightly less hokey way to
603 * support the Velocity.templateExists() utility method, which was broken when per-template encoding was introduced. We can
604 * revisit this.
605 *
606 * @param resourceName Name of template or content resource
607 *
608 * @return class name of loader than can provide it
609 */
610 public String getLoaderNameForResource(String resourceName)
611 {
612 ResourceLoader loader = getLoaderForResource(resourceName);
613 if (loader == null)
614 {
615 return null;
616 }
617 return loader.getClass().toString();
618 }
619
620 /**
621 * Returns the first {@link ResourceLoader} in which the specified
622 * resource exists.
623 */
624 private ResourceLoader getLoaderForResource(String resourceName)
625 {
626 for (Iterator i = resourceLoaders.iterator(); i.hasNext(); )
627 {
628 ResourceLoader loader = (ResourceLoader)i.next();
629 if (loader.resourceExists(resourceName))
630 {
631 return loader;
632 }
633 }
634 return null;
635 }
636
637 }