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