Commit 4d5dcca5 authored by Brian Clozel's avatar Brian Clozel

Add Thymeleaf auto-configuration for WebFlux

Thymeleaf 3.0 implements the Spring 5.0 view infrastructure for WebMVC
and the new WebFlux framework. This commit adds auto-configuration for
the WebFlux support.

In that process, the configuration property for `spring.thymeleaf` has
been changed to add `spring.thymeleaf.servlet` and
`spring.thymeleaf.reactive` for MVC/WebFlux specific properties.

Now that the `spring-boot-starter-thymeleaf` does not only support
Spring MVC, the transitive dependency on `spring-boot-starter-web` is
removed from it.

Fixes gh-8124
parent d4f87ae7
...@@ -20,7 +20,6 @@ import java.util.Collection; ...@@ -20,7 +20,6 @@ import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import javax.servlet.Servlet;
import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect; import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect;
import nz.net.ultraq.thymeleaf.LayoutDialect; import nz.net.ultraq.thymeleaf.LayoutDialect;
...@@ -29,9 +28,12 @@ import org.apache.commons.logging.LogFactory; ...@@ -29,9 +28,12 @@ import org.apache.commons.logging.LogFactory;
import org.thymeleaf.dialect.IDialect; import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect; import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringTemplateEngine; import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver; import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver; import org.thymeleaf.templateresolver.ITemplateResolver;
...@@ -45,6 +47,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat ...@@ -45,6 +47,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.template.TemplateLocation; import org.springframework.boot.autoconfigure.template.TemplateLocation;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
...@@ -63,11 +66,12 @@ import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; ...@@ -63,11 +66,12 @@ import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Brian Clozel * @author Brian Clozel
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Daniel Fernández
*/ */
@Configuration @Configuration
@EnableConfigurationProperties(ThymeleafProperties.class) @EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass(TemplateMode.class) @ConditionalOnClass(TemplateMode.class)
@AutoConfigureAfter(WebMvcAutoConfiguration.class) @AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
public class ThymeleafAutoConfiguration { public class ThymeleafAutoConfiguration {
@Configuration @Configuration
...@@ -123,68 +127,112 @@ public class ThymeleafAutoConfiguration { ...@@ -123,68 +127,112 @@ public class ThymeleafAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnClass({ Servlet.class }) protected static class ThymeleafDefaultConfiguration {
@ConditionalOnWebApplication(type = Type.SERVLET)
static class ThymeleafViewResolverConfiguration {
private final ThymeleafProperties properties; private final Collection<ITemplateResolver> templateResolvers;
private final SpringTemplateEngine templateEngine; private final Collection<IDialect> dialects;
ThymeleafViewResolverConfiguration(ThymeleafProperties properties, public ThymeleafDefaultConfiguration(
SpringTemplateEngine templateEngine) { Collection<ITemplateResolver> templateResolvers,
this.properties = properties; ObjectProvider<Collection<IDialect>> dialectsProvider) {
this.templateEngine = templateEngine; this.templateResolvers = templateResolvers;
this.dialects = dialectsProvider.getIfAvailable();
} }
@Bean @Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver") @ConditionalOnMissingBean(SpringTemplateEngine.class)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) public SpringTemplateEngine templateEngine() {
public ThymeleafViewResolver thymeleafViewResolver() { SpringTemplateEngine engine = new SpringTemplateEngine();
ThymeleafViewResolver resolver = new ThymeleafViewResolver(); for (ITemplateResolver templateResolver : this.templateResolvers) {
resolver.setTemplateEngine(this.templateEngine); engine.addTemplateResolver(templateResolver);
resolver.setCharacterEncoding(this.properties.getEncoding().name()); }
resolver.setContentType(appendCharset(this.properties.getContentType(), if (!CollectionUtils.isEmpty(this.dialects)) {
resolver.getCharacterEncoding())); for (IDialect dialect : this.dialects) {
resolver.setExcludedViewNames(this.properties.getExcludedViewNames()); engine.addDialect(dialect);
resolver.setViewNames(this.properties.getViewNames()); }
// This resolver acts as a fallback resolver (e.g. like a }
// InternalResourceViewResolver) so it needs to have low precedence return engine;
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5); }
resolver.setCache(this.properties.isCache());
return resolver; }
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebMvcConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledResourceChain
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
} }
private String appendCharset(MimeType type, String charset) { @Configuration
if (type.getCharset() != null) { static class ThymeleafViewResolverConfiguration {
return type.toString();
private final ThymeleafProperties properties;
private final SpringTemplateEngine templateEngine;
ThymeleafViewResolverConfiguration(ThymeleafProperties properties,
SpringTemplateEngine templateEngine) {
this.properties = properties;
this.templateEngine = templateEngine;
}
@Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
public ThymeleafViewResolver thymeleafViewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(this.templateEngine);
resolver.setCharacterEncoding(this.properties.getEncoding().name());
resolver.setContentType(appendCharset(this.properties.getServlet().getContentType(),
resolver.getCharacterEncoding()));
resolver.setExcludedViewNames(this.properties.getExcludedViewNames());
resolver.setViewNames(this.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(this.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();
} }
LinkedHashMap<String, String> parameters = new LinkedHashMap<>();
parameters.put("charset", charset);
parameters.putAll(type.getParameters());
return new MimeType(type, parameters).toString();
} }
} }
@Configuration @Configuration
@ConditionalOnMissingBean(SpringTemplateEngine.class) @ConditionalOnWebApplication(type = Type.REACTIVE)
protected static class ThymeleafDefaultConfiguration { @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafReactiveConfiguration {
private final Collection<ITemplateResolver> templateResolvers; private final Collection<ITemplateResolver> templateResolvers;
private final Collection<IDialect> dialects; private final Collection<IDialect> dialects;
public ThymeleafDefaultConfiguration(
Collection<ITemplateResolver> templateResolvers, ThymeleafReactiveConfiguration(Collection<ITemplateResolver> templateResolvers,
ObjectProvider<Collection<IDialect>> dialectsProvider) { ObjectProvider<Collection<IDialect>> dialectsProvider) {
this.templateResolvers = templateResolvers; this.templateResolvers = templateResolvers;
this.dialects = dialectsProvider.getIfAvailable(); this.dialects = dialectsProvider.getIfAvailable();
} }
@Bean @Bean
public SpringTemplateEngine templateEngine() { @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
SpringTemplateEngine engine = new SpringTemplateEngine(); public SpringWebFluxTemplateEngine templateEngine() {
SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
for (ITemplateResolver templateResolver : this.templateResolvers) { for (ITemplateResolver templateResolver : this.templateResolvers) {
engine.addTemplateResolver(templateResolver); engine.addTemplateResolver(templateResolver);
} }
...@@ -195,6 +243,38 @@ public class ThymeleafAutoConfiguration { ...@@ -195,6 +243,38 @@ public class ThymeleafAutoConfiguration {
} }
return engine; return engine;
} }
}
@Configuration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafWebFluxConfiguration {
private final ThymeleafProperties properties;
ThymeleafWebFluxConfiguration(ThymeleafProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver")
public ThymeleafReactiveViewResolver thymeleafViewResolver(ISpringWebFluxTemplateEngine templateEngine) {
ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver();
resolver.setTemplateEngine(templateEngine);
resolver.setDefaultCharset(this.properties.getEncoding());
resolver.setSupportedMediaTypes(this.properties.getReactive().getMediaTypes());
resolver.setExcludedViewNames(this.properties.getExcludedViewNames());
resolver.setViewNames(this.properties.getViewNames());
if (this.properties.getReactive().getMaxChunkSize() > 0) {
resolver.setResponseMaxChunkSizeBytes(this.properties.getReactive().getMaxChunkSize());
}
// 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;
}
} }
...@@ -223,7 +303,7 @@ public class ThymeleafAutoConfiguration { ...@@ -223,7 +303,7 @@ public class ThymeleafAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnClass({ SpringSecurityDialect.class }) @ConditionalOnClass({SpringSecurityDialect.class})
protected static class ThymeleafSecurityDialectConfiguration { protected static class ThymeleafSecurityDialectConfiguration {
@Bean @Bean
...@@ -246,17 +326,4 @@ public class ThymeleafAutoConfiguration { ...@@ -246,17 +326,4 @@ public class ThymeleafAutoConfiguration {
} }
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
protected static class ThymeleafResourceHandlingConfig {
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledResourceChain
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
}
}
} }
...@@ -17,14 +17,20 @@ ...@@ -17,14 +17,20 @@
package org.springframework.boot.autoconfigure.thymeleaf; package org.springframework.boot.autoconfigure.thymeleaf;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.MediaType;
import org.springframework.util.MimeType; import org.springframework.util.MimeType;
/** /**
* Properties for Thymeleaf. * Properties for Thymeleaf.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Brian Clozel
* @author Daniel Fernández
* @since 1.2.0 * @since 1.2.0
*/ */
@ConfigurationProperties(prefix = "spring.thymeleaf") @ConfigurationProperties(prefix = "spring.thymeleaf")
...@@ -32,8 +38,6 @@ public class ThymeleafProperties { ...@@ -32,8 +38,6 @@ public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8"); private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");
private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html");
public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html"; public static final String DEFAULT_SUFFIX = ".html";
...@@ -65,15 +69,10 @@ public class ThymeleafProperties { ...@@ -65,15 +69,10 @@ public class ThymeleafProperties {
private String mode = "HTML"; private String mode = "HTML";
/** /**
* Template encoding. * Template files encoding.
*/ */
private Charset encoding = DEFAULT_ENCODING; private Charset encoding = DEFAULT_ENCODING;
/**
* Content-Type value.
*/
private MimeType contentType = DEFAULT_CONTENT_TYPE;
/** /**
* Enable template caching. * Enable template caching.
*/ */
...@@ -97,10 +96,14 @@ public class ThymeleafProperties { ...@@ -97,10 +96,14 @@ public class ThymeleafProperties {
private String[] excludedViewNames; private String[] excludedViewNames;
/** /**
* Enable MVC Thymeleaf view resolution. * Enable Thymeleaf view resolution for Web frameworks.
*/ */
private boolean enabled = true; private boolean enabled = true;
private final Servlet servlet = new Servlet();
private final Reactive reactive = new Reactive();
public boolean isEnabled() { public boolean isEnabled() {
return this.enabled; return this.enabled;
} }
...@@ -157,14 +160,6 @@ public class ThymeleafProperties { ...@@ -157,14 +160,6 @@ public class ThymeleafProperties {
this.encoding = encoding; this.encoding = encoding;
} }
public MimeType getContentType() {
return this.contentType;
}
public void setContentType(MimeType contentType) {
this.contentType = contentType;
}
public boolean isCache() { public boolean isCache() {
return this.cache; return this.cache;
} }
...@@ -197,4 +192,58 @@ public class ThymeleafProperties { ...@@ -197,4 +192,58 @@ public class ThymeleafProperties {
this.viewNames = viewNames; this.viewNames = viewNames;
} }
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");
public MimeType getContentType() {
return this.contentType;
}
public void setContentType(MimeType contentType) {
this.contentType = contentType;
}
}
public static class Reactive {
/**
* Maximum size of data buffers used for writing to the response, in bytes.
*/
private int maxChunkSize;
/**
* Media types supported by the view technology.
*/
private List<MediaType> mediaTypes =
new ArrayList(Collections.singletonList(MediaType.TEXT_HTML));
public List<MediaType> getMediaTypes() {
return this.mediaTypes;
}
public void setMediaTypes(List<MediaType> mediaTypes) {
this.mediaTypes = mediaTypes;
}
public int getMaxChunkSize() {
return this.maxChunkSize;
}
public void setMaxChunkSize(int maxChunkSize) {
this.maxChunkSize = maxChunkSize;
}
}
} }
/*
* Copyright 2012-2017 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
*
* http://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;
import nz.net.ultraq.thymeleaf.decorators.strategies.GroupingStrategy;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.boot.web.reactive.context.GenericReactiveWebApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
/**
* Tests for {@link ThymeleafAutoConfiguration} in Reactive applications.
*
* @author Brian Clozel
*/
public class ThymeleafReactiveAutoConfigurationTests {
@Rule
public OutputCapture output = new OutputCapture();
private GenericReactiveWebApplicationContext context;
@After
public void close() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void createFromConfigClass() throws Exception {
load(BaseConfiguration.class, "spring.thymeleaf.suffix:.txt");
TemplateEngine engine = this.context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("template", attrs);
assertThat(result).isEqualTo("<html>bar</html>");
}
@Test
public void overrideCharacterEncoding() throws Exception {
load(BaseConfiguration.class, "spring.thymeleaf.encoding:UTF-16");
ITemplateResolver resolver = this.context.getBean(ITemplateResolver.class);
assertThat(resolver instanceof SpringResourceTemplateResolver).isTrue();
assertThat(((SpringResourceTemplateResolver) resolver).getCharacterEncoding())
.isEqualTo("UTF-16");
ThymeleafReactiveViewResolver views = this.context.getBean(ThymeleafReactiveViewResolver.class);
assertThat(views.getDefaultCharset().name()).isEqualTo("UTF-16");
}
@Test
public void overrideMediaTypes() throws Exception {
load(BaseConfiguration.class,
"spring.thymeleaf.reactive.media-types:text/html,text/plain");
ThymeleafReactiveViewResolver views = this.context.getBean(ThymeleafReactiveViewResolver.class);
assertThat(views.getSupportedMediaTypes()).contains(MediaType.TEXT_HTML, MediaType.TEXT_PLAIN);
}
@Test
public void overrideTemplateResolverOrder() throws Exception {
load(BaseConfiguration.class, "spring.thymeleaf.templateResolverOrder:25");
ITemplateResolver resolver = this.context.getBean(ITemplateResolver.class);
assertThat(resolver.getOrder()).isEqualTo(Integer.valueOf(25));
}
@Test
public void overrideViewNames() throws Exception {
load(BaseConfiguration.class, "spring.thymeleaf.viewNames:foo,bar");
ThymeleafReactiveViewResolver views = this.context.getBean(ThymeleafReactiveViewResolver.class);
assertThat(views.getViewNames()).isEqualTo(new String[] {"foo", "bar"});
}
@Test
public void templateLocationDoesNotExist() throws Exception {
load(BaseConfiguration.class, "spring.thymeleaf.prefix:classpath:/no-such-directory/");
this.output.expect(containsString("Cannot find template location"));
}
@Test
public void templateLocationEmpty() throws Exception {
new File("target/test-classes/templates/empty-directory").mkdir();
load(BaseConfiguration.class, "spring.thymeleaf.prefix:classpath:/templates/empty-directory/");
this.output.expect(not(containsString("Cannot find template location")));
}
@Test
public void useDataDialect() throws Exception {
load(BaseConfiguration.class);
ISpringWebFluxTemplateEngine engine = this.context.getBean(ISpringWebFluxTemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("data-dialect", attrs);
assertThat(result).isEqualTo("<html><body data-foo=\"bar\"></body></html>");
}
@Test
public void useJava8TimeDialect() throws Exception {
load(BaseConfiguration.class);
ISpringWebFluxTemplateEngine engine = this.context.getBean(ISpringWebFluxTemplateEngine.class);
Context attrs = new Context(Locale.UK);
String result = engine.process("java8time-dialect", attrs);
assertThat(result).isEqualTo("<html><body>2015-11-24</body></html>");
}
@Test
public void renderTemplate() throws Exception {
load(BaseConfiguration.class);
ISpringWebFluxTemplateEngine engine = this.context.getBean(ISpringWebFluxTemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("home", attrs);
assertThat(result).isEqualTo("<html><body>bar</body></html>");
}
@Test
public void layoutDialectCanBeCustomized() throws Exception {
load(LayoutDialectConfiguration.class);
LayoutDialect layoutDialect = this.context.getBean(LayoutDialect.class);
assertThat(ReflectionTestUtils.getField(layoutDialect, "sortingStrategy"))
.isInstanceOf(GroupingStrategy.class);
}
private void load(Class<?> config, String... envVariables) {
this.context = new GenericReactiveWebApplicationContext();
EnvironmentTestUtils.addEnvironment(this.context, envVariables);
if (config != null) {
this.context.register(config);
}
this.context.register(config);
this.context.refresh();
}
@Configuration
@ImportAutoConfiguration({ThymeleafAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class})
protected static class BaseConfiguration {
}
@Configuration
@Import(BaseConfiguration.class)
static class LayoutDialectConfiguration {
@Bean
public LayoutDialect layoutDialect() {
return new LayoutDialect(new GroupingStrategy());
}
}
}
...@@ -39,6 +39,7 @@ import org.springframework.boot.test.util.EnvironmentTestUtils; ...@@ -39,6 +39,7 @@ import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
...@@ -52,18 +53,19 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -52,18 +53,19 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
/** /**
* Tests for {@link ThymeleafAutoConfiguration}. * Tests for {@link ThymeleafAutoConfiguration} in Servlet-based applications.
* *
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Brian Clozel
*/ */
public class ThymeleafAutoConfigurationTests { public class ThymeleafServletAutoConfigurationTests {
@Rule @Rule
public OutputCapture output = new OutputCapture(); public OutputCapture output = new OutputCapture();
private AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); private AnnotationConfigWebApplicationContext context;
@After @After
public void close() { public void close() {
...@@ -74,11 +76,8 @@ public class ThymeleafAutoConfigurationTests { ...@@ -74,11 +76,8 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void createFromConfigClass() throws Exception { public void createFromConfigClass() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, "spring.thymeleaf.mode:XHTML", load(BaseConfiguration.class, "spring.thymeleaf.mode:XHTML",
"spring.thymeleaf.suffix:"); "spring.thymeleaf.suffix:");
this.context.register(ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
TemplateEngine engine = this.context.getBean(TemplateEngine.class); TemplateEngine engine = this.context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar")); Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("template.txt", attrs); String result = engine.process("template.txt", attrs);
...@@ -87,11 +86,7 @@ public class ThymeleafAutoConfigurationTests { ...@@ -87,11 +86,7 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void overrideCharacterEncoding() throws Exception { public void overrideCharacterEncoding() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, load(BaseConfiguration.class, "spring.thymeleaf.encoding:UTF-16");
"spring.thymeleaf.encoding:UTF-16");
this.context.register(ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
ITemplateResolver resolver = this.context.getBean(ITemplateResolver.class); ITemplateResolver resolver = this.context.getBean(ITemplateResolver.class);
assertThat(resolver instanceof SpringResourceTemplateResolver).isTrue(); assertThat(resolver instanceof SpringResourceTemplateResolver).isTrue();
assertThat(((SpringResourceTemplateResolver) resolver).getCharacterEncoding()) assertThat(((SpringResourceTemplateResolver) resolver).getCharacterEncoding())
...@@ -103,44 +98,29 @@ public class ThymeleafAutoConfigurationTests { ...@@ -103,44 +98,29 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void overrideTemplateResolverOrder() throws Exception { public void overrideTemplateResolverOrder() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, load(BaseConfiguration.class, "spring.thymeleaf.templateResolverOrder:25");
"spring.thymeleaf.templateResolverOrder:25");
this.context.register(ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
ITemplateResolver resolver = this.context.getBean(ITemplateResolver.class); ITemplateResolver resolver = this.context.getBean(ITemplateResolver.class);
assertThat(resolver.getOrder()).isEqualTo(Integer.valueOf(25)); assertThat(resolver.getOrder()).isEqualTo(Integer.valueOf(25));
} }
@Test @Test
public void overrideViewNames() throws Exception { public void overrideViewNames() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, load(BaseConfiguration.class, "spring.thymeleaf.viewNames:foo,bar");
"spring.thymeleaf.viewNames:foo,bar");
this.context.register(ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
ThymeleafViewResolver views = this.context.getBean(ThymeleafViewResolver.class); ThymeleafViewResolver views = this.context.getBean(ThymeleafViewResolver.class);
assertThat(views.getViewNames()).isEqualTo(new String[] { "foo", "bar" }); assertThat(views.getViewNames()).isEqualTo(new String[] {"foo", "bar"});
} }
@Test @Test
public void templateLocationDoesNotExist() throws Exception { public void templateLocationDoesNotExist() throws Exception {
EnvironmentTestUtils.addEnvironment(this.context, load(BaseConfiguration.class, "spring.thymeleaf.prefix:classpath:/no-such-directory/");
"spring.thymeleaf.prefix:classpath:/no-such-directory/");
this.context.register(ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
this.output.expect(containsString("Cannot find template location")); this.output.expect(containsString("Cannot find template location"));
} }
@Test @Test
public void templateLocationEmpty() throws Exception { public void templateLocationEmpty() throws Exception {
new File("target/test-classes/templates/empty-directory").mkdir(); new File("target/test-classes/templates/empty-directory").mkdir();
EnvironmentTestUtils.addEnvironment(this.context, load(BaseConfiguration.class,
"spring.thymeleaf.prefix:classpath:/templates/empty-directory/"); "spring.thymeleaf.prefix:classpath:/templates/empty-directory/");
this.context.register(ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
} }
@Test @Test
...@@ -165,9 +145,7 @@ public class ThymeleafAutoConfigurationTests { ...@@ -165,9 +145,7 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void useDataDialect() throws Exception { public void useDataDialect() throws Exception {
this.context.register(ThymeleafAutoConfiguration.class, load(BaseConfiguration.class);
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
TemplateEngine engine = this.context.getBean(TemplateEngine.class); TemplateEngine engine = this.context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar")); Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("data-dialect", attrs); String result = engine.process("data-dialect", attrs);
...@@ -176,9 +154,7 @@ public class ThymeleafAutoConfigurationTests { ...@@ -176,9 +154,7 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void useJava8TimeDialect() throws Exception { public void useJava8TimeDialect() throws Exception {
this.context.register(ThymeleafAutoConfiguration.class, load(BaseConfiguration.class);
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
TemplateEngine engine = this.context.getBean(TemplateEngine.class); TemplateEngine engine = this.context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK); Context attrs = new Context(Locale.UK);
String result = engine.process("java8time-dialect", attrs); String result = engine.process("java8time-dialect", attrs);
...@@ -187,9 +163,7 @@ public class ThymeleafAutoConfigurationTests { ...@@ -187,9 +163,7 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void renderTemplate() throws Exception { public void renderTemplate() throws Exception {
this.context.register(ThymeleafAutoConfiguration.class, load(BaseConfiguration.class);
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
TemplateEngine engine = this.context.getBean(TemplateEngine.class); TemplateEngine engine = this.context.getBean(TemplateEngine.class);
Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar")); Context attrs = new Context(Locale.UK, Collections.singletonMap("foo", "bar"));
String result = engine.process("home", attrs); String result = engine.process("home", attrs);
...@@ -216,9 +190,7 @@ public class ThymeleafAutoConfigurationTests { ...@@ -216,9 +190,7 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void registerResourceHandlingFilterDisabledByDefault() throws Exception { public void registerResourceHandlingFilterDisabledByDefault() throws Exception {
this.context.register(ThymeleafAutoConfiguration.class, load(BaseConfiguration.class);
PropertyPlaceholderAutoConfiguration.class);
this.context.refresh();
assertThat(this.context.getBeansOfType(ResourceUrlEncodingFilter.class)) assertThat(this.context.getBeansOfType(ResourceUrlEncodingFilter.class))
.isEmpty(); .isEmpty();
} }
...@@ -226,18 +198,13 @@ public class ThymeleafAutoConfigurationTests { ...@@ -226,18 +198,13 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void registerResourceHandlingFilterOnlyIfResourceChainIsEnabled() public void registerResourceHandlingFilterOnlyIfResourceChainIsEnabled()
throws Exception { throws Exception {
this.context.register(ThymeleafAutoConfiguration.class, load(BaseConfiguration.class, "spring.resources.chain.enabled:true");
PropertyPlaceholderAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.resources.chain.enabled:true");
this.context.refresh();
assertThat(this.context.getBean(ResourceUrlEncodingFilter.class)).isNotNull(); assertThat(this.context.getBean(ResourceUrlEncodingFilter.class)).isNotNull();
} }
@Test @Test
public void layoutDialectCanBeCustomized() throws Exception { public void layoutDialectCanBeCustomized() throws Exception {
this.context.register(LayoutDialectConfiguration.class); load(LayoutDialectConfiguration.class);
this.context.refresh();
LayoutDialect layoutDialect = this.context.getBean(LayoutDialect.class); LayoutDialect layoutDialect = this.context.getBean(LayoutDialect.class);
assertThat(ReflectionTestUtils.getField(layoutDialect, "sortingStrategy")) assertThat(ReflectionTestUtils.getField(layoutDialect, "sortingStrategy"))
.isInstanceOf(GroupingStrategy.class); .isInstanceOf(GroupingStrategy.class);
...@@ -245,19 +212,32 @@ public class ThymeleafAutoConfigurationTests { ...@@ -245,19 +212,32 @@ public class ThymeleafAutoConfigurationTests {
@Test @Test
public void cachingCanBeDisabled() { public void cachingCanBeDisabled() {
this.context.register(ThymeleafAutoConfiguration.class, load(BaseConfiguration.class, "spring.thymeleaf.cache:false");
PropertyPlaceholderAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context, "spring.thymeleaf.cache:false");
this.context.refresh();
assertThat(this.context.getBean(ThymeleafViewResolver.class).isCache()).isFalse(); assertThat(this.context.getBean(ThymeleafViewResolver.class).isCache()).isFalse();
SpringResourceTemplateResolver templateResolver = this.context SpringResourceTemplateResolver templateResolver = this.context
.getBean(SpringResourceTemplateResolver.class); .getBean(SpringResourceTemplateResolver.class);
assertThat(templateResolver.isCacheable()).isFalse(); assertThat(templateResolver.isCacheable()).isFalse();
} }
private void load(Class<?> config, String... envVariables) {
this.context = new AnnotationConfigWebApplicationContext();
EnvironmentTestUtils.addEnvironment(this.context, envVariables);
if (config != null) {
this.context.register(config);
}
this.context.register(config);
this.context.refresh();
}
@Configuration
@ImportAutoConfiguration({ThymeleafAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class})
static class BaseConfiguration {
}
@Configuration @Configuration
@ImportAutoConfiguration({ ThymeleafAutoConfiguration.class, @Import(BaseConfiguration.class)
PropertyPlaceholderAutoConfiguration.class })
static class LayoutDialectConfiguration { static class LayoutDialectConfiguration {
@Bean @Bean
......
...@@ -437,12 +437,14 @@ content into your application; rather pick only the properties that you need. ...@@ -437,12 +437,14 @@ content into your application; rather pick only the properties that you need.
spring.thymeleaf.cache=true # Enable template caching. spring.thymeleaf.cache=true # Enable template caching.
spring.thymeleaf.check-template=true # Check that the template exists before rendering it. spring.thymeleaf.check-template=true # Check that the template exists before rendering it.
spring.thymeleaf.check-template-location=true # Check that the templates location exists. spring.thymeleaf.check-template-location=true # Check that the templates location exists.
spring.thymeleaf.content-type=text/html # Content-Type value. spring.thymeleaf.enabled=true # Enable Thymeleaf view resolution for Web frameworks.
spring.thymeleaf.enabled=true # Enable MVC Thymeleaf view resolution. spring.thymeleaf.encoding=UTF-8 # Template files encoding.
spring.thymeleaf.encoding=UTF-8 # Template encoding.
spring.thymeleaf.excluded-view-names= # Comma-separated list of view names that should be excluded from resolution. spring.thymeleaf.excluded-view-names= # Comma-separated list of view names that should be excluded from resolution.
spring.thymeleaf.mode=HTML5 # Template mode to be applied to templates. See also StandardTemplateModeHandlers. spring.thymeleaf.mode=HTML5 # Template mode to be applied to templates. See also StandardTemplateModeHandlers.
spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL. spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.reactive.max-chunk-size= # Maximum size of data buffers used for writing to the response, in bytes.
spring.thymeleaf.reactive.media-types=text/html # Media types supported by the view technology.
spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.
spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL. spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.
spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain. spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved. spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved.
......
...@@ -28,6 +28,10 @@ ...@@ -28,6 +28,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
......
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
......
...@@ -24,6 +24,10 @@ ...@@ -24,6 +24,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
......
...@@ -28,6 +28,10 @@ ...@@ -28,6 +28,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
......
...@@ -33,6 +33,7 @@ repositories { ...@@ -33,6 +33,7 @@ repositories {
} }
dependencies { dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.springframework.boot:spring-boot-starter-thymeleaf")
compile("org.hibernate:hibernate-validator") compile("org.hibernate:hibernate-validator")
testCompile("org.springframework.boot:spring-boot-starter-test") testCompile("org.springframework.boot:spring-boot-starter-test")
......
...@@ -20,6 +20,10 @@ ...@@ -20,6 +20,10 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- Compile --> <!-- Compile -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
......
...@@ -22,10 +22,6 @@ ...@@ -22,10 +22,6 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.thymeleaf</groupId> <groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId> <artifactId>thymeleaf-spring5</artifactId>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment