From bbbb7c396ee5d259c0eb45c7a9480b2bf669e59d Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 20 Oct 2024 17:34:51 +0200 Subject: [PATCH] Update Date/Time formatting tests for pre/post JDK 20 This commit updates our Date/Time formatting/printing tests to demonstrate that the use of fallback patterns can help mitigate locale-based parsing/formatting issues beginning with JDK 20. The documentation within the tests is intentionally rather thorough for two reasons: 1. We need to understand exactly what it is we are testing and why the tests are written that way. 2. We may re-use parts of the documentation and examples in forthcoming documentation that we will provide to users. See gh-33151 --- .../format/datetime/DateFormattingTests.java | 91 +++++++++++++++++++ .../standard/DateTimeFormattingTests.java | 50 ++++++++++ 2 files changed, 141 insertions(+) diff --git a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java index b45a4a28c2..7d1016a639 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java @@ -339,6 +339,70 @@ class DateFormattingTests { assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02"); } + /** + * {@link SimpleDateBean#styleDateTimeWithFallbackPatternsForPreAndPostJdk20} + * configures "SS" as the date/time style to use. Thus, we have to be aware + * of the following if we do not configure fallback patterns for parsing. + * + *
To avoid compatibility issues between JDK versions, we have configured
+ * two fallback patterns which emulate the "SS" style: "MM/dd/yy h:mm a"
+ * matches against a standard space before the "PM", and "MM/dd/yy h:mm\u202Fa"
+ * matches against a narrow non-breaking space (NNBSP) before the "PM".
+ *
+ *
Thus, the following should theoretically be supported on any JDK (or at least
+ * JDK 17 - 23, where we have tested it).
+ *
+ * @see #patternDateTime(String)
+ */
+ @ParameterizedTest(name = "input date: {0}") // gh-33151
+ @ValueSource(strings = {"10/31/09, 12:00 PM", "10/31/09, 12:00\u202FPM"})
+ void styleDateTime_PreAndPostJdk20(String propertyValue) {
+ String propertyName = "styleDateTimeWithFallbackPatternsForPreAndPostJdk20";
+ MutablePropertyValues propertyValues = new MutablePropertyValues();
+ propertyValues.add(propertyName, propertyValue);
+ binder.bind(propertyValues);
+ BindingResult bindingResult = binder.getBindingResult();
+ assertThat(bindingResult.getErrorCount()).isEqualTo(0);
+ String value = binder.getBindingResult().getFieldValue(propertyName).toString();
+ // Since the "SS" style is always used for printing and the underlying format
+ // changes depending on the JDK version, we cannot be certain that a normal
+ // space is used before the "PM". Consequently we have to use a regular
+ // expression to match against any Unicode space character (\p{Zs}).
+ assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
+ }
+
+ /**
+ * To avoid the use of Locale-based styles (such as "MM") for
+ * {@link SimpleDateBean#patternDateTimeWithFallbackPatternForPreAndPostJdk20}, we have configured a
+ * primary pattern ("MM/dd/yy h:mm a"
) that matches against a standard space
+ * before the "PM" and a fallback pattern ("MM/dd/yy h:mm\u202Fa"
that matches
+ * against a narrow non-breaking space (NNBSP) before the "PM".
+ *
+ *
Thus, the following should theoretically be supported on any JDK (or at least + * JDK 17 - 23, where we have tested it). + * + * @see #styleDateTime(String) + */ + @ParameterizedTest(name = "input date: {0}") // gh-33151 + @ValueSource(strings = {"10/31/09 3:45 PM", "10/31/09 3:45\u202FPM"}) + void patternDateTime_PreAndPostJdk20(String propertyValue) { + String propertyName = "patternDateTimeWithFallbackPatternForPreAndPostJdk20"; + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add(propertyName, propertyValue); + binder.bind(propertyValues); + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue(propertyName).toString(); + // Since the "MM/dd/yy h:mm a" primary pattern is always used for printing, we + // can be certain that a normal space is used before the "PM". + assertThat(value).matches("10/31/09 3:45 PM"); + } + @Test void patternDateWithUnsupportedPattern() { String propertyValue = "210302"; @@ -389,12 +453,23 @@ class DateFormattingTests { @DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" }) private Date styleDateWithFallbackPatterns; + // "SS" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM, + // depending on the version of the JDK. + // Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM. + @DateTimeFormat(style = "SS", fallbackPatterns = { "M/d/yy, h:mm a", "M/d/yy, h:mm\u202Fa" }) + private Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20; + @DateTimeFormat(pattern = "M/d/yy h:mm") private Date patternDate; @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" }) private Date patternDateWithFallbackPatterns; + // Primary pattern matches a standard space before AM/PM. + // Fallback pattern matches a narrow non-breaking space (NNBSP) before AM/PM. + @DateTimeFormat(pattern = "MM/dd/yy h:mm a", fallbackPatterns = "MM/dd/yy h:mm\u202Fa") + private Date patternDateTimeWithFallbackPatternForPreAndPostJdk20; + @DateTimeFormat(iso = ISO.DATE) private Date isoDate; @@ -459,6 +534,14 @@ class DateFormattingTests { this.styleDateWithFallbackPatterns = styleDateWithFallbackPatterns; } + public Date getStyleDateTimeWithFallbackPatternsForPreAndPostJdk20() { + return this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20; + } + + public void setStyleDateTimeWithFallbackPatternsForPreAndPostJdk20(Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20) { + this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20 = styleDateTimeWithFallbackPatternsForPreAndPostJdk20; + } + public Date getPatternDate() { return this.patternDate; } @@ -475,6 +558,14 @@ class DateFormattingTests { this.patternDateWithFallbackPatterns = patternDateWithFallbackPatterns; } + public Date getPatternDateTimeWithFallbackPatternForPreAndPostJdk20() { + return this.patternDateTimeWithFallbackPatternForPreAndPostJdk20; + } + + public void setPatternDateTimeWithFallbackPatternForPreAndPostJdk20(Date patternDateTimeWithFallbackPatternForPreAndPostJdk20) { + this.patternDateTimeWithFallbackPatternForPreAndPostJdk20 = patternDateTimeWithFallbackPatternForPreAndPostJdk20; + } + public Date getIsoDate() { return this.isoDate; } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java index 43a8f1c13e..05ee61f7f2 100644 --- a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -605,6 +605,41 @@ class DateTimeFormattingTests { assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\p{Zs}PM"); } + /** + * {@link DateTimeBean#styleLocalTimeWithFallbackPatternsForPreAndPostJdk20} + * configures "-M" as the time style to use. Thus, we have to be aware + * of the following if we do not configure fallback patterns for parsing. + * + *
To avoid compatibility issues between JDK versions, we have configured
+ * two fallback patterns which emulate the "-M" style: "HH:mm:ss a"
+ * matches against a standard space before the "PM", and "HH:mm:ss\u202Fa"
+ * matches against a narrow non-breaking space (NNBSP) before the "PM".
+ *
+ *
Thus, the following should theoretically be supported on any JDK (or at least + * JDK 17 - 23, where we have tested it). + */ + @ParameterizedTest(name = "input date: {0}") // gh-33151 + @ValueSource(strings = { "12:00:00 PM", "12:00:00\u202FPM" }) + void styleLocalTime_PreAndPostJdk20(String propertyValue) { + String propertyName = "styleLocalTimeWithFallbackPatternsForPreAndPostJdk20"; + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add(propertyName, propertyValue); + binder.bind(propertyValues); + BindingResult bindingResult = binder.getBindingResult(); + assertThat(bindingResult.getErrorCount()).isEqualTo(0); + String value = binder.getBindingResult().getFieldValue(propertyName).toString(); + // Since the "-M" style is always used for printing and the underlying format + // changes depending on the JDK version, we cannot be certain that a normal + // space is used before the "PM". Consequently we have to use a regular + // expression to match against any Unicode space character (\p{Zs}). + assertThat(value).matches("12:00:00\\p{Zs}PM"); + } + @ParameterizedTest(name = "input date: {0}") @ValueSource(strings = {"2021-03-02T12:00:00", "2021-03-02 12:00:00", "3/2/21 12:00"}) void isoLocalDateTime(String propertyValue) { @@ -682,6 +717,12 @@ class DateTimeFormattingTests { @DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"}) private LocalTime styleLocalTimeWithFallbackPatterns; + // "-M" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM, + // depending on the version of the JDK. + // Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM. + @DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss a", "HH:mm:ss\u202Fa"}) + private LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20; + private LocalDateTime localDateTime; @DateTimeFormat(style = "MM") @@ -782,6 +823,15 @@ class DateTimeFormattingTests { this.styleLocalTimeWithFallbackPatterns = styleLocalTimeWithFallbackPatterns; } + public LocalTime getStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20() { + return this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20; + } + + public void setStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20( + LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20) { + this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20 = styleLocalTimeWithFallbackPatternsForPreAndPostJdk20; + } + public LocalDateTime getLocalDateTime() { return this.localDateTime; }