Skip to content

Commit b2bcb0f

Browse files
committed
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
1 parent 5593e95 commit b2bcb0f

File tree

11 files changed

+708
-227
lines changed

11 files changed

+708
-227
lines changed

spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java

Lines changed: 63 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,20 @@
2626
* Declares that a field or method parameter should be formatted as a date or time.
2727
*
2828
* <p>Supports formatting by style pattern, ISO date time pattern, or custom format pattern string.
29-
* Can be applied to {@code java.util.Date}, {@code java.util.Calendar}, {@code Long} (for
30-
* millisecond timestamps) as well as JSR-310 <code>java.time</code> and Joda-Time value types.
29+
* Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for
30+
* millisecond timestamps) as well as JSR-310 {@code java.time} and Joda-Time value types.
3131
*
32-
* <p>For style-based formatting, set the {@link #style} attribute to be the style pattern code.
32+
* <p>For style-based formatting, set the {@link #style} attribute to the desired style pattern code.
3333
* The first character of the code is the date style, and the second character is the time style.
3434
* Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full.
35-
* A date or time may be omitted by specifying the style character '-'.
35+
* The date or time may be omitted by specifying the style character '-' &mdash; for example,
36+
* 'M-' specifies a medium format for the date with no time.
3637
*
37-
* <p>For ISO-based formatting, set the {@link #iso} attribute to be the desired {@link ISO} format,
38-
* such as {@link ISO#DATE}. For custom formatting, set the {@link #pattern} attribute to be the
39-
* DateTime pattern, such as {@code "yyyy/MM/dd hh:mm:ss a"}.
38+
* <p>For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format,
39+
* such as {@link ISO#DATE}.
40+
*
41+
* <p>For custom formatting, set the {@link #pattern} attribute to a date time pattern, such as
42+
* {@code "yyyy/MM/dd hh:mm:ss a"}.
4043
*
4144
* <p>Each attribute is mutually exclusive, so only set one attribute per annotation instance
4245
* (the one most convenient for your formatting needs).
@@ -48,8 +51,19 @@
4851
* with a style code of 'SS' (short date, short time).</li>
4952
* </ul>
5053
*
54+
* <h3>Time Zones</h3>
55+
* <p>Whenever the {@link #style} or {@link #pattern} attribute is used, the
56+
* {@linkplain java.util.TimeZone#getDefault() default time zone} of the JVM will
57+
* be used when formatting {@link java.util.Date} values. Whenever the {@link #iso}
58+
* attribute is used when formatting {@link java.util.Date} values, {@code UTC}
59+
* will be used as the time zone. The same time zone will be applied to any
60+
* {@linkplain #fallbackPatterns fallback patterns} as well. In order to enforce
61+
* consistent use of {@code UTC} as the time zone, you can bootstrap the JVM with
62+
* {@code -Duser.timezone=UTC}.
63+
*
5164
* @author Keith Donald
5265
* @author Juergen Hoeller
66+
* @author Sam Brannen
5367
* @since 3.0
5468
* @see java.time.format.DateTimeFormatter
5569
* @see org.joda.time.format.DateTimeFormat
@@ -60,55 +74,80 @@
6074
public @interface DateTimeFormat {
6175

6276
/**
63-
* The style pattern to use to format the field.
64-
* <p>Defaults to 'SS' for short date time. Set this attribute when you wish to format
65-
* your field in accordance with a common style other than the default style.
77+
* The style pattern to use to format the field or method parameter.
78+
* <p>Defaults to 'SS' for short date, short time. Set this attribute when you
79+
* wish to format your field or method parameter in accordance with a common
80+
* style other than the default style.
81+
* @see #fallbackPatterns
6682
*/
6783
String style() default "SS";
6884

6985
/**
70-
* The ISO pattern to use to format the field.
71-
* <p>The possible ISO patterns are defined in the {@link ISO} enum.
86+
* The ISO pattern to use to format the field or method parameter.
87+
* <p>Supported ISO patterns are defined in the {@link ISO} enum.
7288
* <p>Defaults to {@link ISO#NONE}, indicating this attribute should be ignored.
73-
* Set this attribute when you wish to format your field in accordance with an ISO format.
89+
* Set this attribute when you wish to format your field or method parameter
90+
* in accordance with an ISO format.
91+
* @see #fallbackPatterns
7492
*/
7593
ISO iso() default ISO.NONE;
7694

7795
/**
78-
* The custom pattern to use to format the field.
79-
* <p>Defaults to empty String, indicating no custom pattern String has been specified.
80-
* Set this attribute when you wish to format your field in accordance with a custom
81-
* date time pattern not represented by a style or ISO format.
96+
* The custom pattern to use to format the field or method parameter.
97+
* <p>Defaults to empty String, indicating no custom pattern String has been
98+
* specified. Set this attribute when you wish to format your field or method
99+
* parameter in accordance with a custom date time pattern not represented by
100+
* a style or ISO format.
82101
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style,
83102
* as also supported by Joda-Time, with strict parsing semantics towards overflows
84103
* (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy'
85104
* characters indicate a year in the traditional style, not a "year-of-era" as in the
86105
* {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu'
87-
* when going through that {@code DateTimeFormatter} with strict resolution mode).
106+
* when going through a {@code DateTimeFormatter} with strict resolution mode).
107+
* @see #fallbackPatterns
88108
*/
89109
String pattern() default "";
90110

111+
/**
112+
* The set of custom patterns to use as a fallback in case parsing fails for
113+
* the primary {@link #pattern}, {@link #iso}, or {@link #style} attribute.
114+
* <p>For example, if you wish to use the ISO date format for parsing and
115+
* printing but allow for lenient parsing of user input for various date
116+
* formats, you could configure something similar to the following.
117+
* <pre style="code">
118+
* {@literal @}DateTimeFormat(iso = ISO.DATE, fallbackPatterns = { "M/d/yy", "dd.MM.yyyy" })
119+
* </pre>
120+
* <p>Fallback patterns are only used for parsing. They are not used for
121+
* printing the value as a String. The primary {@link #pattern}, {@link #iso},
122+
* or {@link #style} attribute is always used for printing. For details on
123+
* which time zone is used for fallback patterns, see the
124+
* {@linkplain DateTimeFormat class-level documentation}.
125+
* <p>Fallback patterns are not supported for Joda-Time value types.
126+
* @since 5.3.5
127+
*/
128+
String[] fallbackPatterns() default {};
129+
91130

92131
/**
93132
* Common ISO date time format patterns.
94133
*/
95134
enum ISO {
96135

97136
/**
98-
* The most common ISO Date Format {@code yyyy-MM-dd},
99-
* e.g. "2000-10-31".
137+
* The most common ISO Date Format {@code yyyy-MM-dd} &mdash; for example,
138+
* "2000-10-31".
100139
*/
101140
DATE,
102141

103142
/**
104-
* The most common ISO Time Format {@code HH:mm:ss.SSSXXX},
105-
* e.g. "01:30:00.000-05:00".
143+
* The most common ISO Time Format {@code HH:mm:ss.SSSXXX} &mdash; for example,
144+
* "01:30:00.000-05:00".
106145
*/
107146
TIME,
108147

109148
/**
110-
* The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX},
111-
* e.g. "2000-10-31T01:30:00.000-05:00".
149+
* The most common ISO Date Time Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}
150+
* &mdash; for example, "2000-10-31T01:30:00.000-05:00".
112151
*/
113152
DATE_TIME,
114153

spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,17 +27,21 @@
2727
import java.util.TimeZone;
2828

2929
import org.springframework.format.Formatter;
30+
import org.springframework.format.annotation.DateTimeFormat;
3031
import org.springframework.format.annotation.DateTimeFormat.ISO;
3132
import org.springframework.lang.Nullable;
33+
import org.springframework.util.ObjectUtils;
3234
import org.springframework.util.StringUtils;
3335

3436
/**
3537
* A formatter for {@link java.util.Date} types.
36-
* Allows the configuration of an explicit date pattern and locale.
38+
* <p>Supports the configuration of an explicit date time pattern, timezone,
39+
* locale, and fallback date time patterns for lenient parsing.
3740
*
3841
* @author Keith Donald
3942
* @author Juergen Hoeller
4043
* @author Phillip Webb
44+
* @author Sam Brannen
4145
* @since 3.0
4246
* @see SimpleDateFormat
4347
*/
@@ -56,9 +60,15 @@ public class DateFormatter implements Formatter<Date> {
5660
}
5761

5862

63+
@Nullable
64+
private Object source;
65+
5966
@Nullable
6067
private String pattern;
6168

69+
@Nullable
70+
private String[] fallbackPatterns;
71+
6272
private int style = DateFormat.DEFAULT;
6373

6474
@Nullable
@@ -74,19 +84,33 @@ public class DateFormatter implements Formatter<Date> {
7484

7585

7686
/**
77-
* Create a new default DateFormatter.
87+
* Create a new default {@code DateFormatter}.
7888
*/
7989
public DateFormatter() {
8090
}
8191

8292
/**
83-
* Create a new DateFormatter for the given date pattern.
93+
* Create a new {@code DateFormatter} for the given date time pattern.
8494
*/
8595
public DateFormatter(String pattern) {
8696
this.pattern = pattern;
8797
}
8898

8999

100+
/**
101+
* Set the source of the configuration for this {@code DateFormatter} &mdash;
102+
* for example, an instance of the {@link DateTimeFormat @DateTimeFormat}
103+
* annotation if such an annotation was used to configure this {@code DateFormatter}.
104+
* <p>The supplied source object will only be used for descriptive purposes
105+
* by invoking its {@code toString()} method &mdash; for example, when
106+
* generating an exception message to provide further context.
107+
* @param source the source of the configuration
108+
* @since 5.3.5
109+
*/
110+
public void setSource(Object source) {
111+
this.source = source;
112+
}
113+
90114
/**
91115
* Set the pattern to use to format date values.
92116
* <p>If not specified, DateFormat's default style will be used.
@@ -96,7 +120,19 @@ public void setPattern(String pattern) {
96120
}
97121

98122
/**
99-
* Set the ISO format used for this date.
123+
* Set additional patterns to use as a fallback in case parsing fails for the
124+
* configured {@linkplain #setPattern pattern}, {@linkplain #setIso ISO format},
125+
* {@linkplain #setStyle style}, or {@linkplain #setStylePattern style pattern}.
126+
* @param fallbackPatterns the fallback parsing patterns
127+
* @since 5.3.5
128+
* @see DateTimeFormat#fallbackPatterns()
129+
*/
130+
public void setFallbackPatterns(String... fallbackPatterns) {
131+
this.fallbackPatterns = fallbackPatterns;
132+
}
133+
134+
/**
135+
* Set the ISO format to use to format date values.
100136
* @param iso the {@link ISO} format
101137
* @since 3.2
102138
*/
@@ -105,7 +141,7 @@ public void setIso(ISO iso) {
105141
}
106142

107143
/**
108-
* Set the style to use to format date values.
144+
* Set the {@link DateFormat} style to use to format date values.
109145
* <p>If not specified, DateFormat's default style will be used.
110146
* @see DateFormat#DEFAULT
111147
* @see DateFormat#SHORT
@@ -118,8 +154,10 @@ public void setStyle(int style) {
118154
}
119155

120156
/**
121-
* Set the two character to use to format date values. The first character used for
122-
* the date style, the second is for the time style. Supported characters are
157+
* Set the two characters to use to format date values.
158+
* <p>The first character is used for the date style; the second is used for
159+
* the time style.
160+
* <p>Supported characters:
123161
* <ul>
124162
* <li>'S' = Small</li>
125163
* <li>'M' = Medium</li>
@@ -136,7 +174,7 @@ public void setStylePattern(String stylePattern) {
136174
}
137175

138176
/**
139-
* Set the TimeZone to normalize the date values into, if any.
177+
* Set the {@link TimeZone} to normalize the date values into, if any.
140178
*/
141179
public void setTimeZone(TimeZone timeZone) {
142180
this.timeZone = timeZone;
@@ -159,12 +197,43 @@ public String print(Date date, Locale locale) {
159197

160198
@Override
161199
public Date parse(String text, Locale locale) throws ParseException {
162-
return getDateFormat(locale).parse(text);
200+
try {
201+
return getDateFormat(locale).parse(text);
202+
}
203+
catch (ParseException ex) {
204+
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
205+
for (String pattern : this.fallbackPatterns) {
206+
try {
207+
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
208+
// Align timezone for parsing format with printing format if ISO is set.
209+
if (this.iso != null && this.iso != ISO.NONE) {
210+
dateFormat.setTimeZone(UTC);
211+
}
212+
return dateFormat.parse(text);
213+
}
214+
catch (ParseException ignoredException) {
215+
// Ignore fallback parsing exceptions since the exception thrown below
216+
// will include information from the "source" if available -- for example,
217+
// the toString() of a @DateTimeFormat annotation.
218+
}
219+
}
220+
}
221+
if (this.source != null) {
222+
throw new ParseException(
223+
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
224+
ex.getErrorOffset());
225+
}
226+
// else rethrow original exception
227+
throw ex;
228+
}
163229
}
164230

165231

166232
protected DateFormat getDateFormat(Locale locale) {
167-
DateFormat dateFormat = createDateFormat(locale);
233+
return configureDateFormat(createDateFormat(locale));
234+
}
235+
236+
private DateFormat configureDateFormat(DateFormat dateFormat) {
168237
if (this.timeZone != null) {
169238
dateFormat.setTimeZone(this.timeZone);
170239
}

spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2021 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,12 @@
1616

1717
package org.springframework.format.datetime;
1818

19+
import java.util.ArrayList;
1920
import java.util.Calendar;
2021
import java.util.Collections;
2122
import java.util.Date;
2223
import java.util.HashSet;
24+
import java.util.List;
2325
import java.util.Set;
2426

2527
import org.springframework.context.support.EmbeddedValueResolutionSupport;
@@ -34,6 +36,7 @@
3436
* Formats fields annotated with the {@link DateTimeFormat} annotation using a {@link DateFormatter}.
3537
*
3638
* @author Phillip Webb
39+
* @author Sam Brannen
3740
* @since 3.2
3841
* @see org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory
3942
*/
@@ -68,15 +71,30 @@ public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) {
6871

6972
protected Formatter<Date> getFormatter(DateTimeFormat annotation, Class<?> fieldType) {
7073
DateFormatter formatter = new DateFormatter();
74+
formatter.setSource(annotation);
75+
formatter.setIso(annotation.iso());
76+
7177
String style = resolveEmbeddedValue(annotation.style());
7278
if (StringUtils.hasLength(style)) {
7379
formatter.setStylePattern(style);
7480
}
75-
formatter.setIso(annotation.iso());
81+
7682
String pattern = resolveEmbeddedValue(annotation.pattern());
7783
if (StringUtils.hasLength(pattern)) {
7884
formatter.setPattern(pattern);
7985
}
86+
87+
List<String> resolvedFallbackPatterns = new ArrayList<>();
88+
for (String fallbackPattern : annotation.fallbackPatterns()) {
89+
String resolvedFallbackPattern = resolveEmbeddedValue(fallbackPattern);
90+
if (StringUtils.hasLength(resolvedFallbackPattern)) {
91+
resolvedFallbackPatterns.add(resolvedFallbackPattern);
92+
}
93+
}
94+
if (!resolvedFallbackPatterns.isEmpty()) {
95+
formatter.setFallbackPatterns(resolvedFallbackPatterns.toArray(new String[0]));
96+
}
97+
8098
return formatter;
8199
}
82100

0 commit comments

Comments
 (0)