Default fallback parsing for UTC without milliseconds
Closes gh-32856
(cherry picked from commit fee17e11ba)
This commit is contained in:
@@ -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.
|
||||||
@@ -221,8 +244,8 @@ public class DateFormatter implements Formatter<Date> {
|
|||||||
}
|
}
|
||||||
if (this.source != null) {
|
if (this.source != null) {
|
||||||
ParseException parseException = new ParseException(
|
ParseException parseException = new ParseException(
|
||||||
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
|
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
|
||||||
ex.getErrorOffset());
|
ex.getErrorOffset());
|
||||||
parseException.initCause(ex);
|
parseException.initCause(ex);
|
||||||
throw parseException;
|
throw parseException;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2002-2021 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.
|
||||||
@@ -35,75 +35,83 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
|||||||
*
|
*
|
||||||
* @author Keith Donald
|
* @author Keith Donald
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Juergen Hoeller
|
||||||
*/
|
*/
|
||||||
public class DateFormatterTests {
|
class DateFormatterTests {
|
||||||
|
|
||||||
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
|
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldPrintAndParseShort() throws Exception {
|
void shouldPrintAndParseShort() throws Exception {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldPrintAndParseMedium() throws Exception {
|
void shouldPrintAndParseMedium() throws Exception {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldPrintAndParseLong() throws Exception {
|
void shouldPrintAndParseLong() throws Exception {
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldPrintAndParseFull() throws Exception {
|
void shouldPrintAndParseFull() throws Exception {
|
||||||
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
|
||||||
public 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,45 +119,56 @@ public class DateFormatterTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public 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
|
||||||
public 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
|
||||||
public void shouldThrowOnUnsupportedStylePattern() throws Exception {
|
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'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldUseCorrectOrder() throws Exception {
|
void shouldUseCorrectOrder() {
|
||||||
DateFormatter formatter = new DateFormatter();
|
DateFormatter formatter = new DateFormatter();
|
||||||
formatter.setTimeZone(UTC);
|
formatter.setTimeZone(UTC);
|
||||||
formatter.setStyle(DateFormat.SHORT);
|
formatter.setStyle(DateFormat.SHORT);
|
||||||
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("");
|
||||||
|
|||||||
Reference in New Issue
Block a user