1 /* 2 * Copyright 2002-2013 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.ui.velocity; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.util.HashMap; 22 import java.util.Map; 23 import java.util.Properties; 24 25 import org.apache.commons.logging.Log; 26 import org.apache.commons.logging.LogFactory; 27 import org.apache.velocity.app.VelocityEngine; 28 import org.apache.velocity.exception.VelocityException; 29 import org.apache.velocity.runtime.RuntimeConstants; 30 import org.apache.velocity.runtime.log.CommonsLogLogChute; 31 32 import org.springframework.core.io.DefaultResourceLoader; 33 import org.springframework.core.io.Resource; 34 import org.springframework.core.io.ResourceLoader; 35 import org.springframework.core.io.support.PropertiesLoaderUtils; 36 import org.springframework.util.CollectionUtils; 37 import org.springframework.util.StringUtils; 38 39 /** 40 * Factory that configures a VelocityEngine. Can be used standalone, 41 * but typically you will either use {@link VelocityEngineFactoryBean} 42 * for preparing a VelocityEngine as bean reference, or 43 * {@link org.springframework.web.servlet.view.velocity.VelocityConfigurer} 44 * for web views. 45 * 46 * <p>The optional "configLocation" property sets the location of the Velocity 47 * properties file, within the current application. Velocity properties can be 48 * overridden via "velocityProperties", or even completely specified locally, 49 * avoiding the need for an external properties file. 50 * 51 * <p>The "resourceLoaderPath" property can be used to specify the Velocity 52 * resource loader path via Spring's Resource abstraction, possibly relative 53 * to the Spring application context. 54 * 55 * <p>If "overrideLogging" is true (the default), the VelocityEngine will be 56 * configured to log via Commons Logging, that is, using 57 * {@link CommonsLogLogChute} as log system. 58 * 59 * <p>The simplest way to use this class is to specify a 60 * {@link #setResourceLoaderPath(String) "resourceLoaderPath"}; the 61 * VelocityEngine typically then does not need any further configuration. 62 * 63 * @author Juergen Hoeller 64 * @see #setConfigLocation 65 * @see #setVelocityProperties 66 * @see #setResourceLoaderPath 67 * @see #setOverrideLogging 68 * @see #createVelocityEngine 69 * @see VelocityEngineFactoryBean 70 * @see org.springframework.web.servlet.view.velocity.VelocityConfigurer 71 * @see org.apache.velocity.app.VelocityEngine 72 */ 73 public class VelocityEngineFactory { 74 75 protected final Log logger = LogFactory.getLog(getClass()); 76 77 private Resource configLocation; 78 79 private final Map<String, Object> velocityProperties = new HashMap<String, Object>(); 80 81 private String resourceLoaderPath; 82 83 private ResourceLoader resourceLoader = new DefaultResourceLoader(); 84 85 private boolean preferFileSystemAccess = true; 86 87 private boolean overrideLogging = true; 88 89 90 /** 91 * Set the location of the Velocity config file. 92 * Alternatively, you can specify all properties locally. 93 * @see #setVelocityProperties 94 * @see #setResourceLoaderPath 95 */ 96 public void setConfigLocation(Resource configLocation) { 97 this.configLocation = configLocation; 98 } 99 100 /** 101 * Set Velocity properties, like "file.resource.loader.path". 102 * Can be used to override values in a Velocity config file, 103 * or to specify all necessary properties locally. 104 * <p>Note that the Velocity resource loader path also be set to any 105 * Spring resource location via the "resourceLoaderPath" property. 106 * Setting it here is just necessary when using a non-file-based 107 * resource loader. 108 * @see #setVelocityPropertiesMap 109 * @see #setConfigLocation 110 * @see #setResourceLoaderPath 111 */ 112 public void setVelocityProperties(Properties velocityProperties) { 113 CollectionUtils.mergePropertiesIntoMap(velocityProperties, this.velocityProperties); 114 } 115 116 /** 117 * Set Velocity properties as Map, to allow for non-String values 118 * like "ds.resource.loader.instance". 119 * @see #setVelocityProperties 120 */ 121 public void setVelocityPropertiesMap(Map<String, Object> velocityPropertiesMap) { 122 if (velocityPropertiesMap != null) { 123 this.velocityProperties.putAll(velocityPropertiesMap); 124 } 125 } 126 127 /** 128 * Set the Velocity resource loader path via a Spring resource location. 129 * Accepts multiple locations in Velocity's comma-separated path style. 130 * <p>When populated via a String, standard URLs like "file:" and "classpath:" 131 * pseudo URLs are supported, as understood by ResourceLoader. Allows for 132 * relative paths when running in an ApplicationContext. 133 * <p>Will define a path for the default Velocity resource loader with the name 134 * "file". If the specified resource cannot be resolved to a {@code java.io.File}, 135 * a generic SpringResourceLoader will be used under the name "spring", without 136 * modification detection. 137 * <p>Note that resource caching will be enabled in any case. With the file 138 * resource loader, the last-modified timestamp will be checked on access to 139 * detect changes. With SpringResourceLoader, the resource will be cached 140 * forever (for example for class path resources). 141 * <p>To specify a modification check interval for files, use Velocity's 142 * standard "file.resource.loader.modificationCheckInterval" property. By default, 143 * the file timestamp is checked on every access (which is surprisingly fast). 144 * Of course, this just applies when loading resources from the file system. 145 * <p>To enforce the use of SpringResourceLoader, i.e. to not resolve a path 146 * as file system resource in any case, turn off the "preferFileSystemAccess" 147 * flag. See the latter's javadoc for details. 148 * @see #setResourceLoader 149 * @see #setVelocityProperties 150 * @see #setPreferFileSystemAccess 151 * @see SpringResourceLoader 152 * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader 153 */ 154 public void setResourceLoaderPath(String resourceLoaderPath) { 155 this.resourceLoaderPath = resourceLoaderPath; 156 } 157 158 /** 159 * Set the Spring ResourceLoader to use for loading Velocity template files. 160 * The default is DefaultResourceLoader. Will get overridden by the 161 * ApplicationContext if running in a context. 162 * @see org.springframework.core.io.DefaultResourceLoader 163 * @see org.springframework.context.ApplicationContext 164 */ 165 public void setResourceLoader(ResourceLoader resourceLoader) { 166 this.resourceLoader = resourceLoader; 167 } 168 169 /** 170 * Return the Spring ResourceLoader to use for loading Velocity template files. 171 */ 172 protected ResourceLoader getResourceLoader() { 173 return this.resourceLoader; 174 } 175 176 /** 177 * Set whether to prefer file system access for template loading. 178 * File system access enables hot detection of template changes. 179 * <p>If this is enabled, VelocityEngineFactory will try to resolve the 180 * specified "resourceLoaderPath" as file system resource (which will work 181 * for expanded class path resources and ServletContext resources too). 182 * <p>Default is "true". Turn this off to always load via SpringResourceLoader 183 * (i.e. as stream, without hot detection of template changes), which might 184 * be necessary if some of your templates reside in an expanded classes 185 * directory while others reside in jar files. 186 * @see #setResourceLoaderPath 187 */ 188 public void setPreferFileSystemAccess(boolean preferFileSystemAccess) { 189 this.preferFileSystemAccess = preferFileSystemAccess; 190 } 191 192 /** 193 * Return whether to prefer file system access for template loading. 194 */ 195 protected boolean isPreferFileSystemAccess() { 196 return this.preferFileSystemAccess; 197 } 198 199 /** 200 * Set whether Velocity should log via Commons Logging, i.e. whether Velocity's 201 * log system should be set to {@link CommonsLogLogChute}. Default is "true". 202 */ 203 public void setOverrideLogging(boolean overrideLogging) { 204 this.overrideLogging = overrideLogging; 205 } 206 207 208 /** 209 * Prepare the VelocityEngine instance and return it. 210 * @return the VelocityEngine instance 211 * @throws IOException if the config file wasn't found 212 * @throws VelocityException on Velocity initialization failure 213 */ 214 public VelocityEngine createVelocityEngine() throws IOException, VelocityException { 215 VelocityEngine velocityEngine = newVelocityEngine(); 216 Map<String, Object> props = new HashMap<String, Object>(); 217 218 // Load config file if set. 219 if (this.configLocation != null) { 220 if (logger.isInfoEnabled()) { 221 logger.info("Loading Velocity config from [" + this.configLocation + "]"); 222 } 223 CollectionUtils.mergePropertiesIntoMap(PropertiesLoaderUtils.loadProperties(this.configLocation), props); 224 } 225 226 // Merge local properties if set. 227 if (!this.velocityProperties.isEmpty()) { 228 props.putAll(this.velocityProperties); 229 } 230 231 // Set a resource loader path, if required. 232 if (this.resourceLoaderPath != null) { 233 initVelocityResourceLoader(velocityEngine, this.resourceLoaderPath); 234 } 235 236 // Log via Commons Logging? 237 if (this.overrideLogging) { 238 velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new CommonsLogLogChute()); 239 } 240 241 // Apply properties to VelocityEngine. 242 for (Map.Entry<String, Object> entry : props.entrySet()) { 243 velocityEngine.setProperty(entry.getKey(), entry.getValue()); 244 } 245 246 postProcessVelocityEngine(velocityEngine); 247 248 // Perform actual initialization. 249 velocityEngine.init(); 250 251 return velocityEngine; 252 } 253 254 /** 255 * Return a new VelocityEngine. Subclasses can override this for 256 * custom initialization, or for using a mock object for testing. 257 * <p>Called by {@code createVelocityEngine()}. 258 * @return the VelocityEngine instance 259 * @throws IOException if a config file wasn't found 260 * @throws VelocityException on Velocity initialization failure 261 * @see #createVelocityEngine() 262 */ 263 protected VelocityEngine newVelocityEngine() throws IOException, VelocityException { 264 return new VelocityEngine(); 265 } 266 267 /** 268 * Initialize a Velocity resource loader for the given VelocityEngine: 269 * either a standard Velocity FileResourceLoader or a SpringResourceLoader. 270 * <p>Called by {@code createVelocityEngine()}. 271 * @param velocityEngine the VelocityEngine to configure 272 * @param resourceLoaderPath the path to load Velocity resources from 273 * @see org.apache.velocity.runtime.resource.loader.FileResourceLoader 274 * @see SpringResourceLoader 275 * @see #initSpringResourceLoader 276 * @see #createVelocityEngine() 277 */ 278 protected void initVelocityResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) { 279 if (isPreferFileSystemAccess()) { 280 // Try to load via the file system, fall back to SpringResourceLoader 281 // (for hot detection of template changes, if possible). 282 try { 283 StringBuilder resolvedPath = new StringBuilder(); 284 String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath); 285 for (int i = 0; i < paths.length; i++) { 286 String path = paths[i]; 287 Resource resource = getResourceLoader().getResource(path); 288 File file = resource.getFile(); // will fail if not resolvable in the file system 289 if (logger.isDebugEnabled()) { 290 logger.debug("Resource loader path [" + path + "] resolved to file [" + file.getAbsolutePath() + "]"); 291 } 292 resolvedPath.append(file.getAbsolutePath()); 293 if (i < paths.length - 1) { 294 resolvedPath.append(','); 295 } 296 } 297 velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file"); 298 velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_CACHE, "true"); 299 velocityEngine.setProperty(RuntimeConstants.FILE_RESOURCE_LOADER_PATH, resolvedPath.toString()); 300 } 301 catch (IOException ex) { 302 if (logger.isDebugEnabled()) { 303 logger.debug("Cannot resolve resource loader path [" + resourceLoaderPath + 304 "] to [java.io.File]: using SpringResourceLoader", ex); 305 } 306 initSpringResourceLoader(velocityEngine, resourceLoaderPath); 307 } 308 } 309 else { 310 // Always load via SpringResourceLoader 311 // (without hot detection of template changes). 312 if (logger.isDebugEnabled()) { 313 logger.debug("File system access not preferred: using SpringResourceLoader"); 314 } 315 initSpringResourceLoader(velocityEngine, resourceLoaderPath); 316 } 317 } 318 319 /** 320 * Initialize a SpringResourceLoader for the given VelocityEngine. 321 * <p>Called by {@code initVelocityResourceLoader}. 322 * @param velocityEngine the VelocityEngine to configure 323 * @param resourceLoaderPath the path to load Velocity resources from 324 * @see SpringResourceLoader 325 * @see #initVelocityResourceLoader 326 */ 327 protected void initSpringResourceLoader(VelocityEngine velocityEngine, String resourceLoaderPath) { 328 velocityEngine.setProperty( 329 RuntimeConstants.RESOURCE_LOADER, SpringResourceLoader.NAME); 330 velocityEngine.setProperty( 331 SpringResourceLoader.SPRING_RESOURCE_LOADER_CLASS, SpringResourceLoader.class.getName()); 332 velocityEngine.setProperty( 333 SpringResourceLoader.SPRING_RESOURCE_LOADER_CACHE, "true"); 334 velocityEngine.setApplicationAttribute( 335 SpringResourceLoader.SPRING_RESOURCE_LOADER, getResourceLoader()); 336 velocityEngine.setApplicationAttribute( 337 SpringResourceLoader.SPRING_RESOURCE_LOADER_PATH, resourceLoaderPath); 338 } 339 340 /** 341 * To be implemented by subclasses that want to to perform custom 342 * post-processing of the VelocityEngine after this FactoryBean 343 * performed its default configuration (but before VelocityEngine.init). 344 * <p>Called by {@code createVelocityEngine()}. 345 * @param velocityEngine the current VelocityEngine 346 * @throws IOException if a config file wasn't found 347 * @throws VelocityException on Velocity initialization failure 348 * @see #createVelocityEngine() 349 * @see org.apache.velocity.app.VelocityEngine#init 350 */ 351 protected void postProcessVelocityEngine(VelocityEngine velocityEngine) 352 throws IOException, VelocityException { 353 } 354 355 }