Skip to content

Commit 12cd97a

Browse files
committed
Reinstate support for Thymeleaf
1 parent e95f038 commit 12cd97a

File tree

60 files changed

+2304
-30
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+2304
-30
lines changed

Diff for: buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -189,6 +189,7 @@ private void templatePrefixes(Config prefix) {
189189
prefix.accept("spring.freemarker");
190190
prefix.accept("spring.groovy");
191191
prefix.accept("spring.mustache");
192+
prefix.accept("spring.thymeleaf");
192193
prefix.accept("spring.groovy.template.configuration", "See GroovyMarkupConfigurer");
193194
}
194195

Diff for: spring-boot-project/spring-boot-autoconfigure/build.gradle

+6
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ dependencies {
6969
optional("org.apiguardian:apiguardian-api")
7070
optional("org.codehaus.groovy:groovy-templates")
7171
optional("com.github.ben-manes.caffeine:caffeine")
72+
optional("com.github.mxab.thymeleaf.extras:thymeleaf-extras-data-attribute")
7273
optional("com.sendgrid:sendgrid-java") {
7374
exclude group: "commons-logging", module: "commons-logging"
7475
}
7576
optional("com.unboundid:unboundid-ldapsdk")
7677
optional("com.zaxxer:HikariCP")
78+
optional("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect")
7779
optional("org.aspectj:aspectjweaver")
7880
optional("org.eclipse.jetty:jetty-webapp") {
7981
exclude group: "javax.servlet", module: "javax.servlet-api"
@@ -175,6 +177,10 @@ dependencies {
175177
exclude group: "org.eclipse.jetty", module: "jetty-servlet"
176178
exclude group: "jakarta.mail", module: "jakarta.mail-api"
177179
}
180+
optional("org.thymeleaf:thymeleaf")
181+
optional("org.thymeleaf:thymeleaf-spring5")
182+
optional("org.thymeleaf.extras:thymeleaf-extras-java8time")
183+
optional("org.thymeleaf.extras:thymeleaf-extras-springsecurity5")
178184
optional("redis.clients:jedis")
179185

180186
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/*
2+
* Copyright 2012-2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.thymeleaf;
18+
19+
import java.util.LinkedHashMap;
20+
21+
import javax.servlet.DispatcherType;
22+
23+
import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect;
24+
import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect;
25+
import org.apache.commons.logging.Log;
26+
import org.apache.commons.logging.LogFactory;
27+
import org.thymeleaf.dialect.IDialect;
28+
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
29+
import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect;
30+
import org.thymeleaf.spring5.ISpringTemplateEngine;
31+
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
32+
import org.thymeleaf.spring5.SpringTemplateEngine;
33+
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
34+
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
35+
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
36+
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
37+
import org.thymeleaf.templatemode.TemplateMode;
38+
import org.thymeleaf.templateresolver.ITemplateResolver;
39+
40+
import org.springframework.beans.factory.ObjectProvider;
41+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
42+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
43+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
44+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
45+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
46+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
47+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
48+
import org.springframework.boot.autoconfigure.template.TemplateLocation;
49+
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties.Reactive;
50+
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
51+
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
52+
import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean;
53+
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
54+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
55+
import org.springframework.boot.context.properties.PropertyMapper;
56+
import org.springframework.boot.web.servlet.FilterRegistrationBean;
57+
import org.springframework.context.ApplicationContext;
58+
import org.springframework.context.annotation.Bean;
59+
import org.springframework.context.annotation.Configuration;
60+
import org.springframework.core.Ordered;
61+
import org.springframework.util.MimeType;
62+
import org.springframework.util.unit.DataSize;
63+
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
64+
65+
/**
66+
* {@link EnableAutoConfiguration Auto-configuration} for Thymeleaf.
67+
*
68+
* @author Dave Syer
69+
* @author Andy Wilkinson
70+
* @author Stephane Nicoll
71+
* @author Brian Clozel
72+
* @author Eddú Meléndez
73+
* @author Daniel Fernández
74+
* @author Kazuki Shimizu
75+
* @author Artsiom Yudovin
76+
* @since 1.0.0
77+
*/
78+
@Configuration(proxyBeanMethods = false)
79+
@EnableConfigurationProperties(ThymeleafProperties.class)
80+
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
81+
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
82+
public class ThymeleafAutoConfiguration {
83+
84+
@Configuration(proxyBeanMethods = false)
85+
@ConditionalOnMissingBean(name = "defaultTemplateResolver")
86+
static class DefaultTemplateResolverConfiguration {
87+
88+
private static final Log logger = LogFactory.getLog(DefaultTemplateResolverConfiguration.class);
89+
90+
private final ThymeleafProperties properties;
91+
92+
private final ApplicationContext applicationContext;
93+
94+
DefaultTemplateResolverConfiguration(ThymeleafProperties properties, ApplicationContext applicationContext) {
95+
this.properties = properties;
96+
this.applicationContext = applicationContext;
97+
checkTemplateLocationExists();
98+
}
99+
100+
private void checkTemplateLocationExists() {
101+
boolean checkTemplateLocation = this.properties.isCheckTemplateLocation();
102+
if (checkTemplateLocation) {
103+
TemplateLocation location = new TemplateLocation(this.properties.getPrefix());
104+
if (!location.exists(this.applicationContext)) {
105+
logger.warn("Cannot find template location: " + location + " (please add some templates or check "
106+
+ "your Thymeleaf configuration)");
107+
}
108+
}
109+
}
110+
111+
@Bean
112+
SpringResourceTemplateResolver defaultTemplateResolver() {
113+
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
114+
resolver.setApplicationContext(this.applicationContext);
115+
resolver.setPrefix(this.properties.getPrefix());
116+
resolver.setSuffix(this.properties.getSuffix());
117+
resolver.setTemplateMode(this.properties.getMode());
118+
if (this.properties.getEncoding() != null) {
119+
resolver.setCharacterEncoding(this.properties.getEncoding().name());
120+
}
121+
resolver.setCacheable(this.properties.isCache());
122+
Integer order = this.properties.getTemplateResolverOrder();
123+
if (order != null) {
124+
resolver.setOrder(order);
125+
}
126+
resolver.setCheckExistence(this.properties.isCheckTemplate());
127+
return resolver;
128+
}
129+
130+
}
131+
132+
@Configuration(proxyBeanMethods = false)
133+
protected static class ThymeleafDefaultConfiguration {
134+
135+
@Bean
136+
@ConditionalOnMissingBean(ISpringTemplateEngine.class)
137+
SpringTemplateEngine templateEngine(ThymeleafProperties properties,
138+
ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
139+
SpringTemplateEngine engine = new SpringTemplateEngine();
140+
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
141+
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
142+
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
143+
dialects.orderedStream().forEach(engine::addDialect);
144+
return engine;
145+
}
146+
147+
}
148+
149+
@Configuration(proxyBeanMethods = false)
150+
@ConditionalOnWebApplication(type = Type.SERVLET)
151+
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
152+
static class ThymeleafWebMvcConfiguration {
153+
154+
@Bean
155+
@ConditionalOnEnabledResourceChain
156+
@ConditionalOnMissingFilterBean(ResourceUrlEncodingFilter.class)
157+
FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() {
158+
FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>(
159+
new ResourceUrlEncodingFilter());
160+
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR);
161+
return registration;
162+
}
163+
164+
@Configuration(proxyBeanMethods = false)
165+
static class ThymeleafViewResolverConfiguration {
166+
167+
@Bean
168+
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
169+
ThymeleafViewResolver thymeleafViewResolver(ThymeleafProperties properties,
170+
SpringTemplateEngine templateEngine) {
171+
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
172+
resolver.setTemplateEngine(templateEngine);
173+
resolver.setCharacterEncoding(properties.getEncoding().name());
174+
resolver.setContentType(
175+
appendCharset(properties.getServlet().getContentType(), resolver.getCharacterEncoding()));
176+
resolver.setProducePartialOutputWhileProcessing(
177+
properties.getServlet().isProducePartialOutputWhileProcessing());
178+
resolver.setExcludedViewNames(properties.getExcludedViewNames());
179+
resolver.setViewNames(properties.getViewNames());
180+
// This resolver acts as a fallback resolver (e.g. like a
181+
// InternalResourceViewResolver) so it needs to have low precedence
182+
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
183+
resolver.setCache(properties.isCache());
184+
return resolver;
185+
}
186+
187+
private String appendCharset(MimeType type, String charset) {
188+
if (type.getCharset() != null) {
189+
return type.toString();
190+
}
191+
LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
192+
parameters.put("charset", charset);
193+
parameters.putAll(type.getParameters());
194+
return new MimeType(type, parameters).toString();
195+
}
196+
197+
}
198+
199+
}
200+
201+
@Configuration(proxyBeanMethods = false)
202+
@ConditionalOnWebApplication(type = Type.REACTIVE)
203+
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
204+
static class ThymeleafReactiveConfiguration {
205+
206+
@Bean
207+
@ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
208+
SpringWebFluxTemplateEngine templateEngine(ThymeleafProperties properties,
209+
ObjectProvider<ITemplateResolver> templateResolvers, ObjectProvider<IDialect> dialects) {
210+
SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
211+
engine.setEnableSpringELCompiler(properties.isEnableSpringElCompiler());
212+
engine.setRenderHiddenMarkersBeforeCheckboxes(properties.isRenderHiddenMarkersBeforeCheckboxes());
213+
templateResolvers.orderedStream().forEach(engine::addTemplateResolver);
214+
dialects.orderedStream().forEach(engine::addDialect);
215+
return engine;
216+
}
217+
218+
}
219+
220+
@Configuration(proxyBeanMethods = false)
221+
@ConditionalOnWebApplication(type = Type.REACTIVE)
222+
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
223+
static class ThymeleafWebFluxConfiguration {
224+
225+
@Bean
226+
@ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
227+
ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine,
228+
ThymeleafProperties properties) {
229+
ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
230+
resolver.setTemplateEngine(templateEngine);
231+
mapProperties(properties, resolver);
232+
mapReactiveProperties(properties.getReactive(), resolver);
233+
// This resolver acts as a fallback resolver (e.g. like a
234+
// InternalResourceViewResolver) so it needs to have low precedence
235+
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5);
236+
return resolver;
237+
}
238+
239+
private void mapProperties(ThymeleafProperties properties, ThymeleafReactiveViewResolver resolver) {
240+
PropertyMapper map = PropertyMapper.get();
241+
map.from(properties::getEncoding).to(resolver::setDefaultCharset);
242+
resolver.setExcludedViewNames(properties.getExcludedViewNames());
243+
resolver.setViewNames(properties.getViewNames());
244+
}
245+
246+
private void mapReactiveProperties(Reactive properties, ThymeleafReactiveViewResolver resolver) {
247+
PropertyMapper map = PropertyMapper.get();
248+
map.from(properties::getMediaTypes).whenNonNull().to(resolver::setSupportedMediaTypes);
249+
map.from(properties::getMaxChunkSize).asInt(DataSize::toBytes).when((size) -> size > 0)
250+
.to(resolver::setResponseMaxChunkSizeBytes);
251+
map.from(properties::getFullModeViewNames).to(resolver::setFullModeViewNames);
252+
map.from(properties::getChunkedModeViewNames).to(resolver::setChunkedModeViewNames);
253+
}
254+
255+
}
256+
257+
@Configuration(proxyBeanMethods = false)
258+
@ConditionalOnClass(LayoutDialect.class)
259+
static class ThymeleafWebLayoutConfiguration {
260+
261+
@Bean
262+
@ConditionalOnMissingBean
263+
LayoutDialect layoutDialect() {
264+
return new LayoutDialect();
265+
}
266+
267+
}
268+
269+
@Configuration(proxyBeanMethods = false)
270+
@ConditionalOnClass(DataAttributeDialect.class)
271+
static class DataAttributeDialectConfiguration {
272+
273+
@Bean
274+
@ConditionalOnMissingBean
275+
DataAttributeDialect dialect() {
276+
return new DataAttributeDialect();
277+
}
278+
279+
}
280+
281+
@Configuration(proxyBeanMethods = false)
282+
@ConditionalOnClass({ SpringSecurityDialect.class })
283+
static class ThymeleafSecurityDialectConfiguration {
284+
285+
@Bean
286+
@ConditionalOnMissingBean
287+
SpringSecurityDialect securityDialect() {
288+
return new SpringSecurityDialect();
289+
}
290+
291+
}
292+
293+
@Configuration(proxyBeanMethods = false)
294+
@ConditionalOnClass(Java8TimeDialect.class)
295+
static class ThymeleafJava8TimeDialect {
296+
297+
@Bean
298+
@ConditionalOnMissingBean
299+
Java8TimeDialect java8TimeDialect() {
300+
return new Java8TimeDialect();
301+
}
302+
303+
}
304+
305+
}

0 commit comments

Comments
 (0)