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}