From a2fafa112fd98db7335d8b75f2b9980807d3baf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 2 Aug 2024 11:41:23 +0200 Subject: [PATCH] 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 --- .../AbstractFreeMarkerConfiguration.java | 28 +++++++++-- .../FreeMarkerNonWebConfiguration.java | 8 +-- .../FreeMarkerReactiveWebConfiguration.java | 8 +-- .../FreeMarkerServletWebConfiguration.java | 8 +-- .../FreeMarkerVariablesCustomizer.java | 43 ++++++++++++++++ .../FreeMarkerAutoConfigurationTests.java | 49 ++++++++++++++++++- .../modules/how-to/pages/spring-mvc.adoc | 1 + 7 files changed, 132 insertions(+), 13 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerVariablesCustomizer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/AbstractFreeMarkerConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/AbstractFreeMarkerConfiguration.java index 0f4def064dcb..ba352f78a77e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/AbstractFreeMarkerConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/AbstractFreeMarkerConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,21 +16,30 @@ package org.springframework.boot.autoconfigure.freemarker; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Properties; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory; /** * Base class for shared FreeMarker configuration. * * @author Brian Clozel + * @author Stephane Nicoll */ abstract class AbstractFreeMarkerConfiguration { private final FreeMarkerProperties properties; - protected AbstractFreeMarkerConfiguration(FreeMarkerProperties properties) { + private final List variablesCustomizers; + + protected AbstractFreeMarkerConfiguration(FreeMarkerProperties properties, + ObjectProvider variablesCustomizers) { this.properties = properties; + this.variablesCustomizers = variablesCustomizers.orderedStream().toList(); } protected final FreeMarkerProperties getProperties() { @@ -41,10 +50,23 @@ protected void applyProperties(FreeMarkerConfigurationFactory factory) { factory.setTemplateLoaderPaths(this.properties.getTemplateLoaderPath()); factory.setPreferFileSystemAccess(this.properties.isPreferFileSystemAccess()); factory.setDefaultEncoding(this.properties.getCharsetName()); + factory.setFreemarkerSettings(createFreeMarkerSettings()); + factory.setFreemarkerVariables(createFreeMarkerVariables()); + } + + private Properties createFreeMarkerSettings() { Properties settings = new Properties(); settings.put("recognize_standard_file_extensions", "true"); settings.putAll(this.properties.getSettings()); - factory.setFreemarkerSettings(settings); + return settings; + } + + private Map createFreeMarkerVariables() { + Map variables = new HashMap<>(); + for (FreeMarkerVariablesCustomizer customizer : this.variablesCustomizers) { + customizer.customizeFreeMarkerVariables(variables); + } + return variables; } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerNonWebConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerNonWebConfiguration.java index ad362826afb1..89d572c87783 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerNonWebConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerNonWebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.freemarker; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication; import org.springframework.context.annotation.Bean; @@ -32,8 +33,9 @@ @ConditionalOnNotWebApplication class FreeMarkerNonWebConfiguration extends AbstractFreeMarkerConfiguration { - FreeMarkerNonWebConfiguration(FreeMarkerProperties properties) { - super(properties); + FreeMarkerNonWebConfiguration(FreeMarkerProperties properties, + ObjectProvider variablesCustomizers) { + super(properties, variablesCustomizers); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerReactiveWebConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerReactiveWebConfiguration.java index a381f0496a9b..383fc1fd65bc 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerReactiveWebConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerReactiveWebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.boot.autoconfigure.freemarker; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -38,8 +39,9 @@ @AutoConfigureAfter(WebFluxAutoConfiguration.class) class FreeMarkerReactiveWebConfiguration extends AbstractFreeMarkerConfiguration { - FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties) { - super(properties); + FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties, + ObjectProvider variablesCustomizers) { + super(properties, variablesCustomizers); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerServletWebConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerServletWebConfiguration.java index adf878e6b10b..e268c57cf740 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerServletWebConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerServletWebConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.Servlet; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -47,8 +48,9 @@ @AutoConfigureAfter(WebMvcAutoConfiguration.class) class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration { - protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties) { - super(properties); + protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties, + ObjectProvider variablesCustomizers) { + super(properties, variablesCustomizers); } @Bean diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerVariablesCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerVariablesCustomizer.java new file mode 100644 index 000000000000..7d072d63e003 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerVariablesCustomizer.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.freemarker; + +import java.util.Map; + +import freemarker.template.Configuration; + +import org.springframework.ui.freemarker.FreeMarkerConfigurationFactory; + +/** + * Callback interface that can be implemented by beans wishing to customize the FreeMarker + * variables used as {@link Configuration#getSharedVariableNames() shared variables} + * before it is used by an auto-configured {@link FreeMarkerConfigurationFactory}. + * + * @author Stephane Nicoll + * @since 3.4.0 + */ +@FunctionalInterface +public interface FreeMarkerVariablesCustomizer { + + /** + * Customize the {@code variables} to be set as well-known FreeMarker objects. + * @param variables the variables to customize + * @see FreeMarkerConfigurationFactory#setFreemarkerVariables(Map) + */ + void customizeFreeMarkerVariables(Map variables); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java index 77658ef5fb0e..156abc518671 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/freemarker/FreeMarkerAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,8 +27,14 @@ import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; import org.springframework.boot.testsupport.BuildOutput; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; /** * Tests for {@link FreeMarkerAutoConfiguration}. @@ -80,6 +86,24 @@ void nonExistentLocationAndEmptyLocation(CapturedOutput output) { .run((context) -> assertThat(output).doesNotContain("Cannot find template location")); } + @Test + void variableCustomizerShouldBeApplied() { + FreeMarkerVariablesCustomizer customizer = mock(FreeMarkerVariablesCustomizer.class); + this.contextRunner.withBean(FreeMarkerVariablesCustomizer.class, () -> customizer) + .run((context) -> then(customizer).should().customizeFreeMarkerVariables(any())); + } + + @Test + @SuppressWarnings("unchecked") + void variableCustomizersShouldBeAppliedInOrder() { + this.contextRunner.withUserConfiguration(VariablesCustomizersConfiguration.class).run((context) -> { + assertThat(context).hasSingleBean(freemarker.template.Configuration.class); + freemarker.template.Configuration configuration = context.getBean(freemarker.template.Configuration.class); + assertThat(configuration.getSharedVariableNames()).contains("order", "one", "two"); + assertThat(configuration.getSharedVariable("order")).hasToString("5"); + }); + } + public static class DataModel { public String getGreeting() { @@ -88,4 +112,27 @@ public String getGreeting() { } + @Configuration(proxyBeanMethods = false) + static class VariablesCustomizersConfiguration { + + @Bean + @Order(5) + FreeMarkerVariablesCustomizer variablesCustomizer() { + return (variables) -> { + variables.put("order", 5); + variables.put("one", "one"); + }; + } + + @Bean + @Order(2) + FreeMarkerVariablesCustomizer anotherVariablesCustomizer() { + return (variables) -> { + variables.put("order", 2); + variables.put("two", "two"); + }; + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc index 077fbf97035c..f2205752247a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/spring-mvc.adoc @@ -224,6 +224,7 @@ If you add your own, you have to be aware of the order and in which position you The prefix is externalized to `spring.freemarker.prefix`, and the suffix is externalized to `spring.freemarker.suffix`. The default values of the prefix and suffix are empty and '`.ftlh`', respectively. You can override `FreeMarkerViewResolver` by providing a bean of the same name. + FreeMarker variables can be customized by defining a bean of type `FreeMarkerVariablesCustomizer`. * If you use Groovy templates (actually, if `groovy-templates` is on your classpath), you also have a `GroovyMarkupViewResolver` named '`groovyMarkupViewResolver`'. 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`). The prefix and suffix have default values of '`classpath:/templates/`' and '`.tpl`', respectively.