Remove support for Thymeleaf

Closes gh-28611
pull/28862/head
Scott Frederick 3 years ago committed by Andy Wilkinson
parent 22cc9ca6fa
commit 015dca1956

@ -189,7 +189,6 @@ public class DocumentConfigurationProperties extends DefaultTask {
prefix.accept("spring.freemarker"); prefix.accept("spring.freemarker");
prefix.accept("spring.groovy"); prefix.accept("spring.groovy");
prefix.accept("spring.mustache"); prefix.accept("spring.mustache");
prefix.accept("spring.thymeleaf");
prefix.accept("spring.groovy.template.configuration", "See GroovyMarkupConfigurer"); prefix.accept("spring.groovy.template.configuration", "See GroovyMarkupConfigurer");
} }

@ -67,13 +67,11 @@ dependencies {
optional("org.apiguardian:apiguardian-api") optional("org.apiguardian:apiguardian-api")
optional("org.codehaus.groovy:groovy-templates") optional("org.codehaus.groovy:groovy-templates")
optional("com.github.ben-manes.caffeine:caffeine") optional("com.github.ben-manes.caffeine:caffeine")
optional("com.github.mxab.thymeleaf.extras:thymeleaf-extras-data-attribute")
optional("com.sendgrid:sendgrid-java") { optional("com.sendgrid:sendgrid-java") {
exclude group: "commons-logging", module: "commons-logging" exclude group: "commons-logging", module: "commons-logging"
} }
optional("com.unboundid:unboundid-ldapsdk") optional("com.unboundid:unboundid-ldapsdk")
optional("com.zaxxer:HikariCP") optional("com.zaxxer:HikariCP")
optional("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
optional("org.aspectj:aspectjweaver") optional("org.aspectj:aspectjweaver")
optional("org.eclipse.jetty:jetty-webapp") { optional("org.eclipse.jetty:jetty-webapp") {
exclude group: "javax.servlet", module: "javax.servlet-api" exclude group: "javax.servlet", module: "javax.servlet-api"
@ -185,10 +183,6 @@ dependencies {
optional("org.springframework.amqp:spring-rabbit-stream") optional("org.springframework.amqp:spring-rabbit-stream")
optional("org.springframework.kafka:spring-kafka") optional("org.springframework.kafka:spring-kafka")
optional("org.springframework.ws:spring-ws-core") optional("org.springframework.ws:spring-ws-core")
optional("org.thymeleaf:thymeleaf")
optional("org.thymeleaf:thymeleaf-spring5")
optional("org.thymeleaf.extras:thymeleaf-extras-java8time")
optional("org.thymeleaf.extras:thymeleaf-extras-springsecurity5")
optional("redis.clients:jedis") optional("redis.clients:jedis")
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))

@ -1,305 +0,0 @@
/*
* Copyright 2012-2021 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.thymeleaf;
import java.util.LinkedHashMap;
import javax.servlet.DispatcherType;
import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect;
import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect;
import org.thymeleaf.spring5.ISpringTemplateEngine;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.thymeleaf.ThymeleafProperties.Reactive;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
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.core.Ordered;
import org.springframework.util.MimeType;
import org.springframework.util.unit.DataSize;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
/**
* {@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 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.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 or check "
+ "your Thymeleaf configuration)");
}
}
}
@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)
protected static class ThymeleafDefaultConfiguration {
@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.SERVLET)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebMvcConfiguration {
@Bean
@ConditionalOnEnabledResourceChain
@ConditionalOnMissingFilterBean(ResourceUrlEncodingFilter.class)
FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>(
new ResourceUrlEncodingFilter());
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
return registration;
}
@Configuration(proxyBeanMethods = false)
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)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafReactiveConfiguration {
@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;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(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 })
static class ThymeleafSecurityDialectConfiguration {
@Bean
@ConditionalOnMissingBean
SpringSecurityDialect securityDialect() {
return new SpringSecurityDialect();
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(Java8TimeDialect.class)
static class ThymeleafJava8TimeDialect {
@Bean
@ConditionalOnMissingBean
Java8TimeDialect java8TimeDialect() {
return new Java8TimeDialect();
}
}
}

@ -1,321 +0,0 @@
/*
* Copyright 2012-2019 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.thymeleaf;
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 1.2.0
*/
@ConfigurationProperties(prefix = "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;
/**
* Comma-separated list of view names (patterns allowed) that can be resolved.
*/
private String[] viewNames;
/**
* Comma-separated 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;
}
}
}

@ -1,45 +0,0 @@
/*
* Copyright 2012-2019 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.thymeleaf;
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 1.1.0
*/
public class ThymeleafTemplateAvailabilityProvider implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader,
ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("org.thymeleaf.spring5.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;
}
}

@ -1,20 +0,0 @@
/*
* Copyright 2012-2019 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.autoconfigure.thymeleaf;

@ -1977,14 +1977,6 @@
"name": "spring.sql.init.mode", "name": "spring.sql.init.mode",
"defaultValue": "embedded" "defaultValue": "embedded"
}, },
{
"name": "spring.thymeleaf.prefix",
"defaultValue": "classpath:/templates/"
},
{
"name": "spring.thymeleaf.suffix",
"defaultValue": ".html"
},
{ {
"name": "spring.web.locale-resolver", "name": "spring.web.locale-resolver",
"defaultValue": "accept-header" "defaultValue": "accept-header"

@ -123,7 +123,6 @@ org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\ org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\ org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\ org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\ org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\ org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
@ -168,7 +167,6 @@ org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
# DataSource initializer detectors # DataSource initializer detectors

@ -30,8 +30,8 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration;
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration; import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader;
@ -144,12 +144,12 @@ class AutoConfigurationImportSelectorTests {
@Test @Test
void combinedExclusionsAreApplied() { void combinedExclusionsAreApplied() {
this.environment.setProperty("spring.autoconfigure.exclude", ThymeleafAutoConfiguration.class.getName()); this.environment.setProperty("spring.autoconfigure.exclude", GroovyTemplateAutoConfiguration.class.getName());
String[] imports = selectImports(EnableAutoConfigurationWithClassAndClassNameExclusions.class); String[] imports = selectImports(EnableAutoConfigurationWithClassAndClassNameExclusions.class);
assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 3); assertThat(imports).hasSize(getAutoConfigurationClassNames().size() - 3);
assertThat(this.importSelector.getLastEvent().getExclusions()).contains( assertThat(this.importSelector.getLastEvent().getExclusions()).contains(
FreeMarkerAutoConfiguration.class.getName(), MustacheAutoConfiguration.class.getName(), FreeMarkerAutoConfiguration.class.getName(), MustacheAutoConfiguration.class.getName(),
ThymeleafAutoConfiguration.class.getName()); GroovyTemplateAutoConfiguration.class.getName());
} }
@Test @Test

@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration; import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; import org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration;
import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.AliasFor;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.AnnotationMetadata;
@ -79,7 +79,7 @@ class ImportAutoConfigurationImportSelectorTests {
this.environment.setProperty("spring.autoconfigure.exclude", FreeMarkerAutoConfiguration.class.getName()); this.environment.setProperty("spring.autoconfigure.exclude", FreeMarkerAutoConfiguration.class.getName());
AnnotationMetadata annotationMetadata = getAnnotationMetadata(MultipleImports.class); AnnotationMetadata annotationMetadata = getAnnotationMetadata(MultipleImports.class);
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsExactly(ThymeleafAutoConfiguration.class.getName()); assertThat(imports).containsExactly(GroovyTemplateAutoConfiguration.class.getName());
} }
@Test @Test
@ -87,14 +87,14 @@ class ImportAutoConfigurationImportSelectorTests {
AnnotationMetadata annotationMetadata = getAnnotationMetadata(MultipleImports.class); AnnotationMetadata annotationMetadata = getAnnotationMetadata(MultipleImports.class);
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName(), assertThat(imports).containsOnly(FreeMarkerAutoConfiguration.class.getName(),
ThymeleafAutoConfiguration.class.getName()); GroovyTemplateAutoConfiguration.class.getName());
} }
@Test @Test
void selfAnnotatingAnnotationDoesNotCauseStackOverflow() throws IOException { void selfAnnotatingAnnotationDoesNotCauseStackOverflow() throws IOException {
AnnotationMetadata annotationMetadata = getAnnotationMetadata(ImportWithSelfAnnotatingAnnotation.class); AnnotationMetadata annotationMetadata = getAnnotationMetadata(ImportWithSelfAnnotatingAnnotation.class);
String[] imports = this.importSelector.selectImports(annotationMetadata); String[] imports = this.importSelector.selectImports(annotationMetadata);
assertThat(imports).containsOnly(ThymeleafAutoConfiguration.class.getName()); assertThat(imports).containsOnly(GroovyTemplateAutoConfiguration.class.getName());
} }
@Test @Test
@ -198,13 +198,13 @@ class ImportAutoConfigurationImportSelectorTests {
@ImportOne @ImportOne
@ImportTwo @ImportTwo
@ImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(exclude = GroovyTemplateAutoConfiguration.class)
static class MultipleImportsWithExclusion { static class MultipleImportsWithExclusion {
} }
@ImportOne @ImportOne
@ImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(exclude = GroovyTemplateAutoConfiguration.class)
static class ExclusionWithoutImport { static class ExclusionWithoutImport {
} }
@ -214,7 +214,7 @@ class ImportAutoConfigurationImportSelectorTests {
} }
@SelfAnnotating(excludeAutoConfiguration = ThymeleafAutoConfiguration.class) @SelfAnnotating(excludeAutoConfiguration = GroovyTemplateAutoConfiguration.class)
static class ImportWithSelfAnnotatingAnnotationExclude { static class ImportWithSelfAnnotatingAnnotationExclude {
} }
@ -226,7 +226,7 @@ class ImportAutoConfigurationImportSelectorTests {
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration(ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(GroovyTemplateAutoConfiguration.class)
@interface ImportTwo { @interface ImportTwo {
} }
@ -255,25 +255,25 @@ class ImportAutoConfigurationImportSelectorTests {
} }
@ImportAutoConfiguration(classes = ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(classes = GroovyTemplateAutoConfiguration.class)
@UnrelatedOne @UnrelatedOne
static class ImportAutoConfigurationWithItemsOne { static class ImportAutoConfigurationWithItemsOne {
} }
@ImportAutoConfiguration(classes = ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(classes = GroovyTemplateAutoConfiguration.class)
@UnrelatedTwo @UnrelatedTwo
static class ImportAutoConfigurationWithItemsTwo { static class ImportAutoConfigurationWithItemsTwo {
} }
@MetaImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class) @MetaImportAutoConfiguration(exclude = GroovyTemplateAutoConfiguration.class)
@UnrelatedOne @UnrelatedOne
static class ImportMetaAutoConfigurationExcludeWithUnrelatedOne { static class ImportMetaAutoConfigurationExcludeWithUnrelatedOne {
} }
@MetaImportAutoConfiguration(exclude = ThymeleafAutoConfiguration.class) @MetaImportAutoConfiguration(exclude = GroovyTemplateAutoConfiguration.class)
@UnrelatedTwo @UnrelatedTwo
static class ImportMetaAutoConfigurationExcludeWithUnrelatedTwo { static class ImportMetaAutoConfigurationExcludeWithUnrelatedTwo {
@ -301,7 +301,7 @@ class ImportAutoConfigurationImportSelectorTests {
} }
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@ImportAutoConfiguration(ThymeleafAutoConfiguration.class) @ImportAutoConfiguration(GroovyTemplateAutoConfiguration.class)
@SelfAnnotating @SelfAnnotating
@interface SelfAnnotating { @interface SelfAnnotating {
@ -317,7 +317,7 @@ class ImportAutoConfigurationImportSelectorTests {
@Override @Override
protected Collection<String> loadFactoryNames(Class<?> source) { protected Collection<String> loadFactoryNames(Class<?> source) {
if (source == MetaImportAutoConfiguration.class) { if (source == MetaImportAutoConfiguration.class) {
return Arrays.asList(ThymeleafAutoConfiguration.class.getName(), return Arrays.asList(GroovyTemplateAutoConfiguration.class.getName(),
FreeMarkerAutoConfiguration.class.getName()); FreeMarkerAutoConfiguration.class.getName());
} }
return super.loadFactoryNames(source); return super.loadFactoryNames(source);

@ -1,235 +0,0 @@
/*
* Copyright 2012-2021 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.thymeleaf;
import java.io.File;
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.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.IContext;
import org.thymeleaf.extras.springsecurity5.util.SpringSecurityContextUtils;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.context.webflux.SpringWebFluxContext;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.springframework.boot.autoconfigure.AutoConfigurations;
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.BuildOutput;
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
*/
@ExtendWith(OutputCaptureExtension.class)
class ThymeleafReactiveAutoConfigurationTests {
private final BuildOutput buildOutput = new BuildOutput(getClass());
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class));
@Test
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);
assertThat(views.getDefaultCharset().name()).isEqualTo("UTF-16");
});
}
@Test
void overrideMediaTypes() {
this.contextRunner.withPropertyValues("spring.thymeleaf.reactive.media-types:text/html,text/plain").run(
(context) -> assertThat(context.getBean(ThymeleafReactiveViewResolver.class).getSupportedMediaTypes())
.contains(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) {
new File(this.buildOutput.getTestResourcesLocation(), "empty-templates/empty-directory").mkdirs();
this.contextRunner.withPropertyValues("spring.thymeleaf.prefix:classpath:/empty-templates/empty-directory/")
.run((context) -> assertThat(output).doesNotContain("Cannot find template location"));
}
@Test
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
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
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")));
IContext attrs = new SpringWebFluxContext(exchange);
String result = engine.process("security-dialect", attrs);
assertThat(result).isEqualTo("<html><body><div>alice</div></body></html>" + System.lineSeparator());
});
}
@Test
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());
}
}
}

@ -1,351 +0,0 @@
/*
* Copyright 2012-2021 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.thymeleaf;
import java.io.File;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Locale;
import java.util.Map;
import javax.servlet.DispatcherType;
import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
import nz.net.ultraq.thymeleaf.layoutdialect.decorators.strategies.GroupingStrategy;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafView;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
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.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.BuildOutput;
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 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
*/
@ExtendWith(OutputCaptureExtension.class)
class ThymeleafServletAutoConfigurationTests {
private final BuildOutput buildOutput = new BuildOutput(getClass());
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(ThymeleafAutoConfiguration.class));
@Test
void autoConfigurationBackOffWithoutThymeleafSpring() {
this.contextRunner.withClassLoader(new FilteredClassLoader("org.thymeleaf.spring5"))
.run((context) -> assertThat(context).doesNotHaveBean(TemplateEngine.class));
}
@Test
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) {
new File(this.buildOutput.getTestResourcesLocation(), "empty-templates/empty-directory").mkdirs();
this.contextRunner.withPropertyValues("spring.thymeleaf.prefix:classpath:/empty-templates/empty-directory/")
.run((context) -> assertThat(output).doesNotContain("Cannot find template location"));
}
@Test
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();
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
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
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
void useSecurityDialect() {
this.contextRunner.run((context) -> {
TemplateEngine engine = context.getBean(TemplateEngine.class);
WebContext attrs = new WebContext(new MockHttpServletRequest(), new MockHttpServletResponse(),
new MockServletContext());
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>" + System.lineSeparator());
}
finally {
SecurityContextHolder.clearContext();
}
});
}
@Test
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
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) -> {
assertThat(context.getBean(ThymeleafViewResolver.class).isCache()).isFalse();
SpringResourceTemplateResolver templateResolver = context.getBean(SpringResourceTemplateResolver.class);
assertThat(templateResolver.isCacheable()).isFalse();
});
}
@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());
}
}
}

@ -1,67 +0,0 @@
/*
* Copyright 2012-2019 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.thymeleaf;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
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
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
void availabilityOfTemplateWithCustomPrefix() {
this.environment.setProperty("spring.thymeleaf.prefix", "classpath:/custom-templates/");
assertThat(this.provider.isTemplateAvailable("custom", this.environment, getClass().getClassLoader(),
this.resourceLoader)).isTrue();
}
@Test
void availabilityOfTemplateWithCustomSuffix() {
this.environment.setProperty("spring.thymeleaf.suffix", ".thymeleaf");
assertThat(this.provider.isTemplateAvailable("suffixed", this.environment, getClass().getClassLoader(),
this.resourceLoader)).isTrue();
}
}

@ -1,73 +0,0 @@
/*
* Copyright 2012-2021 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.web.servlet;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the welcome page.
*
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "spring.web.resources.chain.strategy.content.enabled=true",
"spring.thymeleaf.prefix=classpath:/templates/thymeleaf/" })
class WelcomePageIntegrationTests {
@LocalServerPort
private int port;
private TestRestTemplate template = new TestRestTemplate();
@Test
void contentStrategyWithWelcomePage() throws Exception {
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
.header("Accept", MediaType.ALL.toString()).build();
ResponseEntity<String> content = this.template.exchange(entity, String.class);
assertThat(content.getBody()).contains("/custom-");
}
@Configuration
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, ThymeleafAutoConfiguration.class })
static class TestConfiguration {
static void main(String[] args) {
new SpringApplicationBuilder(TestConfiguration.class).run(args);
}
}
}

@ -1 +0,0 @@
<html><body th:text="${#temporals.create('2015','11','24')}"></body></html>

@ -1,14 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" xmlns:layout="https://github.com/ultraq/thymeleaf-layout-dialect">
<head>
<title layout:fragment="title">Layout</title>
</head>
<body>
<div class="container">
<h1 layout:fragment="title">Layout</h1>
<div layout:fragment="content">
Fake content
</div>
</div>
</body>
</html>

@ -1 +0,0 @@
<html><body>Message: <span th:text="${greeting}">Hello</span></body></html>

@ -1,10 +0,0 @@
<!doctype html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Test Thymeleaf</title>
<link th:href="@{/custom.css}" rel="stylesheet" />
</head>
<body>
</body>
</html>

@ -1,10 +0,0 @@
<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>

@ -1,33 +0,0 @@
package app
@Grab("thymeleaf-spring5")
@Controller
class Example {
@RequestMapping("/")
public String helloWorld(Map<String,Object> model) {
model.putAll([title: "My Page", date: new Date(), message: "Hello World"])
return "home"
}
}
@Configuration(proxyBeanMethods = false)
@Log
class MvcConfiguration extends WebMvcConfigurerAdapter {
@Override
void addInterceptors(InterceptorRegistry registry) {
log.info "Registering interceptor"
registry.addInterceptor(interceptor())
}
@Bean
HandlerInterceptor interceptor() {
log.info "Creating interceptor"
[
postHandle: { request, response, handler, mav ->
log.info "Intercepted: model=" + mav.model
}
] as HandlerInterceptorAdapter
}
}

@ -87,15 +87,6 @@ class SampleIntegrationTests {
assertThat(this.cli.getHttpOutput()).isEqualTo("World!"); assertThat(this.cli.getHttpOutput()).isEqualTo("World!");
} }
@Test
void uiSample() throws Exception {
this.cli.run("ui.groovy", "--classpath=.:src/test/resources");
String result = this.cli.getHttpOutput();
assertThat(result).contains("Hello World");
result = this.cli.getHttpOutput("/css/bootstrap.min.css");
assertThat(result).contains("container");
}
@Test @Test
void actuatorSample() throws Exception { void actuatorSample() throws Exception {
this.cli.run("actuator.groovy"); this.cli.run("actuator.groovy");

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title th:text="${title}">Title</title>
<link rel="stylesheet" th:href="@{/resources/css/bootstrap.min.css}"
href="../../resources/css/bootstrap.min.css" />
</head>
<body>
<div class="container">
<div class="navbar">
<div class="navbar-inner">
<a class="brand" href="https://www.thymeleaf.org"> Thymeleaf -
Plain </a>
<ul class="nav">
<li><a th:href="@{/}" href="home.html"> Home </a></li>
</ul>
</div>
</div>
<h1 th:text="${title}">Title</h1>
<div th:text="${message}">Fake content</div>
<div id="created" th:text="${#dates.format(date)}">July 11,
2012 2:17:16 PM CDT</div>
</div>
</body>
</html>

@ -1386,7 +1386,6 @@ bom {
"spring-boot-starter-rsocket", "spring-boot-starter-rsocket",
"spring-boot-starter-security", "spring-boot-starter-security",
"spring-boot-starter-test", "spring-boot-starter-test",
"spring-boot-starter-thymeleaf",
"spring-boot-starter-tomcat", "spring-boot-starter-tomcat",
"spring-boot-starter-undertow", "spring-boot-starter-undertow",
"spring-boot-starter-validation", "spring-boot-starter-validation",
@ -1621,42 +1620,6 @@ bom {
] ]
} }
} }
library("Thymeleaf", "3.0.12.RELEASE") {
group("org.thymeleaf") {
modules = [
"thymeleaf",
"thymeleaf-spring5"
]
}
}
library("Thymeleaf Extras Data Attribute", "2.0.1") {
group("com.github.mxab.thymeleaf.extras") {
modules = [
"thymeleaf-extras-data-attribute"
]
}
}
library("Thymeleaf Extras Java8Time", "3.0.4.RELEASE") {
group("org.thymeleaf.extras") {
modules = [
"thymeleaf-extras-java8time"
]
}
}
library("Thymeleaf Extras SpringSecurity", "3.0.4.RELEASE") {
group("org.thymeleaf.extras") {
modules = [
"thymeleaf-extras-springsecurity5"
]
}
}
library("Thymeleaf Layout Dialect", "3.0.0") {
group("nz.net.ultraq.thymeleaf") {
modules = [
"thymeleaf-layout-dialect"
]
}
}
library("Tomcat", "${tomcatVersion}") { library("Tomcat", "${tomcatVersion}") {
prohibit("[10.0.0-M1,)") { prohibit("[10.0.0-M1,)") {
because "it uses the jakarta.* namespace" because "it uses the jakarta.* namespace"

@ -61,7 +61,6 @@ public class DevToolsPropertyDefaultsPostProcessor implements EnvironmentPostPro
static { static {
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
properties.put("spring.thymeleaf.cache", "false");
properties.put("spring.freemarker.cache", "false"); properties.put("spring.freemarker.cache", "false");
properties.put("spring.groovy.template.cache", "false"); properties.put("spring.groovy.template.cache", "false");
properties.put("spring.mustache.cache", "false"); properties.put("spring.mustache.cache", "false");

@ -206,7 +206,6 @@ task aggregatedJavadoc(type: Javadoc) {
"https://docs.spring.io/spring-framework/docs/${versionConstraints["org.springframework:spring-core"]}/javadoc-api/", "https://docs.spring.io/spring-framework/docs/${versionConstraints["org.springframework:spring-core"]}/javadoc-api/",
"https://docs.spring.io/spring-security/site/docs/${versionConstraints["org.springframework.security:spring-security-core"]}/api/", "https://docs.spring.io/spring-security/site/docs/${versionConstraints["org.springframework.security:spring-security-core"]}/api/",
"https://tomcat.apache.org/tomcat-${tomcatDocsVersion}-doc/api/", "https://tomcat.apache.org/tomcat-${tomcatDocsVersion}-doc/api/",
"https://www.thymeleaf.org/apidocs/thymeleaf/${versionConstraints["org.thymeleaf:thymeleaf"]}/"
] as String[] ] as String[]
} }
} }

@ -672,8 +672,6 @@ howto-change-the-user-details-service-and-add-user-accounts=howto.security.chang
howto-enable-https=howto.security.enable-https howto-enable-https=howto.security.enable-https
howto-hotswapping=howto.hotswapping howto-hotswapping=howto.hotswapping
howto-reload-static-content=howto.hotswapping.reload-static-content howto-reload-static-content=howto.hotswapping.reload-static-content
howto-reload-thymeleaf-template-content=howto.hotswapping.reload-templates
howto-reload-thymeleaf-content=howto.hotswapping.reload-templates.thymeleaf
howto-reload-freemarker-content=howto.hotswapping.reload-templates.freemarker howto-reload-freemarker-content=howto.hotswapping.reload-templates.freemarker
howto-reload-groovy-template-content=howto.hotswapping.reload-templates.groovy howto-reload-groovy-template-content=howto.hotswapping.reload-templates.groovy
howto-reload-fast-restart=howto.hotswapping.fast-application-restarts howto-reload-fast-restart=howto.hotswapping.fast-application-restarts

@ -30,13 +30,6 @@ If you use the `spring-boot-devtools` module, these properties are <<using#using
[[howto.hotswapping.reload-templates.thymeleaf]]
==== Thymeleaf Templates
If you use Thymeleaf, set `spring.thymeleaf.cache` to `false`.
See {spring-boot-autoconfigure-module-code}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`] for other Thymeleaf customization options.
[[howto.hotswapping.reload-templates.freemarker]] [[howto.hotswapping.reload-templates.freemarker]]
==== FreeMarker Templates ==== FreeMarker Templates
If you use FreeMarker, set `spring.freemarker.cache` to `false`. If you use FreeMarker, set `spring.freemarker.cache` to `false`.

@ -203,11 +203,6 @@ If you add your own, you have to be aware of the order and in which position you
This is a composite resolver, delegating to all the others and attempting to find a match to the '`Accept`' HTTP header sent by the client. This is a composite resolver, delegating to all the others and attempting to find a match to the '`Accept`' HTTP header sent by the client.
There is a useful https://spring.io/blog/2013/06/03/content-negotiation-using-views[blog about `ContentNegotiatingViewResolver`] that you might like to study to learn more, and you might also look at the source code for detail. There is a useful https://spring.io/blog/2013/06/03/content-negotiation-using-views[blog about `ContentNegotiatingViewResolver`] that you might like to study to learn more, and you might also look at the source code for detail.
You can switch off the auto-configured `ContentNegotiatingViewResolver` by defining a bean named '`viewResolver`'. You can switch off the auto-configured `ContentNegotiatingViewResolver` by defining a bean named '`viewResolver`'.
* If you use Thymeleaf, you also have a `ThymeleafViewResolver` named '`thymeleafViewResolver`'.
It looks for resources by surrounding the view name with a prefix and suffix.
The prefix is `spring.thymeleaf.prefix`, and the suffix is `spring.thymeleaf.suffix`.
The values of the prefix and suffix default to '`classpath:/templates/`' and '`.html`', respectively.
You can override `ThymeleafViewResolver` by providing a bean of the same name.
* If you use FreeMarker, you also have a `FreeMarkerViewResolver` named '`freeMarkerViewResolver`'. * If you use FreeMarker, you also have a `FreeMarkerViewResolver` named '`freeMarkerViewResolver`'.
It looks for resources in a loader path (which is externalized to `spring.freemarker.templateLoaderPath` and has a default value of '`classpath:/templates/`') by surrounding the view name with a prefix and a suffix. It looks for resources in a loader path (which is externalized to `spring.freemarker.templateLoaderPath` and has a default value of '`classpath:/templates/`') by surrounding the view name with a prefix and a suffix.
The prefix is externalized to `spring.freemarker.prefix`, and the suffix is externalized to `spring.freemarker.suffix`. The prefix is externalized to `spring.freemarker.prefix`, and the suffix is externalized to `spring.freemarker.suffix`.
@ -226,6 +221,5 @@ If you add your own, you have to be aware of the order and in which position you
For more detail, see the following sections: For more detail, see the following sections:
* {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`] * {spring-boot-autoconfigure-module-code}/web/servlet/WebMvcAutoConfiguration.java[`WebMvcAutoConfiguration`]
* {spring-boot-autoconfigure-module-code}/thymeleaf/ThymeleafAutoConfiguration.java[`ThymeleafAutoConfiguration`]
* {spring-boot-autoconfigure-module-code}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`] * {spring-boot-autoconfigure-module-code}/freemarker/FreeMarkerAutoConfiguration.java[`FreeMarkerAutoConfiguration`]
* {spring-boot-autoconfigure-module-code}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`] * {spring-boot-autoconfigure-module-code}/groovy/template/GroovyTemplateAutoConfiguration.java[`GroovyTemplateAutoConfiguration`]

@ -50,7 +50,6 @@ While caching is very beneficial in production, it can be counter-productive dur
For this reason, spring-boot-devtools disables the caching options by default. For this reason, spring-boot-devtools disables the caching options by default.
Cache options are usually configured by settings in your `application.properties` file. Cache options are usually configured by settings in your `application.properties` file.
For example, Thymeleaf offers the configprop:spring.thymeleaf.cache[] property.
Rather than needing to set these properties manually, the `spring-boot-devtools` module automatically applies sensible development-time configuration. Rather than needing to set these properties manually, the `spring-boot-devtools` module automatically applies sensible development-time configuration.
Because you need more information about web requests while developing Spring MVC and Spring WebFlux applications, developer tools suggests you to enable `DEBUG` logging for the `web` logging group. Because you need more information about web requests while developing Spring MVC and Spring WebFlux applications, developer tools suggests you to enable `DEBUG` logging for the `web` logging group.

@ -141,7 +141,6 @@ Spring WebFlux supports a variety of templating technologies, including Thymelea
Spring Boot includes auto-configuration support for the following templating engines: Spring Boot includes auto-configuration support for the following templating engines:
* https://freemarker.apache.org/docs/[FreeMarker] * https://freemarker.apache.org/docs/[FreeMarker]
* https://www.thymeleaf.org[Thymeleaf]
* https://mustache.github.io/[Mustache] * https://mustache.github.io/[Mustache]
When you use one of these templating engines with the default configuration, your templates are picked up automatically from `src/main/resources/templates`. When you use one of these templating engines with the default configuration, your templates are picked up automatically from `src/main/resources/templates`.

@ -308,7 +308,6 @@ Spring Boot includes auto-configuration support for the following templating eng
* https://freemarker.apache.org/docs/[FreeMarker] * https://freemarker.apache.org/docs/[FreeMarker]
* https://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html#_the_markuptemplateengine[Groovy] * https://docs.groovy-lang.org/docs/next/html/documentation/template-engines.html#_the_markuptemplateengine[Groovy]
* https://www.thymeleaf.org[Thymeleaf]
* https://mustache.github.io/[Mustache] * https://mustache.github.io/[Mustache]
TIP: If possible, JSPs should be avoided. TIP: If possible, JSPs should be avoided.

@ -1,11 +0,0 @@
plugins {
id "org.springframework.boot.starter"
}
description = "Starter for building MVC web applications using Thymeleaf views"
dependencies {
api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter"))
api("org.thymeleaf:thymeleaf-spring5")
api("org.thymeleaf.extras:thymeleaf-extras-java8time")
}

@ -100,7 +100,6 @@ dependencies {
testImplementation("org.testcontainers:mongodb") testImplementation("org.testcontainers:mongodb")
testImplementation("org.testcontainers:neo4j") testImplementation("org.testcontainers:neo4j")
testImplementation("org.testcontainers:testcontainers") testImplementation("org.testcontainers:testcontainers")
testImplementation("org.thymeleaf:thymeleaf")
} }
configurations { configurations {

@ -44,8 +44,7 @@ public final class WebFluxTypeExcludeFilter extends StandardAnnotationCustomizab
private static final Class<?>[] NO_CONTROLLERS = {}; private static final Class<?>[] NO_CONTROLLERS = {};
private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module", private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module" };
"org.thymeleaf.dialect.IDialect" };
private static final Set<Class<?>> DEFAULT_INCLUDES; private static final Set<Class<?>> DEFAULT_INCLUDES;

@ -53,7 +53,7 @@ public final class WebMvcTypeExcludeFilter extends StandardAnnotationCustomizabl
private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module", private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module",
"org.springframework.security.config.annotation.web.WebSecurityConfigurer", "org.springframework.security.config.annotation.web.WebSecurityConfigurer",
"org.springframework.security.web.SecurityFilterChain", "org.thymeleaf.dialect.IDialect" }; "org.springframework.security.web.SecurityFilterChain" };
private static final Set<Class<?>> DEFAULT_INCLUDES; private static final Set<Class<?>> DEFAULT_INCLUDES;

@ -128,7 +128,6 @@ org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\ org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\ org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\ org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
@ -182,7 +181,6 @@ org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\ org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\ org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\ org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\

@ -20,7 +20,6 @@ import java.io.IOException;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thymeleaf.dialect.IDialect;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
@ -60,7 +59,6 @@ class WebFluxTypeExcludeFilterTests {
assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
@Test @Test
@ -74,7 +72,6 @@ class WebFluxTypeExcludeFilterTests {
assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
@Test @Test
@ -88,7 +85,6 @@ class WebFluxTypeExcludeFilterTests {
assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebFilter.class)).isTrue(); assertThat(excludes(filter, ExampleWebFilter.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isTrue(); assertThat(excludes(filter, ExampleModule.class)).isTrue();
assertThat(excludes(filter, ExampleDialect.class)).isTrue();
} }
@Test @Test
@ -102,7 +98,6 @@ class WebFluxTypeExcludeFilterTests {
assertThat(excludes(filter, ExampleRepository.class)).isFalse(); assertThat(excludes(filter, ExampleRepository.class)).isFalse();
assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
@Test @Test
@ -116,7 +111,6 @@ class WebFluxTypeExcludeFilterTests {
assertThat(excludes(filter, ExampleRepository.class)).isTrue(); assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebFilter.class)).isFalse(); assertThat(excludes(filter, ExampleWebFilter.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
private boolean excludes(WebFluxTypeExcludeFilter filter, Class<?> type) throws IOException { private boolean excludes(WebFluxTypeExcludeFilter filter, Class<?> type) throws IOException {
@ -191,13 +185,4 @@ class WebFluxTypeExcludeFilterTests {
} }
static class ExampleDialect implements IDialect {
@Override
public String getName() {
return "example";
}
}
} }

@ -24,7 +24,6 @@ import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfigura
import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration; import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
@ -67,11 +66,6 @@ class WebFluxTestAutoConfigurationIntegrationTests {
assertThat(this.applicationContext).has(importedAutoConfiguration(FreeMarkerAutoConfiguration.class)); assertThat(this.applicationContext).has(importedAutoConfiguration(FreeMarkerAutoConfiguration.class));
} }
@Test
void thymeleafAutoConfigurationIsImported() {
assertThat(this.applicationContext).has(importedAutoConfiguration(ThymeleafAutoConfiguration.class));
}
@Test @Test
void errorWebFluxAutoConfigurationIsImported() { void errorWebFluxAutoConfigurationIsImported() {
assertThat(this.applicationContext).has(importedAutoConfiguration(ErrorWebFluxAutoConfiguration.class)); assertThat(this.applicationContext).has(importedAutoConfiguration(ErrorWebFluxAutoConfiguration.class));

@ -25,7 +25,6 @@ import org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration
import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.AsyncTaskExecutor;
@ -62,11 +61,6 @@ class WebMvcTestAutoConfigurationIntegrationTests {
assertThat(this.applicationContext).has(importedAutoConfiguration(MustacheAutoConfiguration.class)); assertThat(this.applicationContext).has(importedAutoConfiguration(MustacheAutoConfiguration.class));
} }
@Test
void thymeleafAutoConfigurationWasImported() {
assertThat(this.applicationContext).has(importedAutoConfiguration(ThymeleafAutoConfiguration.class));
}
@Test @Test
void taskExecutionAutoConfigurationWasImported() { void taskExecutionAutoConfigurationWasImported() {
assertThat(this.applicationContext).has(importedAutoConfiguration(TaskExecutionAutoConfiguration.class)); assertThat(this.applicationContext).has(importedAutoConfiguration(TaskExecutionAutoConfiguration.class));

@ -20,7 +20,6 @@ import java.io.IOException;
import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.module.SimpleModule;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.thymeleaf.dialect.IDialect;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations;
import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.ComponentScan.Filter;
@ -65,7 +64,6 @@ class WebMvcTypeExcludeFilterTests {
assertThat(excludes(filter, SecurityFilterChain.class)).isFalse(); assertThat(excludes(filter, SecurityFilterChain.class)).isFalse();
assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
@Test @Test
@ -83,7 +81,6 @@ class WebMvcTypeExcludeFilterTests {
assertThat(excludes(filter, SecurityFilterChain.class)).isFalse(); assertThat(excludes(filter, SecurityFilterChain.class)).isFalse();
assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
@Test @Test
@ -101,7 +98,6 @@ class WebMvcTypeExcludeFilterTests {
assertThat(excludes(filter, SecurityFilterChain.class)).isTrue(); assertThat(excludes(filter, SecurityFilterChain.class)).isTrue();
assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isTrue(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isTrue(); assertThat(excludes(filter, ExampleModule.class)).isTrue();
assertThat(excludes(filter, ExampleDialect.class)).isTrue();
} }
@Test @Test
@ -117,7 +113,6 @@ class WebMvcTypeExcludeFilterTests {
assertThat(excludes(filter, ExampleRepository.class)).isFalse(); assertThat(excludes(filter, ExampleRepository.class)).isFalse();
assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
@Test @Test
@ -135,7 +130,6 @@ class WebMvcTypeExcludeFilterTests {
assertThat(excludes(filter, SecurityFilterChain.class)).isFalse(); assertThat(excludes(filter, SecurityFilterChain.class)).isFalse();
assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse(); assertThat(excludes(filter, ExampleHandlerInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse(); assertThat(excludes(filter, ExampleModule.class)).isFalse();
assertThat(excludes(filter, ExampleDialect.class)).isFalse();
} }
private boolean excludes(WebMvcTypeExcludeFilter filter, Class<?> type) throws IOException { private boolean excludes(WebMvcTypeExcludeFilter filter, Class<?> type) throws IOException {
@ -217,13 +211,4 @@ class WebMvcTypeExcludeFilterTests {
} }
static class ExampleDialect implements IDialect {
@Override
public String getName() {
return "example";
}
}
} }

@ -1,14 +0,0 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}
description = "Spring Boot web Thymeleaf smoke test"
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-thymeleaf"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-validation"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
}

@ -1,55 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
public class InMemoryMessageRepository implements MessageRepository {
private static AtomicLong counter = new AtomicLong();
private final ConcurrentMap<Long, Message> messages = new ConcurrentHashMap<>();
@Override
public Iterable<Message> findAll() {
return this.messages.values();
}
@Override
public Message save(Message message) {
Long id = message.getId();
if (id == null) {
id = counter.incrementAndGet();
message.setId(id);
}
this.messages.put(id, message);
return message;
}
@Override
public Message findMessage(Long id) {
return this.messages.get(id);
}
@Override
public void deleteMessage(Long id) {
this.messages.remove(id);
}
}

@ -1,67 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf;
import java.util.Calendar;
import javax.validation.constraints.NotEmpty;
public class Message {
private Long id;
@NotEmpty(message = "Text is required.")
private String text;
@NotEmpty(message = "Summary is required.")
private String summary;
private Calendar created = Calendar.getInstance();
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public Calendar getCreated() {
return this.created;
}
public void setCreated(Calendar created) {
this.created = created;
}
public String getText() {
return this.text;
}
public void setText(String text) {
this.text = text;
}
public String getSummary() {
return this.summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
}

@ -1,29 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf;
public interface MessageRepository {
Iterable<Message> findAll();
Message save(Message message);
Message findMessage(Long id);
void deleteMessage(Long id);
}

@ -1,46 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
@SpringBootApplication
public class SampleWebUiApplication {
@Bean
public MessageRepository messageRepository() {
return new InMemoryMessageRepository();
}
@Bean
public Converter<String, Message> messageConverter() {
return new Converter<String, Message>() {
@Override
public Message convert(String id) {
return messageRepository().findMessage(Long.valueOf(id));
}
};
}
public static void main(String[] args) {
SpringApplication.run(SampleWebUiApplication.class, args);
}
}

@ -1,87 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf.mvc;
import javax.validation.Valid;
import smoketest.web.thymeleaf.Message;
import smoketest.web.thymeleaf.MessageRepository;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/")
public class MessageController {
private final MessageRepository messageRepository;
public MessageController(MessageRepository messageRepository) {
this.messageRepository = messageRepository;
}
@GetMapping
public ModelAndView list() {
Iterable<Message> messages = this.messageRepository.findAll();
return new ModelAndView("messages/list", "messages", messages);
}
@GetMapping("{id}")
public ModelAndView view(@PathVariable("id") Message message) {
return new ModelAndView("messages/view", "message", message);
}
@GetMapping(params = "form")
public String createForm(@ModelAttribute Message message) {
return "messages/form";
}
@PostMapping
public ModelAndView create(@Valid Message message, BindingResult result, RedirectAttributes redirect) {
if (result.hasErrors()) {
return new ModelAndView("messages/form", "formErrors", result.getAllErrors());
}
message = this.messageRepository.save(message);
redirect.addFlashAttribute("globalMessage", "view.success");
return new ModelAndView("redirect:/{message.id}", "message.id", message.getId());
}
@RequestMapping("foo")
public String foo() {
throw new RuntimeException("Expected exception in controller");
}
@GetMapping("delete/{id}")
public ModelAndView delete(@PathVariable("id") Long id) {
this.messageRepository.deleteMessage(id);
Iterable<Message> messages = this.messageRepository.findAll();
return new ModelAndView("messages/list", "messages", messages);
}
@GetMapping("modify/{id}")
public ModelAndView modifyForm(@PathVariable("id") Message message) {
return new ModelAndView("messages/form", "message", message);
}
}

@ -1,4 +0,0 @@
# Allow Thymeleaf templates to be reloaded at dev time
spring.thymeleaf.cache: false
server.tomcat.access_log_enabled: true
server.tomcat.basedir: target/tomcat

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- logger name="org.springframework" level="DEBUG"/-->
</configuration>

@ -1,21 +0,0 @@
form.message=Message
form.messages=Messages
form.submit=Submit
form.summary=Summary
form.title=Messages : Create
list.create=Create Message
list.table.created=Created
list.table.empty=No messages
list.table.id=Id
list.table.summary=Summary
list.title=Messages : View all
navbar.messages=Messages
navbar.thymeleaf=Thymeleaf
view.delete=delete
view.messages=Messages
view.modify=modify
view.success=Successfully created a new message
view.title=Messages : View

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head th:fragment="head (title)">
<title th:text="${title}">Fragments</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"
href="../../css/bootstrap.min.css" />
</head>
<body>
<div class="container">
<nav th:fragment="navbar" class="navbar navbar-dark bg-primary">
<a class="navbar-brand" href="https://www.thymeleaf.org/" th:text="#{navbar.thymeleaf}">Thymeleaf</a>
<ul class="navbar-nav mr-auto mt-2 mt-lg-0">
<li class="nav-item"><a class="nav-link" th:href="@{/}" href="messages.html" th:text="#{navbar.messages}">Messages</a></li>
</ul>
</nav>
</div>
</body>
</html>

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head th:replace="fragments :: head(title=~{::title/text()})">
<title th:text="#{form.title}">Messages : Create</title>
</head>
<body>
<div class="container">
<div th:replace="fragments :: navbar"></div>
<div class="float-right mt-2">
<a class="btn btn-primary btn-sm" th:href="@{/}" href="messages.html" th:text="#{form.messages}"> Messages </a>
</div>
<h4 class="float-left mt-2" th:text="#{form.title}">Messages : Create</h4>
<div class="clearfix"></div>
<form id="messageForm" th:action="@{/(form)}" th:object="${message}" action="#" method="post">
<div th:if="${#fields.hasErrors('*')}" class="alert alert-danger" role="alert">
<p th:each="error : ${#fields.errors('*')}" class="m-0" th:text="${error}">Validation error</p>
</div>
<input type="hidden" th:field="*{id}" th:class="${'form-control' + (#fields.hasErrors('id') ? ' is-invalid' : '')}"/>
<div class="form-group">
<label for="summary" th:text="#{form.summary}">Summary</label>
<input type="text" th:field="*{summary}" th:class="${'form-control' + (#fields.hasErrors('summary') ? ' is-invalid' : '')}">
</div>
<div class="form-group">
<label for="text" th:text="#{form.message}">Message</label>
<textarea th:field="*{text}" th:class="${'form-control' + (#fields.hasErrors('text') ? ' is-invalid' : '')}"></textarea>
</div>
<button type="submit" class="btn btn-primary" th:text="#{form.submit}">Submit</button>
</form>
</div>
</body>
</html>

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head th:replace="fragments :: head(title=~{::title/text()})">
<title th:text="#{list.title}">Messages : View all</title>
</head>
<body>
<div class="container">
<div th:replace="fragments :: navbar"></div>
<div class="float-right mt-2">
<a class="btn btn-primary btn-sm" href="form.html" th:href="@{/(form)}" th:text="#{list.create}">Create Message</a>
</div>
<h4 class="float-left mt-2" th:text="#{list.title}">Messages : View all</h4>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th th:text="#{list.table.id}">ID</th>
<th th:text="#{list.table.created}">Created</th>
<th th:text="#{list.table.summary}">Summary</th>
</tr>
</thead>
<tbody>
<tr th:if="${messages.empty}">
<td colspan="3" th:text="#{list.table.empty}">No messages</td>
</tr>
<tr th:each="message : ${messages}">
<td th:text="${message.id}">1</td>
<td th:text="${#calendars.format(message.created)}">July 11,
2012 2:17:16 PM CDT</td>
<td><a href="view.html" th:href="@{'/' + ${message.id}}"
th:text="${message.summary}"> The summary </a></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head th:replace="fragments :: head(title=~{::title/text()})">
<title th:text="#{view.title}">Messages : View</title>
</head>
<body>
<div class="container">
<div th:replace="fragments :: navbar"></div>
<div class="float-right mt-2">
<a class="btn btn-primary btn-sm" href="list.html" th:href="@{/}" th:text="#{view.messages}">Messages</a>
</div>
<h4 class="float-left mt-2" th:text="#{view.title}">Messages : View</h4>
<div class="clearfix"></div>
<div class="alert alert-success" th:if="${globalMessage}" th:text="#{${globalMessage}}">Some Success message
</div>
<div class="card">
<div class="card-body">
<h4 class="card-title" th:text="${message.id + ': ' + message.summary}">123 - A short summary...</h4>
<h6 class="card-subtitle mb-2 text-muted" th:text="${#calendars.format(message.created)}">July 11, 2012 2:17:16 PM CDT</h6>
<p class="card-text" th:text="${message.text}">A detailed message that is longer than the summary.</p>
<a class="card-link" href="messages" th:href="@{'/delete/' + ${message.id}}" th:text="#{view.delete}">delete</a>
<a class="card-link" href="form.html" th:href="@{'/modify/' + ${message.id}}" th:text="#{view.modify}"> modify</a>
</div>
</div>
</div>
</body>
</html>

@ -1,105 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf;
import java.util.regex.Pattern;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* A Basic Spring MVC Test for the Sample Controller"
*
* @author Biju Kunjummen
* @author Doo-Hwan, Kwak
*/
@SpringBootTest
class MessageControllerWebTests {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}
@Test
void testHome() throws Exception {
this.mockMvc.perform(get("/")).andExpect(status().isOk())
.andExpect(content().string(containsString("<title>Messages")));
}
@Test
void testCreate() throws Exception {
this.mockMvc.perform(post("/").param("text", "FOO text").param("summary", "FOO")).andExpect(status().isFound())
.andExpect(header().string("location", RegexMatcher.matches("/[0-9]+")));
}
@Test
void testCreateValidation() throws Exception {
this.mockMvc.perform(post("/").param("text", "").param("summary", "")).andExpect(status().isOk())
.andExpect(content().string(containsString("is required")));
}
private static class RegexMatcher extends TypeSafeMatcher<String> {
private final String regex;
RegexMatcher(String regex) {
this.regex = regex;
}
@Override
public boolean matchesSafely(String item) {
return Pattern.compile(this.regex).matcher(item).find();
}
@Override
public void describeMismatchSafely(String item, Description mismatchDescription) {
mismatchDescription.appendText("was \"").appendText(item).appendText("\"");
}
@Override
public void describeTo(Description description) {
description.appendText("a string that matches regex: ").appendText(this.regex);
}
static org.hamcrest.Matcher<java.lang.String> matches(String regex) {
return new RegexMatcher(regex);
}
}
}

@ -1,66 +0,0 @@
/*
* Copyright 2012-2021 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 smoketest.web.thymeleaf;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Basic integration tests for demo application.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SampleWebUiApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
@Test
void testHome() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("<title>Messages");
assertThat(entity.getBody()).doesNotContain("layout:fragment");
}
@Test
void testCreate() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.set("text", "FOO text");
map.set("summary", "FOO");
URI location = this.restTemplate.postForLocation("/", map);
assertThat(location.toString()).contains("localhost:" + this.port);
}
}
Loading…
Cancel
Save