diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java index 06826ddb9e0..c33694a5c11 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java @@ -31,6 +31,7 @@ import org.springframework.format.number.money.Jsr354NumberFormatAnnotationForma import org.springframework.format.number.money.MonetaryAmountFormatter; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; /** * {@link org.springframework.format.support.FormattingConversionService} dedicated to web @@ -53,7 +54,23 @@ public class WebConversionService extends DefaultFormattingConversionService { * @since 2.3.0 */ public WebConversionService(DateTimeFormatters dateTimeFormatters) { - super(false); + this(null, dateTimeFormatters); + } + + /** + * Create a new WebConversionService that configures formatters with the provided + * date, time, and date-time formats, or registers the default if no custom format is + * provided. The given {@code embeddedValueResolver} is used to resolve embedded + * values such as property placeholders in + * {@link org.springframework.format.annotation.DateTimeFormat#pattern()} patterns. + * @param embeddedValueResolver the embedded value resolver to use, or {@code null} + * @param dateTimeFormatters the formatters to use for date, time, and date-time + * formatting + * @since 4.1.0 + */ + public WebConversionService(@Nullable StringValueResolver embeddedValueResolver, + DateTimeFormatters dateTimeFormatters) { + super(embeddedValueResolver, false); if (dateTimeFormatters.isCustomized()) { addFormatters(dateTimeFormatters); } diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java index 4b17ace39f3..5e49ad9da92 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java @@ -29,8 +29,12 @@ import java.time.format.FormatStyle; import java.util.Calendar; import java.util.Date; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.annotation.DateTimeFormat; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -178,10 +182,27 @@ class WebConversionServiceTests { assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isOne(); } + @Test + void embeddedValueResolverIsAppliedToAnnotationPattern() throws Exception { + WebConversionService conversionService = new WebConversionService( + (value) -> value.replace("${my.date.format}", "yyyy-MM-dd"), new DateTimeFormatters()); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + } + private void customDateFormat(Object input) { WebConversionService conversionService = new WebConversionService( new DateTimeFormatters().dateFormat("dd*MM*yyyy")); assertThat(conversionService.convert(input, String.class)).isEqualTo("01*01*2018"); } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + } diff --git a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java index d19f3a49b06..b39129feea4 100644 --- a/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java +++ b/module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfiguration.java @@ -59,6 +59,7 @@ import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversi import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Format; import org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter; import org.springframework.context.ApplicationContext; +import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -73,6 +74,7 @@ import org.springframework.http.CacheControl; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; import org.springframework.validation.Validator; import org.springframework.web.accept.ApiVersionParser; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; @@ -312,7 +314,8 @@ public final class WebFluxAutoConfiguration { @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ WebProperties.class, ServerProperties.class }) @ImportRuntimeHints(WebFluxValidatorRuntimeHints.class) - static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration { + static class EnableWebFluxConfiguration extends DelegatingWebFluxConfiguration + implements EmbeddedValueResolverAware { private final WebFluxProperties webFluxProperties; @@ -322,6 +325,8 @@ public final class WebFluxAutoConfiguration { private final @Nullable WebFluxRegistrations webFluxRegistrations; + private @Nullable StringValueResolver embeddedValueResolver; + EnableWebFluxConfiguration(WebFluxProperties webFluxProperties, WebProperties webProperties, ServerProperties serverProperties, ObjectProvider webFluxRegistrations) { this.webFluxProperties = webFluxProperties; @@ -334,7 +339,7 @@ public final class WebFluxAutoConfiguration { @Override public FormattingConversionService webFluxConversionService() { Format format = this.webFluxProperties.getFormat(); - WebConversionService conversionService = new WebConversionService( + WebConversionService conversionService = new WebConversionService(this.embeddedValueResolver, new DateTimeFormatters().dateFormat(format.getDate()) .timeFormat(format.getTime()) .dateTimeFormat(format.getDateTime())); @@ -342,6 +347,11 @@ public final class WebFluxAutoConfiguration { return conversionService; } + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + @Bean @Override public Validator webFluxValidator() { diff --git a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java index 43084d0f1c4..6eb10ac2724 100644 --- a/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java +++ b/module/spring-boot-webflux/src/test/java/org/springframework/boot/webflux/autoconfigure/WebFluxAutoConfigurationTests.java @@ -74,10 +74,12 @@ import org.springframework.context.i18n.LocaleContext; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.ClassPathResource; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.Parser; import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.ResponseCookie; @@ -340,6 +342,17 @@ class WebFluxAutoConfigurationTests { }); } + @Test + void embeddedValueResolverIsAppliedToConversionService() throws Exception { + this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + }); + } + @Test void validatorWhenNoValidatorShouldUseDefault() { this.contextRunner.run((context) -> { @@ -1333,4 +1346,11 @@ class WebFluxAutoConfigurationTests { } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + } diff --git a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java index 8697b65a3a6..cd00acd2224 100644 --- a/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java +++ b/module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfiguration.java @@ -67,6 +67,7 @@ import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion.Use; import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format; import org.springframework.context.ApplicationContext; +import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -87,6 +88,7 @@ import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.StringValueResolver; import org.springframework.validation.DefaultMessageCodesResolver; import org.springframework.validation.MessageCodesResolver; import org.springframework.validation.Validator; @@ -438,7 +440,8 @@ public final class WebMvcAutoConfiguration { @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(WebProperties.class) @ImportRuntimeHints(MvcValidatorRuntimeHints.class) - static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { + static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration + implements ResourceLoaderAware, EmbeddedValueResolverAware { private final Resources resourceProperties; @@ -450,6 +453,8 @@ public final class WebMvcAutoConfiguration { private final @Nullable WebMvcRegistrations mvcRegistrations; + private @Nullable StringValueResolver embeddedValueResolver; + @SuppressWarnings("NullAway.Init") private ResourceLoader resourceLoader; @@ -566,7 +571,7 @@ public final class WebMvcAutoConfiguration { @Override public FormattingConversionService mvcConversionService() { Format format = this.mvcProperties.getFormat(); - WebConversionService conversionService = new WebConversionService( + WebConversionService conversionService = new WebConversionService(this.embeddedValueResolver, new DateTimeFormatters().dateFormat(format.getDate()) .timeFormat(format.getTime()) .dateTimeFormat(format.getDateTime())); @@ -574,6 +579,11 @@ public final class WebMvcAutoConfiguration { return conversionService; } + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + @Bean @Override public Validator mvcValidator() { diff --git a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java index 38c329ca428..5387a811399 100644 --- a/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java +++ b/module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/WebMvcAutoConfigurationTests.java @@ -78,11 +78,13 @@ import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.format.Parser; import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; @@ -509,6 +511,17 @@ class WebMvcAutoConfigurationTests { }); } + @Test + void embeddedValueResolverIsAppliedToConversionService() throws Exception { + this.contextRunner.withPropertyValues("my.date.format=yyyy-MM-dd").run((context) -> { + FormattingConversionService conversionService = context.getBean(FormattingConversionService.class); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.of("UTC")).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + }); + } + @Test void noMessageCodesResolver() { this.contextRunner.run( @@ -1756,4 +1769,11 @@ class WebMvcAutoConfigurationTests { } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + }