Commit 2fa0539e authored by Brian Clozel's avatar Brian Clozel

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
parent ec26488f
/*
* 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.
*
* <p>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);
}
}
...@@ -34,9 +34,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; ...@@ -34,9 +34,11 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration; 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.validation.ValidatorAdapter;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.ResourceProperties; 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.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.boot.web.codec.CodecCustomizer;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
...@@ -48,6 +50,7 @@ import org.springframework.core.convert.converter.Converter; ...@@ -48,6 +50,7 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.CacheControl; import org.springframework.http.CacheControl;
import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
...@@ -84,7 +87,7 @@ import org.springframework.web.reactive.result.view.ViewResolver; ...@@ -84,7 +87,7 @@ import org.springframework.web.reactive.result.view.ViewResolver;
@ConditionalOnClass(WebFluxConfigurer.class) @ConditionalOnClass(WebFluxConfigurer.class)
@ConditionalOnMissingBean({ WebFluxConfigurationSupport.class }) @ConditionalOnMissingBean({ WebFluxConfigurationSupport.class })
@AutoConfigureAfter({ ReactiveWebServerAutoConfiguration.class, @AutoConfigureAfter({ ReactiveWebServerAutoConfiguration.class,
CodecsAutoConfiguration.class }) CodecsAutoConfiguration.class, ValidationAutoConfiguration.class })
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
public class WebFluxAutoConfiguration { public class WebFluxAutoConfiguration {
...@@ -211,8 +214,22 @@ public class WebFluxAutoConfiguration { ...@@ -211,8 +214,22 @@ public class WebFluxAutoConfiguration {
public static class EnableWebFluxConfiguration public static class EnableWebFluxConfiguration
extends DelegatingWebFluxConfiguration { extends DelegatingWebFluxConfiguration {
private final WebFluxProperties webFluxProperties;
public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties) {
this.webFluxProperties = webFluxProperties;
}
@Bean
@Override @Override
public FormattingConversionService webFluxConversionService() {
WebConversionService conversionService = new WebConversionService(this.webFluxProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
@Bean @Bean
@Override
public Validator webFluxValidator() { public Validator webFluxValidator() {
if (!ClassUtils.isPresent("javax.validation.Validator", if (!ClassUtils.isPresent("javax.validation.Validator",
getClass().getClassLoader())) { getClass().getClassLoader())) {
......
...@@ -27,11 +27,24 @@ import org.springframework.boot.context.properties.ConfigurationProperties; ...@@ -27,11 +27,24 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "spring.webflux") @ConfigurationProperties(prefix = "spring.webflux")
public class WebFluxProperties { public class WebFluxProperties {
/**
* Date format to use. For instance, "dd/MM/yyyy".
*/
private String dateFormat;
/** /**
* Path pattern used for static resources. * Path pattern used for static resources.
*/ */
private String staticPathPattern = "/**"; private String staticPathPattern = "/**";
public String getDateFormat() {
return this.dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public String getStaticPathPattern() { public String getStaticPathPattern() {
return this.staticPathPattern; return this.staticPathPattern;
} }
......
...@@ -21,7 +21,6 @@ import java.util.ArrayList; ...@@ -21,7 +21,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; import java.util.Map;
...@@ -54,6 +53,7 @@ import org.springframework.boot.autoconfigure.validation.ValidatorAdapter; ...@@ -54,6 +53,7 @@ import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain; import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.ResourceProperties; import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy; 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.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter; import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter; import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter;
...@@ -73,7 +73,7 @@ import org.springframework.core.io.Resource; ...@@ -73,7 +73,7 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry; 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.CacheControl;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
...@@ -266,12 +266,6 @@ public class WebMvcAutoConfiguration { ...@@ -266,12 +266,6 @@ public class WebMvcAutoConfiguration {
return localeResolver; return localeResolver;
} }
@Bean
@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
public Formatter<Date> dateFormatter() {
return new DateFormatter(this.mvcProperties.getDateFormat());
}
@Override @Override
public MessageCodesResolver getMessageCodesResolver() { public MessageCodesResolver getMessageCodesResolver() {
if (this.mvcProperties.getMessageCodesResolverFormat() != null) { if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
...@@ -478,6 +472,14 @@ public class WebMvcAutoConfiguration { ...@@ -478,6 +472,14 @@ public class WebMvcAutoConfiguration {
return super.requestMappingHandlerMapping(); return super.requestMappingHandlerMapping();
} }
@Bean
@Override
public FormattingConversionService mvcConversionService() {
WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
@Bean @Bean
@Override @Override
public Validator mvcValidator() { public Validator mvcValidator() {
......
/*
* 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");
}
}
...@@ -465,7 +465,8 @@ content into your application. Rather, pick only the properties that you need. ...@@ -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.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.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.webflux.static-path-pattern=/** # Path pattern used for static resources.
# SPRING WEB SERVICES ({sc-spring-boot-autoconfigure}/webservices/WebServicesProperties.{sc-ext}[WebServicesProperties]) # SPRING WEB SERVICES ({sc-spring-boot-autoconfigure}/webservices/WebServicesProperties.{sc-ext}[WebServicesProperties])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment