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><loader-id>.resource.loader.<property> = <value></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 }