Browse Source

Default fallback parsing for UTC without milliseconds

Closes gh-32856
pull/32864/head
Juergen Hoeller 2 years ago
parent
commit
fee17e11ba
  1. 29
      spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java
  2. 31
      spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java

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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Collections;
import java.util.Date; import java.util.Date;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.TimeZone; import java.util.TimeZone;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
@ -35,9 +37,14 @@ import org.springframework.util.StringUtils;
/** /**
* A formatter for {@link java.util.Date} types. * A formatter for {@link java.util.Date} types.
*
* <p>Supports the configuration of an explicit date time pattern, timezone, * <p>Supports the configuration of an explicit date time pattern, timezone,
* locale, and fallback date time patterns for lenient parsing. * 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 Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Phillip Webb * @author Phillip Webb
@ -49,15 +56,23 @@ public class DateFormatter implements Formatter<Date> {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); 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_PATTERNS;
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
static { static {
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
Map<ISO, String> formats = new EnumMap<>(ISO.class); Map<ISO, String> formats = new EnumMap<>(ISO.class);
formats.put(ISO.DATE, "yyyy-MM-dd"); formats.put(ISO.DATE, "yyyy-MM-dd");
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX"); formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
ISO_PATTERNS = Collections.unmodifiableMap(formats); 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); return getDateFormat(locale).parse(text);
} }
catch (ParseException ex) { 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)) { if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
for (String pattern : this.fallbackPatterns) { Collections.addAll(fallbackPatterns, this.fallbackPatterns);
}
if (!fallbackPatterns.isEmpty()) {
for (String pattern : fallbackPatterns) {
try { try {
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale)); DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
// Align timezone for parsing format with printing format if ISO is set. // Align timezone for parsing format with printing format if ISO is set.

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

@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* *
* @author Keith Donald * @author Keith Donald
* @author Phillip Webb * @author Phillip Webb
* @author Juergen Hoeller
*/ */
class DateFormatterTests { class DateFormatterTests {
@ -45,6 +46,7 @@ class DateFormatterTests {
void shouldPrintAndParseDefault() throws Exception { void shouldPrintAndParseDefault() throws Exception {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
Date date = getDate(2009, Calendar.JUNE, 1); Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
@ -54,6 +56,7 @@ class DateFormatterTests {
void shouldPrintAndParseFromPattern() throws ParseException { void shouldPrintAndParseFromPattern() throws ParseException {
DateFormatter formatter = new DateFormatter("yyyy-MM-dd"); DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
Date date = getDate(2009, Calendar.JUNE, 1); Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01"); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date); assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
@ -64,6 +67,7 @@ class DateFormatterTests {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.SHORT); formatter.setStyle(DateFormat.SHORT);
Date date = getDate(2009, Calendar.JUNE, 1); Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09"); assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date); assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
@ -74,6 +78,7 @@ class DateFormatterTests {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.MEDIUM); formatter.setStyle(DateFormat.MEDIUM);
Date date = getDate(2009, Calendar.JUNE, 1); Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009"); assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date); assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
@ -84,6 +89,7 @@ class DateFormatterTests {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.LONG); formatter.setStyle(DateFormat.LONG);
Date date = getDate(2009, Calendar.JUNE, 1); Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009"); assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date); assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
@ -94,16 +100,18 @@ class DateFormatterTests {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.FULL); formatter.setStyle(DateFormat.FULL);
Date date = getDate(2009, Calendar.JUNE, 1); Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009"); assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date); assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
} }
@Test @Test
void shouldPrintAndParseISODate() throws Exception { void shouldPrintAndParseIsoDate() throws Exception {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setIso(ISO.DATE); formatter.setIso(ISO.DATE);
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)).isEqualTo("2009-06-01"); assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
assertThat(formatter.parse("2009-6-01", Locale.US)) assertThat(formatter.parse("2009-6-01", Locale.US))
@ -111,32 +119,43 @@ class DateFormatterTests {
} }
@Test @Test
void shouldPrintAndParseISOTime() throws Exception { void shouldPrintAndParseIsoTime() throws Exception {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setIso(ISO.TIME); formatter.setIso(ISO.TIME);
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3); Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z"); assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
assertThat(formatter.parse("14:23:05.003Z", Locale.US)) assertThat(formatter.parse("14:23:05.003Z", Locale.US))
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3)); .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 @Test
void shouldPrintAndParseISODateTime() throws Exception { void shouldPrintAndParseIsoDateTime() throws Exception {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC); formatter.setTimeZone(UTC);
formatter.setIso(ISO.DATE_TIME); formatter.setIso(ISO.DATE_TIME);
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)).isEqualTo("2009-06-01T14:23:05.003Z"); 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); 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 @Test
void shouldThrowOnUnsupportedStylePattern() { void shouldThrowOnUnsupportedStylePattern() {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setStylePattern("OO"); formatter.setStylePattern("OO");
assertThatIllegalStateException().isThrownBy(() ->
formatter.parse("2009", Locale.US)) assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
.withMessageContaining("Unsupported style pattern 'OO'"); .withMessageContaining("Unsupported style pattern 'OO'");
} }
@ -148,8 +167,8 @@ class DateFormatterTests {
formatter.setStylePattern("L-"); formatter.setStylePattern("L-");
formatter.setIso(ISO.DATE_TIME); formatter.setIso(ISO.DATE_TIME);
formatter.setPattern("yyyy"); 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"); assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009");
formatter.setPattern(""); formatter.setPattern("");

Loading…
Cancel
Save