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