From b2bcb0f93ad04a6ddc729a487f9346defa6c39d6 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 2 Mar 2021 20:35:05 +0100 Subject: [PATCH] Support multiple parsing patterns in @DateTimeFormat Prior to this commit, @DateTimeFormat only supported a single format for parsing date time values via the style, iso, and pattern attributes. This commit introduces a new fallbackPatterns attribute that can be used to configure multiple fallback patterns for parsing date time values. This allows applications to accept multiple input formats for date time values. For example, if you wish to use the ISO date format for parsing and printing but allow for lenient parsing of user input for various additional date formats, you could annotate a field or method parameter with configuration similar to the following. @DateTimeFormat( iso = ISO.DATE, fallbackPatterns = { "M/d/yy", "dd.MM.yyyy" } ) Closes gh-20292 --- .../format/annotation/DateTimeFormat.java | 87 +++-- .../format/datetime/DateFormatter.java | 91 ++++- ...eTimeFormatAnnotationFormatterFactory.java | 22 +- .../datetime/standard/DateTimeContext.java | 8 +- .../standard/DateTimeContextHolder.java | 5 +- .../standard/DateTimeFormatterFactory.java | 10 +- .../standard/DateTimeFormatterUtils.java | 40 ++ ...eTimeFormatAnnotationFormatterFactory.java | 16 +- .../standard/TemporalAccessorParser.java | 51 ++- .../format/datetime/DateFormattingTests.java | 243 +++++++++--- .../standard/DateTimeFormattingTests.java | 362 ++++++++++++------ 11 files changed, 708 insertions(+), 227 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java index 0b3b351405..54168efb5f 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java @@ -26,17 +26,20 @@ import java.lang.annotation.Target; * Declares that a field or method parameter should be formatted as a date or time. * *

Supports formatting by style pattern, ISO date time pattern, or custom format pattern string. - * Can be applied to {@code java.util.Date}, {@code java.util.Calendar}, {@code Long} (for - * millisecond timestamps) as well as JSR-310 java.time and Joda-Time value types. + * Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for + * millisecond timestamps) as well as JSR-310 {@code java.time} and Joda-Time value types. * - *

For style-based formatting, set the {@link #style} attribute to be the style pattern code. + *

For style-based formatting, set the {@link #style} attribute to the desired style pattern code. * The first character of the code is the date style, and the second character is the time style. * Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full. - * A date or time may be omitted by specifying the style character '-'. + * The date or time may be omitted by specifying the style character '-' — for example, + * 'M-' specifies a medium format for the date with no time. * - *

For ISO-based formatting, set the {@link #iso} attribute to be the desired {@link ISO} format, - * such as {@link ISO#DATE}. For custom formatting, set the {@link #pattern} attribute to be the - * DateTime pattern, such as {@code "yyyy/MM/dd hh:mm:ss a"}. + *

For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format, + * such as {@link ISO#DATE}. + * + *

For custom formatting, set the {@link #pattern} attribute to a date time pattern, such as + * {@code "yyyy/MM/dd hh:mm:ss a"}. * *

Each attribute is mutually exclusive, so only set one attribute per annotation instance * (the one most convenient for your formatting needs). @@ -48,8 +51,19 @@ import java.lang.annotation.Target; * with a style code of 'SS' (short date, short time). * * + *

Time Zones

+ *

Whenever the {@link #style} or {@link #pattern} attribute is used, the + * {@linkplain java.util.TimeZone#getDefault() default time zone} of the JVM will + * be used when formatting {@link java.util.Date} values. Whenever the {@link #iso} + * attribute is used when formatting {@link java.util.Date} values, {@code UTC} + * will be used as the time zone. The same time zone will be applied to any + * {@linkplain #fallbackPatterns fallback patterns} as well. In order to enforce + * consistent use of {@code UTC} as the time zone, you can bootstrap the JVM with + * {@code -Duser.timezone=UTC}. + * * @author Keith Donald * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 * @see java.time.format.DateTimeFormatter * @see org.joda.time.format.DateTimeFormat @@ -60,34 +74,59 @@ import java.lang.annotation.Target; public @interface DateTimeFormat { /** - * The style pattern to use to format the field. - *

Defaults to 'SS' for short date time. Set this attribute when you wish to format - * your field in accordance with a common style other than the default style. + * The style pattern to use to format the field or method parameter. + *

Defaults to 'SS' for short date, short time. Set this attribute when you + * wish to format your field or method parameter in accordance with a common + * style other than the default style. + * @see #fallbackPatterns */ String style() default "SS"; /** - * The ISO pattern to use to format the field. - *

The possible ISO patterns are defined in the {@link ISO} enum. + * The ISO pattern to use to format the field or method parameter. + *

Supported ISO patterns are defined in the {@link ISO} enum. *

Defaults to {@link ISO#NONE}, indicating this attribute should be ignored. - * Set this attribute when you wish to format your field in accordance with an ISO format. + * Set this attribute when you wish to format your field or method parameter + * in accordance with an ISO format. + * @see #fallbackPatterns */ ISO iso() default ISO.NONE; /** - * The custom pattern to use to format the field. - *

Defaults to empty String, indicating no custom pattern String has been specified. - * Set this attribute when you wish to format your field in accordance with a custom - * date time pattern not represented by a style or ISO format. + * The custom pattern to use to format the field or method parameter. + *

Defaults to empty String, indicating no custom pattern String has been + * specified. Set this attribute when you wish to format your field or method + * parameter in accordance with a custom date time pattern not represented by + * a style or ISO format. *

Note: This pattern follows the original {@link java.text.SimpleDateFormat} style, * as also supported by Joda-Time, with strict parsing semantics towards overflows * (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy' * characters indicate a year in the traditional style, not a "year-of-era" as in the * {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu' - * when going through that {@code DateTimeFormatter} with strict resolution mode). + * when going through a {@code DateTimeFormatter} with strict resolution mode). + * @see #fallbackPatterns */ String pattern() default ""; + /** + * The set of custom patterns to use as a fallback in case parsing fails for + * the primary {@link #pattern}, {@link #iso}, or {@link #style} attribute. + *

For example, if you wish to use the ISO date format for parsing and + * printing but allow for lenient parsing of user input for various date + * formats, you could configure something similar to the following. + *

+	 * {@literal @}DateTimeFormat(iso = ISO.DATE, fallbackPatterns = { "M/d/yy", "dd.MM.yyyy" })
+	 * 
+ *

Fallback patterns are only used for parsing. They are not used for + * printing the value as a String. The primary {@link #pattern}, {@link #iso}, + * or {@link #style} attribute is always used for printing. For details on + * which time zone is used for fallback patterns, see the + * {@linkplain DateTimeFormat class-level documentation}. + *

Fallback patterns are not supported for Joda-Time value types. + * @since 5.3.5 + */ + String[] fallbackPatterns() default {}; + /** * Common ISO date time format patterns. @@ -95,20 +134,20 @@ public @interface DateTimeFormat { enum ISO { /** - * The most common ISO Date Format {@code yyyy-MM-dd}, - * e.g. "2000-10-31". + * The most common ISO Date Format {@code yyyy-MM-dd} — for example, + * "2000-10-31". */ DATE, /** - * The most common ISO Time Format {@code HH:mm:ss.SSSXXX}, - * e.g. "01:30:00.000-05:00". + * The most common ISO Time Format {@code HH:mm:ss.SSSXXX} — for example, + * "01:30:00.000-05:00". */ TIME, /** - * The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}, - * e.g. "2000-10-31T01:30:00.000-05:00". + * The most common ISO Date Time Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX} + * — for example, "2000-10-31T01:30:00.000-05:00". */ DATE_TIME, diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index bef0684837..d69e9c15e5 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2021 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. @@ -27,17 +27,21 @@ import java.util.Map; import java.util.TimeZone; import org.springframework.format.Formatter; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * A formatter for {@link java.util.Date} types. - * Allows the configuration of an explicit date pattern and locale. + *

Supports the configuration of an explicit date time pattern, timezone, + * locale, and fallback date time patterns for lenient parsing. * * @author Keith Donald * @author Juergen Hoeller * @author Phillip Webb + * @author Sam Brannen * @since 3.0 * @see SimpleDateFormat */ @@ -56,9 +60,15 @@ public class DateFormatter implements Formatter { } + @Nullable + private Object source; + @Nullable private String pattern; + @Nullable + private String[] fallbackPatterns; + private int style = DateFormat.DEFAULT; @Nullable @@ -74,19 +84,33 @@ public class DateFormatter implements Formatter { /** - * Create a new default DateFormatter. + * Create a new default {@code DateFormatter}. */ public DateFormatter() { } /** - * Create a new DateFormatter for the given date pattern. + * Create a new {@code DateFormatter} for the given date time pattern. */ public DateFormatter(String pattern) { this.pattern = pattern; } + /** + * Set the source of the configuration for this {@code DateFormatter} — + * for example, an instance of the {@link DateTimeFormat @DateTimeFormat} + * annotation if such an annotation was used to configure this {@code DateFormatter}. + *

The supplied source object will only be used for descriptive purposes + * by invoking its {@code toString()} method — for example, when + * generating an exception message to provide further context. + * @param source the source of the configuration + * @since 5.3.5 + */ + public void setSource(Object source) { + this.source = source; + } + /** * Set the pattern to use to format date values. *

If not specified, DateFormat's default style will be used. @@ -96,7 +120,19 @@ public class DateFormatter implements Formatter { } /** - * Set the ISO format used for this date. + * Set additional patterns to use as a fallback in case parsing fails for the + * configured {@linkplain #setPattern pattern}, {@linkplain #setIso ISO format}, + * {@linkplain #setStyle style}, or {@linkplain #setStylePattern style pattern}. + * @param fallbackPatterns the fallback parsing patterns + * @since 5.3.5 + * @see DateTimeFormat#fallbackPatterns() + */ + public void setFallbackPatterns(String... fallbackPatterns) { + this.fallbackPatterns = fallbackPatterns; + } + + /** + * Set the ISO format to use to format date values. * @param iso the {@link ISO} format * @since 3.2 */ @@ -105,7 +141,7 @@ public class DateFormatter implements Formatter { } /** - * Set the style to use to format date values. + * Set the {@link DateFormat} style to use to format date values. *

If not specified, DateFormat's default style will be used. * @see DateFormat#DEFAULT * @see DateFormat#SHORT @@ -118,8 +154,10 @@ public class DateFormatter implements Formatter { } /** - * Set the two character to use to format date values. The first character used for - * the date style, the second is for the time style. Supported characters are + * Set the two characters to use to format date values. + *

The first character is used for the date style; the second is used for + * the time style. + *

Supported characters: *