001/*
002 * Copyright 2002-2018 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.List;
024import java.util.Map;
025import java.util.Properties;
026
027import freemarker.cache.FileTemplateLoader;
028import freemarker.cache.MultiTemplateLoader;
029import freemarker.cache.TemplateLoader;
030import freemarker.template.Configuration;
031import freemarker.template.SimpleHash;
032import freemarker.template.TemplateException;
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035
036import org.springframework.core.io.DefaultResourceLoader;
037import org.springframework.core.io.Resource;
038import org.springframework.core.io.ResourceLoader;
039import org.springframework.core.io.support.PropertiesLoaderUtils;
040import org.springframework.lang.Nullable;
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        @Nullable
081        private Resource configLocation;
082
083        @Nullable
084        private Properties freemarkerSettings;
085
086        @Nullable
087        private Map<String, Object> freemarkerVariables;
088
089        @Nullable
090        private String defaultEncoding;
091
092        private final List<TemplateLoader> templateLoaders = new ArrayList<>();
093
094        @Nullable
095        private List<TemplateLoader> preTemplateLoaders;
096
097        @Nullable
098        private List<TemplateLoader> postTemplateLoaders;
099
100        @Nullable
101        private String[] templateLoaderPaths;
102
103        private ResourceLoader resourceLoader = new DefaultResourceLoader();
104
105        private boolean preferFileSystemAccess = true;
106
107
108        /**
109         * Set the location of the FreeMarker config file.
110         * Alternatively, you can specify all setting locally.
111         * @see #setFreemarkerSettings
112         * @see #setTemplateLoaderPath
113         */
114        public void setConfigLocation(Resource resource) {
115                this.configLocation = resource;
116        }
117
118        /**
119         * Set properties that contain well-known FreeMarker keys which will be
120         * passed to FreeMarker's {@code Configuration.setSettings} method.
121         * @see freemarker.template.Configuration#setSettings
122         */
123        public void setFreemarkerSettings(Properties settings) {
124                this.freemarkerSettings = settings;
125        }
126
127        /**
128         * Set a Map that contains well-known FreeMarker objects which will be passed
129         * to FreeMarker's {@code Configuration.setAllSharedVariables()} method.
130         * @see freemarker.template.Configuration#setAllSharedVariables
131         */
132        public void setFreemarkerVariables(Map<String, Object> variables) {
133                this.freemarkerVariables = variables;
134        }
135
136        /**
137         * Set the default encoding for the FreeMarker configuration.
138         * If not specified, FreeMarker will use the platform file encoding.
139         * <p>Used for template rendering unless there is an explicit encoding specified
140         * for the rendering process (for example, on Spring's FreeMarkerView).
141         * @see freemarker.template.Configuration#setDefaultEncoding
142         * @see org.springframework.web.servlet.view.freemarker.FreeMarkerView#setEncoding
143         */
144        public void setDefaultEncoding(String defaultEncoding) {
145                this.defaultEncoding = defaultEncoding;
146        }
147
148        /**
149         * Set a List of {@code TemplateLoader}s that will be used to search
150         * for templates. For example, one or more custom loaders such as database
151         * loaders could be configured and injected here.
152         * <p>The {@link TemplateLoader TemplateLoaders} specified here will be
153         * registered <i>before</i> the default template loaders that this factory
154         * registers (such as loaders for specified "templateLoaderPaths" or any
155         * loaders registered in {@link #postProcessTemplateLoaders}).
156         * @see #setTemplateLoaderPaths
157         * @see #postProcessTemplateLoaders
158         */
159        public void setPreTemplateLoaders(TemplateLoader... preTemplateLoaders) {
160                this.preTemplateLoaders = Arrays.asList(preTemplateLoaders);
161        }
162
163        /**
164         * Set a List of {@code TemplateLoader}s that will be used to search
165         * for templates. For example, one or more custom loaders such as database
166         * loaders can be configured.
167         * <p>The {@link TemplateLoader TemplateLoaders} specified here will be
168         * registered <i>after</i> the default template loaders that this factory
169         * registers (such as loaders for specified "templateLoaderPaths" or any
170         * loaders registered in {@link #postProcessTemplateLoaders}).
171         * @see #setTemplateLoaderPaths
172         * @see #postProcessTemplateLoaders
173         */
174        public void setPostTemplateLoaders(TemplateLoader... postTemplateLoaders) {
175                this.postTemplateLoaders = Arrays.asList(postTemplateLoaders);
176        }
177
178        /**
179         * Set the Freemarker template loader path via a Spring resource location.
180         * See the "templateLoaderPaths" property for details on path handling.
181         * @see #setTemplateLoaderPaths
182         */
183        public void setTemplateLoaderPath(String templateLoaderPath) {
184                this.templateLoaderPaths = new String[] {templateLoaderPath};
185        }
186
187        /**
188         * Set multiple Freemarker template loader paths via Spring resource locations.
189         * <p>When populated via a String, standard URLs like "file:" and "classpath:"
190         * pseudo URLs are supported, as understood by ResourceEditor. Allows for
191         * relative paths when running in an ApplicationContext.
192         * <p>Will define a path for the default FreeMarker template loader.
193         * If a specified resource cannot be resolved to a {@code java.io.File},
194         * a generic SpringTemplateLoader will be used, without modification detection.
195         * <p>To enforce the use of SpringTemplateLoader, i.e. to not resolve a path
196         * as file system resource in any case, turn off the "preferFileSystemAccess"
197         * flag. See the latter's javadoc for details.
198         * <p>If you wish to specify your own list of TemplateLoaders, do not set this
199         * property and instead use {@code setTemplateLoaders(List templateLoaders)}
200         * @see org.springframework.core.io.ResourceEditor
201         * @see org.springframework.context.ApplicationContext#getResource
202         * @see freemarker.template.Configuration#setDirectoryForTemplateLoading
203         * @see SpringTemplateLoader
204         */
205        public void setTemplateLoaderPaths(String... templateLoaderPaths) {
206                this.templateLoaderPaths = templateLoaderPaths;
207        }
208
209        /**
210         * Set the Spring ResourceLoader to use for loading FreeMarker template files.
211         * The default is DefaultResourceLoader. Will get overridden by the
212         * ApplicationContext if running in a context.
213         * @see org.springframework.core.io.DefaultResourceLoader
214         */
215        public void setResourceLoader(ResourceLoader resourceLoader) {
216                this.resourceLoader = resourceLoader;
217        }
218
219        /**
220         * Return the Spring ResourceLoader to use for loading FreeMarker template files.
221         */
222        protected ResourceLoader getResourceLoader() {
223                return this.resourceLoader;
224        }
225
226        /**
227         * Set whether to prefer file system access for template loading.
228         * File system access enables hot detection of template changes.
229         * <p>If this is enabled, FreeMarkerConfigurationFactory will try to resolve
230         * the specified "templateLoaderPath" as file system resource (which will work
231         * for expanded class path resources and ServletContext resources too).
232         * <p>Default is "true". Turn this off to always load via SpringTemplateLoader
233         * (i.e. as stream, without hot detection of template changes), which might
234         * be necessary if some of your templates reside in an expanded classes
235         * directory while others reside in jar files.
236         * @see #setTemplateLoaderPath
237         */
238        public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
239                this.preferFileSystemAccess = preferFileSystemAccess;
240        }
241
242        /**
243         * Return whether to prefer file system access for template loading.
244         */
245        protected boolean isPreferFileSystemAccess() {
246                return this.preferFileSystemAccess;
247        }
248
249
250        /**
251         * Prepare the FreeMarker Configuration and return it.
252         * @return the FreeMarker Configuration object
253         * @throws IOException if the config file wasn't found
254         * @throws TemplateException on FreeMarker initialization failure
255         */
256        public Configuration createConfiguration() throws IOException, TemplateException {
257                Configuration config = newConfiguration();
258                Properties props = new Properties();
259
260                // Load config file if specified.
261                if (this.configLocation != null) {
262                        if (logger.isDebugEnabled()) {
263                                logger.debug("Loading FreeMarker configuration from " + this.configLocation);
264                        }
265                        PropertiesLoaderUtils.fillProperties(props, this.configLocation);
266                }
267
268                // Merge local properties if specified.
269                if (this.freemarkerSettings != null) {
270                        props.putAll(this.freemarkerSettings);
271                }
272
273                // FreeMarker will only accept known keys in its setSettings and
274                // setAllSharedVariables methods.
275                if (!props.isEmpty()) {
276                        config.setSettings(props);
277                }
278
279                if (!CollectionUtils.isEmpty(this.freemarkerVariables)) {
280                        config.setAllSharedVariables(new SimpleHash(this.freemarkerVariables, config.getObjectWrapper()));
281                }
282
283                if (this.defaultEncoding != null) {
284                        config.setDefaultEncoding(this.defaultEncoding);
285                }
286
287                List<TemplateLoader> templateLoaders = new ArrayList<>(this.templateLoaders);
288
289                // Register template loaders that are supposed to kick in early.
290                if (this.preTemplateLoaders != null) {
291                        templateLoaders.addAll(this.preTemplateLoaders);
292                }
293
294                // Register default template loaders.
295                if (this.templateLoaderPaths != null) {
296                        for (String path : this.templateLoaderPaths) {
297                                templateLoaders.add(getTemplateLoaderForPath(path));
298                        }
299                }
300                postProcessTemplateLoaders(templateLoaders);
301
302                // Register template loaders that are supposed to kick in late.
303                if (this.postTemplateLoaders != null) {
304                        templateLoaders.addAll(this.postTemplateLoaders);
305                }
306
307                TemplateLoader loader = getAggregateTemplateLoader(templateLoaders);
308                if (loader != null) {
309                        config.setTemplateLoader(loader);
310                }
311
312                postProcessConfiguration(config);
313                return config;
314        }
315
316        /**
317         * Return a new Configuration object. Subclasses can override this for custom
318         * initialization (e.g. specifying a FreeMarker compatibility level which is a
319         * new feature in FreeMarker 2.3.21), or for using a mock object for testing.
320         * <p>Called by {@code createConfiguration()}.
321         * @return the Configuration object
322         * @throws IOException if a config file wasn't found
323         * @throws TemplateException on FreeMarker initialization failure
324         * @see #createConfiguration()
325         */
326        protected Configuration newConfiguration() throws IOException, TemplateException {
327                return new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
328        }
329
330        /**
331         * Determine a FreeMarker TemplateLoader for the given path.
332         * <p>Default implementation creates either a FileTemplateLoader or
333         * a SpringTemplateLoader.
334         * @param templateLoaderPath the path to load templates from
335         * @return an appropriate TemplateLoader
336         * @see freemarker.cache.FileTemplateLoader
337         * @see SpringTemplateLoader
338         */
339        protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) {
340                if (isPreferFileSystemAccess()) {
341                        // Try to load via the file system, fall back to SpringTemplateLoader
342                        // (for hot detection of template changes, if possible).
343                        try {
344                                Resource path = getResourceLoader().getResource(templateLoaderPath);
345                                File file = path.getFile();  // will fail if not resolvable in the file system
346                                if (logger.isDebugEnabled()) {
347                                        logger.debug(
348                                                        "Template loader path [" + path + "] resolved to file path [" + file.getAbsolutePath() + "]");
349                                }
350                                return new FileTemplateLoader(file);
351                        }
352                        catch (Exception ex) {
353                                if (logger.isDebugEnabled()) {
354                                        logger.debug("Cannot resolve template loader path [" + templateLoaderPath +
355                                                        "] to [java.io.File]: using SpringTemplateLoader as fallback", ex);
356                                }
357                                return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
358                        }
359                }
360                else {
361                        // Always load via SpringTemplateLoader (without hot detection of template changes).
362                        logger.debug("File system access not preferred: using SpringTemplateLoader");
363                        return new SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
364                }
365        }
366
367        /**
368         * To be overridden by subclasses that want to register custom
369         * TemplateLoader instances after this factory created its default
370         * template loaders.
371         * <p>Called by {@code createConfiguration()}. Note that specified
372         * "postTemplateLoaders" will be registered <i>after</i> any loaders
373         * registered by this callback; as a consequence, they are <i>not</i>
374         * included in the given List.
375         * @param templateLoaders the current List of TemplateLoader instances,
376         * to be modified by a subclass
377         * @see #createConfiguration()
378         * @see #setPostTemplateLoaders
379         */
380        protected void postProcessTemplateLoaders(List<TemplateLoader> templateLoaders) {
381        }
382
383        /**
384         * Return a TemplateLoader based on the given TemplateLoader list.
385         * If more than one TemplateLoader has been registered, a FreeMarker
386         * MultiTemplateLoader needs to be created.
387         * @param templateLoaders the final List of TemplateLoader instances
388         * @return the aggregate TemplateLoader
389         */
390        @Nullable
391        protected TemplateLoader getAggregateTemplateLoader(List<TemplateLoader> templateLoaders) {
392                switch (templateLoaders.size()) {
393                        case 0:
394                                logger.debug("No FreeMarker TemplateLoaders specified");
395                                return null;
396                        case 1:
397                                return templateLoaders.get(0);
398                        default:
399                                TemplateLoader[] loaders = templateLoaders.toArray(new TemplateLoader[0]);
400                                return new MultiTemplateLoader(loaders);
401                }
402        }
403
404        /**
405         * To be overridden by subclasses that want to perform custom
406         * post-processing of the Configuration object after this factory
407         * performed its default initialization.
408         * <p>Called by {@code createConfiguration()}.
409         * @param config the current Configuration object
410         * @throws IOException if a config file wasn't found
411         * @throws TemplateException on FreeMarker initialization failure
412         * @see #createConfiguration()
413         */
414        protected void postProcessConfiguration(Configuration config) throws IOException, TemplateException {
415        }
416
417}