Create spring-boot-thymeleaf module

This commit is contained in:
Moritz Halbritter
2025-03-25 11:22:30 +01:00
committed by Phillip Webb
parent b2b849b30d
commit 843867c0f4
26 changed files with 135 additions and 82 deletions

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import org.thymeleaf.ITemplateEngine;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.spring6.ISpringTemplateEngine;
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.SpringWebFluxTemplateEngine;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configuration classes for Thymeleaf's {@link ITemplateEngine}. Imported by
* {@link ThymeleafAutoConfiguration}.
*
* @author Andy Wilkinson
*/
class TemplateEngineConfigurations {
@Configuration(proxyBeanMethods = false)
static class DefaultTemplateEngineConfiguration {
@Bean
@ConditionalOnMissingBean(ISpringTemplateEngine.class)
SpringTemplateEngine templateEngine(ThymeleafProperties properties,
ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
SpringTemplateEngine engine = new SpringTemplateEngine();
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
dialects.orderedStream().forEach(engine::addDialect);
return engine;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnBooleanProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ReactiveTemplateEngineConfiguration {
@Bean
@ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties,
ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
dialects.orderedStream().forEach(engine::addDialect);
return engine;
}
}
}

View File

@@ -0,0 +1,257 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import java.util.LinkedHashMap;
import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect;
import jakarta.servlet.DispatcherType;
import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.template.TemplateLocation;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.thymeleaf.autoconfigure.ThymeleafProperties.Reactive;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.security.web.server.csrf.CsrfToken;
import org.springframework.util.MimeType;
import org.springframework.util.unit.DataSize;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
import org.springframework.web.servlet.view.AbstractCachingViewResolver;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Thymeleaf.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Brian Clozel
* @author Eddú Meléndez
* @author Daniel Fernández
* @author Kazuki Shimizu
* @author Artsiom Yudovin
* @since 4.0.0
*/
@AutoConfiguration(afterName = { "org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration",
"org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration" })
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@Import({ TemplateEngineConfigurations.ReactiveTemplateEngineConfiguration.class,
TemplateEngineConfigurations.DefaultTemplateEngineConfiguration.class })
public class ThymeleafAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "defaultTemplateResolver")
static class DefaultTemplateResolverConfiguration {
private static final Log logger = LogFactory.getLog(DefaultTemplateResolverConfiguration.class);
private final ThymeleafProperties properties;
private final ApplicationContext applicationContext;
DefaultTemplateResolverConfiguration(ThymeleafProperties properties, ApplicationContext applicationContext) {
this.properties = properties;
this.applicationContext = applicationContext;
checkTemplateLocationExists();
}
private void checkTemplateLocationExists() {
boolean checkTemplateLocation = this.properties.isCheckTemplateLocation();
if (checkTemplateLocation) {
TemplateLocation location = new TemplateLocation(this.properties.getPrefix());
if (!location.exists(this.applicationContext)) {
logger.warn("Cannot find template location: " + location
+ " (please add some templates, check your Thymeleaf configuration, or set spring.thymeleaf."
+ "check-template-location=false)");
}
}
}
@Bean
SpringResourceTemplateResolver defaultTemplateResolver() {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(this.applicationContext);
resolver.setPrefix(this.properties.getPrefix());
resolver.setSuffix(this.properties.getSuffix());
resolver.setTemplateMode(this.properties.getMode());
if (this.properties.getEncoding() != null) {
resolver.setCharacterEncoding(this.properties.getEncoding().name());
}
resolver.setCacheable(this.properties.isCache());
Integer order = this.properties.getTemplateResolverOrder();
if (order != null) {
resolver.setOrder(order);
}
resolver.setCheckExistence(this.properties.isCheckTemplate());
return resolver;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnBooleanProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebMvcConfiguration {
@Bean
@ConditionalOnEnabledResourceChain
@ConditionalOnMissingFilterBean
FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>(
new ResourceUrlEncodingFilter());
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return registration;
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(AbstractCachingViewResolver.class)
static class ThymeleafViewResolverConfiguration {
@Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties,
SpringTemplateEngine templateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setCharacterEncoding(properties.getEncoding().name());
resolver.setContentType(
appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
resolver.setProducePartialOutputWhileProcessing(
properties.getServlet().isProducePartialOutputWhileProcessing());
resolver.setExcludedViewNames(properties.getExcludedViewNames());
resolver.setViewNames(properties.getViewNames());
// This resolver acts as a fallback resolver (e.g. like a
// InternalResourceViewResolver) so it needs to have low precedence
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
resolver.setCache(properties.isCache());
return resolver;
}
private String appendCharset(MimeType type, String charset) {
if (type.getCharset() != null) {
return type.toString();
}
LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
parameters.put("charset", charset);
parameters.putAll(type.getParameters());
return new MimeType(type, parameters).toString();
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnBooleanProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebFluxConfiguration {
@Bean
@ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine,
ThymeleafProperties properties) {
ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
resolver.setTemplateEngine(templateEngine);
mapProperties(properties, resolver);
mapReactiveProperties(properties.getReactive(), resolver);
// This resolver acts as a fallback resolver (e.g. like a
// InternalResourceViewResolver) so it needs to have low precedence
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
return resolver;
}
private void mapProperties(ThymeleafProperties properties, ThymeleafReactiveViewResolver resolver) {
PropertyMapper map = PropertyMapper.get();
map.from(properties::getEncoding).to(resolver::setDefaultCharset);
resolver.setExcludedViewNames(properties.getExcludedViewNames());
resolver.setViewNames(properties.getViewNames());
}
private void mapReactiveProperties(Reactive properties, ThymeleafReactiveViewResolver resolver) {
PropertyMapper map = PropertyMapper.get();
map.from(properties::getMediaTypes).whenNonNull().to(resolver::setSupportedMediaTypes);
map.from(properties::getMaxChunkSize)
.asInt(DataSize::toBytes)
.when((size) -> size > 0)
.to(resolver::setResponseMaxChunkSizeBytes);
map.from(properties::getFullModeViewNames).to(resolver::setFullModeViewNames);
map.from(properties::getChunkedModeViewNames).to(resolver::setChunkedModeViewNames);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(LayoutDialect.class)
static class ThymeleafWebLayoutConfiguration {
@Bean
@ConditionalOnMissingBean
LayoutDialect layoutDialect() {
return new LayoutDialect();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(DataAttributeDialect.class)
static class DataAttributeDialectConfiguration {
@Bean
@ConditionalOnMissingBean
DataAttributeDialect dialect() {
return new DataAttributeDialect();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ SpringSecurityDialect.class, CsrfToken.class })
static class ThymeleafSecurityDialectConfiguration {
@Bean
@ConditionalOnMissingBean
SpringSecurityDialect securityDialect() {
return new SpringSecurityDialect();
}
}
}

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType;
import org.springframework.util.unit.DataSize;
/**
* Properties for Thymeleaf.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @author Daniel Fernández
* @author Kazuki Shimizu
* @since 4.0.0
*/
@ConfigurationProperties("spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
/**
* Whether to check that the template exists before rendering it.
*/
private boolean checkTemplate = true;
/**
* Whether to check that the templates location exists.
*/
private boolean checkTemplateLocation = true;
/**
* Prefix that gets prepended to view names when building a URL.
*/
private String prefix = DEFAULT_PREFIX;
/**
* Suffix that gets appended to view names when building a URL.
*/
private String suffix = DEFAULT_SUFFIX;
/**
* Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
*/
private String mode = "HTML";
/**
* Template files encoding.
*/
private Charset encoding = DEFAULT_ENCODING;
/**
* Whether to enable template caching.
*/
private boolean cache = true;
/**
* Order of the template resolver in the chain. By default, the template resolver is
* first in the chain. Order start at 1 and should only be set if you have defined
* additional "TemplateResolver" beans.
*/
private Integer templateResolverOrder;
/**
* List of view names (patterns allowed) that can be resolved.
*/
private String[] viewNames;
/**
* List of view names (patterns allowed) that should be excluded from resolution.
*/
private String[] excludedViewNames;
/**
* Enable the SpringEL compiler in SpringEL expressions.
*/
private boolean enableSpringElCompiler;
/**
* Whether hidden form inputs acting as markers for checkboxes should be rendered
* before the checkbox element itself.
*/
private boolean renderHiddenMarkersBeforeCheckboxes = false;
/**
* Whether to enable Thymeleaf view resolution for Web frameworks.
*/
private boolean enabled = true;
private final Servlet servlet = new Servlet();
private final Reactive reactive = new Reactive();
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isCheckTemplate() {
return this.checkTemplate;
}
public void setCheckTemplate(boolean checkTemplate) {
this.checkTemplate = checkTemplate;
}
public boolean isCheckTemplateLocation() {
return this.checkTemplateLocation;
}
public void setCheckTemplateLocation(boolean checkTemplateLocation) {
this.checkTemplateLocation = checkTemplateLocation;
}
public String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return this.suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
public String getMode() {
return this.mode;
}
public void setMode(String mode) {
this.mode = mode;
}
public Charset getEncoding() {
return this.encoding;
}
public void setEncoding(Charset encoding) {
this.encoding = encoding;
}
public boolean isCache() {
return this.cache;
}
public void setCache(boolean cache) {
this.cache = cache;
}
public Integer getTemplateResolverOrder() {
return this.templateResolverOrder;
}
public void setTemplateResolverOrder(Integer templateResolverOrder) {
this.templateResolverOrder = templateResolverOrder;
}
public String[] getExcludedViewNames() {
return this.excludedViewNames;
}
public void setExcludedViewNames(String[] excludedViewNames) {
this.excludedViewNames = excludedViewNames;
}
public String[] getViewNames() {
return this.viewNames;
}
public void setViewNames(String[] viewNames) {
this.viewNames = viewNames;
}
public boolean isEnableSpringElCompiler() {
return this.enableSpringElCompiler;
}
public void setEnableSpringElCompiler(boolean enableSpringElCompiler) {
this.enableSpringElCompiler = enableSpringElCompiler;
}
public boolean isRenderHiddenMarkersBeforeCheckboxes() {
return this.renderHiddenMarkersBeforeCheckboxes;
}
public void setRenderHiddenMarkersBeforeCheckboxes(boolean renderHiddenMarkersBeforeCheckboxes) {
this.renderHiddenMarkersBeforeCheckboxes = renderHiddenMarkersBeforeCheckboxes;
}
public Reactive getReactive() {
return this.reactive;
}
public Servlet getServlet() {
return this.servlet;
}
public static class Servlet {
/**
* Content-Type value written to HTTP responses.
*/
private MimeType contentType = MimeType.valueOf("text/html");
/**
* Whether Thymeleaf should start writing partial output as soon as possible or
* buffer until template processing is finished.
*/
private boolean producePartialOutputWhileProcessing = true;
public MimeType getContentType() {
return this.contentType;
}
public void setContentType(MimeType contentType) {
this.contentType = contentType;
}
public boolean isProducePartialOutputWhileProcessing() {
return this.producePartialOutputWhileProcessing;
}
public void setProducePartialOutputWhileProcessing(boolean producePartialOutputWhileProcessing) {
this.producePartialOutputWhileProcessing = producePartialOutputWhileProcessing;
}
}
public static class Reactive {
/**
* Maximum size of data buffers used for writing to the response. Templates will
* execute in CHUNKED mode by default if this is set.
*/
private DataSize maxChunkSize = DataSize.ofBytes(0);
/**
* Media types supported by the view technology.
*/
private List<MediaType> mediaTypes;
/**
* Comma-separated list of view names (patterns allowed) that should be executed
* in FULL mode even if a max chunk size is set.
*/
private String[] fullModeViewNames;
/**
* Comma-separated list of view names (patterns allowed) that should be the only
* ones executed in CHUNKED mode when a max chunk size is set.
*/
private String[] chunkedModeViewNames;
public List<MediaType> getMediaTypes() {
return this.mediaTypes;
}
public void setMediaTypes(List<MediaType> mediaTypes) {
this.mediaTypes = mediaTypes;
}
public DataSize getMaxChunkSize() {
return this.maxChunkSize;
}
public void setMaxChunkSize(DataSize maxChunkSize) {
this.maxChunkSize = maxChunkSize;
}
public String[] getFullModeViewNames() {
return this.fullModeViewNames;
}
public void setFullModeViewNames(String[] fullModeViewNames) {
this.fullModeViewNames = fullModeViewNames;
}
public String[] getChunkedModeViewNames() {
return this.chunkedModeViewNames;
}
public void setChunkedModeViewNames(String[] chunkedModeViewNames) {
this.chunkedModeViewNames = chunkedModeViewNames;
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ClassUtils;
/**
* {@link TemplateAvailabilityProvider} that provides availability information for
* Thymeleaf view templates.
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @since 4.0.0
*/
public class ThymeleafTemplateAvailabilityProvider implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader,
ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("org.thymeleaf.spring6.SpringTemplateEngine", classLoader)) {
String prefix = environment.getProperty("spring.thymeleaf.prefix", ThymeleafProperties.DEFAULT_PREFIX);
String suffix = environment.getProperty("spring.thymeleaf.suffix", ThymeleafProperties.DEFAULT_SUFFIX);
return resourceLoader.getResource(prefix + view + suffix).exists();
}
return false;
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* Auto-configuration for Thymeleaf.
*/
package org.springframework.boot.thymeleaf.autoconfigure;

View File

@@ -0,0 +1,37 @@
{
"groups": [],
"properties": [
{
"name": "spring.thymeleaf.prefix",
"defaultValue": "classpath:/templates/"
},
{
"name": "spring.thymeleaf.reactive.media-types",
"defaultValue": [
"text/html",
"application/xhtml+xml",
"application/xml",
"text/xml",
"application/rss+xml",
"application/atom+xml",
"application/javascript",
"application/ecmascript",
"text/javascript",
"text/ecmascript",
"application/json",
"text/css",
"text/plain",
"text/event-stream"
]
},
{
"name": "spring.thymeleaf.suffix",
"defaultValue": ".html"
}
],
"hints": [],
"ignored": {
"properties": [
]
}
}

View File

@@ -0,0 +1,3 @@
# Template Availability Providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.thymeleaf.autoconfigure.ThymeleafTemplateAvailabilityProvider

View File

@@ -0,0 +1 @@
org.springframework.boot.thymeleaf.autoconfigure.ThymeleafAutoConfiguration

View File

@@ -0,0 +1,271 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Locale;
import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import nz.net.ultraq.thymeleaf.layoutdialect.decorators.strategies.GroupingStrategy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import org.thymeleaf.extras.springsecurity6.util.SpringSecurityContextUtils;
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring6.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.spring6.web.webflux.SpringWebFluxWebApplication;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ThymeleafAutoConfiguration} in Reactive applications.
*
* @author Brian Clozel
* @author Kazuki Shimizu
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
@ExtendWith(OutputCaptureExtension.class)
class ThymeleafReactiveAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class));
@Test
@WithResource(name = "templates/template.html", content = "<html th:text=\"${foo}\">foo</html>")
void createFromConfigClass() {
this.contextRunner.withPropertyValues("spring.thymeleaf.suffix:.html").run((context) -> {
TemplateEngine engine = context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("template", attrs).trim();
assertThat(result).isEqualTo("<html>bar</html>");
});
}
@Test
void overrideCharacterEncoding() {
this.contextRunner.withPropertyValues("spring.thymeleaf.encoding:UTF-16").run((context) -> {
ITemplateResolver resolver = context.getBean(ITemplateResolver.class);
assertThat(resolver).isInstanceOf(SpringResourceTemplateResolver.class);
assertThat(((SpringResourceTemplateResolver) resolver).getCharacterEncoding()).isEqualTo("UTF-16");
ThymeleafReactiveViewResolver views = context.getBean(ThymeleafReactiveViewResolver.class);
Assertions.assertThat(views.getDefaultCharset().name()).isEqualTo("UTF-16");
});
}
@Test
void defaultMediaTypes() {
this.contextRunner.run((context) -> Assertions
.assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getSupportedMediaTypes())
.containsExactly(MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML, MediaType.APPLICATION_XML,
MediaType.TEXT_XML, MediaType.APPLICATION_RSS_XML, MediaType.APPLICATION_ATOM_XML,
new MediaType("application", "javascript"), new MediaType("application", "ecmascript"),
new MediaType("text", "javascript"), new MediaType("text", "ecmascript"),
MediaType.APPLICATION_JSON, new MediaType("text", "css"), MediaType.TEXT_PLAIN,
MediaType.TEXT_EVENT_STREAM));
}
@Test
void overrideMediaTypes() {
this.contextRunner.withPropertyValues("spring.thymeleaf.reactive.media-types:text/html,text/plain")
.run((context) -> Assertions
.assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getSupportedMediaTypes())
.containsExactly(MediaType.TEXT_HTML, MediaType.TEXT_PLAIN));
}
@Test
void overrideTemplateResolverOrder() {
this.contextRunner.withPropertyValues("spring.thymeleaf.templateResolverOrder:25")
.run((context) -> assertThat(context.getBean(ITemplateResolver.class).getOrder())
.isEqualTo(Integer.valueOf(25)));
}
@Test
void overrideViewNames() {
this.contextRunner.withPropertyValues("spring.thymeleaf.viewNames:foo,bar")
.run((context) -> assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getViewNames())
.isEqualTo(new String[] { "foo", "bar" }));
}
@Test
void overrideMaxChunkSize() {
this.contextRunner.withPropertyValues("spring.thymeleaf.reactive.maxChunkSize:8KB")
.run((context) -> assertThat(
context.getBean(ThymeleafReactiveViewResolver.class).getResponseMaxChunkSizeBytes())
.isEqualTo(Integer.valueOf(8192)));
}
@Test
void overrideFullModeViewNames() {
this.contextRunner.withPropertyValues("spring.thymeleaf.reactive.fullModeViewNames:foo,bar")
.run((context) -> assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getFullModeViewNames())
.isEqualTo(new String[] { "foo", "bar" }));
}
@Test
void overrideChunkedModeViewNames() {
this.contextRunner.withPropertyValues("spring.thymeleaf.reactive.chunkedModeViewNames:foo,bar")
.run((context) -> assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getChunkedModeViewNames())
.isEqualTo(new String[] { "foo", "bar" }));
}
@Test
void overrideEnableSpringElCompiler() {
this.contextRunner.withPropertyValues("spring.thymeleaf.enable-spring-el-compiler:true")
.run((context) -> assertThat(context.getBean(SpringWebFluxTemplateEngine.class).getEnableSpringELCompiler())
.isTrue());
}
@Test
void enableSpringElCompilerIsDisabledByDefault() {
this.contextRunner
.run((context) -> assertThat(context.getBean(SpringWebFluxTemplateEngine.class).getEnableSpringELCompiler())
.isFalse());
}
@Test
void overrideRenderHiddenMarkersBeforeCheckboxes() {
this.contextRunner.withPropertyValues("spring.thymeleaf.render-hidden-markers-before-checkboxes:true")
.run((context) -> assertThat(
context.getBean(SpringWebFluxTemplateEngine.class).getRenderHiddenMarkersBeforeCheckboxes())
.isTrue());
}
@Test
void enableRenderHiddenMarkersBeforeCheckboxesIsDisabledByDefault() {
this.contextRunner.run((context) -> assertThat(
context.getBean(SpringWebFluxTemplateEngine.class).getRenderHiddenMarkersBeforeCheckboxes())
.isFalse());
}
@Test
void templateLocationDoesNotExist(CapturedOutput output) {
this.contextRunner.withPropertyValues("spring.thymeleaf.prefix:classpath:/no-such-directory/")
.run((context) -> assertThat(output).contains("Cannot find template location"));
}
@Test
void templateLocationEmpty(CapturedOutput output, @TempDir Path tempDir) throws IOException {
Path directory = tempDir.resolve("empty-templates/empty-directory").toAbsolutePath();
Files.createDirectories(directory);
this.contextRunner.withPropertyValues("spring.thymeleaf.prefix:file:" + directory)
.run((context) -> assertThat(output).doesNotContain("Cannot find template location"));
}
@Test
@WithResource(name = "templates/data-dialect.html", content = "<html><body data:foo=\"${foo}\"></body></html>")
void useDataDialect() {
this.contextRunner.run((context) -> {
ISpringWebFluxTemplateEngine engine = context.getBean(ISpringWebFluxTemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("data-dialect", attrs).trim();
assertThat(result).isEqualTo("<html><body data-foo=\"bar\"></body></html>");
});
}
@Test
@WithResource(name = "templates/java8time-dialect.html",
content = "<html><body th:text=\"${#temporals.create('2015','11','24')}\"></body></html>")
void useJava8TimeDialect() {
this.contextRunner.run((context) -> {
ISpringWebFluxTemplateEngine engine = context.getBean(ISpringWebFluxTemplateEngine.class);
Context attrs = new Context(Locale.UK);
String result = engine.process("java8time-dialect", attrs).trim();
assertThat(result).isEqualTo("<html><body>2015-11-24</body></html>");
});
}
@Test
@WithResource(name = "templates/security-dialect.html",
content = "<html><body><div sec:authentication=\"name\"></div></body></html>")
void useSecurityDialect() {
this.contextRunner.run((context) -> {
ISpringWebFluxTemplateEngine engine = context.getBean(ISpringWebFluxTemplateEngine.class);
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/test").build());
exchange.getAttributes()
.put(SpringSecurityContextUtils.SECURITY_CONTEXT_MODEL_ATTRIBUTE_NAME,
new SecurityContextImpl(new TestingAuthenticationToken("alice", "admin")));
WebContext attrs = new WebContext(SpringWebFluxWebApplication.buildApplication(null)
.buildExchange(exchange, Locale.US, MediaType.TEXT_HTML, StandardCharsets.UTF_8));
String result = engine.process("security-dialect", attrs);
assertThat(result).isEqualTo("<html><body><div>alice</div></body></html>");
});
}
@Test
void securityDialectAutoConfigurationBacksOffWithoutSpringSecurity() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.security"))
.run((context) -> assertThat(context).doesNotHaveBean(SpringSecurityDialect.class));
}
@Test
@WithResource(name = "templates/home.html", content = "<html><body th:text=\"${foo}\">Home</body></html>")
void renderTemplate() {
this.contextRunner.run((context) -> {
ISpringWebFluxTemplateEngine engine = context.getBean(ISpringWebFluxTemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("home", attrs).trim();
assertThat(result).isEqualTo("<html><body>bar</body></html>");
});
}
@Test
void layoutDialectCanBeCustomized() {
this.contextRunner.withUserConfiguration(LayoutDialectConfiguration.class)
.run((context) -> assertThat(
ReflectionTestUtils.getField(context.getBean(LayoutDialect.class), "sortingStrategy"))
.isInstanceOf(GroupingStrategy.class));
}
@Configuration(proxyBeanMethods = false)
static class LayoutDialectConfiguration {
@Bean
LayoutDialect layoutDialect() {
return new LayoutDialect(new GroupingStrategy());
}
}
}

View File

@@ -0,0 +1,405 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import nz.net.ultraq.thymeleaf.layoutdialect.decorators.strategies.GroupingStrategy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafView;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.thymeleaf.web.servlet.JakartaServletWebApplication;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.view.AbstractCachingViewResolver;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ThymeleafAutoConfiguration} in Servlet-based applications.
*
* @author Dave Syer
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Brian Clozel
* @author Kazuki Shimizu
* @author Artsiom Yudovin
* @author Moritz Halbritter
*/
@ExtendWith(OutputCaptureExtension.class)
class ThymeleafServletAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class));
@Test
void autoConfigurationBackOffWithoutThymeleafSpring() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.thymeleaf.spring6"))
.run((context) -> assertThat(context).doesNotHaveBean(TemplateEngine.class));
}
@Test
@WithResource(name = "templates/template.html", content = "<html th:text=\"${foo}\">foo</html>")
void createFromConfigClass() {
this.contextRunner.withPropertyValues("spring.thymeleaf.mode:HTML", "spring.thymeleaf.suffix:")
.run((context) -> {
assertThat(context).hasSingleBean(TemplateEngine.class);
TemplateEngine engine = context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("template.html", attrs).trim();
assertThat(result).isEqualTo("<html>bar</html>");
});
}
@Test
void overrideCharacterEncoding() {
this.contextRunner.withPropertyValues("spring.thymeleaf.encoding:UTF-16").run((context) -> {
ITemplateResolver resolver = context.getBean(ITemplateResolver.class);
assertThat(resolver).isInstanceOf(SpringResourceTemplateResolver.class);
assertThat(((SpringResourceTemplateResolver) resolver).getCharacterEncoding()).isEqualTo("UTF-16");
ThymeleafViewResolver views = context.getBean(ThymeleafViewResolver.class);
assertThat(views.getCharacterEncoding()).isEqualTo("UTF-16");
assertThat(views.getContentType()).isEqualTo("text/html;charset=UTF-16");
});
}
@Test
void overrideDisableProducePartialOutputWhileProcessing() {
this.contextRunner.withPropertyValues("spring.thymeleaf.servlet.produce-partial-output-while-processing:false")
.run((context) -> assertThat(
context.getBean(ThymeleafViewResolver.class).getProducePartialOutputWhileProcessing())
.isFalse());
}
@Test
void disableProducePartialOutputWhileProcessingIsEnabledByDefault() {
this.contextRunner.run((context) -> assertThat(
context.getBean(ThymeleafViewResolver.class).getProducePartialOutputWhileProcessing())
.isTrue());
}
@Test
void overrideTemplateResolverOrder() {
this.contextRunner.withPropertyValues("spring.thymeleaf.templateResolverOrder:25")
.run((context) -> assertThat(context.getBean(ITemplateResolver.class).getOrder())
.isEqualTo(Integer.valueOf(25)));
}
@Test
void overrideViewNames() {
this.contextRunner.withPropertyValues("spring.thymeleaf.viewNames:foo,bar")
.run((context) -> assertThat(context.getBean(ThymeleafViewResolver.class).getViewNames())
.isEqualTo(new String[] { "foo", "bar" }));
}
@Test
void overrideEnableSpringElCompiler() {
this.contextRunner.withPropertyValues("spring.thymeleaf.enable-spring-el-compiler:true")
.run((context) -> assertThat(context.getBean(SpringTemplateEngine.class).getEnableSpringELCompiler())
.isTrue());
}
@Test
void enableSpringElCompilerIsDisabledByDefault() {
this.contextRunner
.run((context) -> assertThat(context.getBean(SpringTemplateEngine.class).getEnableSpringELCompiler())
.isFalse());
}
@Test
void overrideRenderHiddenMarkersBeforeCheckboxes() {
this.contextRunner.withPropertyValues("spring.thymeleaf.render-hidden-markers-before-checkboxes:true")
.run((context) -> assertThat(
context.getBean(SpringTemplateEngine.class).getRenderHiddenMarkersBeforeCheckboxes())
.isTrue());
}
@Test
void enableRenderHiddenMarkersBeforeCheckboxesIsDisabledByDefault() {
this.contextRunner.run((context) -> assertThat(
context.getBean(SpringTemplateEngine.class).getRenderHiddenMarkersBeforeCheckboxes())
.isFalse());
}
@Test
void templateLocationDoesNotExist(CapturedOutput output) {
this.contextRunner.withPropertyValues("spring.thymeleaf.prefix:classpath:/no-such-directory/")
.run((context) -> assertThat(output).contains("Cannot find template location"));
}
@Test
void templateLocationEmpty(CapturedOutput output, @TempDir Path tempDir) throws IOException {
Path directory = tempDir.resolve("empty-templates/empty-directory").toAbsolutePath();
Files.createDirectories(directory);
this.contextRunner.withPropertyValues("spring.thymeleaf.prefix:file:" + directory)
.run((context) -> assertThat(output).doesNotContain("Cannot find template location"));
}
@Test
@WithResource(name = "templates/view.html",
content = """
<html xmlns:th="https://www.thymeleaf.org" xmlns:layout="https://www.ultraq.net.nz/web/thymeleaf/layout" layout:decorator="layout">
<head>
<title layout:fragment="title">Content</title>
</head>
<body>
<div layout:fragment="content">
<span th:text="${foo}">foo</span>
</div>
</body>
</html>
""")
void createLayoutFromConfigClass() {
this.contextRunner.run((context) -> {
ThymeleafView view = (ThymeleafView) context.getBean(ThymeleafViewResolver.class)
.resolveViewName("view", Locale.UK);
MockHttpServletResponse response = new MockHttpServletResponse();
MockHttpServletRequest request = new MockHttpServletRequest(context.getBean(ServletContext.class));
request.setAttribute(RequestContext.WEB_APPLICATION_CONTEXT_ATTRIBUTE, context);
view.render(Collections.singletonMap("foo", "bar"), request, response);
String result = response.getContentAsString();
assertThat(result).contains("<title>Content</title>");
assertThat(result).contains("<span>bar</span>");
context.close();
});
}
@Test
@WithResource(name = "templates/data-dialect.html", content = "<html><body data:foo=\"${foo}\"></body></html>")
void useDataDialect() {
this.contextRunner.run((context) -> {
TemplateEngine engine = context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("data-dialect", attrs).trim();
assertThat(result).isEqualTo("<html><body data-foo=\"bar\"></body></html>");
});
}
@Test
@WithResource(name = "templates/java8time-dialect.html",
content = "<html><body th:text=\"${#temporals.create('2015','11','24')}\"></body></html>")
void useJava8TimeDialect() {
this.contextRunner.run((context) -> {
TemplateEngine engine = context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK);
String result = engine.process("java8time-dialect", attrs).trim();
assertThat(result).isEqualTo("<html><body>2015-11-24</body></html>");
});
}
@Test
@WithResource(name = "templates/security-dialect.html",
content = "<html><body><div sec:authentication=\"name\"></div></body></html>")
void useSecurityDialect() {
this.contextRunner.run((context) -> {
TemplateEngine engine = context.getBean(TemplateEngine.class);
MockServletContext servletContext = new MockServletContext();
JakartaServletWebApplication webApplication = JakartaServletWebApplication.buildApplication(servletContext);
WebContext attrs = new WebContext(webApplication.buildExchange(new MockHttpServletRequest(servletContext),
new MockHttpServletResponse()));
try {
SecurityContextHolder
.setContext(new SecurityContextImpl(new TestingAuthenticationToken("alice", "admin")));
String result = engine.process("security-dialect", attrs);
assertThat(result).isEqualTo("<html><body><div>alice</div></body></html>");
}
finally {
SecurityContextHolder.clearContext();
}
});
}
@Test
void securityDialectAutoConfigurationBacksOffWithoutSpringSecurity() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.springframework.security"))
.run((context) -> assertThat(context).doesNotHaveBean(SpringSecurityDialect.class));
}
@Test
@WithResource(name = "templates/home.html", content = "<html><body th:text=\"${foo}\">Home</body></html>")
void renderTemplate() {
this.contextRunner.run((context) -> {
TemplateEngine engine = context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("home", attrs).trim();
assertThat(result).isEqualTo("<html><body>bar</body></html>");
});
}
@Test
@WithResource(name = "templates/message.html",
content = "<html><body>Message: <span th:text=\"${greeting}\">Hello</span></body></html>")
void renderNonWebAppTemplate() {
new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class))
.run((context) -> {
assertThat(context).doesNotHaveBean(ViewResolver.class);
TemplateEngine engine = context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("greeting", "Hello World"));
String result = engine.process("message", attrs);
assertThat(result).contains("Hello World");
});
}
@Test
void registerResourceHandlingFilterDisabledByDefault() {
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
}
@Test
void registerResourceHandlingFilterOnlyIfResourceChainIsEnabled() {
this.contextRunner.withPropertyValues("spring.web.resources.chain.enabled:true").run((context) -> {
FilterRegistrationBean<?> registration = context.getBean(FilterRegistrationBean.class);
assertThat(registration.getFilter()).isInstanceOf(ResourceUrlEncodingFilter.class);
assertThat(registration).hasFieldOrPropertyWithValue("dispatcherTypes",
EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR));
});
}
@Test
@SuppressWarnings("rawtypes")
void registerResourceHandlingFilterWithOtherRegistrationBean() {
// gh-14897
this.contextRunner.withUserConfiguration(FilterRegistrationOtherConfiguration.class)
.withPropertyValues("spring.web.resources.chain.enabled:true")
.run((context) -> {
Map<String, FilterRegistrationBean> beans = context.getBeansOfType(FilterRegistrationBean.class);
assertThat(beans).hasSize(2);
FilterRegistrationBean registration = beans.values()
.stream()
.filter((r) -> r.getFilter() instanceof ResourceUrlEncodingFilter)
.findFirst()
.get();
assertThat(registration).hasFieldOrPropertyWithValue("dispatcherTypes",
EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR));
});
}
@Test
@SuppressWarnings("rawtypes")
void registerResourceHandlingFilterWithResourceRegistrationBean() {
// gh-14926
this.contextRunner.withUserConfiguration(FilterRegistrationResourceConfiguration.class)
.withPropertyValues("spring.web.resources.chain.enabled:true")
.run((context) -> {
Map<String, FilterRegistrationBean> beans = context.getBeansOfType(FilterRegistrationBean.class);
assertThat(beans).hasSize(1);
FilterRegistrationBean registration = beans.values()
.stream()
.filter((r) -> r.getFilter() instanceof ResourceUrlEncodingFilter)
.findFirst()
.get();
assertThat(registration).hasFieldOrPropertyWithValue("dispatcherTypes",
EnumSet.of(DispatcherType.INCLUDE));
});
}
@Test
void layoutDialectCanBeCustomized() {
this.contextRunner.withUserConfiguration(LayoutDialectConfiguration.class)
.run((context) -> assertThat(
ReflectionTestUtils.getField(context.getBean(LayoutDialect.class), "sortingStrategy"))
.isInstanceOf(GroupingStrategy.class));
}
@Test
void cachingCanBeDisabled() {
this.contextRunner.withPropertyValues("spring.thymeleaf.cache:false").run((context) -> {
Assertions.assertThat(context.getBean(ThymeleafViewResolver.class).isCache()).isFalse();
SpringResourceTemplateResolver templateResolver = context.getBean(SpringResourceTemplateResolver.class);
assertThat(templateResolver.isCacheable()).isFalse();
});
}
@Test
void missingAbstractCachingViewResolver() {
this.contextRunner.withClassLoader(new FilteredClassLoader(AbstractCachingViewResolver.class))
.run((context) -> assertThat(context).hasNotFailed().doesNotHaveBean("thymeleafViewResolver"));
}
@Configuration(proxyBeanMethods = false)
static class LayoutDialectConfiguration {
@Bean
LayoutDialect layoutDialect() {
return new LayoutDialect(new GroupingStrategy());
}
}
@Configuration(proxyBeanMethods = false)
static class FilterRegistrationResourceConfiguration {
@Bean
FilterRegistrationBean<ResourceUrlEncodingFilter> filterRegistration() {
FilterRegistrationBean<ResourceUrlEncodingFilter> bean = new FilterRegistrationBean<>(
new ResourceUrlEncodingFilter());
bean.setDispatcherTypes(EnumSet.of(DispatcherType.INCLUDE));
return bean;
}
}
@Configuration(proxyBeanMethods = false)
static class FilterRegistrationOtherConfiguration {
@Bean
FilterRegistrationBean<OrderedCharacterEncodingFilter> filterRegistration() {
return new FilterRegistrationBean<>(new OrderedCharacterEncodingFilter());
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2012-2025 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.thymeleaf.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ThymeleafTemplateAvailabilityProvider}.
*
* @author Andy Wilkinson
*/
class ThymeleafTemplateAvailabilityProviderTests {
private final TemplateAvailabilityProvider provider = new ThymeleafTemplateAvailabilityProvider();
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
private final MockEnvironment environment = new MockEnvironment();
@Test
@WithResource(name = "templates/home.html")
void availabilityOfTemplateInDefaultLocation() {
assertThat(this.provider.isTemplateAvailable("home", this.environment, getClass().getClassLoader(),
this.resourceLoader))
.isTrue();
}
@Test
void availabilityOfTemplateThatDoesNotExist() {
assertThat(this.provider.isTemplateAvailable("whatever", this.environment, getClass().getClassLoader(),
this.resourceLoader))
.isFalse();
}
@Test
@WithResource(name = "custom-templates/custom.html")
void availabilityOfTemplateWithCustomPrefix() {
this.environment.setProperty("spring.thymeleaf.prefix", "classpath:/custom-templates/");
assertThat(this.provider.isTemplateAvailable("custom", this.environment, getClass().getClassLoader(),
this.resourceLoader))
.isTrue();
}
@Test
@WithResource(name = "templates/suffixed.thymeleaf")
void availabilityOfTemplateWithCustomSuffix() {
this.environment.setProperty("spring.thymeleaf.suffix", ".thymeleaf");
assertThat(this.provider.isTemplateAvailable("suffixed", this.environment, getClass().getClassLoader(),
this.resourceLoader))
.isTrue();
}
}