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
This commit is contained in:
Stéphane Nicoll 2024-08-02 11:41:23 +02:00
parent 9e3e067a4c
commit a2fafa112f
7 changed files with 132 additions and 13 deletions

View File

@ -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<FreeMarkerVariablesCustomizer> variablesCustomizers;
protected AbstractFreeMarkerConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
this.properties = properties;
this.variablesCustomizers = variablesCustomizers.orderedStream().toList();
}
protected final FreeMarkerProperties getProperties() {
@ -41,10 +50,23 @@ abstract class AbstractFreeMarkerConfiguration {
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<String, Object> createFreeMarkerVariables() {
Map<String, Object> variables = new HashMap<>();
for (FreeMarkerVariablesCustomizer customizer : this.variablesCustomizers) {
customizer.customizeFreeMarkerVariables(variables);
}
return variables;
}
}

View File

@ -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 @@ import org.springframework.ui.freemarker.FreeMarkerConfigurationFactoryBean;
@ConditionalOnNotWebApplication
class FreeMarkerNonWebConfiguration extends AbstractFreeMarkerConfiguration {
FreeMarkerNonWebConfiguration(FreeMarkerProperties properties) {
super(properties);
FreeMarkerNonWebConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
super(properties, variablesCustomizers);
}
@Bean

View File

@ -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 @@ import org.springframework.web.reactive.result.view.freemarker.FreeMarkerViewRes
@AutoConfigureAfter(WebFluxAutoConfiguration.class)
class FreeMarkerReactiveWebConfiguration extends AbstractFreeMarkerConfiguration {
FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties) {
super(properties);
FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
super(properties, variablesCustomizers);
}
@Bean

View File

@ -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 @@ package org.springframework.boot.autoconfigure.freemarker;
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 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration {
protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties) {
super(properties);
protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
super(properties, variablesCustomizers);
}
@Bean

View File

@ -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<String, Object> variables);
}

View File

@ -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.context.runner.ApplicationContextRunner;
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 @@ class FreeMarkerAutoConfigurationTests {
.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 @@ class FreeMarkerAutoConfigurationTests {
}
@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");
};
}
}
}

View File

@ -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.