Skip to content

Commit 0e5e81e

Browse files
committed
Merge branch '6.1.x'
2 parents 8c9b6e2 + 20dea0d commit 0e5e81e

File tree

5 files changed

+72
-30
lines changed

5 files changed

+72
-30
lines changed

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

+28-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 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
@@ -49,15 +56,23 @@ public class DateFormatter implements Formatter<Date> {
4956

5057
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
5158

52-
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
5359
private static final Map<ISO, String> ISO_PATTERNS;
5460

61+
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
62+
5563
static {
64+
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
5665
Map<ISO, String> formats = new EnumMap<>(ISO.class);
5766
formats.put(ISO.DATE, "yyyy-MM-dd");
5867
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
5968
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
6069
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);
6176
}
6277

6378

@@ -202,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException {
202217
return getDateFormat(locale).parse(text);
203218
}
204219
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+
}
205225
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
206-
for (String pattern : this.fallbackPatterns) {
226+
Collections.addAll(fallbackPatterns, this.fallbackPatterns);
227+
}
228+
if (!fallbackPatterns.isEmpty()) {
229+
for (String pattern : fallbackPatterns) {
207230
try {
208231
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
209232
// Align timezone for parsing format with printing format if ISO is set.
@@ -221,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException {
221244
}
222245
if (this.source != null) {
223246
ParseException parseException = new ParseException(
224-
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
225-
ex.getErrorOffset());
247+
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
248+
ex.getErrorOffset());
226249
parseException.initCause(ex);
227250
throw parseException;
228251
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 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.
@@ -45,12 +45,12 @@ public Instant parse(String text, Locale locale) throws ParseException {
4545
return Instant.ofEpochMilli(Long.parseLong(text));
4646
}
4747
catch (NumberFormatException ex) {
48-
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
48+
if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) {
4949
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
5050
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
5151
}
5252
else {
53-
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
53+
// assuming UTC instant a la "2007-12-03T10:15:30.000Z"
5454
return Instant.parse(text);
5555
}
5656
}

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

+26-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
*
3636
* @author Keith Donald
3737
* @author Phillip Webb
38+
* @author Juergen Hoeller
3839
*/
3940
class DateFormatterTests {
4041

@@ -45,6 +46,7 @@ class DateFormatterTests {
4546
void shouldPrintAndParseDefault() throws Exception {
4647
DateFormatter formatter = new DateFormatter();
4748
formatter.setTimeZone(UTC);
49+
4850
Date date = getDate(2009, Calendar.JUNE, 1);
4951
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
5052
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
@@ -54,6 +56,7 @@ void shouldPrintAndParseDefault() throws Exception {
5456
void shouldPrintAndParseFromPattern() throws ParseException {
5557
DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
5658
formatter.setTimeZone(UTC);
59+
5760
Date date = getDate(2009, Calendar.JUNE, 1);
5861
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
5962
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
@@ -64,6 +67,7 @@ void shouldPrintAndParseShort() throws Exception {
6467
DateFormatter formatter = new DateFormatter();
6568
formatter.setTimeZone(UTC);
6669
formatter.setStyle(DateFormat.SHORT);
70+
6771
Date date = getDate(2009, Calendar.JUNE, 1);
6872
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
6973
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
@@ -74,6 +78,7 @@ void shouldPrintAndParseMedium() throws Exception {
7478
DateFormatter formatter = new DateFormatter();
7579
formatter.setTimeZone(UTC);
7680
formatter.setStyle(DateFormat.MEDIUM);
81+
7782
Date date = getDate(2009, Calendar.JUNE, 1);
7883
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
7984
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
@@ -84,6 +89,7 @@ void shouldPrintAndParseLong() throws Exception {
8489
DateFormatter formatter = new DateFormatter();
8590
formatter.setTimeZone(UTC);
8691
formatter.setStyle(DateFormat.LONG);
92+
8793
Date date = getDate(2009, Calendar.JUNE, 1);
8894
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
8995
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
@@ -94,50 +100,63 @@ void shouldPrintAndParseFull() throws Exception {
94100
DateFormatter formatter = new DateFormatter();
95101
formatter.setTimeZone(UTC);
96102
formatter.setStyle(DateFormat.FULL);
103+
97104
Date date = getDate(2009, Calendar.JUNE, 1);
98105
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
99106
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
100107
}
101108

102109
@Test
103-
void shouldPrintAndParseISODate() throws Exception {
110+
void shouldPrintAndParseIsoDate() throws Exception {
104111
DateFormatter formatter = new DateFormatter();
105112
formatter.setTimeZone(UTC);
106113
formatter.setIso(ISO.DATE);
114+
107115
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
108116
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
109117
assertThat(formatter.parse("2009-6-01", Locale.US))
110118
.isEqualTo(getDate(2009, Calendar.JUNE, 1));
111119
}
112120

113121
@Test
114-
void shouldPrintAndParseISOTime() throws Exception {
122+
void shouldPrintAndParseIsoTime() throws Exception {
115123
DateFormatter formatter = new DateFormatter();
116124
formatter.setTimeZone(UTC);
117125
formatter.setIso(ISO.TIME);
126+
118127
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
119128
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
120129
assertThat(formatter.parse("14:23:05.003Z", Locale.US))
121130
.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());
122136
}
123137

124138
@Test
125-
void shouldPrintAndParseISODateTime() throws Exception {
139+
void shouldPrintAndParseIsoDateTime() throws Exception {
126140
DateFormatter formatter = new DateFormatter();
127141
formatter.setTimeZone(UTC);
128142
formatter.setIso(ISO.DATE_TIME);
143+
129144
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
130145
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z");
131146
assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date);
147+
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());
132151
}
133152

134153
@Test
135154
void shouldThrowOnUnsupportedStylePattern() {
136155
DateFormatter formatter = new DateFormatter();
137156
formatter.setStylePattern("OO");
138-
assertThatIllegalStateException().isThrownBy(() ->
139-
formatter.parse("2009", Locale.US))
140-
.withMessageContaining("Unsupported style pattern 'OO'");
157+
158+
assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
159+
.withMessageContaining("Unsupported style pattern 'OO'");
141160
}
142161

143162
@Test
@@ -148,8 +167,8 @@ void shouldUseCorrectOrder() {
148167
formatter.setStylePattern("L-");
149168
formatter.setIso(ISO.DATE_TIME);
150169
formatter.setPattern("yyyy");
151-
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
152170

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

155174
formatter.setPattern("");

spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -644,18 +644,18 @@ public static class DateTimeBean {
644644
@DateTimeFormat(style = "M-")
645645
private LocalDate styleLocalDate;
646646

647-
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
647+
@DateTimeFormat(style = "S-", fallbackPatterns = {"yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd"})
648648
private LocalDate styleLocalDateWithFallbackPatterns;
649649

650-
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
650+
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = {"M/d/yy", "yyyyMMdd", "yyyy.MM.dd"})
651651
private LocalDate patternLocalDateWithFallbackPatterns;
652652

653653
private LocalTime localTime;
654654

655655
@DateTimeFormat(style = "-M")
656656
private LocalTime styleLocalTime;
657657

658-
@DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"})
658+
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"})
659659
private LocalTime styleLocalTimeWithFallbackPatterns;
660660

661661
private LocalDateTime localDateTime;
@@ -675,7 +675,7 @@ public static class DateTimeBean {
675675
@DateTimeFormat(iso = ISO.DATE_TIME)
676676
private LocalDateTime isoLocalDateTime;
677677

678-
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
678+
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = {"yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
679679
private LocalDateTime isoLocalDateTimeWithFallbackPatterns;
680680

681681
private Instant instant;

spring-context/src/test/java/org/springframework/format/datetime/standard/InstantFormatterTests.java

+11-11
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.time.Instant;
2121
import java.time.format.DateTimeFormatter;
2222
import java.time.temporal.ChronoUnit;
23+
import java.util.Locale;
2324
import java.util.Random;
2425
import java.util.stream.Stream;
2526

@@ -50,44 +51,39 @@ class InstantFormatterTests {
5051

5152
private final InstantFormatter instantFormatter = new InstantFormatter();
5253

54+
5355
@ParameterizedTest
5456
@ArgumentsSource(ISOSerializedInstantProvider.class)
5557
void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException {
5658
Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from);
57-
58-
Instant actual = instantFormatter.parse(input, null);
59-
59+
Instant actual = instantFormatter.parse(input, Locale.US);
6060
assertThat(actual).isEqualTo(expected);
6161
}
6262

6363
@ParameterizedTest
6464
@ArgumentsSource(RFC1123SerializedInstantProvider.class)
6565
void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException {
6666
Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from);
67-
68-
Instant actual = instantFormatter.parse(input, null);
69-
67+
Instant actual = instantFormatter.parse(input, Locale.US);
7068
assertThat(actual).isEqualTo(expected);
7169
}
7270

7371
@ParameterizedTest
7472
@ArgumentsSource(RandomInstantProvider.class)
7573
void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) {
7674
String expected = DateTimeFormatter.ISO_INSTANT.format(input);
77-
78-
String actual = instantFormatter.print(input, null);
79-
75+
String actual = instantFormatter.print(input, Locale.US);
8076
assertThat(actual).isEqualTo(expected);
8177
}
8278

8379
@ParameterizedTest
8480
@ArgumentsSource(RandomEpochMillisProvider.class)
8581
void should_parse_into_an_Instant_from_epoch_milli(Instant input) throws ParseException {
86-
Instant actual = instantFormatter.parse(Long.toString(input.toEpochMilli()), null);
87-
82+
Instant actual = instantFormatter.parse(Long.toString(input.toEpochMilli()), Locale.US);
8883
assertThat(actual).isEqualTo(input);
8984
}
9085

86+
9187
private static class RandomInstantProvider implements ArgumentsProvider {
9288

9389
private static final long DATA_SET_SIZE = 10;
@@ -109,6 +105,7 @@ Stream<Instant> randomInstantStream(Instant min, Instant max) {
109105
}
110106
}
111107

108+
112109
private static class ISOSerializedInstantProvider extends RandomInstantProvider {
113110

114111
@Override
@@ -117,6 +114,7 @@ Stream<?> provideArguments() {
117114
}
118115
}
119116

117+
120118
private static class RFC1123SerializedInstantProvider extends RandomInstantProvider {
121119

122120
// RFC-1123 supports only 4-digit years
@@ -130,6 +128,8 @@ Stream<?> provideArguments() {
130128
.map(DateTimeFormatter.RFC_1123_DATE_TIME.withZone(systemDefault())::format);
131129
}
132130
}
131+
132+
133133
private static final class RandomEpochMillisProvider implements ArgumentsProvider {
134134

135135
private static final long DATA_SET_SIZE = 10;

0 commit comments

Comments
 (0)