Default fallback parsing for UTC without milliseconds
Closes gh-32856
This commit is contained in:
parent
65e1337d35
commit
fee17e11ba
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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.
|
||||
|
|
@ -22,8 +22,10 @@ import java.text.SimpleDateFormat;
|
|||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.EnumMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.springframework.format.Formatter;
|
||||
|
|
@ -35,9 +37,14 @@ import org.springframework.util.StringUtils;
|
|||
|
||||
/**
|
||||
* A formatter for {@link java.util.Date} types.
|
||||
*
|
||||
* <p>Supports the configuration of an explicit date time pattern, timezone,
|
||||
* locale, and fallback date time patterns for lenient parsing.
|
||||
*
|
||||
* <p>Common ISO patterns for UTC instants are applied at millisecond precision.
|
||||
* Note that {@link org.springframework.format.datetime.standard.InstantFormatter}
|
||||
* is recommended for flexible UTC parsing into a {@link java.time.Instant} instead.
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Juergen Hoeller
|
||||
* @author Phillip Webb
|
||||
|
|
@ -49,15 +56,23 @@ public class DateFormatter implements Formatter<Date> {
|
|||
|
||||
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
|
||||
|
||||
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
|
||||
private static final Map<ISO, String> ISO_PATTERNS;
|
||||
|
||||
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
|
||||
|
||||
static {
|
||||
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
|
||||
Map<ISO, String> formats = new EnumMap<>(ISO.class);
|
||||
formats.put(ISO.DATE, "yyyy-MM-dd");
|
||||
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
|
||||
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
|
||||
ISO_PATTERNS = Collections.unmodifiableMap(formats);
|
||||
|
||||
// Fallback format for the time part without milliseconds.
|
||||
Map<ISO, String> fallbackFormats = new EnumMap<>(ISO.class);
|
||||
fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX");
|
||||
fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX");
|
||||
ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -202,8 +217,16 @@ public class DateFormatter implements Formatter<Date> {
|
|||
return getDateFormat(locale).parse(text);
|
||||
}
|
||||
catch (ParseException ex) {
|
||||
Set<String> fallbackPatterns = new LinkedHashSet<>();
|
||||
String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso);
|
||||
if (isoPattern != null) {
|
||||
fallbackPatterns.add(isoPattern);
|
||||
}
|
||||
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
|
||||
for (String pattern : this.fallbackPatterns) {
|
||||
Collections.addAll(fallbackPatterns, this.fallbackPatterns);
|
||||
}
|
||||
if (!fallbackPatterns.isEmpty()) {
|
||||
for (String pattern : fallbackPatterns) {
|
||||
try {
|
||||
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
|
||||
// Align timezone for parsing format with printing format if ISO is set.
|
||||
|
|
@ -221,8 +244,8 @@ public class DateFormatter implements Formatter<Date> {
|
|||
}
|
||||
if (this.source != null) {
|
||||
ParseException parseException = new ParseException(
|
||||
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
|
||||
ex.getErrorOffset());
|
||||
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
|
||||
ex.getErrorOffset());
|
||||
parseException.initCause(ex);
|
||||
throw parseException;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
|||
*
|
||||
* @author Keith Donald
|
||||
* @author Phillip Webb
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class DateFormatterTests {
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ class DateFormatterTests {
|
|||
void shouldPrintAndParseDefault() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
|
||||
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
|
||||
|
|
@ -54,6 +56,7 @@ class DateFormatterTests {
|
|||
void shouldPrintAndParseFromPattern() throws ParseException {
|
||||
DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
|
||||
formatter.setTimeZone(UTC);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
|
||||
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
|
||||
|
|
@ -64,6 +67,7 @@ class DateFormatterTests {
|
|||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.SHORT);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
|
||||
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
|
||||
|
|
@ -74,6 +78,7 @@ class DateFormatterTests {
|
|||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.MEDIUM);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
|
||||
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
|
||||
|
|
@ -84,6 +89,7 @@ class DateFormatterTests {
|
|||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.LONG);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
|
||||
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
|
||||
|
|
@ -94,16 +100,18 @@ class DateFormatterTests {
|
|||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setStyle(DateFormat.FULL);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
|
||||
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAndParseISODate() throws Exception {
|
||||
void shouldPrintAndParseIsoDate() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setIso(ISO.DATE);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
|
||||
assertThat(formatter.parse("2009-6-01", Locale.US))
|
||||
|
|
@ -111,33 +119,44 @@ class DateFormatterTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAndParseISOTime() throws Exception {
|
||||
void shouldPrintAndParseIsoTime() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setIso(ISO.TIME);
|
||||
|
||||
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
|
||||
assertThat(formatter.parse("14:23:05.003Z", Locale.US))
|
||||
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3));
|
||||
|
||||
date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z");
|
||||
assertThat(formatter.parse("14:23:05Z", Locale.US))
|
||||
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPrintAndParseISODateTime() throws Exception {
|
||||
void shouldPrintAndParseIsoDateTime() throws Exception {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setTimeZone(UTC);
|
||||
formatter.setIso(ISO.DATE_TIME);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z");
|
||||
assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date);
|
||||
|
||||
date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0);
|
||||
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z");
|
||||
assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowOnUnsupportedStylePattern() {
|
||||
DateFormatter formatter = new DateFormatter();
|
||||
formatter.setStylePattern("OO");
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
formatter.parse("2009", Locale.US))
|
||||
.withMessageContaining("Unsupported style pattern 'OO'");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
|
||||
.withMessageContaining("Unsupported style pattern 'OO'");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -148,8 +167,8 @@ class DateFormatterTests {
|
|||
formatter.setStylePattern("L-");
|
||||
formatter.setIso(ISO.DATE_TIME);
|
||||
formatter.setPattern("yyyy");
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
|
||||
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
|
||||
assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009");
|
||||
|
||||
formatter.setPattern("");
|
||||
|
|
|
|||
Loading…
Reference in New Issue