001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.ui.freemarker;
018
019import java.io.File;
020import java.io.IOException;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Properties;
027
028import freemarker.cache.FileTemplateLoader;
029import freemarker.cache.MultiTemplateLoader;
030import freemarker.cache.TemplateLoader;
031import freemarker.template.Configuration;
032import freemarker.template.SimpleHash;
033import freemarker.template.TemplateException;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036
037import org.springframework.core.io.DefaultResourceLoader;
038import org.springframework.core.io.Resource;
039import org.springframework.core.io.ResourceLoader;
040import org.springframework.core.io.support.PropertiesLoaderUtils;
041import org.springframework.util.CollectionUtils;
042
043/**
044 * Factory that configures a FreeMarker Configuration. Can be used standalone, but
045 * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a
046 * Configuration as bean reference, or FreeMarkerConfigurer for web views.
047 *
048 * <p>The optional "configLocation" property sets the location of a FreeMarker
049 * properties file, within the current application. FreeMarker properties can be
050 * overridden via "freemarkerSettings". All of these properties will be set by
051 * calling FreeMarker's {@code Configuration.setSettings()} method and are
052 * subject to constraints set by FreeMarker.
053 *
054 * <p>The "freemarkerVariables" property can be used to specify a Map of
055 * shared variables that will be applied to the Configuration via the
056 * {@code setAllSharedVariables()} method. Like {@code setSettings()},
057 * these entries are subject to FreeMarker constraints.
058 *
059 * <p>The simplest way to use this class is to specify a "templateLoaderPath";
060 * FreeMarker does not need any further configuration then.
061 *
062 * <p>Note: Spring's FreeMarker support requires FreeMarker 2.3 or higher.
063 *
064 * @author Darren Davison
065 * @author Juergen Hoeller
066 * @since 03.03.2004
067 * @see #setConfigLocation
068 * @see #setFreemarkerSettings
069 * @see #setFreemarkerVariables
070 * @see #setTemplateLoaderPath
071 * @see #createConfiguration
072 * @see FreeMarkerConfigurationFactoryBean
073 * @see org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
074 * @see freemarker.template.Configuration
075 */
076public class FreeMarkerConfigurationFactory {
077
078        protected final Log logger = LogFactory.getLog(getClass());
079
080        private Resource configLocation;
081
082        private Properties freemarkerSettings;
083
084        private Map<String, Object> freemarkerVariables;
085
086        private String defaultEncoding;
087
088        private final List<TemplateLoader> templateLoaders = new ArrayList<TemplateLoader>();
089
090        private List<TemplateLoader> preTemplateLoaders;
091
092        private List<TemplateLoader> postTemplateLoaders;
093
094        private String[] templateLoaderPaths;
095
096        private ResourceLoader resourceLoader = new DefaultResourceLoader();
097
098        private boolean preferFileSystemAccess = true;
099
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        protected Configuration newConfiguration() throws IOException, TemplateException {
320                return new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
321        }
322
323        /**
324         * Determine a FreeMarker TemplateLoader for the given path.
325         * <p>Default implementation creates either a FileTemplateLoader or
326         * a SpringTemplateLoader.
327         * @param templateLoaderPath the path to load templates from
328         * @return an appropriate TemplateLoader
329         * @see freemarker.cache.FileTemplateLoader
330         * @see SpringTemplateLoader
331         */
332        protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) {
333                if (isPreferFileSystemAccess()) {
334                        // Try to load via the file system, fall back to SpringTemplateLoader
335                        // (for hot detection of template changes, if possible).
336                        try {
337                                Resource path = getResourceLoader().getResource(templateLoaderPath);
338                                File file = path.getFile();  // will fail if not resolvable in the file system
339                                if (logger.isDebugEnabled()) {
340                                        logger.debug(
341                                                        "Template loader path [" + path + "] resolved to file path [" + file.getAbsolutePath() + "]");
342                                }
343                                return new FileTemplateLoader(file);
344                        }
345                        catch (Exception ex) {
346                                if (logger.isDebugEnabled()) {
347                                        logger.debug("Cannot resolve template loader path [" + templateLoaderPath +
348                                                        "] to [java.io.File]: using SpringTemplateLoader as fallback", ex);
349                                }
350                                return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
351                        }
352                }
353                else {
354                        // Always load via SpringTemplateLoader (without hot detection of template changes).
355                        logger.debug("File system access not preferred: using SpringTemplateLoader");
356                        return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
357                }
358        }
359
360        /**
361         * To be overridden by subclasses that want to register custom
362         * TemplateLoader instances after this factory created its default
363         * template loaders.
364         * <p>Called by {@code createConfiguration()}. Note that specified
365         * "postTemplateLoaders" will be registered <i>after</i> any loaders
366         * registered by this callback; as a consequence, they are <i>not</i>
367         * included in the given List.
368         * @param templateLoaders the current List of TemplateLoader instances,
369         * to be modified by a subclass
370         * @see #createConfiguration()
371         * @see #setPostTemplateLoaders
372         */
373        protected void postProcessTemplateLoaders(List<TemplateLoader> templateLoaders) {
374        }
375
376        /**
377         * Return a TemplateLoader based on the given TemplateLoader list.
378         * If more than one TemplateLoader has been registered, a FreeMarker
379         * MultiTemplateLoader needs to be created.
380         * @param templateLoaders the final List of TemplateLoader instances
381         * @return the aggregate TemplateLoader
382         */
383        protected TemplateLoader getAggregateTemplateLoader(List<TemplateLoader> templateLoaders) {
384                int loaderCount = templateLoaders.size();
385                switch (loaderCount) {
386                        case 0:
387                                logger.info("No FreeMarker TemplateLoaders specified");
388                                return null;
389                        case 1:
390                                return templateLoaders.get(0);
391                        default:
392                                TemplateLoader[] loaders = templateLoaders.toArray(new TemplateLoader[loaderCount]);
393                                return new MultiTemplateLoader(loaders);
394                }
395        }
396
397        /**
398         * To be overridden by subclasses that want to perform custom
399         * post-processing of the Configuration object after this factory
400         * performed its default initialization.
401         * <p>Called by {@code createConfiguration()}.
402         * @param config the current Configuration object
403         * @throws IOException if a config file wasn't found
404         * @throws TemplateException on FreeMarker initialization failure
405         * @see #createConfiguration()
406         */
407        protected void postProcessConfiguration(Configuration config) throws IOException, TemplateException {
408        }
409
410}