Skip to content

Commit

Permalink
Add support for customizing FreeMarker variables
Browse files Browse the repository at this point in the history
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
  • Loading branch information
snicoll committed Aug 2, 2024
1 parent 9e3e067 commit a2fafa1
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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() {
Expand All @@ -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<String, Object> createFreeMarkerVariables() {
Map<String, Object> variables = new HashMap<>();
for (FreeMarkerVariablesCustomizer customizer : this.variablesCustomizers) {
customizer.customizeFreeMarkerVariables(variables);
}
return variables;
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -32,8 +33,9 @@
@ConditionalOnNotWebApplication
class FreeMarkerNonWebConfiguration extends AbstractFreeMarkerConfiguration {

FreeMarkerNonWebConfiguration(FreeMarkerProperties properties) {
super(properties);
FreeMarkerNonWebConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
super(properties, variablesCustomizers);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -38,8 +39,9 @@
@AutoConfigureAfter(WebFluxAutoConfiguration.class)
class FreeMarkerReactiveWebConfiguration extends AbstractFreeMarkerConfiguration {

FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties) {
super(properties);
FreeMarkerReactiveWebConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
super(properties, variablesCustomizers);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -47,8 +48,9 @@
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
class FreeMarkerServletWebConfiguration extends AbstractFreeMarkerConfiguration {

protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties) {
super(properties);
protected FreeMarkerServletWebConfiguration(FreeMarkerProperties properties,
ObjectProvider<FreeMarkerVariablesCustomizer> variablesCustomizers) {
super(properties, variablesCustomizers);
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -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);

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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}.
Expand Down Expand Up @@ -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() {
Expand All @@ -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");
};
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit a2fafa1

Please sign in to comment.