Skip to content

Commit a2fafa1

Browse files
committed
Add support for customizing FreeMarker variables
This commit updates the auto-configuration to allow custom FreeMarker variables to be provided programmatically. As these variables are usually objects, they cannot be specified via properties. Closes gh-8965
1 parent 9e3e067 commit a2fafa1

File tree

7 files changed

+132
-13
lines changed

7 files changed

+132
-13
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/AbstractFreeMarkerConfiguration.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,21 +16,30 @@
1616

1717
package org.springframework.boot.autoconfigure.freemarker;
1818

19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
1922
import java.util.Properties;
2023

24+
import org.springframework.beans.factory.ObjectProvider;
2125
import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory;
2226

2327
/**
2428
* Base class for shared FreeMarker configuration.
2529
*
2630
* @author Brian Clozel
31+
* @author Stephane Nicoll
2732
*/
2833
abstract class AbstractFreeMarkerConfiguration {
2934

3035
private final FreeMarkerProperties properties;
3136

32-
protected AbstractFreeMarkerConfiguration(FreeMarkerProperties properties) {
37+
private final List<FreeMarkerVariablesCustomizer> variablesCustomizers;
38+
39+
protected AbstractFreeMarkerConfiguration(FreeMarkerProperties properties,
40+
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
3341
this.properties = properties;
42+
this.variablesCustomizers = variablesCustomizers.orderedStream().toList();
3443
}
3544

3645
protected final FreeMarkerProperties getProperties() {
@@ -41,10 +50,23 @@ protected void applyProperties(FreeMarkerConfigurationFactory factory) {
4150
factory.setTemplateLoaderPaths(this.properties.getTemplateLoaderPath());
4251
factory.setPreferFileSystemAccess(this.properties.isPreferFileSystemAccess());
4352
factory.setDefaultEncoding(this.properties.getCharsetName());
53+
factory.setFreemarkerSettings(createFreeMarkerSettings());
54+
factory.setFreemarkerVariables(createFreeMarkerVariables());
55+
}
56+
57+
private Properties createFreeMarkerSettings() {
4458
Properties settings = new Properties();
4559
settings.put("recognize_standard_file_extensions", "true");
4660
settings.putAll(this.properties.getSettings());
47-
factory.setFreemarkerSettings(settings);
61+
return settings;
62+
}
63+
64+
private Map<String, Object> createFreeMarkerVariables() {
65+
Map<String, Object> variables = new HashMap<>();
66+
for (FreeMarkerVariablesCustomizer customizer : this.variablesCustomizers) {
67+
customizer.customizeFreeMarkerVariables(variables);
68+
}
69+
return variables;
4870
}
4971

5072
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerNonWebConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.autoconfigure.freemarker;
1818

19+
import org.springframework.beans.factory.ObjectProvider;
1920
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2021
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
2122
import org.springframework.context.annotation.Bean;
@@ -32,8 +33,9 @@
3233
@ConditionalOnNotWebApplication
3334
class FreeMarkerNonWebConfiguration extends AbstractFreeMarkerConfiguration {
3435

35-
FreeMarkerNonWebConfiguration(FreeMarkerProperties properties) {
36-
super(properties);
36+
FreeMarkerNonWebConfiguration(FreeMarkerProperties properties,
37+
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
38+
super(properties, variablesCustomizers);
3739
}
3840

3941
@Bean

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerReactiveWebConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.boot.autoconfigure.freemarker;
1818

19+
import org.springframework.beans.factory.ObjectProvider;
1920
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2021
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2122
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -38,8 +39,9 @@
3839
@AutoConfigureAfter(WebFluxAutoConfiguration.class)
3940
class FreeMarkerReactiveWebConfiguration extends AbstractFreeMarkerConfiguration {
4041

41-
FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties) {
42-
super(properties);
42+
FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties,
43+
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
44+
super(properties, variablesCustomizers);
4345
}
4446

4547
@Bean

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerServletWebConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import jakarta.servlet.DispatcherType;
2020
import jakarta.servlet.Servlet;
2121

22+
import org.springframework.beans.factory.ObjectProvider;
2223
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
2324
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2425
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -47,8 +48,9 @@
4748
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
4849
class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration {
4950

50-
protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties) {
51-
super(properties);
51+
protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties,
52+
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
53+
super(properties, variablesCustomizers);
5254
}
5355

5456
@Bean
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-2024 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+
* https://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.boot.autoconfigure.freemarker;
18+
19+
import java.util.Map;
20+
21+
import freemarker.template.Configuration;
22+
23+
import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory;
24+
25+
/**
26+
* Callback interface that can be implemented by beans wishing to customize the FreeMarker
27+
* variables used as {@link Configuration#getSharedVariableNames() shared variables}
28+
* before it is used by an auto-configured {@link FreeMarkerConfigurationFactory}.
29+
*
30+
* @author Stephane Nicoll
31+
* @since 3.4.0
32+
*/
33+
@FunctionalInterface
34+
public interface FreeMarkerVariablesCustomizer {
35+
36+
/**
37+
* Customize the {@code variables} to be set as well-known FreeMarker objects.
38+
* @param variables the variables to customize
39+
* @see FreeMarkerConfigurationFactory#setFreemarkerVariables(Map)
40+
*/
41+
void customizeFreeMarkerVariables(Map<String, Object> variables);
42+
43+
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,8 +27,14 @@
2727
import org.springframework.boot.test.system.CapturedOutput;
2828
import org.springframework.boot.test.system.OutputCaptureExtension;
2929
import org.springframework.boot.testsupport.BuildOutput;
30+
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.core.annotation.Order;
3033

3134
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.mockito.ArgumentMatchers.any;
36+
import static org.mockito.BDDMockito.then;
37+
import static org.mockito.Mockito.mock;
3238

3339
/**
3440
* Tests for {@link FreeMarkerAutoConfiguration}.
@@ -80,6 +86,24 @@ void nonExistentLocationAndEmptyLocation(CapturedOutput output) {
8086
.run((context) -> assertThat(output).doesNotContain("Cannot find template location"));
8187
}
8288

89+
@Test
90+
void variableCustomizerShouldBeApplied() {
91+
FreeMarkerVariablesCustomizer customizer = mock(FreeMarkerVariablesCustomizer.class);
92+
this.contextRunner.withBean(FreeMarkerVariablesCustomizer.class, () -> customizer)
93+
.run((context) -> then(customizer).should().customizeFreeMarkerVariables(any()));
94+
}
95+
96+
@Test
97+
@SuppressWarnings("unchecked")
98+
void variableCustomizersShouldBeAppliedInOrder() {
99+
this.contextRunner.withUserConfiguration(VariablesCustomizersConfiguration.class).run((context) -> {
100+
assertThat(context).hasSingleBean(freemarker.template.Configuration.class);
101+
freemarker.template.Configuration configuration = context.getBean(freemarker.template.Configuration.class);
102+
assertThat(configuration.getSharedVariableNames()).contains("order", "one", "two");
103+
assertThat(configuration.getSharedVariable("order")).hasToString("5");
104+
});
105+
}
106+
83107
public static class DataModel {
84108

85109
public String getGreeting() {
@@ -88,4 +112,27 @@ public String getGreeting() {
88112

89113
}
90114

115+
@Configuration(proxyBeanMethods = false)
116+
static class VariablesCustomizersConfiguration {
117+
118+
@Bean
119+
@Order(5)
120+
FreeMarkerVariablesCustomizer variablesCustomizer() {
121+
return (variables) -> {
122+
variables.put("order", 5);
123+
variables.put("one", "one");
124+
};
125+
}
126+
127+
@Bean
128+
@Order(2)
129+
FreeMarkerVariablesCustomizer anotherVariablesCustomizer() {
130+
return (variables) -> {
131+
variables.put("order", 2);
132+
variables.put("two", "two");
133+
};
134+
}
135+
136+
}
137+
91138
}

spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ If you add your own, you have to be aware of the order and in which position you
224224
The prefix is externalized to `spring.freemarker.prefix`, and the suffix is externalized to `spring.freemarker.suffix`.
225225
The default values of the prefix and suffix are empty and '`.ftlh`', respectively.
226226
You can override `FreeMarkerViewResolver` by providing a bean of the same name.
227+
FreeMarker variables can be customized by defining a bean of type `FreeMarkerVariablesCustomizer`.
227228
* If you use Groovy templates (actually, if `groovy-templates` is on your classpath), you also have a `GroovyMarkupViewResolver` named '`groovyMarkupViewResolver`'.
228229
It looks for resources in a loader path by surrounding the view name with a prefix and suffix (externalized to `spring.groovy.template.prefix` and `spring.groovy.template.suffix`).
229230
The prefix and suffix have default values of '`classpath:/templates/`' and '`.tpl`', respectively.

0 commit comments

Comments
 (0)