diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java
index 5235ca74324c..e4185c3b0859 100644
--- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java
+++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java
@@ -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");
* you may not use this file except in compliance with the License.
@@ -22,8 +22,10 @@
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 @@
/**
* A formatter for {@link java.util.Date} types.
+ *
*
Supports the configuration of an explicit date time pattern, timezone,
* locale, and fallback date time patterns for lenient parsing.
*
+ *
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
@@ -51,12 +58,21 @@ public class DateFormatter implements Formatter {
private static final Map ISO_PATTERNS;
+ private static final Map ISO_FALLBACK_PATTERNS;
+
static {
+ // We use an EnumMap instead of Map.of(...) since the former provides better performance.
Map 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 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);
}
@@ -201,8 +217,16 @@ public Date parse(String text, Locale locale) throws ParseException {
return getDateFormat(locale).parse(text);
}
catch (ParseException ex) {
+ Set 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.
@@ -220,8 +244,8 @@ public Date parse(String text, Locale locale) throws ParseException {
}
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;
}
diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java
index 7737902082a9..f74f36edbd95 100644
--- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java
+++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormatterTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 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.
@@ -23,9 +23,6 @@
import java.util.Locale;
import java.util.TimeZone;
-import org.joda.time.DateTimeZone;
-import org.joda.time.format.DateTimeFormat;
-import org.joda.time.format.DateTimeFormatter;
import org.junit.jupiter.api.Test;
import org.springframework.format.annotation.DateTimeFormat.ISO;
@@ -33,83 +30,88 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
-
-
-
/**
* Tests for {@link DateFormatter}.
*
* @author Keith Donald
* @author Phillip Webb
+ * @author Juergen Hoeller
*/
-public class DateFormatterTests {
+class DateFormatterTests {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
@Test
- public void shouldPrintAndParseDefault() throws Exception {
+ 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);
}
@Test
- public void shouldPrintAndParseFromPattern() throws ParseException {
+ 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);
}
@Test
- public void shouldPrintAndParseShort() throws Exception {
+ void shouldPrintAndParseShort() throws Exception {
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);
}
@Test
- public void shouldPrintAndParseMedium() throws Exception {
+ void shouldPrintAndParseMedium() throws Exception {
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);
}
@Test
- public void shouldPrintAndParseLong() throws Exception {
+ void shouldPrintAndParseLong() throws Exception {
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);
}
@Test
- public void shouldPrintAndParseFull() throws Exception {
+ void shouldPrintAndParseFull() throws Exception {
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
- public 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))
@@ -117,79 +119,56 @@ public void shouldPrintAndParseISODate() throws Exception {
}
@Test
- public 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
- public 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);
- }
- @Test
- public void shouldSupportJodaStylePatterns() throws Exception {
- String[] chars = { "S", "M", "-" };
- for (String d : chars) {
- for (String t : chars) {
- String style = d + t;
- if (!style.equals("--")) {
- Date date = getDate(2009, Calendar.JUNE, 10, 14, 23, 0, 0);
- if (t.equals("-")) {
- date = getDate(2009, Calendar.JUNE, 10);
- }
- else if (d.equals("-")) {
- date = getDate(1970, Calendar.JANUARY, 1, 14, 23, 0, 0);
- }
- testJodaStylePatterns(style, Locale.US, date);
- }
- }
- }
- }
-
- private void testJodaStylePatterns(String style, Locale locale, Date date) throws Exception {
- DateFormatter formatter = new DateFormatter();
- formatter.setTimeZone(UTC);
- formatter.setStylePattern(style);
- DateTimeFormatter jodaFormatter = DateTimeFormat.forStyle(style).withLocale(locale).withZone(DateTimeZone.UTC);
- String jodaPrinted = jodaFormatter.print(date.getTime());
- assertThat(formatter.print(date, locale))
- .as("Unable to print style pattern " + style)
- .isEqualTo(jodaPrinted);
- assertThat(formatter.parse(jodaPrinted, locale))
- .as("Unable to parse style pattern " + style)
- .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
- public void shouldThrowOnUnsupportedStylePattern() throws Exception {
+ 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
- public void shouldUseCorrectOrder() throws Exception {
+ void shouldUseCorrectOrder() {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.SHORT);
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("");