Merge pull request #18772 from knittl

* gh-18772:
  Polish "Enable users to provide custom time and datetime formats"
  Enable users to provide custom time and datetime formats

Closes gh-18772
This commit is contained in:
Andy Wilkinson 2020-04-29 11:47:17 +01:00
commit 7c6c703ec7
9 changed files with 407 additions and 40 deletions

View File

@ -0,0 +1,96 @@
/*
* Copyright 2012-2020 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
*
* https://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.springframework.util.StringUtils;
/**
* {@link DateTimeFormatter Formatters} for dates, times, and date-times.
*
* @author Andy Wilkinson
* @since 2.3.0
*/
public class DateTimeFormatters {
private DateTimeFormatter dateFormatter;
private String datePattern;
private DateTimeFormatter timeFormatter;
private DateTimeFormatter dateTimeFormatter;
/**
* Configures the date format using the given {@code pattern}.
* @param pattern the pattern for formatting dates
* @return {@code this} for chained method invocation
*/
public DateTimeFormatters dateFormat(String pattern) {
this.dateFormatter = formatter(pattern);
this.datePattern = pattern;
return this;
}
/**
* Configures the time format using the given {@code pattern}.
* @param pattern the pattern for formatting times
* @return {@code this} for chained method invocation
*/
public DateTimeFormatters timeFormat(String pattern) {
this.timeFormatter = formatter(pattern);
return this;
}
/**
* Configures the date-time format using the given {@code pattern}.
* @param pattern the pattern for formatting date-times
* @return {@code this} for chained method invocation
*/
public DateTimeFormatters dateTimeFormat(String pattern) {
this.dateTimeFormatter = formatter(pattern);
return this;
}
DateTimeFormatter getDateFormatter() {
return this.dateFormatter;
}
String getDatePattern() {
return this.datePattern;
}
DateTimeFormatter getTimeFormatter() {
return this.timeFormatter;
}
DateTimeFormatter getDateTimeFormatter() {
return this.dateTimeFormatter;
}
boolean isCustomized() {
return this.dateFormatter != null || this.timeFormatter != null || this.dateTimeFormatter != null;
}
private static DateTimeFormatter formatter(String pattern) {
return StringUtils.hasText(pattern)
? DateTimeFormatter.ofPattern(pattern).withResolverStyle(ResolverStyle.SMART) : null;
}
}

View File

@ -17,7 +17,8 @@
package org.springframework.boot.autoconfigure.web.format;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.format.datetime.DateFormatterRegistrar;
@ -28,7 +29,6 @@ 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.StringUtils;
/**
* {@link org.springframework.format.support.FormattingConversionService} dedicated to web
@ -46,48 +46,67 @@ public class WebConversionService extends DefaultFormattingConversionService {
private static final boolean JSR_354_PRESENT = ClassUtils.isPresent("javax.money.MonetaryAmount",
WebConversionService.class.getClassLoader());
private final 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
* @deprecated since 2.3.0 in favor of
* {@link #WebConversionService(DateTimeFormatters)}
*/
@Deprecated
public WebConversionService(String dateFormat) {
this(new DateTimeFormatters().dateFormat(dateFormat));
}
/**
* 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.
* @param dateTimeFormatters the formatters to use for date, time, and date-time
* formatting
* @since 2.3.0
*/
public WebConversionService(DateTimeFormatters dateTimeFormatters) {
super(false);
this.dateFormat = StringUtils.hasText(dateFormat) ? dateFormat : null;
if (this.dateFormat != null) {
addFormatters();
if (dateTimeFormatters.isCustomized()) {
addFormatters(dateTimeFormatters);
}
else {
addDefaultFormatters(this);
}
}
private void addFormatters() {
private void addFormatters(DateTimeFormatters dateTimeFormatters) {
addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
if (JSR_354_PRESENT) {
addFormatter(new CurrencyUnitFormatter());
addFormatter(new MonetaryAmountFormatter());
addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
}
registerJsr310();
registerJavaDate();
registerJsr310(dateTimeFormatters);
registerJavaDate(dateTimeFormatters);
}
private void registerJsr310() {
private void registerJsr310(DateTimeFormatters dateTimeFormatters) {
DateTimeFormatterRegistrar dateTime = new DateTimeFormatterRegistrar();
if (this.dateFormat != null) {
dateTime.setDateFormatter(
DateTimeFormatter.ofPattern(this.dateFormat).withResolverStyle(ResolverStyle.SMART));
}
configure(dateTimeFormatters::getDateFormatter, dateTime::setDateFormatter);
configure(dateTimeFormatters::getTimeFormatter, dateTime::setTimeFormatter);
configure(dateTimeFormatters::getDateTimeFormatter, dateTime::setDateTimeFormatter);
dateTime.registerFormatters(this);
}
private void registerJavaDate() {
private void configure(Supplier<DateTimeFormatter> supplier, Consumer<DateTimeFormatter> consumer) {
DateTimeFormatter formatter = supplier.get();
if (formatter != null) {
consumer.accept(formatter);
}
}
private void registerJavaDate(DateTimeFormatters dateTimeFormatters) {
DateFormatterRegistrar dateFormatterRegistrar = new DateFormatterRegistrar();
if (this.dateFormat != null) {
DateFormatter dateFormatter = new DateFormatter(this.dateFormat);
String datePattern = dateTimeFormatters.getDatePattern();
if (datePattern != null) {
DateFormatter dateFormatter = new DateFormatter(datePattern);
dateFormatterRegistrar.setFormatter(dateFormatter);
}
dateFormatterRegistrar.registerFormatters(this);

View File

@ -35,7 +35,9 @@ import org.springframework.boot.autoconfigure.validation.ValidationAutoConfigura
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.DateTimeFormatters;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties.Format;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.web.codec.CodecCustomizer;
@ -203,7 +205,9 @@ public class WebFluxAutoConfiguration {
@Bean
@Override
public FormattingConversionService webFluxConversionService() {
WebConversionService conversionService = new WebConversionService(this.webFluxProperties.getDateFormat());
Format format = this.webFluxProperties.getFormat();
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
.dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.web.reactive;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.util.StringUtils;
/**
@ -33,10 +34,7 @@ public class WebFluxProperties {
*/
private String basePath;
/**
* Date format to use. For instance, `dd/MM/yyyy`.
*/
private String dateFormat;
private final Format format = new Format();
/**
* Path pattern used for static resources.
@ -64,12 +62,19 @@ public class WebFluxProperties {
return candidate;
}
@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.webflux.format.date")
public String getDateFormat() {
return this.dateFormat;
return this.format.getDate();
}
@Deprecated
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
this.format.setDate(dateFormat);
}
public Format getFormat() {
return this.format;
}
public String getStaticPathPattern() {
@ -80,4 +85,47 @@ public class WebFluxProperties {
this.staticPathPattern = staticPathPattern;
}
public static class Format {
/**
* Date format to use, for example `dd/MM/yyyy`.
*/
private String date;
/**
* Time format to use, for example `HH:mm:ss`.
*/
private String time;
/**
* Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
*/
private String dateTime;
public String getDate() {
return this.date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return this.time;
}
public void setTime(String time) {
this.time = time;
}
public String getDateTime() {
return this.dateTime;
}
public void setDateTime(String dateTime) {
this.dateTime = dateTime;
}
}
}

View File

@ -51,7 +51,9 @@ 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.DateTimeFormatters;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
@ -427,7 +429,9 @@ public class WebMvcAutoConfiguration {
@Bean
@Override
public FormattingConversionService mvcConversionService() {
WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
.dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}

View File

@ -56,10 +56,7 @@ public class WebMvcProperties {
*/
private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;
/**
* Date format to use. For instance, `dd/MM/yyyy`.
*/
private String dateFormat;
private final Format format = new Format();
/**
* Whether to dispatch TRACE requests to the FrameworkServlet doService method.
@ -139,12 +136,19 @@ public class WebMvcProperties {
this.localeResolver = localeResolver;
}
@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.mvc.format.date")
public String getDateFormat() {
return this.dateFormat;
return this.format.getDate();
}
@Deprecated
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
this.format.setDate(dateFormat);
}
public Format getFormat() {
return this.format;
}
public boolean isIgnoreDefaultModelOnRedirect() {
@ -447,6 +451,49 @@ public class WebMvcProperties {
}
public static class Format {
/**
* Date format to use, for example `dd/MM/yyyy`.
*/
private String date;
/**
* Time format to use, for example `HH:mm:ss`.
*/
private String time;
/**
* Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
*/
private String dateTime;
public String getDate() {
return this.date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return this.time;
}
public void setTime(String time) {
this.time = time;
}
public String getDateTime() {
return this.dateTime;
}
public void setDateTime(String dateTime) {
this.dateTime = dateTime;
}
}
public enum LocaleResolver {
/**

View File

@ -16,8 +16,13 @@
package org.springframework.boot.autoconfigure.web.format;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Date;
import org.junit.jupiter.api.Test;
@ -32,6 +37,14 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class WebConversionServiceTests {
@Test
void defaultDateFormat() {
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters());
LocalDate date = LocalDate.of(2020, 4, 26);
assertThat(conversionService.convert(date, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).format(date));
}
@Test
void customDateFormatWithJavaUtilDate() {
customDateFormat(Date.from(ZonedDateTime.of(2018, 1, 1, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant()));
@ -42,16 +55,50 @@ class WebConversionServiceTests {
customDateFormat(java.time.LocalDate.of(2018, 1, 1));
}
private void customDateFormat(Object input) {
WebConversionService conversionService = new WebConversionService("dd*MM*yyyy");
assertThat(conversionService.convert(input, String.class)).isEqualTo("01*01*2018");
@Test
void defaultTimeFormat() {
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters());
LocalTime time = LocalTime.of(12, 45, 23);
assertThat(conversionService.convert(time, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(time));
}
@Test
void customTimeFormat() {
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters().timeFormat("HH*mm*ss"));
LocalTime time = LocalTime.of(12, 45, 23);
assertThat(conversionService.convert(time, String.class)).isEqualTo("12*45*23");
}
@Test
void defaultDateTimeFormat() {
WebConversionService conversionService = new WebConversionService(new DateTimeFormatters());
LocalDateTime dateTime = LocalDateTime.of(2020, 4, 26, 12, 45, 23);
assertThat(conversionService.convert(dateTime, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(dateTime));
}
@Test
void customDateTimeFormat() {
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters().dateTimeFormat("dd*MM*yyyy HH*mm*ss"));
LocalDateTime dateTime = LocalDateTime.of(2020, 4, 26, 12, 45, 23);
assertThat(conversionService.convert(dateTime, String.class)).isEqualTo("26*04*2020 12*45*23");
}
@Test
void convertFromStringToDate() {
WebConversionService conversionService = new WebConversionService("yyyy-MM-dd");
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters().dateFormat("yyyy-MM-dd"));
java.time.LocalDate date = conversionService.convert("2018-01-01", java.time.LocalDate.class);
assertThat(date).isEqualTo(java.time.LocalDate.of(2018, 1, 1));
}
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");
}
}

View File

@ -16,8 +16,12 @@
package org.springframework.boot.autoconfigure.web.reactive;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -185,7 +189,7 @@ class WebFluxAutoConfigurationTests {
}
@Test
void noDateFormat() {
void defaultDateFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant());
@ -195,7 +199,7 @@ class WebFluxAutoConfigurationTests {
}
@Test
void overrideDateFormat() {
void customDateFormatWithDeprecatedProperty() {
this.contextRunner.withPropertyValues("spring.webflux.date-format:dd*MM*yyyy").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant());
@ -203,6 +207,53 @@ class WebFluxAutoConfigurationTests {
});
}
@Test
void customDateFormat() {
this.contextRunner.withPropertyValues("spring.webflux.format.date:dd*MM*yyyy").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant());
assertThat(conversionService.convert(date, String.class)).isEqualTo("25*06*1988");
});
}
@Test
void defaultTimeFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalTime time = LocalTime.of(11, 43, 10);
assertThat(conversionService.convert(time, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(time));
});
}
@Test
void customTimeFormat() {
this.contextRunner.withPropertyValues("spring.webflux.format.time=HH:mm:ss").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalTime time = LocalTime.of(11, 43, 10);
assertThat(conversionService.convert(time, String.class)).isEqualTo("11:43:10");
});
}
@Test
void defaultDateTimeFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalDateTime dateTime = LocalDateTime.of(2020, 4, 28, 11, 43, 10);
assertThat(conversionService.convert(dateTime, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(dateTime));
});
}
@Test
void customDateTimeTimeFormat() {
this.contextRunner.withPropertyValues("spring.webflux.format.date-time=yyyy-MM-dd HH:mm:ss").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalDateTime dateTime = LocalDateTime.of(2020, 4, 28, 11, 43, 10);
assertThat(conversionService.convert(dateTime, String.class)).isEqualTo("2020-04-28 11:43:10");
});
}
@Test
void validatorWhenNoValidatorShouldUseDefault() {
this.contextRunner.run((context) -> {

View File

@ -16,8 +16,12 @@
package org.springframework.boot.autoconfigure.web.servlet;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
@ -322,7 +326,7 @@ class WebMvcAutoConfigurationTests {
}
@Test
void noDateFormat() {
void defaultDateFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant());
@ -332,7 +336,16 @@ class WebMvcAutoConfigurationTests {
}
@Test
void overrideDateFormat() {
void customDateFormat() {
this.contextRunner.withPropertyValues("spring.mvc.format.date:dd*MM*yyyy").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant());
assertThat(conversionService.convert(date, String.class)).isEqualTo("25*06*1988");
});
}
@Test
void customDateFormatWithDeprecatedProperty() {
this.contextRunner.withPropertyValues("spring.mvc.date-format:dd*MM*yyyy").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
Date date = Date.from(ZonedDateTime.of(1988, 6, 25, 20, 30, 0, 0, ZoneId.systemDefault()).toInstant());
@ -340,6 +353,44 @@ class WebMvcAutoConfigurationTests {
});
}
@Test
void defaultTimeFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalTime time = LocalTime.of(11, 43, 10);
assertThat(conversionService.convert(time, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(time));
});
}
@Test
void customTimeFormat() {
this.contextRunner.withPropertyValues("spring.mvc.format.time=HH:mm:ss").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalTime time = LocalTime.of(11, 43, 10);
assertThat(conversionService.convert(time, String.class)).isEqualTo("11:43:10");
});
}
@Test
void defaultDateTimeFormat() {
this.contextRunner.run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalDateTime dateTime = LocalDateTime.of(2020, 4, 28, 11, 43, 10);
assertThat(conversionService.convert(dateTime, String.class))
.isEqualTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT).format(dateTime));
});
}
@Test
void customDateTimeTimeFormat() {
this.contextRunner.withPropertyValues("spring.mvc.format.date-time=yyyy-MM-dd HH:mm:ss").run((context) -> {
FormattingConversionService conversionService = context.getBean(FormattingConversionService.class);
LocalDateTime dateTime = LocalDateTime.of(2020, 4, 28, 11, 43, 10);
assertThat(conversionService.convert(dateTime, String.class)).isEqualTo("2020-04-28 11:43:10");
});
}
@Test
void noMessageCodesResolver() {
this.contextRunner.run(