001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.thymeleaf; 018 019import java.util.Collection; 020import java.util.LinkedHashMap; 021 022import javax.annotation.PostConstruct; 023import javax.servlet.DispatcherType; 024 025import com.github.mxab.thymeleaf.extras.dataattribute.dialect.DataAttributeDialect; 026import nz.net.ultraq.thymeleaf.LayoutDialect; 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029import org.thymeleaf.dialect.IDialect; 030import org.thymeleaf.extras.java8time.dialect.Java8TimeDialect; 031import org.thymeleaf.extras.springsecurity5.dialect.SpringSecurityDialect; 032import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine; 033import org.thymeleaf.spring5.SpringTemplateEngine; 034import org.thymeleaf.spring5.SpringWebFluxTemplateEngine; 035import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver; 036import org.thymeleaf.spring5.view.ThymeleafViewResolver; 037import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver; 038import org.thymeleaf.templatemode.TemplateMode; 039import org.thymeleaf.templateresolver.ITemplateResolver; 040 041import org.springframework.beans.factory.ObjectProvider; 042import org.springframework.boot.autoconfigure.AutoConfigureAfter; 043import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 044import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 045import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 046import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 047import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 048import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; 049import org.springframework.boot.autoconfigure.template.TemplateLocation; 050import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties.Reactive; 051import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; 052import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; 053import org.springframework.boot.autoconfigure.web.servlet.ConditionalOnMissingFilterBean; 054import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; 055import org.springframework.boot.context.properties.EnableConfigurationProperties; 056import org.springframework.boot.context.properties.PropertyMapper; 057import org.springframework.boot.web.servlet.FilterRegistrationBean; 058import org.springframework.context.ApplicationContext; 059import org.springframework.context.annotation.Bean; 060import org.springframework.context.annotation.Configuration; 061import org.springframework.core.Ordered; 062import org.springframework.util.MimeType; 063import org.springframework.util.unit.DataSize; 064import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter; 065 066/** 067 * {@link EnableAutoConfiguration Auto-configuration} for Thymeleaf. 068 * 069 * @author Dave Syer 070 * @author Andy Wilkinson 071 * @author Stephane Nicoll 072 * @author Brian Clozel 073 * @author Eddú Meléndez 074 * @author Daniel Fernández 075 * @author Kazuki Shimizu 076 * @author Artsiom Yudovin 077 */ 078@Configuration 079@EnableConfigurationProperties(ThymeleafProperties.class) 080@ConditionalOnClass(TemplateMode.class) 081@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) 082public class ThymeleafAutoConfiguration { 083 084 @Configuration 085 @ConditionalOnMissingBean(name = "defaultTemplateResolver") 086 static class DefaultTemplateResolverConfiguration { 087 088 private static final Log logger = LogFactory 089 .getLog(DefaultTemplateResolverConfiguration.class); 090 091 private final ThymeleafProperties properties; 092 093 private final ApplicationContext applicationContext; 094 095 DefaultTemplateResolverConfiguration(ThymeleafProperties properties, 096 ApplicationContext applicationContext) { 097 this.properties = properties; 098 this.applicationContext = applicationContext; 099 } 100 101 @PostConstruct 102 public void checkTemplateLocationExists() { 103 boolean checkTemplateLocation = this.properties.isCheckTemplateLocation(); 104 if (checkTemplateLocation) { 105 TemplateLocation location = new TemplateLocation( 106 this.properties.getPrefix()); 107 if (!location.exists(this.applicationContext)) { 108 logger.warn("Cannot find template location: " + location 109 + " (please add some templates or check " 110 + "your Thymeleaf configuration)"); 111 } 112 } 113 } 114 115 @Bean 116 public SpringResourceTemplateResolver defaultTemplateResolver() { 117 SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver(); 118 resolver.setApplicationContext(this.applicationContext); 119 resolver.setPrefix(this.properties.getPrefix()); 120 resolver.setSuffix(this.properties.getSuffix()); 121 resolver.setTemplateMode(this.properties.getMode()); 122 if (this.properties.getEncoding() != null) { 123 resolver.setCharacterEncoding(this.properties.getEncoding().name()); 124 } 125 resolver.setCacheable(this.properties.isCache()); 126 Integer order = this.properties.getTemplateResolverOrder(); 127 if (order != null) { 128 resolver.setOrder(order); 129 } 130 resolver.setCheckExistence(this.properties.isCheckTemplate()); 131 return resolver; 132 } 133 134 } 135 136 @Configuration 137 protected static class ThymeleafDefaultConfiguration { 138 139 private final ThymeleafProperties properties; 140 141 private final Collection<ITemplateResolver> templateResolvers; 142 143 private final ObjectProvider<IDialect> dialects; 144 145 public ThymeleafDefaultConfiguration(ThymeleafProperties properties, 146 Collection<ITemplateResolver> templateResolvers, 147 ObjectProvider<IDialect> dialectsProvider) { 148 this.properties = properties; 149 this.templateResolvers = templateResolvers; 150 this.dialects = dialectsProvider; 151 } 152 153 @Bean 154 @ConditionalOnMissingBean 155 public SpringTemplateEngine templateEngine() { 156 SpringTemplateEngine engine = new SpringTemplateEngine(); 157 engine.setEnableSpringELCompiler(this.properties.isEnableSpringElCompiler()); 158 engine.setRenderHiddenMarkersBeforeCheckboxes( 159 this.properties.isRenderHiddenMarkersBeforeCheckboxes()); 160 this.templateResolvers.forEach(engine::addTemplateResolver); 161 this.dialects.orderedStream().forEach(engine::addDialect); 162 return engine; 163 } 164 165 } 166 167 @Configuration 168 @ConditionalOnWebApplication(type = Type.SERVLET) 169 @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) 170 static class ThymeleafWebMvcConfiguration { 171 172 @Bean 173 @ConditionalOnEnabledResourceChain 174 @ConditionalOnMissingFilterBean(ResourceUrlEncodingFilter.class) 175 public FilterRegistrationBean<ResourceUrlEncodingFilter> resourceUrlEncodingFilter() { 176 FilterRegistrationBean<ResourceUrlEncodingFilter> registration = new FilterRegistrationBean<>( 177 new ResourceUrlEncodingFilter()); 178 registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ERROR); 179 return registration; 180 } 181 182 @Configuration 183 static class ThymeleafViewResolverConfiguration { 184 185 private final ThymeleafProperties properties; 186 187 private final SpringTemplateEngine templateEngine; 188 189 ThymeleafViewResolverConfiguration(ThymeleafProperties properties, 190 SpringTemplateEngine templateEngine) { 191 this.properties = properties; 192 this.templateEngine = templateEngine; 193 } 194 195 @Bean 196 @ConditionalOnMissingBean(name = "thymeleafViewResolver") 197 public ThymeleafViewResolver thymeleafViewResolver() { 198 ThymeleafViewResolver resolver = new ThymeleafViewResolver(); 199 resolver.setTemplateEngine(this.templateEngine); 200 resolver.setCharacterEncoding(this.properties.getEncoding().name()); 201 resolver.setContentType( 202 appendCharset(this.properties.getServlet().getContentType(), 203 resolver.getCharacterEncoding())); 204 resolver.setProducePartialOutputWhileProcessing(this.properties 205 .getServlet().isProducePartialOutputWhileProcessing()); 206 resolver.setExcludedViewNames(this.properties.getExcludedViewNames()); 207 resolver.setViewNames(this.properties.getViewNames()); 208 // This resolver acts as a fallback resolver (e.g. like a 209 // InternalResourceViewResolver) so it needs to have low precedence 210 resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5); 211 resolver.setCache(this.properties.isCache()); 212 return resolver; 213 } 214 215 private String appendCharset(MimeType type, String charset) { 216 if (type.getCharset() != null) { 217 return type.toString(); 218 } 219 LinkedHashMap<String, String> parameters = new LinkedHashMap<>(); 220 parameters.put("charset", charset); 221 parameters.putAll(type.getParameters()); 222 return new MimeType(type, parameters).toString(); 223 } 224 225 } 226 227 } 228 229 @Configuration 230 @ConditionalOnWebApplication(type = Type.REACTIVE) 231 @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) 232 static class ThymeleafReactiveConfiguration { 233 234 private final ThymeleafProperties properties; 235 236 private final Collection<ITemplateResolver> templateResolvers; 237 238 private final ObjectProvider<IDialect> dialects; 239 240 ThymeleafReactiveConfiguration(ThymeleafProperties properties, 241 Collection<ITemplateResolver> templateResolvers, 242 ObjectProvider<IDialect> dialectsProvider) { 243 this.properties = properties; 244 this.templateResolvers = templateResolvers; 245 this.dialects = dialectsProvider; 246 } 247 248 @Bean 249 @ConditionalOnMissingBean(ISpringWebFluxTemplateEngine.class) 250 public SpringWebFluxTemplateEngine templateEngine() { 251 SpringWebFluxTemplateEngine engine = new SpringWebFluxTemplateEngine(); 252 engine.setEnableSpringELCompiler(this.properties.isEnableSpringElCompiler()); 253 engine.setRenderHiddenMarkersBeforeCheckboxes( 254 this.properties.isRenderHiddenMarkersBeforeCheckboxes()); 255 this.templateResolvers.forEach(engine::addTemplateResolver); 256 this.dialects.orderedStream().forEach(engine::addDialect); 257 return engine; 258 } 259 260 } 261 262 @Configuration 263 @ConditionalOnWebApplication(type = Type.REACTIVE) 264 @ConditionalOnProperty(name = "spring.thymeleaf.enabled", matchIfMissing = true) 265 static class ThymeleafWebFluxConfiguration { 266 267 private final ThymeleafProperties properties; 268 269 ThymeleafWebFluxConfiguration(ThymeleafProperties properties) { 270 this.properties = properties; 271 } 272 273 @Bean 274 @ConditionalOnMissingBean(name = "thymeleafReactiveViewResolver") 275 public ThymeleafReactiveViewResolver thymeleafViewResolver( 276 ISpringWebFluxTemplateEngine templateEngine) { 277 ThymeleafReactiveViewResolver resolver = new ThymeleafReactiveViewResolver(); 278 resolver.setTemplateEngine(templateEngine); 279 mapProperties(this.properties, resolver); 280 mapReactiveProperties(this.properties.getReactive(), resolver); 281 // This resolver acts as a fallback resolver (e.g. like a 282 // InternalResourceViewResolver) so it needs to have low precedence 283 resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 5); 284 return resolver; 285 } 286 287 private void mapProperties(ThymeleafProperties properties, 288 ThymeleafReactiveViewResolver resolver) { 289 PropertyMapper map = PropertyMapper.get(); 290 map.from(properties::getEncoding).to(resolver::setDefaultCharset); 291 resolver.setExcludedViewNames(properties.getExcludedViewNames()); 292 resolver.setViewNames(properties.getViewNames()); 293 } 294 295 private void mapReactiveProperties(Reactive properties, 296 ThymeleafReactiveViewResolver resolver) { 297 PropertyMapper map = PropertyMapper.get(); 298 map.from(properties::getMediaTypes).whenNonNull() 299 .to(resolver::setSupportedMediaTypes); 300 map.from(properties::getMaxChunkSize).asInt(DataSize::toBytes) 301 .when((size) -> size > 0).to(resolver::setResponseMaxChunkSizeBytes); 302 map.from(properties::getFullModeViewNames).to(resolver::setFullModeViewNames); 303 map.from(properties::getChunkedModeViewNames) 304 .to(resolver::setChunkedModeViewNames); 305 } 306 307 } 308 309 @Configuration 310 @ConditionalOnClass(name = "nz.net.ultraq.thymeleaf.LayoutDialect") 311 protected static class ThymeleafWebLayoutConfiguration { 312 313 @Bean 314 @ConditionalOnMissingBean 315 public LayoutDialect layoutDialect() { 316 return new LayoutDialect(); 317 } 318 319 } 320 321 @Configuration 322 @ConditionalOnClass(DataAttributeDialect.class) 323 protected static class DataAttributeDialectConfiguration { 324 325 @Bean 326 @ConditionalOnMissingBean 327 public DataAttributeDialect dialect() { 328 return new DataAttributeDialect(); 329 } 330 331 } 332 333 @Configuration 334 @ConditionalOnClass({ SpringSecurityDialect.class }) 335 protected static class ThymeleafSecurityDialectConfiguration { 336 337 @Bean 338 @ConditionalOnMissingBean 339 public SpringSecurityDialect securityDialect() { 340 return new SpringSecurityDialect(); 341 } 342 343 } 344 345 @Configuration 346 @ConditionalOnClass(Java8TimeDialect.class) 347 protected static class ThymeleafJava8TimeDialect { 348 349 @Bean 350 @ConditionalOnMissingBean 351 public Java8TimeDialect java8TimeDialect() { 352 return new Java8TimeDialect(); 353 } 354 355 } 356 357}