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 }