Reinstate support for Thymeleaf
parent
e95f038c01
commit
12cd97a20c
@ -0,0 +1,305 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,321 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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;
|
@ -0,0 +1,235 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,351 @@
|
||||
/*
|
||||
* 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1 @@
|
||||
<html><body th:text="${foo}">Home</body></html>
|
@ -0,0 +1 @@
|
||||
<html><body th:text="${#temporals.create('2015','11','24')}"></body></html>
|
@ -0,0 +1,14 @@
|
||||
<!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>
|
@ -0,0 +1 @@
|
||||
<html><body>Message: <span th:text="${greeting}">Hello</span></body></html>
|
@ -0,0 +1 @@
|
||||
<html th:text="${foo}">foo</html>
|
@ -0,0 +1,10 @@
|
||||
<!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>
|
@ -0,0 +1,10 @@
|
||||
<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>
|
@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
<!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>
|
@ -0,0 +1,11 @@
|
||||
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")
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
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"))
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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);
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
# Allow Thymeleaf templates to be reloaded at dev time
|
||||
spring.thymeleaf.cache: false
|
||||
server.tomcat.access_log_enabled: true
|
||||
server.tomcat.basedir: target/tomcat
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
|
||||
<include resource="org/springframework/boot/logging/logback/base.xml"/>
|
||||
|
||||
<!-- logger name="org.springframework" level="DEBUG"/-->
|
||||
|
||||
</configuration>
|
@ -0,0 +1,21 @@
|
||||
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
|
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,18 @@
|
||||
<!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>
|
@ -0,0 +1,31 @@
|
||||
<!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>
|
@ -0,0 +1,36 @@
|
||||
<!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>
|
@ -0,0 +1,27 @@
|
||||
<!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>
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2012-2022 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…
Reference in New Issue