Skip to content

Commit

Permalink
Add Thymeleaf auto-configuration for WebFlux
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bclozel committed Apr 28, 2017
1 parent d4f87ae commit 4d5dcca
Show file tree
Hide file tree
Showing 12 changed files with 435 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import java.util.LinkedHashMap;

import javax.annotation.PostConstruct;
import javax.servlet.Servlet;

import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect;
import nz.net.ultraq.thymeleaf.LayoutDialect;
Expand All @@ -29,9 +28,12 @@
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect;
import org.thymeleaf.extras.springsecurity4.dialect.SpringSecurityDialect;
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;

Expand All @@ -45,6 +47,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.template.TemplateLocation;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
Expand All @@ -63,11 +66,12 @@
* @author Stephane Nicoll
* @author Brian Clozel
* @author Eddú Meléndez
* @author Daniel Fernández
*/
@Configuration
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass(TemplateMode.class)
@AutoConfigureAfter(WebMvcAutoConfiguration.class)
@AutoConfigureAfter({WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class})
public class ThymeleafAutoConfiguration {

@Configuration
Expand Down Expand Up @@ -123,68 +127,112 @@ public SpringResourceTemplateResolver defaultTemplateResolver() {
}

@Configuration
@ConditionalOnClass({ Servlet.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
static class ThymeleafViewResolverConfiguration {
protected static class ThymeleafDefaultConfiguration {

private final ThymeleafProperties properties;
private final Collection<ITemplateResolver> templateResolvers;

private final SpringTemplateEngine templateEngine;
private final Collection<IDialect> dialects;

ThymeleafViewResolverConfiguration(ThymeleafProperties properties,
SpringTemplateEngine templateEngine) {
this.properties = properties;
this.templateEngine = templateEngine;
public ThymeleafDefaultConfiguration(
Collection<ITemplateResolver> templateResolvers,
ObjectProvider<Collection<IDialect>> dialectsProvider) {
this.templateResolvers = templateResolvers;
this.dialects = dialectsProvider.getIfAvailable();
}

@Bean
@ConditionalOnMissingBean(name = "thymeleafViewResolver")
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
public ThymeleafViewResolver thymeleafViewResolver() {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(this.templateEngine);
resolver.setCharacterEncoding(this.properties.getEncoding().name());
resolver.setContentType(appendCharset(this.properties.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;
@ConditionalOnMissingBean(SpringTemplateEngine.class)
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
for (ITemplateResolver templateResolver : this.templateResolvers) {
engine.addTemplateResolver(templateResolver);
}
if (!CollectionUtils.isEmpty(this.dialects)) {
for (IDialect dialect : this.dialects) {
engine.addDialect(dialect);
}
}
return engine;
}

}

@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) {
if (type.getCharset() != null) {
return type.toString();
@Configuration
static class ThymeleafViewResolverConfiguration {

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
@ConditionalOnMissingBean(SpringTemplateEngine.class)
protected static class ThymeleafDefaultConfiguration {
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true)
static class ThymeleafReactiveConfiguration {

private final Collection<ITemplateResolver> templateResolvers;

private final Collection<IDialect> dialects;

public ThymeleafDefaultConfiguration(
Collection<ITemplateResolver> templateResolvers,

ThymeleafReactiveConfiguration(Collection<ITemplateResolver> templateResolvers,
ObjectProvider<Collection<IDialect>> dialectsProvider) {
this.templateResolvers = templateResolvers;
this.dialects = dialectsProvider.getIfAvailable();
}

@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine engine = new SpringTemplateEngine();
@ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class)
public SpringWebFluxTemplateEngine templateEngine() {
SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine();
for (ITemplateResolver templateResolver : this.templateResolvers) {
engine.addTemplateResolver(templateResolver);
}
Expand All @@ -195,6 +243,38 @@ public SpringTemplateEngine templateEngine() {
}
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;
}

}

Expand Down Expand Up @@ -223,7 +303,7 @@ public DataAttributeDialect dialect() {
}

@Configuration
@ConditionalOnClass({ SpringSecurityDialect.class })
@ConditionalOnClass({SpringSecurityDialect.class})
protected static class ThymeleafSecurityDialectConfiguration {

@Bean
Expand All @@ -246,17 +326,4 @@ public Java8TimeDialect java8TimeDialect() {

}

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
protected static class ThymeleafResourceHandlingConfig {

@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledResourceChain
public ResourceUrlEncodingFilter resourceUrlEncodingFilter() {
return new ResourceUrlEncodingFilter();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,27 @@
package org.springframework.boot.autoconfigure.thymeleaf;

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.http.MediaType;
import org.springframework.util.MimeType;

/**
* Properties for Thymeleaf.
*
* @author Stephane Nicoll
* @author Brian Clozel
* @author Daniel Fernández
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

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_SUFFIX = ".html";
Expand Down Expand Up @@ -65,15 +69,10 @@ public class ThymeleafProperties {
private String mode = "HTML";

/**
* Template encoding.
* Template files encoding.
*/
private Charset encoding = DEFAULT_ENCODING;

/**
* Content-Type value.
*/
private MimeType contentType = DEFAULT_CONTENT_TYPE;

/**
* Enable template caching.
*/
Expand All @@ -97,10 +96,14 @@ public class ThymeleafProperties {
private String[] excludedViewNames;

/**
* Enable MVC Thymeleaf view resolution.
* 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;
}
Expand Down Expand Up @@ -157,14 +160,6 @@ public void setEncoding(Charset encoding) {
this.encoding = encoding;
}

public MimeType getContentType() {
return this.contentType;
}

public void setContentType(MimeType contentType) {
this.contentType = contentType;
}

public boolean isCache() {
return this.cache;
}
Expand Down Expand Up @@ -197,4 +192,58 @@ public void setViewNames(String[] 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;
}
}
}
Loading

0 comments on commit 4d5dcca

Please sign in to comment.