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}