View Javadoc
1   /*
2    * Copyright 2002-2014 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.freemarker;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.LinkedList;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Properties;
27  
28  import freemarker.cache.FileTemplateLoader;
29  import freemarker.cache.MultiTemplateLoader;
30  import freemarker.cache.TemplateLoader;
31  import freemarker.template.Configuration;
32  import freemarker.template.SimpleHash;
33  import freemarker.template.TemplateException;
34  import org.apache.commons.logging.Log;
35  import org.apache.commons.logging.LogFactory;
36  
37  import org.springframework.core.io.DefaultResourceLoader;
38  import org.springframework.core.io.Resource;
39  import org.springframework.core.io.ResourceLoader;
40  import org.springframework.core.io.support.PropertiesLoaderUtils;
41  import org.springframework.util.CollectionUtils;
42  
43  /**
44   * Factory that configures a FreeMarker Configuration. Can be used standalone, but
45   * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a
46   * Configuration as bean reference, or FreeMarkerConfigurer for web views.
47   *
48   * <p>The optional "configLocation" property sets the location of a FreeMarker
49   * properties file, within the current application. FreeMarker properties can be
50   * overridden via "freemarkerSettings". All of these properties will be set by
51   * calling FreeMarker's {@code Configuration.setSettings()} method and are
52   * subject to constraints set by FreeMarker.
53   *
54   * <p>The "freemarkerVariables" property can be used to specify a Map of
55   * shared variables that will be applied to the Configuration via the
56   * {@code setAllSharedVariables()} method. Like {@code setSettings()},
57   * these entries are subject to FreeMarker constraints.
58   *
59   * <p>The simplest way to use this class is to specify a "templateLoaderPath";
60   * FreeMarker does not need any further configuration then.
61   *
62   * <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
63   *
64   * @author Darren Davison
65   * @author Juergen Hoeller
66   * @since 03.03.2004
67   * @see #setConfigLocation
68   * @see #setFreemarkerSettings
69   * @see #setFreemarkerVariables
70   * @see #setTemplateLoaderPath
71   * @see #createConfiguration
72   * @see FreeMarkerConfigurationFactoryBean
73   * @see org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
74   * @see freemarker.template.Configuration
75   */
76  public class FreeMarkerConfigurationFactory {
77  
78  	protected final Log logger = LogFactory.getLog(getClass());
79  
80  	private Resource configLocation;
81  
82  	private Properties freemarkerSettings;
83  
84  	private Map<String, Object> freemarkerVariables;
85  
86  	private String defaultEncoding;
87  
88  	private final List<TemplateLoader> templateLoaders = new ArrayList<TemplateLoader>();
89  
90  	private List<TemplateLoader> preTemplateLoaders;
91  
92  	private List<TemplateLoader> postTemplateLoaders;
93  
94  	private String[] templateLoaderPaths;
95  
96  	private ResourceLoader resourceLoader = new DefaultResourceLoader();
97  
98  	private boolean preferFileSystemAccess = true;
99  
100 
101 	/**
102 	 * Set the location of the FreeMarker config file.
103 	 * Alternatively, you can specify all setting locally.
104 	 * @see #setFreemarkerSettings
105 	 * @see #setTemplateLoaderPath
106 	 */
107 	public void setConfigLocation(Resource resource) {
108 		configLocation = resource;
109 	}
110 
111 	/**
112 	 * Set properties that contain well-known FreeMarker keys which will be
113 	 * passed to FreeMarker's {@code Configuration.setSettings} method.
114 	 * @see freemarker.template.Configuration#setSettings
115 	 */
116 	public void setFreemarkerSettings(Properties settings) {
117 		this.freemarkerSettings = settings;
118 	}
119 
120 	/**
121 	 * Set a Map that contains well-known FreeMarker objects which will be passed
122 	 * to FreeMarker's {@code Configuration.setAllSharedVariables()} method.
123 	 * @see freemarker.template.Configuration#setAllSharedVariables
124 	 */
125 	public void setFreemarkerVariables(Map<String, Object> variables) {
126 		this.freemarkerVariables = variables;
127 	}
128 
129 	/**
130 	 * Set the default encoding for the FreeMarker configuration.
131 	 * If not specified, FreeMarker will use the platform file encoding.
132 	 * <p>Used for template rendering unless there is an explicit encoding specified
133 	 * for the rendering process (for example, on Spring's FreeMarkerView).
134 	 * @see freemarker.template.Configuration#setDefaultEncoding
135 	 * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding
136 	 */
137 	public void setDefaultEncoding(String defaultEncoding) {
138 		this.defaultEncoding = defaultEncoding;
139 	}
140 
141 	/**
142 	 * Set a List of {@code TemplateLoader}s that will be used to search
143 	 * for templates. For example, one or more custom loaders such as database
144 	 * loaders could be configured and injected here.
145 	 * <p>The {@link TemplateLoader TemplateLoaders} specified here will be
146 	 * registered <i>before</i> the default template loaders that this factory
147 	 * registers (such as loaders for specified "templateLoaderPaths" or any
148 	 * loaders registered in {@link #postProcessTemplateLoaders}).
149 	 * @see #setTemplateLoaderPaths
150 	 * @see #postProcessTemplateLoaders
151 	 */
152 	public void setPreTemplateLoaders(TemplateLoader... preTemplateLoaders) {
153 		this.preTemplateLoaders = Arrays.asList(preTemplateLoaders);
154 	}
155 
156 	/**
157 	 * Set a List of {@code TemplateLoader}s that will be used to search
158 	 * for templates. For example, one or more custom loaders such as database
159 	 * loaders can be configured.
160 	 * <p>The {@link TemplateLoader TemplateLoaders} specified here will be
161 	 * registered <i>after</i> the default template loaders that this factory
162 	 * registers (such as loaders for specified "templateLoaderPaths" or any
163 	 * loaders registered in {@link #postProcessTemplateLoaders}).
164 	 * @see #setTemplateLoaderPaths
165 	 * @see #postProcessTemplateLoaders
166 	 */
167 	public void setPostTemplateLoaders(TemplateLoader... postTemplateLoaders) {
168 		this.postTemplateLoaders = Arrays.asList(postTemplateLoaders);
169 	}
170 
171 	/**
172 	 * Set the Freemarker template loader path via a Spring resource location.
173 	 * See the "templateLoaderPaths" property for details on path handling.
174 	 * @see #setTemplateLoaderPaths
175 	 */
176 	public void setTemplateLoaderPath(String templateLoaderPath) {
177 		this.templateLoaderPaths = new String[] {templateLoaderPath};
178 	}
179 
180 	/**
181 	 * Set multiple Freemarker template loader paths via Spring resource locations.
182 	 * <p>When populated via a String, standard URLs like "file:" and "classpath:"
183 	 * pseudo URLs are supported, as understood by ResourceEditor. Allows for
184 	 * relative paths when running in an ApplicationContext.
185 	 * <p>Will define a path for the default FreeMarker template loader.
186 	 * If a specified resource cannot be resolved to a {@code java.io.File},
187 	 * a generic SpringTemplateLoader will be used, without modification detection.
188 	 * <p>To enforce the use of SpringTemplateLoader, i.e. to not resolve a path
189 	 * as file system resource in any case, turn off the "preferFileSystemAccess"
190 	 * flag. See the latter's javadoc for details.
191 	 * <p>If you wish to specify your own list of TemplateLoaders, do not set this
192 	 * property and instead use {@code setTemplateLoaders(List templateLoaders)}
193 	 * @see org.springframework.core.io.ResourceEditor
194 	 * @see org.springframework.context.ApplicationContext#getResource
195 	 * @see freemarker.template.Configuration#setDirectoryForTemplateLoading
196 	 * @see SpringTemplateLoader
197 	 */
198 	public void setTemplateLoaderPaths(String... templateLoaderPaths) {
199 		this.templateLoaderPaths = templateLoaderPaths;
200 	}
201 
202 	/**
203 	 * Set the Spring ResourceLoader to use for loading FreeMarker template files.
204 	 * The default is DefaultResourceLoader. Will get overridden by the
205 	 * ApplicationContext if running in a context.
206 	 * @see org.springframework.core.io.DefaultResourceLoader
207 	 */
208 	public void setResourceLoader(ResourceLoader resourceLoader) {
209 		this.resourceLoader = resourceLoader;
210 	}
211 
212 	/**
213 	 * Return the Spring ResourceLoader to use for loading FreeMarker template files.
214 	 */
215 	protected ResourceLoader getResourceLoader() {
216 		return this.resourceLoader;
217 	}
218 
219 	/**
220 	 * Set whether to prefer file system access for template loading.
221 	 * File system access enables hot detection of template changes.
222 	 * <p>If this is enabled, FreeMarkerConfigurationFactory will try to resolve
223 	 * the specified "templateLoaderPath" as file system resource (which will work
224 	 * for expanded class path resources and ServletContext resources too).
225 	 * <p>Default is "true". Turn this off to always load via SpringTemplateLoader
226 	 * (i.e. as stream, without hot detection of template changes), which might
227 	 * be necessary if some of your templates reside in an expanded classes
228 	 * directory while others reside in jar files.
229 	 * @see #setTemplateLoaderPath
230 	 */
231 	public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
232 		this.preferFileSystemAccess = preferFileSystemAccess;
233 	}
234 
235 	/**
236 	 * Return whether to prefer file system access for template loading.
237 	 */
238 	protected boolean isPreferFileSystemAccess() {
239 		return this.preferFileSystemAccess;
240 	}
241 
242 
243 	/**
244 	 * Prepare the FreeMarker Configuration and return it.
245 	 * @return the FreeMarker Configuration object
246 	 * @throws IOException if the config file wasn't found
247 	 * @throws TemplateException on FreeMarker initialization failure
248 	 */
249 	public Configuration createConfiguration() throws IOException, TemplateException {
250 		Configuration config = newConfiguration();
251 		Properties props = new Properties();
252 
253 		// Load config file if specified.
254 		if (this.configLocation != null) {
255 			if (logger.isInfoEnabled()) {
256 				logger.info("Loading FreeMarker configuration from " + this.configLocation);
257 			}
258 			PropertiesLoaderUtils.fillProperties(props, this.configLocation);
259 		}
260 
261 		// Merge local properties if specified.
262 		if (this.freemarkerSettings != null) {
263 			props.putAll(this.freemarkerSettings);
264 		}
265 
266 		// FreeMarker will only accept known keys in its setSettings and
267 		// setAllSharedVariables methods.
268 		if (!props.isEmpty()) {
269 			config.setSettings(props);
270 		}
271 
272 		if (!CollectionUtils.isEmpty(this.freemarkerVariables)) {
273 			config.setAllSharedVariables(new SimpleHash(this.freemarkerVariables, config.getObjectWrapper()));
274 		}
275 
276 		if (this.defaultEncoding != null) {
277 			config.setDefaultEncoding(this.defaultEncoding);
278 		}
279 
280 		List<TemplateLoader> templateLoaders = new LinkedList<TemplateLoader>(this.templateLoaders);
281 
282 		// Register template loaders that are supposed to kick in early.
283 		if (this.preTemplateLoaders != null) {
284 			templateLoaders.addAll(this.preTemplateLoaders);
285 		}
286 
287 		// Register default template loaders.
288 		if (this.templateLoaderPaths != null) {
289 			for (String path : this.templateLoaderPaths) {
290 				templateLoaders.add(getTemplateLoaderForPath(path));
291 			}
292 		}
293 		postProcessTemplateLoaders(templateLoaders);
294 
295 		// Register template loaders that are supposed to kick in late.
296 		if (this.postTemplateLoaders != null) {
297 			templateLoaders.addAll(this.postTemplateLoaders);
298 		}
299 
300 		TemplateLoader loader = getAggregateTemplateLoader(templateLoaders);
301 		if (loader != null) {
302 			config.setTemplateLoader(loader);
303 		}
304 
305 		postProcessConfiguration(config);
306 		return config;
307 	}
308 
309 	/**
310 	 * Return a new Configuration object. Subclasses can override this for custom
311 	 * initialization (e.g. specifying a FreeMarker compatibility level which is a
312 	 * new feature in FreeMarker 2.3.21), or for using a mock object for testing.
313 	 * <p>Called by {@code createConfiguration()}.
314 	 * @return the Configuration object
315 	 * @throws IOException if a config file wasn't found
316 	 * @throws TemplateException on FreeMarker initialization failure
317 	 * @see #createConfiguration()
318 	 */
319 	@SuppressWarnings("deprecation")
320 	protected Configuration newConfiguration() throws IOException, TemplateException {
321 		// The default Configuration constructor is deprecated as of FreeMarker 2.3.21,
322 		// in favor of specifying a compatibility version - which is a 2.3.21 feature.
323 		// We won't be able to call that for a long while, but custom subclasses can.
324 		return new Configuration();
325 	}
326 
327 	/**
328 	 * Determine a FreeMarker TemplateLoader for the given path.
329 	 * <p>Default implementation creates either a FileTemplateLoader or
330 	 * a SpringTemplateLoader.
331 	 * @param templateLoaderPath the path to load templates from
332 	 * @return an appropriate TemplateLoader
333 	 * @see freemarker.cache.FileTemplateLoader
334 	 * @see SpringTemplateLoader
335 	 */
336 	protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) {
337 		if (isPreferFileSystemAccess()) {
338 			// Try to load via the file system, fall back to SpringTemplateLoader
339 			// (for hot detection of template changes, if possible).
340 			try {
341 				Resource path = getResourceLoader().getResource(templateLoaderPath);
342 				File file = path.getFile();  // will fail if not resolvable in the file system
343 				if (logger.isDebugEnabled()) {
344 					logger.debug(
345 							"Template loader path [" + path + "] resolved to file path [" + file.getAbsolutePath() + "]");
346 				}
347 				return new FileTemplateLoader(file);
348 			}
349 			catch (IOException ex) {
350 				if (logger.isDebugEnabled()) {
351 					logger.debug("Cannot resolve template loader path [" + templateLoaderPath +
352 							"] to [java.io.File]: using SpringTemplateLoader as fallback", ex);
353 				}
354 				return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
355 			}
356 		}
357 		else {
358 			// Always load via SpringTemplateLoader (without hot detection of template changes).
359 			logger.debug("File system access not preferred: using SpringTemplateLoader");
360 			return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
361 		}
362 	}
363 
364 	/**
365 	 * To be overridden by subclasses that want to to register custom
366 	 * TemplateLoader instances after this factory created its default
367 	 * template loaders.
368 	 * <p>Called by {@code createConfiguration()}. Note that specified
369 	 * "postTemplateLoaders" will be registered <i>after</i> any loaders
370 	 * registered by this callback; as a consequence, they are are <i>not</i>
371 	 * included in the given List.
372 	 * @param templateLoaders the current List of TemplateLoader instances,
373 	 * to be modified by a subclass
374 	 * @see #createConfiguration()
375 	 * @see #setPostTemplateLoaders
376 	 */
377 	protected void postProcessTemplateLoaders(List<TemplateLoader> templateLoaders) {
378 	}
379 
380 	/**
381 	 * Return a TemplateLoader based on the given TemplateLoader list.
382 	 * If more than one TemplateLoader has been registered, a FreeMarker
383 	 * MultiTemplateLoader needs to be created.
384 	 * @param templateLoaders the final List of TemplateLoader instances
385 	 * @return the aggregate TemplateLoader
386 	 */
387 	protected TemplateLoader getAggregateTemplateLoader(List<TemplateLoader> templateLoaders) {
388 		int loaderCount = templateLoaders.size();
389 		switch (loaderCount) {
390 			case 0:
391 				logger.info("No FreeMarker TemplateLoaders specified");
392 				return null;
393 			case 1:
394 				return templateLoaders.get(0);
395 			default:
396 				TemplateLoader[] loaders = templateLoaders.toArray(new TemplateLoader[loaderCount]);
397 				return new MultiTemplateLoader(loaders);
398 		}
399 	}
400 
401 	/**
402 	 * To be overridden by subclasses that want to to perform custom
403 	 * post-processing of the Configuration object after this factory
404 	 * performed its default initialization.
405 	 * <p>Called by {@code createConfiguration()}.
406 	 * @param config the current Configuration object
407 	 * @throws IOException if a config file wasn't found
408 	 * @throws TemplateException on FreeMarker initialization failure
409 	 * @see #createConfiguration()
410 	 */
411 	protected void postProcessConfiguration(Configuration config) throws IOException, TemplateException {
412 	}
413 
414 }