Skip to content

Commit d2e7cf4

Browse files
committed
Default fallback parsing for UTC without milliseconds
Closes gh-32856 (cherry picked from commit fee17e1)
1 parent 47a7abe commit d2e7cf4

File tree

2 files changed

+63
-60
lines changed

2 files changed

+63
-60
lines changed

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

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -22,8 +22,10 @@
2222
import java.util.Collections;
2323
import java.util.Date;
2424
import java.util.EnumMap;
25+
import java.util.LinkedHashSet;
2526
import java.util.Locale;
2627
import java.util.Map;
28+
import java.util.Set;
2729
import java.util.TimeZone;
2830

2931
import org.springframework.format.Formatter;
@@ -35,9 +37,14 @@
3537

3638
/**
3739
* A formatter for {@link java.util.Date} types.
40+
*
3841
* <p>Supports the configuration of an explicit date time pattern, timezone,
3942
* locale, and fallback date time patterns for lenient parsing.
4043
*
44+
* <p>Common ISO patterns for UTC instants are applied at millisecond precision.
45+
* Note that {@link org.springframework.format.datetime.standard.InstantFormatter}
46+
* is recommended for flexible UTC parsing into a {@link java.time.Instant} instead.
47+
*
4148
* @author Keith Donald
4249
* @author Juergen Hoeller
4350
* @author Phillip Webb
@@ -51,12 +58,21 @@ public class DateFormatter implements Formatter<Date> {
5158

5259
private static final Map<ISO, String> ISO_PATTERNS;
5360

61+
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
62+
5463
static {
64+
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
5565
Map<ISO, String> formats = new EnumMap<>(ISO.class);
5666
formats.put(ISO.DATE, "yyyy-MM-dd");
5767
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
5868
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
5969
ISO_PATTERNS = Collections.unmodifiableMap(formats);
70+
71+
// Fallback format for the time part without milliseconds.
72+
Map<ISO, String> fallbackFormats = new EnumMap<>(ISO.class);
73+
fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX");
74+
fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX");
75+
ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats);
6076
}
6177

6278

@@ -201,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException {
201217
return getDateFormat(locale).parse(text);
202218
}
203219
catch (ParseException ex) {
220+
Set<String> fallbackPatterns = new LinkedHashSet<>();
221+
String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso);
222+
if (isoPattern != null) {
223+
fallbackPatterns.add(isoPattern);
224+
}
204225
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
205-
for (String pattern : this.fallbackPatterns) {
226+
Collections.addAll(fallbackPatterns, this.fallbackPatterns);
227+
}
228+
if (!fallbackPatterns.isEmpty()) {
229+
for (String pattern : fallbackPatterns) {
206230
try {
207231
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
208232
// Align timezone for parsing format with printing format if ISO is set.
@@ -220,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException {
220244
}
221245
if (this.source != null) {
222246
ParseException parseException = new ParseException(
223-
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
224-
ex.getErrorOffset());
247+
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
248+
ex.getErrorOffset());
225249
parseException.initCause(ex);
226250
throw parseException;
227251
}

Diff for: spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java

+35-56
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 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.
@@ -23,173 +23,152 @@
2323
import java.util.Locale;
2424
import java.util.TimeZone;
2525

26-
import org.joda.time.DateTimeZone;
27-
import org.joda.time.format.DateTimeFormat;
28-
import org.joda.time.format.DateTimeFormatter;
2926
import org.junit.jupiter.api.Test;
3027

3128
import org.springframework.format.annotation.DateTimeFormat.ISO;
3229

3330
import static org.assertj.core.api.Assertions.assertThat;
3431
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3532

36-
37-
38-
3933
/**
4034
* Tests for {@link DateFormatter}.
4135
*
4236
* @author Keith Donald
4337
* @author Phillip Webb
38+
* @author Juergen Hoeller
4439
*/
45-
public class DateFormatterTests {
40+
class DateFormatterTests {
4641

4742
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
4843

4944

5045
@Test
51-
public void shouldPrintAndParseDefault() throws Exception {
46+
void shouldPrintAndParseDefault() throws Exception {
5247
DateFormatter formatter = new DateFormatter();
5348
formatter.setTimeZone(UTC);
49+
5450
Date date = getDate(2009, Calendar.JUNE, 1);
5551
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
5652
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
5753
}
5854

5955
@Test
60-
public void shouldPrintAndParseFromPattern() throws ParseException {
56+
void shouldPrintAndParseFromPattern() throws ParseException {
6157
DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
6258
formatter.setTimeZone(UTC);
59+
6360
Date date = getDate(2009, Calendar.JUNE, 1);
6461
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
6562
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
6663
}
6764

6865
@Test
69-
public void shouldPrintAndParseShort() throws Exception {
66+
void shouldPrintAndParseShort() throws Exception {
7067
DateFormatter formatter = new DateFormatter();
7168
formatter.setTimeZone(UTC);
7269
formatter.setStyle(DateFormat.SHORT);
70+
7371
Date date = getDate(2009, Calendar.JUNE, 1);
7472
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
7573
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
7674
}
7775

7876
@Test
79-
public void shouldPrintAndParseMedium() throws Exception {
77+
void shouldPrintAndParseMedium() throws Exception {
8078
DateFormatter formatter = new DateFormatter();
8179
formatter.setTimeZone(UTC);
8280
formatter.setStyle(DateFormat.MEDIUM);
81+
8382
Date date = getDate(2009, Calendar.JUNE, 1);
8483
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
8584
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
8685
}
8786

8887
@Test
89-
public void shouldPrintAndParseLong() throws Exception {
88+
void shouldPrintAndParseLong() throws Exception {
9089
DateFormatter formatter = new DateFormatter();
9190
formatter.setTimeZone(UTC);
9291
formatter.setStyle(DateFormat.LONG);
92+
9393
Date date = getDate(2009, Calendar.JUNE, 1);
9494
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
9595
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
9696
}
9797

9898
@Test
99-
public void shouldPrintAndParseFull() throws Exception {
99+
void shouldPrintAndParseFull() throws Exception {
100100
DateFormatter formatter = new DateFormatter();
101101
formatter.setTimeZone(UTC);
102102
formatter.setStyle(DateFormat.FULL);
103+
103104
Date date = getDate(2009, Calendar.JUNE, 1);
104105
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
105106
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
106107
}
107108

108109
@Test
109-
public void shouldPrintAndParseISODate() throws Exception {
110+
void shouldPrintAndParseIsoDate() throws Exception {
110111
DateFormatter formatter = new DateFormatter();
111112
formatter.setTimeZone(UTC);
112113
formatter.setIso(ISO.DATE);
114+
113115
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
114116
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
115117
assertThat(formatter.parse("2009-6-01", Locale.US))
116118
.isEqualTo(getDate(2009, Calendar.JUNE, 1));
117119
}
118120

119121
@Test
120-
public void shouldPrintAndParseISOTime() throws Exception {
122+
void shouldPrintAndParseIsoTime() throws Exception {
121123
DateFormatter formatter = new DateFormatter();
122124
formatter.setTimeZone(UTC);
123125
formatter.setIso(ISO.TIME);
126+
124127
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
125128
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
126129
assertThat(formatter.parse("14:23:05.003Z", Locale.US))
127130
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3));
131+
132+
date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0);
133+
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z");
134+
assertThat(formatter.parse("14:23:05Z", Locale.US))
135+
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant());
128136
}
129137

130138
@Test
131-
public void shouldPrintAndParseISODateTime() throws Exception {
139+
void shouldPrintAndParseIsoDateTime() throws Exception {
132140
DateFormatter formatter = new DateFormatter();
133141
formatter.setTimeZone(UTC);
134142
formatter.setIso(ISO.DATE_TIME);
143+
135144
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
136145
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z");
137146
assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date);
138-
}
139147

140-
@Test
141-
public void shouldSupportJodaStylePatterns() throws Exception {
142-
String[] chars = { "S", "M", "-" };
143-
for (String d : chars) {
144-
for (String t : chars) {
145-
String style = d + t;
146-
if (!style.equals("--")) {
147-
Date date = getDate(2009, Calendar.JUNE, 10, 14, 23, 0, 0);
148-
if (t.equals("-")) {
149-
date = getDate(2009, Calendar.JUNE, 10);
150-
}
151-
else if (d.equals("-")) {
152-
date = getDate(1970, Calendar.JANUARY, 1, 14, 23, 0, 0);
153-
}
154-
testJodaStylePatterns(style, Locale.US, date);
155-
}
156-
}
157-
}
158-
}
159-
160-
private void testJodaStylePatterns(String style, Locale locale, Date date) throws Exception {
161-
DateFormatter formatter = new DateFormatter();
162-
formatter.setTimeZone(UTC);
163-
formatter.setStylePattern(style);
164-
DateTimeFormatter jodaFormatter = DateTimeFormat.forStyle(style).withLocale(locale).withZone(DateTimeZone.UTC);
165-
String jodaPrinted = jodaFormatter.print(date.getTime());
166-
assertThat(formatter.print(date, locale))
167-
.as("Unable to print style pattern " + style)
168-
.isEqualTo(jodaPrinted);
169-
assertThat(formatter.parse(jodaPrinted, locale))
170-
.as("Unable to parse style pattern " + style)
171-
.isEqualTo(date);
148+
date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0);
149+
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z");
150+
assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant());
172151
}
173152

174153
@Test
175-
public void shouldThrowOnUnsupportedStylePattern() throws Exception {
154+
void shouldThrowOnUnsupportedStylePattern() {
176155
DateFormatter formatter = new DateFormatter();
177156
formatter.setStylePattern("OO");
178-
assertThatIllegalStateException().isThrownBy(() ->
179-
formatter.parse("2009", Locale.US))
180-
.withMessageContaining("Unsupported style pattern 'OO'");
157+
158+
assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
159+
.withMessageContaining("Unsupported style pattern 'OO'");
181160
}
182161

183162
@Test
184-
public void shouldUseCorrectOrder() throws Exception {
163+
void shouldUseCorrectOrder() {
185164
DateFormatter formatter = new DateFormatter();
186165
formatter.setTimeZone(UTC);
187166
formatter.setStyle(DateFormat.SHORT);
188167
formatter.setStylePattern("L-");
189168
formatter.setIso(ISO.DATE_TIME);
190169
formatter.setPattern("yyyy");
191-
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
192170

171+
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
193172
assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009");
194173

195174
formatter.setPattern("");

0 commit comments

Comments
 (0)