Skip to content

Commit 2fa0539

Browse files
committed
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
1 parent ec26488 commit 2fa0539

File tree

7 files changed

+410
-192
lines changed

7 files changed

+410
-192
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.format;
18+
19+
import java.time.format.DateTimeFormatter;
20+
import java.time.format.ResolverStyle;
21+
22+
import org.joda.time.format.DateTimeFormatterBuilder;
23+
24+
import org.springframework.format.datetime.DateFormatter;
25+
import org.springframework.format.datetime.DateFormatterRegistrar;
26+
import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar;
27+
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
28+
import org.springframework.format.number.NumberFormatAnnotationFormatterFactory;
29+
import org.springframework.format.number.money.CurrencyUnitFormatter;
30+
import org.springframework.format.number.money.Jsr354NumberFormatAnnotationFormatterFactory;
31+
import org.springframework.format.number.money.MonetaryAmountFormatter;
32+
import org.springframework.format.support.DefaultFormattingConversionService;
33+
import org.springframework.util.ClassUtils;
34+
import org.springframework.util.StringUtils;
35+
36+
/**
37+
* {@link org.springframework.format.support.FormattingConversionService} dedicated
38+
* to web applications for formatting and converting values to/from the web.
39+
*
40+
* <p>This service replaces the default implementations provided by
41+
* {@link org.springframework.web.servlet.config.annotation.EnableWebMvc}
42+
* and {@link org.springframework.web.reactive.config.EnableWebFlux}.
43+
*
44+
* @author Brian Clozel
45+
* @since 2.0.0
46+
*/
47+
public class WebConversionService extends DefaultFormattingConversionService {
48+
49+
private static final boolean jsr354Present = ClassUtils
50+
.isPresent("javax.money.MonetaryAmount", WebConversionService.class.getClassLoader());
51+
52+
private static final boolean jodaTimePresent = ClassUtils
53+
.isPresent("org.joda.time.LocalDate", WebConversionService.class.getClassLoader());
54+
55+
private String dateFormat;
56+
57+
/**
58+
* Create a new WebConversionService that configures formatters with the provided date format,
59+
* or register the default ones if no custom format is provided.
60+
* @param dateFormat the custom date format to use for date conversions
61+
*/
62+
public WebConversionService(String dateFormat) {
63+
super(false);
64+
if (StringUtils.hasText(dateFormat)) {
65+
this.dateFormat = dateFormat;
66+
addFormatters();
67+
}
68+
else {
69+
addDefaultFormatters(this);
70+
}
71+
}
72+
73+
74+
private void addFormatters() {
75+
addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
76+
if (jsr354Present) {
77+
addFormatter(new CurrencyUnitFormatter());
78+
addFormatter(new MonetaryAmountFormatter());
79+
addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
80+
}
81+
registerJsr310();
82+
if (jodaTimePresent) {
83+
registerJodaTime();
84+
}
85+
registerJavaDate();
86+
}
87+
88+
private void registerJsr310() {
89+
DateTimeFormatterRegistrar dateTime = new DateTimeFormatterRegistrar();
90+
if (this.dateFormat != null) {
91+
DateTimeFormatter dateTimeFormatter = DateTimeFormatter
92+
.ofPattern(this.dateFormat)
93+
.withResolverStyle(ResolverStyle.STRICT);
94+
dateTime.setDateFormatter(dateTimeFormatter);
95+
}
96+
dateTime.registerFormatters(this);
97+
}
98+
99+
private void registerJodaTime() {
100+
JodaTimeFormatterRegistrar jodaTime = new JodaTimeFormatterRegistrar();
101+
if (this.dateFormat != null) {
102+
org.joda.time.format.DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
103+
.appendPattern(this.dateFormat)
104+
.toFormatter();
105+
jodaTime.setDateFormatter(dateTimeFormatter);
106+
}
107+
jodaTime.registerFormatters(this);
108+
}
109+
110+
private void registerJavaDate() {
111+
DateFormatterRegistrar dateFormatterRegistrar = new DateFormatterRegistrar();
112+
if (this.dateFormat != null) {
113+
DateFormatter dateFormatter = new DateFormatter(this.dateFormat);
114+
dateFormatterRegistrar.setFormatter(dateFormatter);
115+
}
116+
dateFormatterRegistrar.registerFormatters(this);
117+
}
118+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxAutoConfiguration.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@
3434
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3535
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
3636
import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
37+
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
3738
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
3839
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
3940
import org.springframework.boot.autoconfigure.web.ResourceProperties;
41+
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
4042
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4143
import org.springframework.boot.web.codec.CodecCustomizer;
4244
import org.springframework.context.annotation.Bean;
@@ -48,6 +50,7 @@
4850
import org.springframework.core.convert.converter.GenericConverter;
4951
import org.springframework.format.Formatter;
5052
import org.springframework.format.FormatterRegistry;
53+
import org.springframework.format.support.FormattingConversionService;
5154
import org.springframework.http.CacheControl;
5255
import org.springframework.http.codec.ServerCodecConfigurer;
5356
import org.springframework.util.ClassUtils;
@@ -84,7 +87,7 @@
8487
@ConditionalOnClass(WebFluxConfigurer.class)
8588
@ConditionalOnMissingBean({ WebFluxConfigurationSupport.class })
8689
@AutoConfigureAfter({ ReactiveWebServerAutoConfiguration.class,
87-
CodecsAutoConfiguration.class })
90+
CodecsAutoConfiguration.class, ValidationAutoConfiguration.class })
8891
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
8992
public class WebFluxAutoConfiguration {
9093

@@ -211,8 +214,22 @@ private void customizeResourceHandlerRegistration(
211214
public static class EnableWebFluxConfiguration
212215
extends DelegatingWebFluxConfiguration {
213216

217+
private final WebFluxProperties webFluxProperties;
218+
219+
public EnableWebFluxConfiguration(WebFluxProperties webFluxProperties) {
220+
this.webFluxProperties = webFluxProperties;
221+
}
222+
223+
@Bean
214224
@Override
225+
public FormattingConversionService webFluxConversionService() {
226+
WebConversionService conversionService = new WebConversionService(this.webFluxProperties.getDateFormat());
227+
addFormatters(conversionService);
228+
return conversionService;
229+
}
230+
215231
@Bean
232+
@Override
216233
public Validator webFluxValidator() {
217234
if (!ClassUtils.isPresent("javax.validation.Validator",
218235
getClass().getClassLoader())) {

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/reactive/WebFluxProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,24 @@
2727
@ConfigurationProperties(prefix = "spring.webflux")
2828
public class WebFluxProperties {
2929

30+
/**
31+
* Date format to use. For instance, "dd/MM/yyyy".
32+
*/
33+
private String dateFormat;
34+
3035
/**
3136
* Path pattern used for static resources.
3237
*/
3338
private String staticPathPattern = "/**";
3439

40+
public String getDateFormat() {
41+
return this.dateFormat;
42+
}
43+
44+
public void setDateFormat(String dateFormat) {
45+
this.dateFormat = dateFormat;
46+
}
47+
3548
public String getStaticPathPattern() {
3649
return this.staticPathPattern;
3750
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration.java

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.Arrays;
2222
import java.util.Collection;
2323
import java.util.Collections;
24-
import java.util.Date;
2524
import java.util.List;
2625
import java.util.ListIterator;
2726
import java.util.Map;
@@ -54,6 +53,7 @@
5453
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
5554
import org.springframework.boot.autoconfigure.web.ResourceProperties;
5655
import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy;
56+
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
5757
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5858
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
5959
import org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter;
@@ -73,7 +73,7 @@
7373
import org.springframework.core.io.ResourceLoader;
7474
import org.springframework.format.Formatter;
7575
import org.springframework.format.FormatterRegistry;
76-
import org.springframework.format.datetime.DateFormatter;
76+
import org.springframework.format.support.FormattingConversionService;
7777
import org.springframework.http.CacheControl;
7878
import org.springframework.http.MediaType;
7979
import org.springframework.http.converter.HttpMessageConverter;
@@ -266,12 +266,6 @@ public LocaleResolver localeResolver() {
266266
return localeResolver;
267267
}
268268

269-
@Bean
270-
@ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")
271-
public Formatter<Date> dateFormatter() {
272-
return new DateFormatter(this.mvcProperties.getDateFormat());
273-
}
274-
275269
@Override
276270
public MessageCodesResolver getMessageCodesResolver() {
277271
if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
@@ -478,6 +472,14 @@ public RequestMappingHandlerMapping requestMappingHandlerMapping() {
478472
return super.requestMappingHandlerMapping();
479473
}
480474

475+
@Bean
476+
@Override
477+
public FormattingConversionService mvcConversionService() {
478+
WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
479+
addFormatters(conversionService);
480+
return conversionService;
481+
}
482+
481483
@Bean
482484
@Override
483485
public Validator mvcValidator() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2012-2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.web.format;
18+
19+
import java.util.Date;
20+
21+
import org.joda.time.DateTime;
22+
import org.joda.time.LocalDate;
23+
import org.junit.Test;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Tests for {@link WebConversionService}.
29+
*
30+
* @author Brian Clozel
31+
*/
32+
public class WebConversionServiceTests {
33+
34+
@Test
35+
public void customDateFormat() {
36+
WebConversionService conversionService = new WebConversionService("dd*MM*yyyy");
37+
38+
Date date = new DateTime(2018, 1, 1, 20, 30).toDate();
39+
assertThat(conversionService.convert(date, String.class))
40+
.isEqualTo("01*01*2018");
41+
42+
LocalDate jodaDate = LocalDate.fromDateFields(date);
43+
assertThat(conversionService.convert(jodaDate, String.class))
44+
.isEqualTo("01*01*2018");
45+
46+
java.time.LocalDate localDate = java.time.LocalDate.of(2018, 1, 1);
47+
assertThat(conversionService.convert(localDate, String.class))
48+
.isEqualTo("01*01*2018");
49+
}
50+
}

0 commit comments

Comments
 (0)