• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 20632861616

01 Jan 2026 04:53AM UTC coverage: 84.18% (-0.3%) from 84.439%
20632861616

push

github

TAKETODAY
:sparkles: ApplicationType 支持通过 SPI 获取

55643 of 70608 branches covered (78.81%)

Branch coverage included in aggregate %.

130472 of 150485 relevant lines covered (86.7%)

3.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.84
today-context/src/main/java/infra/ui/freemarker/FreeMarkerConfigurationFactory.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.ui.freemarker;
19

20
import org.jspecify.annotations.Nullable;
21

22
import java.io.File;
23
import java.io.IOException;
24
import java.nio.charset.Charset;
25
import java.util.ArrayList;
26
import java.util.Arrays;
27
import java.util.List;
28
import java.util.Map;
29
import java.util.Properties;
30

31
import freemarker.cache.FileTemplateLoader;
32
import freemarker.cache.MultiTemplateLoader;
33
import freemarker.cache.TemplateLoader;
34
import freemarker.template.Configuration;
35
import freemarker.template.SimpleHash;
36
import freemarker.template.TemplateException;
37
import infra.core.io.DefaultResourceLoader;
38
import infra.core.io.PropertiesUtils;
39
import infra.core.io.Resource;
40
import infra.core.io.ResourceLoader;
41
import infra.logging.Logger;
42
import infra.logging.LoggerFactory;
43
import infra.util.CollectionUtils;
44

45
/**
46
 * Factory that configures a FreeMarker Configuration. Can be used standalone, but
47
 * typically you will either use FreeMarkerConfigurationFactoryBean for preparing a
48
 * Configuration as bean reference, or FreeMarkerConfigurer for web views.
49
 *
50
 * <p>The optional "configLocation" property sets the location of a FreeMarker
51
 * properties file, within the current application. FreeMarker properties can be
52
 * overridden via "freemarkerSettings". All of these properties will be set by
53
 * calling FreeMarker's {@code Configuration.setSettings()} method and are
54
 * subject to constraints set by FreeMarker.
55
 *
56
 * <p>The "freemarkerVariables" property can be used to specify a Map of
57
 * shared variables that will be applied to the Configuration via the
58
 * {@code setAllSharedVariables()} method. Like {@code setSettings()},
59
 * these entries are subject to FreeMarker constraints.
60
 *
61
 * <p>The simplest way to use this class is to specify a "templateLoaderPath";
62
 * FreeMarker does not need any further configuration then.
63
 *
64
 * <p>Note: Framework's FreeMarker support requires FreeMarker 2.3 or higher.
65
 *
66
 * @author Darren Davison
67
 * @author Juergen Hoeller
68
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
69
 * @see #setConfigLocation
70
 * @see #setFreemarkerSettings
71
 * @see #setFreemarkerVariables
72
 * @see #setTemplateLoaderPath
73
 * @see #createConfiguration
74
 * @see FreeMarkerConfigurationFactoryBean
75
 * @see infra.web.view.freemarker.FreeMarkerConfigurer
76
 * @see freemarker.template.Configuration
77
 * @since 4.0 2022/2/5 13:02
78
 */
79
public class FreeMarkerConfigurationFactory {
2✔
80
  private static final Logger log = LoggerFactory.getLogger(FreeMarkerConfigurationFactory.class);
4✔
81

82
  @Nullable
83
  private Resource configLocation;
84

85
  @Nullable
86
  private Properties freemarkerSettings;
87

88
  @Nullable
89
  private Map<String, Object> freemarkerVariables;
90

91
  @Nullable
92
  private String defaultEncoding;
93

94
  private final ArrayList<TemplateLoader> templateLoaders = new ArrayList<>();
5✔
95

96
  @Nullable
97
  private List<TemplateLoader> preTemplateLoaders;
98

99
  @Nullable
100
  private List<TemplateLoader> postTemplateLoaders;
101

102
  private String @Nullable []templateLoaderPaths;
103

104
  private ResourceLoader resourceLoader = new DefaultResourceLoader();
5✔
105

106
  private boolean preferFileSystemAccess = true;
4✔
107

108
  /**
109
   * Set the location of the FreeMarker config file.
110
   * Alternatively, you can specify all setting locally.
111
   *
112
   * @see #setFreemarkerSettings
113
   * @see #setTemplateLoaderPath
114
   */
115
  public void setConfigLocation(@Nullable Resource resource) {
116
    this.configLocation = resource;
3✔
117
  }
1✔
118

119
  /**
120
   * Set properties that contain well-known FreeMarker keys which will be
121
   * passed to FreeMarker's {@code Configuration.setSettings} method.
122
   *
123
   * @see freemarker.template.Configuration#setSettings
124
   */
125
  public void setFreemarkerSettings(@Nullable Properties settings) {
126
    this.freemarkerSettings = settings;
3✔
127
  }
1✔
128

129
  /**
130
   * Set a Map that contains well-known FreeMarker objects which will be passed
131
   * to FreeMarker's {@code Configuration.setAllSharedVariables()} method.
132
   *
133
   * @see freemarker.template.Configuration#setAllSharedVariables
134
   */
135
  public void setFreemarkerVariables(@Nullable Map<String, Object> variables) {
136
    this.freemarkerVariables = variables;
3✔
137
  }
1✔
138

139
  /**
140
   * Set the default encoding for the FreeMarker configuration.
141
   * If not specified, FreeMarker will use the platform file encoding.
142
   * <p>Used for template rendering unless there is an explicit encoding specified
143
   * for the rendering process (for example, on Framework's FreeMarkerView).
144
   *
145
   * @see freemarker.template.Configuration#setDefaultEncoding
146
   * @see infra.web.view.freemarker.FreeMarkerView#setEncoding
147
   */
148
  public void setDefaultEncoding(@Nullable String defaultEncoding) {
149
    this.defaultEncoding = defaultEncoding;
3✔
150
  }
1✔
151

152
  /**
153
   * Set the {@link Charset} for the default encoding for the FreeMarker
154
   * {@link Configuration}, which is used to decode byte sequences to character
155
   * sequences when reading template files.
156
   * <p>See {@link #setDefaultEncoding(String)} for details.
157
   *
158
   * @see java.nio.charset.StandardCharsets
159
   * @since 5.0
160
   */
161
  public void setDefaultCharset(Charset defaultCharset) {
162
    this.defaultEncoding = defaultCharset.name();
×
163
  }
×
164

165
  /**
166
   * Set a List of {@code TemplateLoader}s that will be used to search
167
   * for templates. For example, one or more custom loaders such as database
168
   * loaders could be configured and injected here.
169
   * <p>The {@link TemplateLoader TemplateLoaders} specified here will be
170
   * registered <i>before</i> the default template loaders that this factory
171
   * registers (such as loaders for specified "templateLoaderPaths" or any
172
   * loaders registered in {@link #postProcessTemplateLoaders}).
173
   *
174
   * @see #setTemplateLoaderPaths
175
   * @see #postProcessTemplateLoaders
176
   */
177
  public void setPreTemplateLoaders(TemplateLoader... preTemplateLoaders) {
178
    this.preTemplateLoaders = Arrays.asList(preTemplateLoaders);
4✔
179
  }
1✔
180

181
  /**
182
   * Set a List of {@code TemplateLoader}s that will be used to search
183
   * for templates. For example, one or more custom loaders such as database
184
   * loaders can be configured.
185
   * <p>The {@link TemplateLoader TemplateLoaders} specified here will be
186
   * registered <i>after</i> the default template loaders that this factory
187
   * registers (such as loaders for specified "templateLoaderPaths" or any
188
   * loaders registered in {@link #postProcessTemplateLoaders}).
189
   *
190
   * @see #setTemplateLoaderPaths
191
   * @see #postProcessTemplateLoaders
192
   */
193
  public void setPostTemplateLoaders(TemplateLoader... postTemplateLoaders) {
194
    this.postTemplateLoaders = Arrays.asList(postTemplateLoaders);
4✔
195
  }
1✔
196

197
  /**
198
   * Set the Freemarker template loader path via a Framework resource location.
199
   * See the "templateLoaderPaths" property for details on path handling.
200
   *
201
   * @see #setTemplateLoaderPaths
202
   */
203
  public void setTemplateLoaderPath(String templateLoaderPath) {
204
    this.templateLoaderPaths = new String[] { templateLoaderPath };
8✔
205
  }
1✔
206

207
  /**
208
   * Set multiple Freemarker template loader paths via Framework resource locations.
209
   * <p>When populated via a String, standard URLs like "file:" and "classpath:"
210
   * pseudo URLs are supported, as understood by ResourceEditor. Allows for
211
   * relative paths when running in an ApplicationContext.
212
   * <p>Will define a path for the default FreeMarker template loader.
213
   * If a specified resource cannot be resolved to a {@code java.io.File},
214
   * a generic InfraTemplateLoader will be used, without modification detection.
215
   * <p>To enforce the use of InfraTemplateLoader, i.e. to not resolve a path
216
   * as file system resource in any case, turn off the "preferFileSystemAccess"
217
   * flag. See the latter's javadoc for details.
218
   * <p>If you wish to specify your own list of TemplateLoaders, do not set this
219
   * property and instead use {@code setTemplateLoaders(List templateLoaders)}
220
   *
221
   * @see infra.context.ApplicationContext#getResource
222
   * @see freemarker.template.Configuration#setDirectoryForTemplateLoading
223
   * @see InfraTemplateLoader
224
   */
225
  public void setTemplateLoaderPaths(String @Nullable ... templateLoaderPaths) {
226
    this.templateLoaderPaths = templateLoaderPaths;
3✔
227
  }
1✔
228

229
  /**
230
   * Add multiple Freemarker template loader
231
   *
232
   * @see InfraTemplateLoader
233
   */
234
  public void addTemplateLoader(TemplateLoader... templateLoader) {
235
    CollectionUtils.addAll(templateLoaders, templateLoader);
×
236
  }
×
237

238
  /**
239
   * Set multiple Freemarker template loader
240
   *
241
   * @see InfraTemplateLoader
242
   */
243
  public void setTemplateLoaders(List<TemplateLoader> templateLoaders) {
244
    this.templateLoaders.clear();
×
245
    this.templateLoaders.addAll(templateLoaders);
×
246
  }
×
247

248
  /**
249
   * Set the Framework ResourceLoader to use for loading FreeMarker template files.
250
   * The default is DefaultResourceLoader. Will get overridden by the
251
   * ApplicationContext if running in a context.
252
   *
253
   * @see DefaultResourceLoader
254
   */
255
  public void setResourceLoader(ResourceLoader resourceLoader) {
256
    this.resourceLoader = resourceLoader;
3✔
257
  }
1✔
258

259
  /**
260
   * Return the Framework ResourceLoader to use for loading FreeMarker template files.
261
   */
262
  protected ResourceLoader getResourceLoader() {
263
    return this.resourceLoader;
3✔
264
  }
265

266
  /**
267
   * Set whether to prefer file system access for template loading.
268
   * File system access enables hot detection of template changes.
269
   * <p>If this is enabled, FreeMarkerConfigurationFactory will try to resolve
270
   * the specified "templateLoaderPath" as file system resource (which will work
271
   * for expanded class path resources and MockContext resources too).
272
   * <p>Default is "true". Turn this off to always load via InfraTemplateLoader
273
   * (i.e. as stream, without hot detection of template changes), which might
274
   * be necessary if some of your templates reside in an expanded classes
275
   * directory while others reside in jar files.
276
   *
277
   * @see #setTemplateLoaderPath
278
   */
279
  public void setPreferFileSystemAccess(boolean preferFileSystemAccess) {
280
    this.preferFileSystemAccess = preferFileSystemAccess;
3✔
281
  }
1✔
282

283
  /**
284
   * Return whether to prefer file system access for template loading.
285
   */
286
  protected boolean isPreferFileSystemAccess() {
287
    return this.preferFileSystemAccess;
3✔
288
  }
289

290
  /**
291
   * Prepare the FreeMarker Configuration and return it.
292
   *
293
   * @return the FreeMarker Configuration object
294
   * @throws IOException if the config file wasn't found
295
   * @throws TemplateException on FreeMarker initialization failure
296
   */
297
  public Configuration createConfiguration() throws IOException, TemplateException {
298
    Configuration config = newConfiguration();
3✔
299
    Properties props = new Properties();
4✔
300

301
    // Load config file if specified.
302
    if (this.configLocation != null) {
3✔
303
      if (log.isDebugEnabled()) {
3!
304
        log.debug("Loading FreeMarker configuration from {}", configLocation);
×
305
      }
306
      PropertiesUtils.fillProperties(props, this.configLocation);
×
307
    }
308

309
    // Merge local properties if specified.
310
    if (this.freemarkerSettings != null) {
3✔
311
      props.putAll(this.freemarkerSettings);
4✔
312
    }
313

314
    // FreeMarker will only accept known keys in its setSettings and
315
    // setAllSharedVariables methods.
316
    if (!props.isEmpty()) {
3✔
317
      config.setSettings(props);
3✔
318
    }
319

320
    if (CollectionUtils.isNotEmpty(this.freemarkerVariables)) {
4✔
321
      config.setAllSharedVariables(new SimpleHash(this.freemarkerVariables, config.getObjectWrapper()));
9✔
322
    }
323

324
    if (this.defaultEncoding != null) {
3✔
325
      config.setDefaultEncoding(this.defaultEncoding);
4✔
326
    }
327

328
    var templateLoaders = new ArrayList<>(this.templateLoaders);
6✔
329

330
    // Register template loaders that are supposed to kick in early.
331
    if (this.preTemplateLoaders != null) {
3✔
332
      templateLoaders.addAll(this.preTemplateLoaders);
5✔
333
    }
334

335
    // Register default template loaders.
336
    if (this.templateLoaderPaths != null) {
3✔
337
      for (String path : this.templateLoaderPaths) {
17✔
338
        templateLoaders.add(getTemplateLoaderForPath(path));
6✔
339
      }
340
    }
341
    postProcessTemplateLoaders(templateLoaders);
3✔
342

343
    // Register template loaders that are supposed to kick in late.
344
    if (this.postTemplateLoaders != null) {
3✔
345
      templateLoaders.addAll(this.postTemplateLoaders);
5✔
346
    }
347

348
    TemplateLoader loader = getAggregateTemplateLoader(templateLoaders);
4✔
349
    if (loader != null) {
2✔
350
      config.setTemplateLoader(loader);
3✔
351
    }
352

353
    postProcessConfiguration(config);
3✔
354
    return config;
2✔
355
  }
356

357
  /**
358
   * Return a new Configuration object. Subclasses can override this for custom
359
   * initialization (e.g. specifying a FreeMarker compatibility level which is a
360
   * new feature in FreeMarker 2.3.21), or for using a mock object for testing.
361
   * <p>Called by {@code createConfiguration()}.
362
   *
363
   * @return the Configuration object
364
   * @throws IOException if a config file wasn't found
365
   * @throws TemplateException on FreeMarker initialization failure
366
   * @see #createConfiguration()
367
   */
368
  protected Configuration newConfiguration() throws IOException, TemplateException {
369
    return new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
5✔
370
  }
371

372
  /**
373
   * Determine a FreeMarker TemplateLoader for the given path.
374
   * <p>Default implementation creates either a FileTemplateLoader or
375
   * a InfraTemplateLoader.
376
   *
377
   * @param templateLoaderPath the path to load templates from
378
   * @return an appropriate TemplateLoader
379
   * @see freemarker.cache.FileTemplateLoader
380
   * @see InfraTemplateLoader
381
   */
382
  protected TemplateLoader getTemplateLoaderForPath(String templateLoaderPath) {
383
    if (isPreferFileSystemAccess()) {
3✔
384
      // Try to load via the file system, fall back to InfraTemplateLoader
385
      // (for hot detection of template changes, if possible).
386
      try {
387
        Resource path = getResourceLoader().getResource(templateLoaderPath);
5✔
388
        File file = path.getFile();  // will fail if not resolvable in the file system
3✔
389
        if (log.isDebugEnabled()) {
3!
390
          log.debug("Template loader path [{}] resolved to file path [{}]", path, file.getAbsolutePath());
×
391
        }
392
        return new FileTemplateLoader(file);
×
393
      }
394
      catch (Exception ex) {
1✔
395
        if (log.isDebugEnabled()) {
3!
396
          log.debug("Cannot resolve template loader path [{}] to [java.io.File]: using InfraTemplateLoader as fallback",
×
397
                  templateLoaderPath, ex);
398
        }
399
        return new InfraTemplateLoader(getResourceLoader(), templateLoaderPath);
7✔
400
      }
401
    }
402
    else {
403
      // Always load via InfraTemplateLoader (without hot detection of template changes).
404
      log.debug("File system access not preferred: using InfraTemplateLoader");
3✔
405
      return new InfraTemplateLoader(getResourceLoader(), templateLoaderPath);
7✔
406
    }
407
  }
408

409
  /**
410
   * To be overridden by subclasses that want to register custom
411
   * TemplateLoader instances after this factory created its default
412
   * template loaders.
413
   * <p>Called by {@code createConfiguration()}. Note that specified
414
   * "postTemplateLoaders" will be registered <i>after</i> any loaders
415
   * registered by this callback; as a consequence, they are <i>not</i>
416
   * included in the given List.
417
   *
418
   * @param templateLoaders the current List of TemplateLoader instances,
419
   * to be modified by a subclass
420
   * @see #createConfiguration()
421
   * @see #setPostTemplateLoaders
422
   */
423
  protected void postProcessTemplateLoaders(List<TemplateLoader> templateLoaders) {
424

425
  }
1✔
426

427
  /**
428
   * Return a TemplateLoader based on the given TemplateLoader list.
429
   * If more than one TemplateLoader has been registered, a FreeMarker
430
   * MultiTemplateLoader needs to be created.
431
   *
432
   * @param templateLoaders the final List of TemplateLoader instances
433
   * @return the aggregate TemplateLoader
434
   */
435
  @Nullable
436
  protected TemplateLoader getAggregateTemplateLoader(List<TemplateLoader> templateLoaders) {
437
    switch (templateLoaders.size()) {
3✔
438
      case 0 -> {
439
        log.debug("No FreeMarker TemplateLoaders specified");
3✔
440
        return null;
2✔
441
      }
442
      case 1 -> {
443
        return templateLoaders.get(0);
5✔
444
      }
445
      default -> {
446
        TemplateLoader[] loaders = templateLoaders.toArray(new TemplateLoader[0]);
6✔
447
        return new MultiTemplateLoader(loaders);
5✔
448
      }
449
    }
450
  }
451

452
  /**
453
   * To be overridden by subclasses that want to perform custom
454
   * post-processing of the Configuration object after this factory
455
   * performed its default initialization.
456
   * <p>Called by {@code createConfiguration()}.
457
   *
458
   * @param config the current Configuration object
459
   * @throws IOException if a config file wasn't found
460
   * @throws TemplateException on FreeMarker initialization failure
461
   * @see #createConfiguration()
462
   */
463
  protected void postProcessConfiguration(Configuration config) throws IOException, TemplateException {
464

465
  }
1✔
466

467
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc