View Javadoc
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 }