From 2fa0539e7f7bf93505f67303955cc7da6f9f5846 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Fri, 22 Dec 2017 15:42:10 +0100 Subject: [PATCH] Support date conversion format for java.time types Prior to this change, the Spring MVC auto-configuration would add a new formatter to convert `java.util.Date` to/from `String` using the configured configuration property `spring.mvc.date-format`. This commit adds a new `WebConversionService` class that registers date formatters with a custom date format, or register the default ones if no custom configuration is provided. This avoids duplicating equivalent formatters in the registry. With this change, date types from `java.util`, `org.joda.time` and `java.time` are now all supported. This commit also replicates this feature for WebFlux applications by adding a new `spring.webflux.date-format` configuration property. Closes gh-5523 Closes gh-11402 --- .../web/format/WebConversionService.java | 118 ++++++ .../reactive/WebFluxAutoConfiguration.java | 21 +- .../web/reactive/WebFluxProperties.java | 13 + .../web/servlet/WebMvcAutoConfiguration.java | 18 +- .../web/format/WebConversionServiceTests.java | 50 +++ .../WebFluxAutoConfigurationTests.java | 381 +++++++++--------- .../appendix-application-properties.adoc | 3 +- 7 files changed, 411 insertions(+), 193 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java new file mode 100644 index 0000000000..1692ea2263 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java @@ -0,0 +1,118 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.format; + +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; + +import org.joda.time.format.DateTimeFormatterBuilder; + +import org.springframework.format.datetime.DateFormatter; +import org.springframework.format.datetime.DateFormatterRegistrar; +import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; +import org.springframework.format.number.money.CurrencyUnitFormatter; +import org.springframework.format.number.money.Jsr354NumberFormatAnnotationFormatterFactory; +import org.springframework.format.number.money.MonetaryAmountFormatter; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.format.support.FormattingConversionService} dedicated + * to web applications for formatting and converting values to/from the web. + * + *

This service replaces the default implementations provided by + * {@link org.springframework.web.servlet.config.annotation.EnableWebMvc} + * and {@link org.springframework.web.reactive.config.EnableWebFlux}. + * + * @author Brian Clozel + * @since 2.0.0 + */ +public class WebConversionService extends DefaultFormattingConversionService { + + private static final boolean jsr354Present = ClassUtils + .isPresent("javax.money.MonetaryAmount", WebConversionService.class.getClassLoader()); + + private static final boolean jodaTimePresent = ClassUtils + .isPresent("org.joda.time.LocalDate", WebConversionService.class.getClassLoader()); + + private String dateFormat; + + /** + * Create a new WebConversionService that configures formatters with the provided date format, + * or register the default ones if no custom format is provided. + * @param dateFormat the custom date format to use for date conversions + */ + public WebConversionService(String dateFormat) { + super(false); + if (StringUtils.hasText(dateFormat)) { + this.dateFormat = dateFormat; + addFormatters(); + } + else { + addDefaultFormatters(this); + } + } + + + private void addFormatters() { + addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + if (jsr354Present) { + addFormatter(new CurrencyUnitFormatter()); + addFormatter(new MonetaryAmountFormatter()); + addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory()); + } + registerJsr310(); + if (jodaTimePresent) { + registerJodaTime(); + } + registerJavaDate(); + } + + private void registerJsr310() { + DateTimeFormatterRegistrar dateTime = new DateTimeFormatterRegistrar(); + if (this.dateFormat != null) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter + .ofPattern(this.dateFormat) + .withResolverStyle(ResolverStyle.STRICT); + dateTime.setDateFormatter(dateTimeFormatter); + } + dateTime.registerFormatters(this); + } + + private void registerJodaTime() { + JodaTimeFormatterRegistrar jodaTime = new JodaTimeFormatterRegistrar(); + if (this.dateFormat != null) { + org.joda.time.format.DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder() + .appendPattern(this.dateFormat) + .toFormatter(); + jodaTime.setDateFormatter(dateTimeFormatter); + } + jodaTime.registerFormatters(this); + } + + private void registerJavaDate() { + DateFormatterRegistrar dateFormatterRegistrar = new DateFormatterRegistrar(); + if (this.dateFormat != null) { + DateFormatter dateFormatter = new DateFormatter(this.dateFormat); + dateFormatterRegistrar.setFormatter(dateFormatter); + } + dateFormatterRegistrar.registerFormatters(this); + } +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java index a375227d17..c173e543d4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java @@ -34,9 +34,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; +import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; @@ -48,6 +50,7 @@ import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.ClassUtils; @@ -84,7 +87,7 @@ import org.springframework.web.reactive.result.view.ViewResolver; @ConditionalOnClass(WebFluxConfigurer.class) @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) @AutoConfigureAfter({ ReactiveWebServerAutoConfiguration.class, - CodecsAutoConfiguration.class }) + CodecsAutoConfiguration.class, ValidationAutoConfiguration.class }) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) public class WebFluxAutoConfiguration { @@ -211,8 +214,22 @@ public class WebFluxAutoConfiguration { public static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { - @Override + private final WebFluxProperties webFluxProperties; + + public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties) { + this.webFluxProperties = webFluxProperties; + } + @Bean + @Override + public FormattingConversionService webFluxConversionService() { + WebConversionService conversionService = new WebConversionService(this.webFluxProperties.getDateFormat()); + addFormatters(conversionService); + return conversionService; + } + + @Bean + @Override public Validator webFluxValidator() { if (!ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java index cfbf061acb..fe8c512fd4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java @@ -27,11 +27,24 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.webflux") public class WebFluxProperties { + /** + * Date format to use. For instance, "dd/MM/yyyy". + */ + private String dateFormat; + /** * Path pattern used for static resources. */ private String staticPathPattern = "/**"; + public String getDateFormat() { + return this.dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + public String getStaticPathPattern() { return this.staticPathPattern; } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java index 580446c7c8..43b130b0d9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -54,6 +53,7 @@ import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; +import org.springframework.boot.autoconfigure.web.format.WebConversionService; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter; @@ -73,7 +73,7 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; -import org.springframework.format.datetime.DateFormatter; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -266,12 +266,6 @@ public class WebMvcAutoConfiguration { return localeResolver; } - @Bean - @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format") - public Formatter dateFormatter() { - return new DateFormatter(this.mvcProperties.getDateFormat()); - } - @Override public MessageCodesResolver getMessageCodesResolver() { if (this.mvcProperties.getMessageCodesResolverFormat() != null) { @@ -478,6 +472,14 @@ public class WebMvcAutoConfiguration { return super.requestMappingHandlerMapping(); } + @Bean + @Override + public FormattingConversionService mvcConversionService() { + WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat()); + addFormatters(conversionService); + return conversionService; + } + @Bean @Override public Validator mvcValidator() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java new file mode 100644 index 0000000000..469945aa9e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.web.format; + +import java.util.Date; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link WebConversionService}. + * + * @author Brian Clozel + */ +public class WebConversionServiceTests { + + @Test + public void customDateFormat() { + WebConversionService conversionService = new WebConversionService("dd*MM*yyyy"); + + Date date = new DateTime(2018, 1, 1, 20, 30).toDate(); + assertThat(conversionService.convert(date, String.class)) + .isEqualTo("01*01*2018"); + + LocalDate jodaDate = LocalDate.fromDateFields(date); + assertThat(conversionService.convert(jodaDate, String.class)) + .isEqualTo("01*01*2018"); + + java.time.LocalDate localDate = java.time.LocalDate.of(2018, 1, 1); + assertThat(conversionService.convert(localDate, String.class)) + .isEqualTo("01*01*2018"); + } +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java index f7d3af48ab..ed51987b83 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfigurationTests.java @@ -16,32 +16,29 @@ package org.springframework.boot.autoconfigure.web.reactive; -import java.util.ArrayList; -import java.util.Arrays; +import java.util.Date; import java.util.List; import javax.validation.ValidatorFactory; +import org.joda.time.DateTime; import org.junit.Test; import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration; import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfigurationTests.Config; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.boot.web.codec.CodecCustomizer; -import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; +import org.springframework.format.support.FormattingConversionService; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.server.reactive.HttpHandler; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.util.ObjectUtils; import org.springframework.validation.Validator; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.reactive.HandlerMapping; @@ -72,240 +69,262 @@ import static org.mockito.Mockito.verify; */ public class WebFluxAutoConfigurationTests { - private AnnotationConfigReactiveWebApplicationContext context; + private static final MockReactiveWebServerFactory mockReactiveWebServerFactory + = new MockReactiveWebServerFactory(); + + private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class)) + .withUserConfiguration(Config.class); @Test public void shouldNotProcessIfExistingWebReactiveConfiguration() { - load(WebFluxConfigurationSupport.class); - assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size()) - .isEqualTo(1); - assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size()) - .isEqualTo(1); + this.contextRunner.withUserConfiguration(WebFluxConfigurationSupport.class) + .run(context -> { + assertThat(context).getBeans(RequestMappingHandlerMapping.class).hasSize(1); + assertThat(context).getBeans(RequestMappingHandlerAdapter.class).hasSize(1); + }); } @Test public void shouldCreateDefaultBeans() { - load(); - assertThat(this.context.getBeansOfType(RequestMappingHandlerMapping.class).size()) - .isEqualTo(1); - assertThat(this.context.getBeansOfType(RequestMappingHandlerAdapter.class).size()) - .isEqualTo(1); - assertThat(this.context.getBeansOfType(RequestedContentTypeResolver.class).size()) - .isEqualTo(1); - assertThat(this.context.getBean("resourceHandlerMapping", HandlerMapping.class)) - .isNotNull(); + this.contextRunner.run(context -> { + assertThat(context).getBeans(RequestMappingHandlerMapping.class).hasSize(1); + assertThat(context).getBeans(RequestMappingHandlerAdapter.class).hasSize(1); + assertThat(context).getBeans(RequestedContentTypeResolver.class).hasSize(1); + assertThat(context.getBean("resourceHandlerMapping", HandlerMapping.class)) + .isNotNull(); + }); } @SuppressWarnings("unchecked") @Test public void shouldRegisterCustomHandlerMethodArgumentResolver() { - load(CustomArgumentResolvers.class); - RequestMappingHandlerAdapter adapter = this.context - .getBean(RequestMappingHandlerAdapter.class); - List customResolvers = (List) ReflectionTestUtils - .getField(adapter.getArgumentResolverConfigurer(), "customResolvers"); - assertThat(customResolvers).contains( - this.context.getBean("firstResolver", - HandlerMethodArgumentResolver.class), - this.context.getBean("secondResolver", - HandlerMethodArgumentResolver.class)); + this.contextRunner.withUserConfiguration(CustomArgumentResolvers.class).run(context -> { + RequestMappingHandlerAdapter adapter = context + .getBean(RequestMappingHandlerAdapter.class); + List customResolvers = + (List) ReflectionTestUtils + .getField(adapter.getArgumentResolverConfigurer(), "customResolvers"); + assertThat(customResolvers).contains( + context.getBean("firstResolver", + HandlerMethodArgumentResolver.class), + context.getBean("secondResolver", + HandlerMethodArgumentResolver.class)); + }); } @Test public void shouldCustomizeCodecs() { - load(CustomCodecCustomizers.class); - CodecCustomizer codecCustomizer = this.context.getBean("firstCodecCustomizer", - CodecCustomizer.class); - assertThat(codecCustomizer).isNotNull(); - verify(codecCustomizer).customize(any(ServerCodecConfigurer.class)); + this.contextRunner.withUserConfiguration(CustomCodecCustomizers.class).run(context -> { + CodecCustomizer codecCustomizer = context.getBean("firstCodecCustomizer", + CodecCustomizer.class); + assertThat(codecCustomizer).isNotNull(); + verify(codecCustomizer).customize(any(ServerCodecConfigurer.class)); + }); } @Test public void shouldRegisterResourceHandlerMapping() { - load(); - SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping", - SimpleUrlHandlerMapping.class); - assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); - ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); - assertThat(staticHandler.getLocations()).hasSize(4); - assertThat(hm.getUrlMap().get("/webjars/**")) - .isInstanceOf(ResourceWebHandler.class); - ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap() - .get("/webjars/**"); - assertThat(webjarsHandler.getLocations()).hasSize(1); - assertThat(webjarsHandler.getLocations().get(0)) - .isEqualTo(new ClassPathResource("/META-INF/resources/webjars/")); + this.contextRunner.run(context -> { + SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", + SimpleUrlHandlerMapping.class); + assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); + assertThat(staticHandler.getLocations()).hasSize(4); + assertThat(hm.getUrlMap().get("/webjars/**")) + .isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler webjarsHandler = (ResourceWebHandler) hm.getUrlMap() + .get("/webjars/**"); + assertThat(webjarsHandler.getLocations()).hasSize(1); + assertThat(webjarsHandler.getLocations().get(0)) + .isEqualTo(new ClassPathResource("/META-INF/resources/webjars/")); + }); } @Test public void shouldMapResourcesToCustomPath() { - load("spring.webflux.static-path-pattern:/static/**"); - SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping", - SimpleUrlHandlerMapping.class); - assertThat(hm.getUrlMap().get("/static/**")) - .isInstanceOf(ResourceWebHandler.class); - ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap() - .get("/static/**"); - assertThat(staticHandler.getLocations()).hasSize(4); + this.contextRunner.withPropertyValues("spring.webflux.static-path-pattern:/static/**") + .run(context -> { + SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", + SimpleUrlHandlerMapping.class); + assertThat(hm.getUrlMap().get("/static/**")) + .isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap() + .get("/static/**"); + assertThat(staticHandler.getLocations()).hasSize(4); + }); } @Test public void shouldNotMapResourcesWhenDisabled() { - load("spring.resources.add-mappings:false"); - assertThat(this.context.getBean("resourceHandlerMapping")) - .isNotInstanceOf(SimpleUrlHandlerMapping.class); + this.contextRunner.withPropertyValues("spring.resources.add-mappings:false") + .run(context -> { + assertThat(context.getBean("resourceHandlerMapping")) + .isNotInstanceOf(SimpleUrlHandlerMapping.class); + }); } @Test public void resourceHandlerChainEnabled() { - load("spring.resources.chain.enabled:true"); - SimpleUrlHandlerMapping hm = this.context.getBean("resourceHandlerMapping", - SimpleUrlHandlerMapping.class); - assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); - ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); - assertThat(staticHandler.getResourceResolvers()).extractingResultOf("getClass") - .containsOnly(CachingResourceResolver.class, PathResourceResolver.class); - assertThat(staticHandler.getResourceTransformers()).extractingResultOf("getClass") - .containsOnly(CachingResourceTransformer.class); + this.contextRunner.withPropertyValues("spring.resources.chain.enabled:true") + .run(context -> { + SimpleUrlHandlerMapping hm = context.getBean("resourceHandlerMapping", + SimpleUrlHandlerMapping.class); + assertThat(hm.getUrlMap().get("/**")).isInstanceOf(ResourceWebHandler.class); + ResourceWebHandler staticHandler = (ResourceWebHandler) hm.getUrlMap().get("/**"); + assertThat(staticHandler.getResourceResolvers()).extractingResultOf("getClass") + .containsOnly(CachingResourceResolver.class, PathResourceResolver.class); + assertThat(staticHandler.getResourceTransformers()).extractingResultOf("getClass") + .containsOnly(CachingResourceTransformer.class); + }); } @Test public void shouldRegisterViewResolvers() { - load(ViewResolvers.class); - ViewResolutionResultHandler resultHandler = this.context - .getBean(ViewResolutionResultHandler.class); - assertThat(resultHandler.getViewResolvers()).containsExactly( - this.context.getBean("aViewResolver", ViewResolver.class), - this.context.getBean("anotherViewResolver", ViewResolver.class)); + this.contextRunner.withUserConfiguration(ViewResolvers.class).run(context -> { + ViewResolutionResultHandler resultHandler = context + .getBean(ViewResolutionResultHandler.class); + assertThat(resultHandler.getViewResolvers()).containsExactly( + context.getBean("aViewResolver", ViewResolver.class), + context.getBean("anotherViewResolver", ViewResolver.class)); + }); + } + + @Test + public void noDateFormat() { + this.contextRunner.run((context) -> { + FormattingConversionService conversionService = context + .getBean(FormattingConversionService.class); + Date date = new DateTime(1988, 6, 25, 20, 30).toDate(); + // formatting conversion service should use simple toString() + assertThat(conversionService.convert(date, String.class)) + .isEqualTo(date.toString()); + }); + } + + @Test + public void overrideDateFormat() { + this.contextRunner.withPropertyValues("spring.webflux.date-format:dd*MM*yyyy") + .run((context) -> { + FormattingConversionService conversionService = context + .getBean(FormattingConversionService.class); + Date date = new DateTime(1988, 6, 25, 20, 30).toDate(); + assertThat(conversionService.convert(date, String.class)) + .isEqualTo("25*06*1988"); + }); } @Test public void validatorWhenNoValidatorShouldUseDefault() { - load(null, new Class[] { ValidationAutoConfiguration.class }); - assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); - assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) - .isEmpty(); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(springValidatorBeans).containsExactly("webFluxValidator"); + this.contextRunner + .run(context -> { + assertThat(context).doesNotHaveBean(ValidatorFactory.class); + assertThat(context).doesNotHaveBean(javax.validation.Validator.class); + assertThat(context).getBeanNames(Validator.class).containsExactly("webFluxValidator"); + }); } @Test public void validatorWhenNoCustomizationShouldUseAutoConfigured() { - load(); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(jsrValidatorBeans).containsExactly("defaultValidator"); - assertThat(springValidatorBeans).containsExactly("defaultValidator", - "webFluxValidator"); - Validator validator = this.context.getBean("webFluxValidator", Validator.class); - assertThat(validator).isInstanceOf(ValidatorAdapter.class); - Object defaultValidator = this.context.getBean("defaultValidator"); - assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator); - // Primary Spring validator is the one used by WebFlux behind the scenes - assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator); + this.contextRunner.withConfiguration( + AutoConfigurations.of(ValidationAutoConfiguration.class)) + .run(context -> { + assertThat(context).getBeanNames(javax.validation.Validator.class) + .containsExactly("defaultValidator"); + assertThat(context).getBeanNames(Validator.class) + .containsExactlyInAnyOrder("defaultValidator", "webFluxValidator"); + Validator validator = context.getBean("webFluxValidator", Validator.class); + assertThat(validator).isInstanceOf(ValidatorAdapter.class); + Object defaultValidator = context.getBean("defaultValidator"); + assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator); + // Primary Spring validator is the one used by WebFlux behind the scenes + assertThat(context.getBean(Validator.class)).isEqualTo(defaultValidator); + }); } @Test public void validatorWithConfigurerShouldUseSpringValidator() { - load(ValidatorWebFluxConfigurer.class, - new Class[] { ValidationAutoConfiguration.class }); - assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); - assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) - .isEmpty(); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(springValidatorBeans).containsExactly("webFluxValidator"); - assertThat(this.context.getBean("webFluxValidator")).isSameAs( - this.context.getBean(ValidatorWebFluxConfigurer.class).validator); + this.contextRunner.withUserConfiguration(ValidatorWebFluxConfigurer.class) + .run(context -> { + assertThat(context).doesNotHaveBean(ValidatorFactory.class); + assertThat(context).doesNotHaveBean(javax.validation.Validator.class); + assertThat(context).getBeanNames(Validator.class) + .containsOnly("webFluxValidator"); + assertThat(context.getBean("webFluxValidator")) + .isSameAs(context.getBean(ValidatorWebFluxConfigurer.class).validator); + }); } @Test public void validatorWithConfigurerDoesNotExposeJsr303() { - load(ValidatorJsr303WebFluxConfigurer.class, - new Class[] { ValidationAutoConfiguration.class }); - assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); - assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) - .isEmpty(); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(springValidatorBeans).containsExactly("webFluxValidator"); - Validator validator = this.context.getBean("webFluxValidator", Validator.class); - assertThat(validator).isInstanceOf(ValidatorAdapter.class); - assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs( - this.context.getBean(ValidatorJsr303WebFluxConfigurer.class).validator); + this.contextRunner + .withUserConfiguration(ValidatorJsr303WebFluxConfigurer.class) + .run(context -> { + assertThat(context).doesNotHaveBean(ValidatorFactory.class); + assertThat(context).doesNotHaveBean(javax.validation.Validator.class); + assertThat(context).getBeanNames(Validator.class) + .containsOnly("webFluxValidator"); + Validator validator = context.getBean("webFluxValidator", + Validator.class); + assertThat(validator).isInstanceOf(ValidatorAdapter.class); + assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs( + context.getBean(ValidatorJsr303WebFluxConfigurer.class).validator); + }); } @Test public void validationCustomConfigurerTakesPrecedence() { - load(ValidatorWebFluxConfigurer.class); - assertThat(this.context.getBeansOfType(ValidatorFactory.class)).hasSize(1); - assertThat(this.context.getBeansOfType(javax.validation.Validator.class)) - .hasSize(1); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(springValidatorBeans).containsExactly("defaultValidator", - "webFluxValidator"); - assertThat(this.context.getBean("webFluxValidator")).isSameAs( - this.context.getBean(ValidatorWebFluxConfigurer.class).validator); - // Primary Spring validator is the auto-configured one as the WebFlux one has been - // customized via a WebFluxConfigurer - assertThat(this.context.getBean(Validator.class)) - .isEqualTo(this.context.getBean("defaultValidator")); + this.contextRunner + .withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class)) + .withUserConfiguration(ValidatorWebFluxConfigurer.class) + .run(context -> { + assertThat(context).getBeans(ValidatorFactory.class).hasSize(1); + assertThat(context).getBeans(javax.validation.Validator.class).hasSize(1); + assertThat(context).getBeanNames(Validator.class) + .containsExactlyInAnyOrder("defaultValidator", "webFluxValidator"); + assertThat(context.getBean("webFluxValidator")).isSameAs( + context.getBean(ValidatorWebFluxConfigurer.class).validator); + // Primary Spring validator is the auto-configured one as the WebFlux one has been + // customized via a WebFluxConfigurer + assertThat(context.getBean(Validator.class)) + .isEqualTo(context.getBean("defaultValidator")); + }); } @Test public void validatorWithCustomSpringValidatorIgnored() { - load(CustomSpringValidator.class); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(jsrValidatorBeans).containsExactly("defaultValidator"); - assertThat(springValidatorBeans).containsExactly("customValidator", - "defaultValidator", "webFluxValidator"); - Validator validator = this.context.getBean("webFluxValidator", Validator.class); - assertThat(validator).isInstanceOf(ValidatorAdapter.class); - Object defaultValidator = this.context.getBean("defaultValidator"); - assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator); - // Primary Spring validator is the one used by WebFlux behind the scenes - assertThat(this.context.getBean(Validator.class)).isEqualTo(defaultValidator); + this.contextRunner + .withConfiguration(AutoConfigurations.of(ValidationAutoConfiguration.class)) + .withUserConfiguration(CustomSpringValidator.class) + .run(context -> { + assertThat(context).getBeanNames(javax.validation.Validator.class) + .containsExactly("defaultValidator"); + assertThat(context).getBeanNames(Validator.class) + .containsExactlyInAnyOrder("customValidator", "defaultValidator", "webFluxValidator"); + Validator validator = context.getBean("webFluxValidator", Validator.class); + assertThat(validator).isInstanceOf(ValidatorAdapter.class); + Object defaultValidator = context.getBean("defaultValidator"); + assertThat(((ValidatorAdapter) validator).getTarget()).isSameAs(defaultValidator); + // Primary Spring validator is the one used by WebFlux behind the scenes + assertThat(context.getBean(Validator.class)).isEqualTo(defaultValidator); + }); } @Test public void validatorWithCustomJsr303ValidatorExposedAsSpringValidator() { - load(CustomJsr303Validator.class); - assertThat(this.context.getBeansOfType(ValidatorFactory.class)).isEmpty(); - String[] jsrValidatorBeans = this.context - .getBeanNamesForType(javax.validation.Validator.class); - String[] springValidatorBeans = this.context.getBeanNamesForType(Validator.class); - assertThat(jsrValidatorBeans).containsExactly("customValidator"); - assertThat(springValidatorBeans).containsExactly("webFluxValidator"); - Validator validator = this.context.getBean(Validator.class); - assertThat(validator).isInstanceOf(ValidatorAdapter.class); - Validator target = ((ValidatorAdapter) validator).getTarget(); - assertThat(new DirectFieldAccessor(target).getPropertyValue("targetValidator")) - .isSameAs(this.context.getBean("customValidator")); - } - - private void load(String... environment) { - load(null, environment); - } - - private void load(Class config, String... environment) { - load(config, null, environment); - } - - private void load(Class config, Class[] exclude, String... environment) { - this.context = new AnnotationConfigReactiveWebApplicationContext(); - TestPropertyValues.of(environment).applyTo(this.context); - List> configClasses = new ArrayList<>(); - if (config != null) { - configClasses.add(config); - } - configClasses.addAll(Arrays.asList(Config.class, - ValidationAutoConfiguration.class, BaseConfiguration.class)); - if (!ObjectUtils.isEmpty(exclude)) { - configClasses.removeAll(Arrays.asList(exclude)); - } - this.context.register(configClasses.toArray(new Class[configClasses.size()])); - this.context.refresh(); + this.contextRunner.withUserConfiguration(CustomJsr303Validator.class) + .run(context -> { + assertThat(context).doesNotHaveBean(ValidatorFactory.class); + assertThat(context).getBeanNames(javax.validation.Validator.class) + .containsExactly("customValidator"); + assertThat(context).getBeanNames(Validator.class) + .containsExactly("webFluxValidator"); + Validator validator = context.getBean(Validator.class); + assertThat(validator).isInstanceOf(ValidatorAdapter.class); + Validator target = ((ValidatorAdapter) validator).getTarget(); + assertThat(new DirectFieldAccessor(target).getPropertyValue("targetValidator")) + .isSameAs(context.getBean("customValidator")); + }); } @Configuration @@ -348,13 +367,11 @@ public class WebFluxAutoConfigurationTests { } @Configuration - @Import({ WebFluxAutoConfiguration.class }) - @EnableConfigurationProperties(WebFluxProperties.class) - protected static class BaseConfiguration { + protected static class Config { @Bean public MockReactiveWebServerFactory mockReactiveWebServerFactory() { - return new MockReactiveWebServerFactory(); + return mockReactiveWebServerFactory; } } diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 4fe7d392f5..94b868ea33 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -465,7 +465,8 @@ content into your application. Rather, pick only the properties that you need. spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain. spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved. - # SPRING WEB FLUX ({sc-spring-boot-autoconfigure}/web/reactive/WebFluxProperties.{sc-ext}[WebFluxProperties]) + # SPRING WEBFLUX ({sc-spring-boot-autoconfigure}/web/reactive/WebFluxProperties.{sc-ext}[WebFluxProperties]) + spring.webflux.date-format= # Date format to use. For instance, `dd/MM/yyyy`. spring.webflux.static-path-pattern=/** # Path pattern used for static resources. # SPRING WEB SERVICES ({sc-spring-boot-autoconfigure}/webservices/WebServicesProperties.{sc-ext}[WebServicesProperties])