Browse Source

Support multiple parsing patterns in @DateTimeFormat

Prior to this commit, @DateTimeFormat only supported a single format
for parsing date time values via the style, iso, and pattern attributes.

This commit introduces a new fallbackPatterns attribute that can be
used to configure multiple fallback patterns for parsing date time
values. This allows applications to accept multiple input formats for
date time values.

For example, if you wish to use the ISO date format for parsing and
printing but allow for lenient parsing of user input for various
additional date formats, you could annotate a field or method parameter
with configuration similar to the following.

    @DateTimeFormat(
        iso = ISO.DATE,
        fallbackPatterns = { "M/d/yy", "dd.MM.yyyy" }
    )

Closes gh-20292
pull/26673/head
Sam Brannen 4 years ago
parent
commit
b2bcb0f93a
  1. 87
      spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java
  2. 91
      spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java
  3. 22
      spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java
  4. 8
      spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java
  5. 5
      spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java
  6. 10
      spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java
  7. 40
      spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java
  8. 16
      spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java
  9. 51
      spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java
  10. 243
      spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java
  11. 362
      spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java

87
spring-context/src/main/java/org/springframework/format/annotation/DateTimeFormat.java

@ -26,17 +26,20 @@ import java.lang.annotation.Target;
* Declares that a field or method parameter should be formatted as a date or time. * Declares that a field or method parameter should be formatted as a date or time.
* *
* <p>Supports formatting by style pattern, ISO date time pattern, or custom format pattern string. * <p>Supports formatting by style pattern, ISO date time pattern, or custom format pattern string.
* Can be applied to {@code java.util.Date}, {@code java.util.Calendar}, {@code Long} (for * Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for
* millisecond timestamps) as well as JSR-310 <code>java.time</code> and Joda-Time value types. * millisecond timestamps) as well as JSR-310 {@code java.time} and Joda-Time value types.
* *
* <p>For style-based formatting, set the {@link #style} attribute to be the style pattern code. * <p>For style-based formatting, set the {@link #style} attribute to the desired style pattern code.
* The first character of the code is the date style, and the second character is the time style. * The first character of the code is the date style, and the second character is the time style.
* Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full. * Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full.
* A date or time may be omitted by specifying the style character '-'. * The date or time may be omitted by specifying the style character '-' &mdash; for example,
* 'M-' specifies a medium format for the date with no time.
* *
* <p>For ISO-based formatting, set the {@link #iso} attribute to be the desired {@link ISO} format, * <p>For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format,
* such as {@link ISO#DATE}. For custom formatting, set the {@link #pattern} attribute to be the * such as {@link ISO#DATE}.
* DateTime pattern, such as {@code "yyyy/MM/dd hh:mm:ss a"}. *
* <p>For custom formatting, set the {@link #pattern} attribute to a date time pattern, such as
* {@code "yyyy/MM/dd hh:mm:ss a"}.
* *
* <p>Each attribute is mutually exclusive, so only set one attribute per annotation instance * <p>Each attribute is mutually exclusive, so only set one attribute per annotation instance
* (the one most convenient for your formatting needs). * (the one most convenient for your formatting needs).
@ -48,8 +51,19 @@ import java.lang.annotation.Target;
* with a style code of 'SS' (short date, short time).</li> * with a style code of 'SS' (short date, short time).</li>
* </ul> * </ul>
* *
* <h3>Time Zones</h3>
* <p>Whenever the {@link #style} or {@link #pattern} attribute is used, the
* {@linkplain java.util.TimeZone#getDefault() default time zone} of the JVM will
* be used when formatting {@link java.util.Date} values. Whenever the {@link #iso}
* attribute is used when formatting {@link java.util.Date} values, {@code UTC}
* will be used as the time zone. The same time zone will be applied to any
* {@linkplain #fallbackPatterns fallback patterns} as well. In order to enforce
* consistent use of {@code UTC} as the time zone, you can bootstrap the JVM with
* {@code -Duser.timezone=UTC}.
*
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0 * @since 3.0
* @see java.time.format.DateTimeFormatter * @see java.time.format.DateTimeFormatter
* @see org.joda.time.format.DateTimeFormat * @see org.joda.time.format.DateTimeFormat
@ -60,34 +74,59 @@ import java.lang.annotation.Target;
public @interface DateTimeFormat { public @interface DateTimeFormat {
/** /**
* The style pattern to use to format the field. * The style pattern to use to format the field or method parameter.
* <p>Defaults to 'SS' for short date time. Set this attribute when you wish to format * <p>Defaults to 'SS' for short date, short time. Set this attribute when you
* your field in accordance with a common style other than the default style. * wish to format your field or method parameter in accordance with a common
* style other than the default style.
* @see #fallbackPatterns
*/ */
String style() default "SS"; String style() default "SS";
/** /**
* The ISO pattern to use to format the field. * The ISO pattern to use to format the field or method parameter.
* <p>The possible ISO patterns are defined in the {@link ISO} enum. * <p>Supported ISO patterns are defined in the {@link ISO} enum.
* <p>Defaults to {@link ISO#NONE}, indicating this attribute should be ignored. * <p>Defaults to {@link ISO#NONE}, indicating this attribute should be ignored.
* Set this attribute when you wish to format your field in accordance with an ISO format. * Set this attribute when you wish to format your field or method parameter
* in accordance with an ISO format.
* @see #fallbackPatterns
*/ */
ISO iso() default ISO.NONE; ISO iso() default ISO.NONE;
/** /**
* The custom pattern to use to format the field. * The custom pattern to use to format the field or method parameter.
* <p>Defaults to empty String, indicating no custom pattern String has been specified. * <p>Defaults to empty String, indicating no custom pattern String has been
* Set this attribute when you wish to format your field in accordance with a custom * specified. Set this attribute when you wish to format your field or method
* date time pattern not represented by a style or ISO format. * parameter in accordance with a custom date time pattern not represented by
* a style or ISO format.
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style, * <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style,
* as also supported by Joda-Time, with strict parsing semantics towards overflows * as also supported by Joda-Time, with strict parsing semantics towards overflows
* (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy' * (e.g. rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy'
* characters indicate a year in the traditional style, not a "year-of-era" as in the * characters indicate a year in the traditional style, not a "year-of-era" as in the
* {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu' * {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu'
* when going through that {@code DateTimeFormatter} with strict resolution mode). * when going through a {@code DateTimeFormatter} with strict resolution mode).
* @see #fallbackPatterns
*/ */
String pattern() default ""; String pattern() default "";
/**
* The set of custom patterns to use as a fallback in case parsing fails for
* the primary {@link #pattern}, {@link #iso}, or {@link #style} attribute.
* <p>For example, if you wish to use the ISO date format for parsing and
* printing but allow for lenient parsing of user input for various date
* formats, you could configure something similar to the following.
* <pre style="code">
* {@literal @}DateTimeFormat(iso = ISO.DATE, fallbackPatterns = { "M/d/yy", "dd.MM.yyyy" })
* </pre>
* <p>Fallback patterns are only used for parsing. They are not used for
* printing the value as a String. The primary {@link #pattern}, {@link #iso},
* or {@link #style} attribute is always used for printing. For details on
* which time zone is used for fallback patterns, see the
* {@linkplain DateTimeFormat class-level documentation}.
* <p>Fallback patterns are not supported for Joda-Time value types.
* @since 5.3.5
*/
String[] fallbackPatterns() default {};
/** /**
* Common ISO date time format patterns. * Common ISO date time format patterns.
@ -95,20 +134,20 @@ public @interface DateTimeFormat {
enum ISO { enum ISO {
/** /**
* The most common ISO Date Format {@code yyyy-MM-dd}, * The most common ISO Date Format {@code yyyy-MM-dd} &mdash; for example,
* e.g. "2000-10-31". * "2000-10-31".
*/ */
DATE, DATE,
/** /**
* The most common ISO Time Format {@code HH:mm:ss.SSSXXX}, * The most common ISO Time Format {@code HH:mm:ss.SSSXXX} &mdash; for example,
* e.g. "01:30:00.000-05:00". * "01:30:00.000-05:00".
*/ */
TIME, TIME,
/** /**
* The most common ISO DateTime Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}, * The most common ISO Date Time Format {@code yyyy-MM-dd'T'HH:mm:ss.SSSXXX}
* e.g. "2000-10-31T01:30:00.000-05:00". * &mdash; for example, "2000-10-31T01:30:00.000-05:00".
*/ */
DATE_TIME, DATE_TIME,

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2021 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.
@ -27,17 +27,21 @@ import java.util.Map;
import java.util.TimeZone; import java.util.TimeZone;
import org.springframework.format.Formatter; import org.springframework.format.Formatter;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* A formatter for {@link java.util.Date} types. * A formatter for {@link java.util.Date} types.
* Allows the configuration of an explicit date pattern and locale. * <p>Supports the configuration of an explicit date time pattern, timezone,
* locale, and fallback date time patterns for lenient parsing.
* *
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
* @since 3.0 * @since 3.0
* @see SimpleDateFormat * @see SimpleDateFormat
*/ */
@ -56,9 +60,15 @@ public class DateFormatter implements Formatter<Date> {
} }
@Nullable
private Object source;
@Nullable @Nullable
private String pattern; private String pattern;
@Nullable
private String[] fallbackPatterns;
private int style = DateFormat.DEFAULT; private int style = DateFormat.DEFAULT;
@Nullable @Nullable
@ -74,19 +84,33 @@ public class DateFormatter implements Formatter<Date> {
/** /**
* Create a new default DateFormatter. * Create a new default {@code DateFormatter}.
*/ */
public DateFormatter() { public DateFormatter() {
} }
/** /**
* Create a new DateFormatter for the given date pattern. * Create a new {@code DateFormatter} for the given date time pattern.
*/ */
public DateFormatter(String pattern) { public DateFormatter(String pattern) {
this.pattern = pattern; this.pattern = pattern;
} }
/**
* Set the source of the configuration for this {@code DateFormatter} &mdash;
* for example, an instance of the {@link DateTimeFormat @DateTimeFormat}
* annotation if such an annotation was used to configure this {@code DateFormatter}.
* <p>The supplied source object will only be used for descriptive purposes
* by invoking its {@code toString()} method &mdash; for example, when
* generating an exception message to provide further context.
* @param source the source of the configuration
* @since 5.3.5
*/
public void setSource(Object source) {
this.source = source;
}
/** /**
* Set the pattern to use to format date values. * Set the pattern to use to format date values.
* <p>If not specified, DateFormat's default style will be used. * <p>If not specified, DateFormat's default style will be used.
@ -96,7 +120,19 @@ public class DateFormatter implements Formatter<Date> {
} }
/** /**
* Set the ISO format used for this date. * Set additional patterns to use as a fallback in case parsing fails for the
* configured {@linkplain #setPattern pattern}, {@linkplain #setIso ISO format},
* {@linkplain #setStyle style}, or {@linkplain #setStylePattern style pattern}.
* @param fallbackPatterns the fallback parsing patterns
* @since 5.3.5
* @see DateTimeFormat#fallbackPatterns()
*/
public void setFallbackPatterns(String... fallbackPatterns) {
this.fallbackPatterns = fallbackPatterns;
}
/**
* Set the ISO format to use to format date values.
* @param iso the {@link ISO} format * @param iso the {@link ISO} format
* @since 3.2 * @since 3.2
*/ */
@ -105,7 +141,7 @@ public class DateFormatter implements Formatter<Date> {
} }
/** /**
* Set the style to use to format date values. * Set the {@link DateFormat} style to use to format date values.
* <p>If not specified, DateFormat's default style will be used. * <p>If not specified, DateFormat's default style will be used.
* @see DateFormat#DEFAULT * @see DateFormat#DEFAULT
* @see DateFormat#SHORT * @see DateFormat#SHORT
@ -118,8 +154,10 @@ public class DateFormatter implements Formatter<Date> {
} }
/** /**
* Set the two character to use to format date values. The first character used for * Set the two characters to use to format date values.
* the date style, the second is for the time style. Supported characters are * <p>The first character is used for the date style; the second is used for
* the time style.
* <p>Supported characters:
* <ul> * <ul>
* <li>'S' = Small</li> * <li>'S' = Small</li>
* <li>'M' = Medium</li> * <li>'M' = Medium</li>
@ -136,7 +174,7 @@ public class DateFormatter implements Formatter<Date> {
} }
/** /**
* Set the TimeZone to normalize the date values into, if any. * Set the {@link TimeZone} to normalize the date values into, if any.
*/ */
public void setTimeZone(TimeZone timeZone) { public void setTimeZone(TimeZone timeZone) {
this.timeZone = timeZone; this.timeZone = timeZone;
@ -159,12 +197,43 @@ public class DateFormatter implements Formatter<Date> {
@Override @Override
public Date parse(String text, Locale locale) throws ParseException { public Date parse(String text, Locale locale) throws ParseException {
return getDateFormat(locale).parse(text); try {
return getDateFormat(locale).parse(text);
}
catch (ParseException ex) {
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
for (String pattern : this.fallbackPatterns) {
try {
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
// Align timezone for parsing format with printing format if ISO is set.
if (this.iso != null && this.iso != ISO.NONE) {
dateFormat.setTimeZone(UTC);
}
return dateFormat.parse(text);
}
catch (ParseException ignoredException) {
// Ignore fallback parsing exceptions since the exception thrown below
// will include information from the "source" if available -- for example,
// the toString() of a @DateTimeFormat annotation.
}
}
}
if (this.source != null) {
throw new ParseException(
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
ex.getErrorOffset());
}
// else rethrow original exception
throw ex;
}
} }
protected DateFormat getDateFormat(Locale locale) { protected DateFormat getDateFormat(Locale locale) {
DateFormat dateFormat = createDateFormat(locale); return configureDateFormat(createDateFormat(locale));
}
private DateFormat configureDateFormat(DateFormat dateFormat) {
if (this.timeZone != null) { if (this.timeZone != null) {
dateFormat.setTimeZone(this.timeZone); dateFormat.setTimeZone(this.timeZone);
} }

22
spring-context/src/main/java/org/springframework/format/datetime/DateTimeFormatAnnotationFormatterFactory.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2021 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.
@ -16,10 +16,12 @@
package org.springframework.format.datetime; package org.springframework.format.datetime;
import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.springframework.context.support.EmbeddedValueResolutionSupport; import org.springframework.context.support.EmbeddedValueResolutionSupport;
@ -34,6 +36,7 @@ import org.springframework.util.StringUtils;
* Formats fields annotated with the {@link DateTimeFormat} annotation using a {@link DateFormatter}. * Formats fields annotated with the {@link DateTimeFormat} annotation using a {@link DateFormatter}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
* @since 3.2 * @since 3.2
* @see org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory * @see org.springframework.format.datetime.joda.JodaDateTimeFormatAnnotationFormatterFactory
*/ */
@ -68,15 +71,30 @@ public class DateTimeFormatAnnotationFormatterFactory extends EmbeddedValueReso
protected Formatter<Date> getFormatter(DateTimeFormat annotation, Class<?> fieldType) { protected Formatter<Date> getFormatter(DateTimeFormat annotation, Class<?> fieldType) {
DateFormatter formatter = new DateFormatter(); DateFormatter formatter = new DateFormatter();
formatter.setSource(annotation);
formatter.setIso(annotation.iso());
String style = resolveEmbeddedValue(annotation.style()); String style = resolveEmbeddedValue(annotation.style());
if (StringUtils.hasLength(style)) { if (StringUtils.hasLength(style)) {
formatter.setStylePattern(style); formatter.setStylePattern(style);
} }
formatter.setIso(annotation.iso());
String pattern = resolveEmbeddedValue(annotation.pattern()); String pattern = resolveEmbeddedValue(annotation.pattern());
if (StringUtils.hasLength(pattern)) { if (StringUtils.hasLength(pattern)) {
formatter.setPattern(pattern); formatter.setPattern(pattern);
} }
List<String> resolvedFallbackPatterns = new ArrayList<>();
for (String fallbackPattern : annotation.fallbackPatterns()) {
String resolvedFallbackPattern = resolveEmbeddedValue(fallbackPattern);
if (StringUtils.hasLength(resolvedFallbackPattern)) {
resolvedFallbackPatterns.add(resolvedFallbackPattern);
}
}
if (!resolvedFallbackPatterns.isEmpty()) {
formatter.setFallbackPatterns(resolvedFallbackPatterns.toArray(new String[0]));
}
return formatter; return formatter;
} }

8
spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2021 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.
@ -29,7 +29,7 @@ import org.springframework.lang.Nullable;
/** /**
* A context that holds user-specific <code>java.time</code> (JSR-310) settings * A context that holds user-specific <code>java.time</code> (JSR-310) settings
* such as the user's Chronology (calendar system) and time zone. * such as the user's Chronology (calendar system) and time zone.
* A {@code null} property value indicate the user has not specified a setting. * <p>A {@code null} property value indicates the user has not specified a setting.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @since 4.0 * @since 4.0
@ -81,8 +81,8 @@ public class DateTimeContext {
/** /**
* Get the DateTimeFormatter with the this context's settings * Get the DateTimeFormatter with this context's settings applied to the
* applied to the base {@code formatter}. * base {@code formatter}.
* @param formatter the base formatter that establishes default * @param formatter the base formatter that establishes default
* formatting rules, generally context-independent * formatting rules, generally context-independent
* @return the contextual DateTimeFormatter * @return the contextual DateTimeFormatter

5
spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2021 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.
@ -69,9 +69,8 @@ public final class DateTimeContextHolder {
return dateTimeContextHolder.get(); return dateTimeContextHolder.get();
} }
/** /**
* Obtain a DateTimeFormatter with user-specific settings applied to the given base Formatter. * Obtain a DateTimeFormatter with user-specific settings applied to the given base formatter.
* @param formatter the base formatter that establishes default formatting rules * @param formatter the base formatter that establishes default formatting rules
* (generally user independent) * (generally user independent)
* @param locale the current user locale (may be {@code null} if not known) * @param locale the current user locale (may be {@code null} if not known)

10
spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2021 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.
@ -18,7 +18,6 @@ package org.springframework.format.datetime.standard;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.time.format.ResolverStyle;
import java.util.TimeZone; import java.util.TimeZone;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
@ -34,6 +33,7 @@ import org.springframework.util.StringUtils;
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
* @since 4.0 * @since 4.0
* @see #createDateTimeFormatter() * @see #createDateTimeFormatter()
* @see #createDateTimeFormatter(DateTimeFormatter) * @see #createDateTimeFormatter(DateTimeFormatter)
@ -180,11 +180,7 @@ public class DateTimeFormatterFactory {
public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) { public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) {
DateTimeFormatter dateTimeFormatter = null; DateTimeFormatter dateTimeFormatter = null;
if (StringUtils.hasLength(this.pattern)) { if (StringUtils.hasLength(this.pattern)) {
// Using strict parsing to align with Joda-Time and standard DateFormat behavior: dateTimeFormatter = DateTimeFormatterUtils.createStrictDateTimeFormatter(this.pattern);
// otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected.
// However, with strict parsing, a year digit needs to be specified as 'u'...
String patternToUse = StringUtils.replace(this.pattern, "yy", "uu");
dateTimeFormatter = DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT);
} }
else if (this.iso != null && this.iso != ISO.NONE) { else if (this.iso != null && this.iso != ISO.NONE) {
switch (this.iso) { switch (this.iso) {

40
spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterUtils.java

@ -0,0 +1,40 @@
/*
* Copyright 2002-2021 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.format.datetime.standard;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;
import org.springframework.util.StringUtils;
/**
* Internal {@link DateTimeFormatter} utilities.
*
* @author Juergen Hoeller
* @since 5.3.5
*/
abstract class DateTimeFormatterUtils {
static DateTimeFormatter createStrictDateTimeFormatter(String pattern) {
// Using strict parsing to align with Joda-Time and standard DateFormat behavior:
// otherwise, an overflow like e.g. Feb 29 for a non-leap-year wouldn't get rejected.
// However, with strict parsing, a year digit needs to be specified as 'u'...
String patternToUse = StringUtils.replace(pattern, "yy", "uu");
return DateTimeFormatter.ofPattern(patternToUse).withResolverStyle(ResolverStyle.STRICT);
}
}

16
spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2021 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.
@ -24,8 +24,10 @@ import java.time.OffsetTime;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.springframework.context.support.EmbeddedValueResolutionSupport; import org.springframework.context.support.EmbeddedValueResolutionSupport;
@ -40,6 +42,7 @@ import org.springframework.util.StringUtils;
* JSR-310 <code>java.time</code> package in JDK 8. * JSR-310 <code>java.time</code> package in JDK 8.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 4.0 * @since 4.0
* @see org.springframework.format.annotation.DateTimeFormat * @see org.springframework.format.annotation.DateTimeFormat
*/ */
@ -93,8 +96,17 @@ public class Jsr310DateTimeFormatAnnotationFormatterFactory extends EmbeddedValu
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) { public Parser<?> getParser(DateTimeFormat annotation, Class<?> fieldType) {
List<String> resolvedFallbackPatterns = new ArrayList<>();
for (String fallbackPattern : annotation.fallbackPatterns()) {
String resolvedFallbackPattern = resolveEmbeddedValue(fallbackPattern);
if (StringUtils.hasLength(resolvedFallbackPattern)) {
resolvedFallbackPatterns.add(resolvedFallbackPattern);
}
}
DateTimeFormatter formatter = getFormatter(annotation, fieldType); DateTimeFormatter formatter = getFormatter(annotation, fieldType);
return new TemporalAccessorParser((Class<? extends TemporalAccessor>) fieldType, formatter); return new TemporalAccessorParser((Class<? extends TemporalAccessor>) fieldType,
formatter, resolvedFallbackPatterns.toArray(new String[0]), annotation);
} }
/** /**

51
spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2021 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.
@ -24,16 +24,20 @@ import java.time.OffsetDateTime;
import java.time.OffsetTime; import java.time.OffsetTime;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAccessor;
import java.util.Locale; import java.util.Locale;
import org.springframework.format.Parser; import org.springframework.format.Parser;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
/** /**
* {@link Parser} implementation for a JSR-310 {@link java.time.temporal.TemporalAccessor}, * {@link Parser} implementation for a JSR-310 {@link java.time.temporal.TemporalAccessor},
* using a {@link java.time.format.DateTimeFormatter}) (the contextual one, if available). * using a {@link java.time.format.DateTimeFormatter} (the contextual one, if available).
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
* @since 4.0 * @since 4.0
* @see DateTimeContextHolder#getFormatter * @see DateTimeContextHolder#getFormatter
* @see java.time.LocalDate#parse(CharSequence, java.time.format.DateTimeFormatter) * @see java.time.LocalDate#parse(CharSequence, java.time.format.DateTimeFormatter)
@ -49,6 +53,12 @@ public final class TemporalAccessorParser implements Parser<TemporalAccessor> {
private final DateTimeFormatter formatter; private final DateTimeFormatter formatter;
@Nullable
private final String[] fallbackPatterns;
@Nullable
private final Object source;
/** /**
* Create a new TemporalAccessorParser for the given TemporalAccessor type. * Create a new TemporalAccessorParser for the given TemporalAccessor type.
@ -57,14 +67,49 @@ public final class TemporalAccessorParser implements Parser<TemporalAccessor> {
* @param formatter the base DateTimeFormatter instance * @param formatter the base DateTimeFormatter instance
*/ */
public TemporalAccessorParser(Class<? extends TemporalAccessor> temporalAccessorType, DateTimeFormatter formatter) { public TemporalAccessorParser(Class<? extends TemporalAccessor> temporalAccessorType, DateTimeFormatter formatter) {
this(temporalAccessorType, formatter, null, null);
}
TemporalAccessorParser(Class<? extends TemporalAccessor> temporalAccessorType, DateTimeFormatter formatter,
@Nullable String[] fallbackPatterns, @Nullable Object source) {
this.temporalAccessorType = temporalAccessorType; this.temporalAccessorType = temporalAccessorType;
this.formatter = formatter; this.formatter = formatter;
this.fallbackPatterns = fallbackPatterns;
this.source = source;
} }
@Override @Override
public TemporalAccessor parse(String text, Locale locale) throws ParseException { public TemporalAccessor parse(String text, Locale locale) throws ParseException {
DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale); try {
return doParse(text, locale, this.formatter);
}
catch (DateTimeParseException ex) {
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
for (String pattern : this.fallbackPatterns) {
try {
DateTimeFormatter fallbackFormatter = DateTimeFormatterUtils.createStrictDateTimeFormatter(pattern);
return doParse(text, locale, fallbackFormatter);
}
catch (DateTimeParseException ignoredException) {
// Ignore fallback parsing exceptions since the exception thrown below
// will include information from the "source" if available -- for example,
// the toString() of a @DateTimeFormat annotation.
}
}
}
if (this.source != null) {
throw new DateTimeParseException(
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
text, ex.getErrorIndex());
}
// else rethrow original exception
throw ex;
}
}
private TemporalAccessor doParse(String text, Locale locale, DateTimeFormatter formatter) throws DateTimeParseException {
DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(formatter, locale);
if (LocalDate.class == this.temporalAccessorType) { if (LocalDate.class == this.temporalAccessorType) {
return LocalDate.parse(text, formatterToUse); return LocalDate.parse(text, formatterToUse);
} }

243
spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2021 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.
@ -16,6 +16,7 @@
package org.springframework.format.datetime; package org.springframework.format.datetime;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
@ -25,16 +26,23 @@ import java.util.Locale;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder; import org.springframework.validation.DataBinder;
import org.springframework.validation.FieldError;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -42,10 +50,11 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Phillip Webb * @author Phillip Webb
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen
*/ */
public class DateFormattingTests { public class DateFormattingTests {
private FormattingConversionService conversionService; private final FormattingConversionService conversionService = new FormattingConversionService();
private DataBinder binder; private DataBinder binder;
@ -57,7 +66,6 @@ public class DateFormattingTests {
} }
private void setup(DateFormatterRegistrar registrar) { private void setup(DateFormatterRegistrar registrar) {
conversionService = new FormattingConversionService();
DefaultConversionService.addDefaultConverters(conversionService); DefaultConversionService.addDefaultConverters(conversionService);
registrar.registerFormatters(conversionService); registrar.registerFormatters(conversionService);
@ -87,34 +95,34 @@ public class DateFormattingTests {
@Test @Test
void testBindLongAnnotated() { void testBindLongAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("millisAnnotated", "10/31/09"); propertyValues.add("styleMillis", "10/31/09");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("millisAnnotated")).isEqualTo("10/31/09"); assertThat(binder.getBindingResult().getFieldValue("styleMillis")).isEqualTo("10/31/09");
} }
@Test @Test
void testBindCalendarAnnotated() { void testBindCalendarAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("calendarAnnotated", "10/31/09"); propertyValues.add("styleCalendar", "10/31/09");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("calendarAnnotated")).isEqualTo("10/31/09"); assertThat(binder.getBindingResult().getFieldValue("styleCalendar")).isEqualTo("10/31/09");
} }
@Test @Test
void testBindDateAnnotated() { void testBindDateAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotated", "10/31/09"); propertyValues.add("styleDate", "10/31/09");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("10/31/09"); assertThat(binder.getBindingResult().getFieldValue("styleDate")).isEqualTo("10/31/09");
} }
@Test @Test
void testBindDateArray() { void testBindDateArray() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotated", new String[]{"10/31/09 12:00 PM"}); propertyValues.add("styleDate", new String[]{"10/31/09 12:00 PM"});
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
} }
@ -122,10 +130,10 @@ public class DateFormattingTests {
@Test @Test
void testBindDateAnnotatedWithError() { void testBindDateAnnotatedWithError() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotated", "Oct X31, 2009"); propertyValues.add("styleDate", "Oct X31, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getFieldErrorCount("dateAnnotated")).isEqualTo(1); assertThat(binder.getBindingResult().getFieldErrorCount("styleDate")).isEqualTo(1);
assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("Oct X31, 2009"); assertThat(binder.getBindingResult().getFieldValue("styleDate")).isEqualTo("Oct X31, 2009");
} }
@Test @Test
@ -133,19 +141,19 @@ public class DateFormattingTests {
void testBindDateAnnotatedWithFallbackError() { void testBindDateAnnotatedWithFallbackError() {
// TODO This currently passes because of the Date(String) constructor fallback is used // TODO This currently passes because of the Date(String) constructor fallback is used
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotated", "Oct 031, 2009"); propertyValues.add("styleDate", "Oct 031, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getFieldErrorCount("dateAnnotated")).isEqualTo(1); assertThat(binder.getBindingResult().getFieldErrorCount("styleDate")).isEqualTo(1);
assertThat(binder.getBindingResult().getFieldValue("dateAnnotated")).isEqualTo("Oct 031, 2009"); assertThat(binder.getBindingResult().getFieldValue("styleDate")).isEqualTo("Oct 031, 2009");
} }
@Test @Test
void testBindDateAnnotatedPattern() { void testBindDateAnnotatedPattern() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotatedPattern", "10/31/09 1:05"); propertyValues.add("patternDate", "10/31/09 1:05");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("dateAnnotatedPattern")).isEqualTo("10/31/09 1:05"); assertThat(binder.getBindingResult().getFieldValue("patternDate")).isEqualTo("10/31/09 1:05");
} }
@Test @Test
@ -156,16 +164,17 @@ public class DateFormattingTests {
registrar.setFormatter(dateFormatter); registrar.setFormatter(dateFormatter);
setup(registrar); setup(registrar);
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotatedPattern", "10/31/09 1:05"); propertyValues.add("patternDate", "10/31/09 1:05");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); BindingResult bindingResult = binder.getBindingResult();
assertThat(binder.getBindingResult().getFieldValue("dateAnnotatedPattern")).isEqualTo("10/31/09 1:05"); assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue("patternDate")).isEqualTo("10/31/09 1:05");
} }
@Test @Test
void testBindDateTimeOverflow() { void testBindDateTimeOverflow() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateAnnotatedPattern", "02/29/09 12:00 PM"); propertyValues.add("patternDate", "02/29/09 12:00 PM");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1);
} }
@ -200,10 +209,10 @@ public class DateFormattingTests {
@Test @Test
void testBindNestedDateAnnotated() { void testBindNestedDateAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("children[0].dateAnnotated", "10/31/09"); propertyValues.add("children[0].styleDate", "10/31/09");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("children[0].dateAnnotated")).isEqualTo("10/31/09"); assertThat(binder.getBindingResult().getFieldValue("children[0].styleDate")).isEqualTo("10/31/09");
} }
@Test @Test
@ -247,35 +256,127 @@ public class DateFormattingTests {
} }
@Nested
class FallbackPatternTests {
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02", "2021.03.02", "20210302", "3/2/21"})
void styleCalendar(String propertyValue) {
String propertyName = "styleCalendarWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("3/2/21");
}
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02", "2021.03.02", "20210302", "3/2/21"})
void styleDate(String propertyValue) {
String propertyName = "styleDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("3/2/21");
}
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02", "2021.03.02", "20210302", "3/2/21"})
void patternDate(String propertyValue) {
String propertyName = "patternDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
}
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02", "2021.03.02", "20210302", "3/2/21"})
void isoDate(String propertyValue) {
String propertyName = "isoDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
}
@Test
void patternDateWithUnsupportedPattern() {
String propertyValue = "210302";
String propertyName = "patternDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(1);
FieldError fieldError = bindingResult.getFieldError(propertyName);
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).getCause()
.hasMessageContaining("for value '210302'")
.hasCauseInstanceOf(IllegalArgumentException.class).getCause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(ParseException.class).getCause()
// Unable to parse date time value "210302" using configuration from
// @org.springframework.format.annotation.DateTimeFormat(
// pattern=yyyy-MM-dd, style=SS, iso=NONE, fallbackPatterns=[M/d/yy, yyyyMMdd, yyyy.MM.dd])
.hasMessageContainingAll(
"Unable to parse date time value \"210302\" using configuration from",
"@org.springframework.format.annotation.DateTimeFormat",
"yyyy-MM-dd", "M/d/yy", "yyyyMMdd", "yyyy.MM.dd");
}
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static class SimpleDateBean { private static class SimpleDateBean {
private Long millis; private Long millis;
private Long millisAnnotated; private Long styleMillis;
@DateTimeFormat(style="S-") @DateTimeFormat(style = "S-")
private Calendar calendarAnnotated; private Calendar styleCalendar;
@DateTimeFormat(style="S-") @DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
private Date dateAnnotated; private Calendar styleCalendarWithFallbackPatterns;
@DateTimeFormat(style = "S-")
private Date styleDate;
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
private Date styleDateWithFallbackPatterns;
@DateTimeFormat(pattern = "M/d/yy h:mm")
private Date patternDate;
@DateTimeFormat(pattern="M/d/yy h:mm") @DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
private Date dateAnnotatedPattern; private Date patternDateWithFallbackPatterns;
@DateTimeFormat(iso=ISO.DATE) @DateTimeFormat(iso = ISO.DATE)
private Date isoDate; private Date isoDate;
@DateTimeFormat(iso=ISO.TIME) @DateTimeFormat(iso = ISO.DATE, fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
private Date isoDateWithFallbackPatterns;
@DateTimeFormat(iso = ISO.TIME)
private Date isoTime; private Date isoTime;
@DateTimeFormat(iso=ISO.DATE_TIME) @DateTimeFormat(iso = ISO.DATE_TIME)
private Date isoDateTime; private Date isoDateTime;
private final List<SimpleDateBean> children = new ArrayList<>(); private final List<SimpleDateBean> children = new ArrayList<>();
public Long getMillis() { public Long getMillis() {
return millis; return this.millis;
} }
public void setMillis(Long millis) { public void setMillis(Long millis) {
@ -283,48 +384,80 @@ public class DateFormattingTests {
} }
@DateTimeFormat(style="S-") @DateTimeFormat(style="S-")
public Long getMillisAnnotated() { public Long getStyleMillis() {
return millisAnnotated; return this.styleMillis;
}
public void setStyleMillis(@DateTimeFormat(style="S-") Long styleMillis) {
this.styleMillis = styleMillis;
}
public Calendar getStyleCalendar() {
return this.styleCalendar;
}
public void setStyleCalendar(Calendar styleCalendar) {
this.styleCalendar = styleCalendar;
}
public Calendar getStyleCalendarWithFallbackPatterns() {
return this.styleCalendarWithFallbackPatterns;
}
public void setStyleCalendarWithFallbackPatterns(Calendar styleCalendarWithFallbackPatterns) {
this.styleCalendarWithFallbackPatterns = styleCalendarWithFallbackPatterns;
}
public Date getStyleDate() {
return this.styleDate;
} }
public void setMillisAnnotated(@DateTimeFormat(style="S-") Long millisAnnotated) { public void setStyleDate(Date styleDate) {
this.millisAnnotated = millisAnnotated; this.styleDate = styleDate;
} }
public Calendar getCalendarAnnotated() { public Date getStyleDateWithFallbackPatterns() {
return calendarAnnotated; return this.styleDateWithFallbackPatterns;
} }
public void setCalendarAnnotated(Calendar calendarAnnotated) { public void setStyleDateWithFallbackPatterns(Date styleDateWithFallbackPatterns) {
this.calendarAnnotated = calendarAnnotated; this.styleDateWithFallbackPatterns = styleDateWithFallbackPatterns;
} }
public Date getDateAnnotated() { public Date getPatternDate() {
return dateAnnotated; return this.patternDate;
} }
public void setDateAnnotated(Date dateAnnotated) { public void setPatternDate(Date patternDate) {
this.dateAnnotated = dateAnnotated; this.patternDate = patternDate;
} }
public Date getDateAnnotatedPattern() { public Date getPatternDateWithFallbackPatterns() {
return dateAnnotatedPattern; return this.patternDateWithFallbackPatterns;
} }
public void setDateAnnotatedPattern(Date dateAnnotatedPattern) { public void setPatternDateWithFallbackPatterns(Date patternDateWithFallbackPatterns) {
this.dateAnnotatedPattern = dateAnnotatedPattern; this.patternDateWithFallbackPatterns = patternDateWithFallbackPatterns;
} }
public Date getIsoDate() { public Date getIsoDate() {
return isoDate; return this.isoDate;
} }
public void setIsoDate(Date isoDate) { public void setIsoDate(Date isoDate) {
this.isoDate = isoDate; this.isoDate = isoDate;
} }
public Date getIsoDateWithFallbackPatterns() {
return this.isoDateWithFallbackPatterns;
}
public void setIsoDateWithFallbackPatterns(Date isoDateWithFallbackPatterns) {
this.isoDateWithFallbackPatterns = isoDateWithFallbackPatterns;
}
public Date getIsoTime() { public Date getIsoTime() {
return isoTime; return this.isoTime;
} }
public void setIsoTime(Date isoTime) { public void setIsoTime(Date isoTime) {
@ -332,7 +465,7 @@ public class DateFormattingTests {
} }
public Date getIsoDateTime() { public Date getIsoDateTime() {
return isoDateTime; return this.isoDateTime;
} }
public void setIsoDateTime(Date isoDateTime) { public void setIsoDateTime(Date isoDateTime) {
@ -340,7 +473,7 @@ public class DateFormattingTests {
} }
public List<SimpleDateBean> getChildren() { public List<SimpleDateBean> getChildren() {
return children; return this.children;
} }
} }

362
spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2021 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.
@ -28,6 +28,7 @@ import java.time.Year;
import java.time.YearMonth; import java.time.YearMonth;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle; import java.time.format.FormatStyle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
@ -38,15 +39,22 @@ import java.util.TimeZone;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.TypeMismatchException;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO; import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder; import org.springframework.validation.DataBinder;
import org.springframework.validation.FieldError;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -54,22 +62,22 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
*/ */
public class DateTimeFormattingTests { class DateTimeFormattingTests {
private FormattingConversionService conversionService; private final FormattingConversionService conversionService = new FormattingConversionService();
private DataBinder binder; private DataBinder binder;
@BeforeEach @BeforeEach
public void setup() { void setup() {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
setup(registrar); setup(registrar);
} }
private void setup(DateTimeFormatterRegistrar registrar) { private void setup(DateTimeFormatterRegistrar registrar) {
conversionService = new FormattingConversionService();
DefaultConversionService.addDefaultConverters(conversionService); DefaultConversionService.addDefaultConverters(conversionService);
registrar.registerFormatters(conversionService); registrar.registerFormatters(conversionService);
@ -85,14 +93,14 @@ public class DateTimeFormattingTests {
} }
@AfterEach @AfterEach
public void cleanup() { void cleanup() {
LocaleContextHolder.setLocale(null); LocaleContextHolder.setLocale(null);
DateTimeContextHolder.setDateTimeContext(null); DateTimeContextHolder.setDateTimeContext(null);
} }
@Test @Test
public void testBindLocalDate() { void testBindLocalDate() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDate", "10/31/09"); propertyValues.add("localDate", "10/31/09");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -101,7 +109,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalDateWithSpecificStyle() { void testBindLocalDateWithSpecificStyle() {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateStyle(FormatStyle.LONG); registrar.setDateStyle(FormatStyle.LONG);
setup(registrar); setup(registrar);
@ -113,7 +121,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalDateWithSpecificFormatter() { void testBindLocalDateWithSpecificFormatter() {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd"));
setup(registrar); setup(registrar);
@ -125,7 +133,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalDateArray() { void testBindLocalDateArray() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDate", new String[] {"10/31/09"}); propertyValues.add("localDate", new String[] {"10/31/09"});
binder.bind(propertyValues); binder.bind(propertyValues);
@ -133,54 +141,54 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalDateAnnotated() { void testBindLocalDateAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateAnnotated", "Oct 31, 2009"); propertyValues.add("styleLocalDate", "Oct 31, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 31, 2009"); assertThat(binder.getBindingResult().getFieldValue("styleLocalDate")).isEqualTo("Oct 31, 2009");
} }
@Test @Test
public void testBindLocalDateAnnotatedWithError() { void testBindLocalDateAnnotatedWithError() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateAnnotated", "Oct -31, 2009"); propertyValues.add("styleLocalDate", "Oct -31, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getFieldErrorCount("localDateAnnotated")).isEqualTo(1); assertThat(binder.getBindingResult().getFieldErrorCount("styleLocalDate")).isEqualTo(1);
assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct -31, 2009"); assertThat(binder.getBindingResult().getFieldValue("styleLocalDate")).isEqualTo("Oct -31, 2009");
} }
@Test @Test
public void testBindNestedLocalDateAnnotated() { void testBindNestedLocalDateAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("children[0].localDateAnnotated", "Oct 31, 2009"); propertyValues.add("children[0].styleLocalDate", "Oct 31, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("children[0].localDateAnnotated")).isEqualTo("Oct 31, 2009"); assertThat(binder.getBindingResult().getFieldValue("children[0].styleLocalDate")).isEqualTo("Oct 31, 2009");
} }
@Test @Test
public void testBindLocalDateAnnotatedWithDirectFieldAccess() { void testBindLocalDateAnnotatedWithDirectFieldAccess() {
binder.initDirectFieldAccess(); binder.initDirectFieldAccess();
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateAnnotated", "Oct 31, 2009"); propertyValues.add("styleLocalDate", "Oct 31, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct 31, 2009"); assertThat(binder.getBindingResult().getFieldValue("styleLocalDate")).isEqualTo("Oct 31, 2009");
} }
@Test @Test
public void testBindLocalDateAnnotatedWithDirectFieldAccessAndError() { void testBindLocalDateAnnotatedWithDirectFieldAccessAndError() {
binder.initDirectFieldAccess(); binder.initDirectFieldAccess();
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateAnnotated", "Oct -31, 2009"); propertyValues.add("styleLocalDate", "Oct -31, 2009");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getFieldErrorCount("localDateAnnotated")).isEqualTo(1); assertThat(binder.getBindingResult().getFieldErrorCount("styleLocalDate")).isEqualTo(1);
assertThat(binder.getBindingResult().getFieldValue("localDateAnnotated")).isEqualTo("Oct -31, 2009"); assertThat(binder.getBindingResult().getFieldValue("styleLocalDate")).isEqualTo("Oct -31, 2009");
} }
@Test @Test
public void testBindLocalDateFromJavaUtilCalendar() { void testBindLocalDateFromJavaUtilCalendar() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDate", new GregorianCalendar(2009, 9, 31, 0, 0)); propertyValues.add("localDate", new GregorianCalendar(2009, 9, 31, 0, 0));
binder.bind(propertyValues); binder.bind(propertyValues);
@ -189,7 +197,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalTime() { void testBindLocalTime() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localTime", "12:00 PM"); propertyValues.add("localTime", "12:00 PM");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -198,7 +206,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalTimeWithSpecificStyle() { void testBindLocalTimeWithSpecificStyle() {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setTimeStyle(FormatStyle.MEDIUM); registrar.setTimeStyle(FormatStyle.MEDIUM);
setup(registrar); setup(registrar);
@ -210,7 +218,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalTimeWithSpecificFormatter() { void testBindLocalTimeWithSpecificFormatter() {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setTimeFormatter(DateTimeFormatter.ofPattern("HHmmss")); registrar.setTimeFormatter(DateTimeFormatter.ofPattern("HHmmss"));
setup(registrar); setup(registrar);
@ -222,16 +230,16 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalTimeAnnotated() { void testBindLocalTimeAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localTimeAnnotated", "12:00:00 PM"); propertyValues.add("styleLocalTime", "12:00:00 PM");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("localTimeAnnotated")).isEqualTo("12:00:00 PM"); assertThat(binder.getBindingResult().getFieldValue("styleLocalTime")).isEqualTo("12:00:00 PM");
} }
@Test @Test
public void testBindLocalTimeFromJavaUtilCalendar() { void testBindLocalTimeFromJavaUtilCalendar() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localTime", new GregorianCalendar(1970, 0, 0, 12, 0)); propertyValues.add("localTime", new GregorianCalendar(1970, 0, 0, 12, 0));
binder.bind(propertyValues); binder.bind(propertyValues);
@ -240,7 +248,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalDateTime() { void testBindLocalDateTime() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateTime", LocalDateTime.of(2009, 10, 31, 12, 0)); propertyValues.add("localDateTime", LocalDateTime.of(2009, 10, 31, 12, 0));
binder.bind(propertyValues); binder.bind(propertyValues);
@ -251,18 +259,18 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindLocalDateTimeAnnotated() { void testBindLocalDateTimeAnnotated() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateTimeAnnotated", LocalDateTime.of(2009, 10, 31, 12, 0)); propertyValues.add("styleLocalDateTime", LocalDateTime.of(2009, 10, 31, 12, 0));
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
String value = binder.getBindingResult().getFieldValue("localDateTimeAnnotated").toString(); String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString();
assertThat(value.startsWith("Oct 31, 2009")).isTrue(); assertThat(value.startsWith("Oct 31, 2009")).isTrue();
assertThat(value.endsWith("12:00:00 PM")).isTrue(); assertThat(value.endsWith("12:00:00 PM")).isTrue();
} }
@Test @Test
public void testBindLocalDateTimeFromJavaUtilCalendar() { void testBindLocalDateTimeFromJavaUtilCalendar() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("localDateTime", new GregorianCalendar(2009, 9, 31, 12, 0)); propertyValues.add("localDateTime", new GregorianCalendar(2009, 9, 31, 12, 0));
binder.bind(propertyValues); binder.bind(propertyValues);
@ -273,7 +281,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindDateTimeWithSpecificStyle() { void testBindDateTimeWithSpecificStyle() {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setDateTimeStyle(FormatStyle.MEDIUM); registrar.setDateTimeStyle(FormatStyle.MEDIUM);
setup(registrar); setup(registrar);
@ -287,69 +295,69 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindDateTimeAnnotatedPattern() { void testBindPatternLocalDateTime() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateTimeAnnotatedPattern", "10/31/09 12:00 PM"); propertyValues.add("patternLocalDateTime", "10/31/09 12:00 PM");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("dateTimeAnnotatedPattern")).isEqualTo("10/31/09 12:00 PM"); assertThat(binder.getBindingResult().getFieldValue("patternLocalDateTime")).isEqualTo("10/31/09 12:00 PM");
} }
@Test @Test
public void testBindDateTimeOverflow() { void testBindDateTimeOverflow() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("dateTimeAnnotatedPattern", "02/29/09 12:00 PM"); propertyValues.add("patternLocalDateTime", "02/29/09 12:00 PM");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(1);
} }
@Test @Test
public void testBindISODate() { void testBindISODate() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("isoDate", "2009-10-31"); propertyValues.add("isoLocalDate", "2009-10-31");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("isoDate")).isEqualTo("2009-10-31"); assertThat(binder.getBindingResult().getFieldValue("isoLocalDate")).isEqualTo("2009-10-31");
} }
@Test @Test
public void testBindISOTime() { void testBindISOTime() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("isoTime", "12:00:00"); propertyValues.add("isoLocalTime", "12:00:00");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("isoTime")).isEqualTo("12:00:00"); assertThat(binder.getBindingResult().getFieldValue("isoLocalTime")).isEqualTo("12:00:00");
} }
@Test @Test
public void testBindISOTimeWithZone() { void testBindISOTimeWithZone() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("isoTime", "12:00:00.000-05:00"); propertyValues.add("isoLocalTime", "12:00:00.000-05:00");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("isoTime")).isEqualTo("12:00:00"); assertThat(binder.getBindingResult().getFieldValue("isoLocalTime")).isEqualTo("12:00:00");
} }
@Test @Test
public void testBindISODateTime() { void testBindISODateTime() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("isoDateTime", "2009-10-31T12:00:00"); propertyValues.add("isoLocalDateTime", "2009-10-31T12:00:00");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("isoDateTime")).isEqualTo("2009-10-31T12:00:00"); assertThat(binder.getBindingResult().getFieldValue("isoLocalDateTime")).isEqualTo("2009-10-31T12:00:00");
} }
@Test @Test
public void testBindISODateTimeWithZone() { void testBindISODateTimeWithZone() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("isoDateTime", "2009-10-31T12:00:00.000Z"); propertyValues.add("isoLocalDateTime", "2009-10-31T12:00:00.000Z");
binder.bind(propertyValues); binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0); assertThat(binder.getBindingResult().getErrorCount()).isEqualTo(0);
assertThat(binder.getBindingResult().getFieldValue("isoDateTime")).isEqualTo("2009-10-31T12:00:00"); assertThat(binder.getBindingResult().getFieldValue("isoLocalDateTime")).isEqualTo("2009-10-31T12:00:00");
} }
@Test @Test
public void testBindInstant() { void testBindInstant() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("instant", "2009-10-31T12:00:00.000Z"); propertyValues.add("instant", "2009-10-31T12:00:00.000Z");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -359,7 +367,7 @@ public class DateTimeFormattingTests {
@Test @Test
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public void testBindInstantFromJavaUtilDate() { void testBindInstantFromJavaUtilDate() {
TimeZone defaultZone = TimeZone.getDefault(); TimeZone defaultZone = TimeZone.getDefault();
TimeZone.setDefault(TimeZone.getTimeZone("GMT")); TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
try { try {
@ -375,7 +383,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindPeriod() { void testBindPeriod() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("period", "P6Y3M1D"); propertyValues.add("period", "P6Y3M1D");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -384,7 +392,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindDuration() { void testBindDuration() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("duration", "PT8H6M12.345S"); propertyValues.add("duration", "PT8H6M12.345S");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -393,7 +401,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindYear() { void testBindYear() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("year", "2007"); propertyValues.add("year", "2007");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -402,7 +410,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindMonth() { void testBindMonth() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("month", "JULY"); propertyValues.add("month", "JULY");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -411,7 +419,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindMonthInAnyCase() { void testBindMonthInAnyCase() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("month", "July"); propertyValues.add("month", "July");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -420,7 +428,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindYearMonth() { void testBindYearMonth() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("yearMonth", "2007-12"); propertyValues.add("yearMonth", "2007-12");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -429,7 +437,7 @@ public class DateTimeFormattingTests {
} }
@Test @Test
public void testBindMonthDay() { void testBindMonthDay() {
MutablePropertyValues propertyValues = new MutablePropertyValues(); MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("monthDay", "--12-03"); propertyValues.add("monthDay", "--12-03");
binder.bind(propertyValues); binder.bind(propertyValues);
@ -437,35 +445,125 @@ public class DateTimeFormattingTests {
assertThat(binder.getBindingResult().getFieldValue("monthDay").toString().equals("--12-03")).isTrue(); assertThat(binder.getBindingResult().getFieldValue("monthDay").toString().equals("--12-03")).isTrue();
} }
@Nested
class FallbackPatternTests {
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02", "2021.03.02", "20210302", "3/2/21"})
void styleLocalDate(String propertyValue) {
String propertyName = "styleLocalDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("3/2/21");
}
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"2021-03-02", "2021.03.02", "20210302", "3/2/21"})
void patternLocalDate(String propertyValue) {
String propertyName = "patternLocalDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
}
@ParameterizedTest(name = "input date: {0}")
@ValueSource(strings = {"12:00:00 PM", "12:00:00", "12:00"})
void styleLocalTime(String propertyValue) {
String propertyName = "styleLocalTimeWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("12:00:00 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) {
String propertyName = "isoLocalDateTimeWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02T12:00:00");
}
@Test
void patternLocalDateWithUnsupportedPattern() {
String propertyValue = "210302";
String propertyName = "patternLocalDateWithFallbackPatterns";
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add(propertyName, propertyValue);
binder.bind(propertyValues);
BindingResult bindingResult = binder.getBindingResult();
assertThat(bindingResult.getErrorCount()).isEqualTo(1);
FieldError fieldError = bindingResult.getFieldError(propertyName);
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternLocalDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).getCause()
.hasMessageContaining("for value '210302'")
.hasCauseInstanceOf(IllegalArgumentException.class).getCause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(DateTimeParseException.class).getCause()
// Unable to parse date time value "210302" using configuration from
// @org.springframework.format.annotation.DateTimeFormat(
// pattern=yyyy-MM-dd, style=SS, iso=NONE, fallbackPatterns=[M/d/yy, yyyyMMdd, yyyy.MM.dd])
.hasMessageContainingAll(
"Unable to parse date time value \"210302\" using configuration from",
"@org.springframework.format.annotation.DateTimeFormat",
"yyyy-MM-dd", "M/d/yy", "yyyyMMdd", "yyyy.MM.dd");
}
}
public static class DateTimeBean { public static class DateTimeBean {
private LocalDate localDate; private LocalDate localDate;
@DateTimeFormat(style = "M-") @DateTimeFormat(style = "M-")
private LocalDate localDateAnnotated; private LocalDate styleLocalDate;
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
private LocalDate styleLocalDateWithFallbackPatterns;
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
private LocalDate patternLocalDateWithFallbackPatterns;
private LocalTime localTime; private LocalTime localTime;
@DateTimeFormat(style = "-M") @DateTimeFormat(style = "-M")
private LocalTime localTimeAnnotated; private LocalTime styleLocalTime;
@DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"})
private LocalTime styleLocalTimeWithFallbackPatterns;
private LocalDateTime localDateTime; private LocalDateTime localDateTime;
@DateTimeFormat(style = "MM") @DateTimeFormat(style = "MM")
private LocalDateTime localDateTimeAnnotated; private LocalDateTime styleLocalDateTime;
@DateTimeFormat(pattern = "M/d/yy h:mm a") @DateTimeFormat(pattern = "M/d/yy h:mm a")
private LocalDateTime dateTimeAnnotatedPattern; private LocalDateTime patternLocalDateTime;
@DateTimeFormat(iso = ISO.DATE) @DateTimeFormat(iso = ISO.DATE)
private LocalDate isoDate; private LocalDate isoLocalDate;
@DateTimeFormat(iso = ISO.TIME) @DateTimeFormat(iso = ISO.TIME)
private LocalTime isoTime; private LocalTime isoLocalTime;
@DateTimeFormat(iso = ISO.DATE_TIME) @DateTimeFormat(iso = ISO.DATE_TIME)
private LocalDateTime isoDateTime; private LocalDateTime isoLocalDateTime;
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
private LocalDateTime isoLocalDateTimeWithFallbackPatterns;
private Instant instant; private Instant instant;
@ -483,88 +581,120 @@ public class DateTimeFormattingTests {
private final List<DateTimeBean> children = new ArrayList<>(); private final List<DateTimeBean> children = new ArrayList<>();
public LocalDate getLocalDate() { public LocalDate getLocalDate() {
return localDate; return this.localDate;
} }
public void setLocalDate(LocalDate localDate) { public void setLocalDate(LocalDate localDate) {
this.localDate = localDate; this.localDate = localDate;
} }
public LocalDate getLocalDateAnnotated() { public LocalDate getStyleLocalDate() {
return localDateAnnotated; return this.styleLocalDate;
} }
public void setLocalDateAnnotated(LocalDate localDateAnnotated) { public void setStyleLocalDate(LocalDate styleLocalDate) {
this.localDateAnnotated = localDateAnnotated; this.styleLocalDate = styleLocalDate;
}
public LocalDate getStyleLocalDateWithFallbackPatterns() {
return this.styleLocalDateWithFallbackPatterns;
}
public void setStyleLocalDateWithFallbackPatterns(LocalDate styleLocalDateWithFallbackPatterns) {
this.styleLocalDateWithFallbackPatterns = styleLocalDateWithFallbackPatterns;
}
public LocalDate getPatternLocalDateWithFallbackPatterns() {
return this.patternLocalDateWithFallbackPatterns;
}
public void setPatternLocalDateWithFallbackPatterns(LocalDate patternLocalDateWithFallbackPatterns) {
this.patternLocalDateWithFallbackPatterns = patternLocalDateWithFallbackPatterns;
} }
public LocalTime getLocalTime() { public LocalTime getLocalTime() {
return localTime; return this.localTime;
} }
public void setLocalTime(LocalTime localTime) { public void setLocalTime(LocalTime localTime) {
this.localTime = localTime; this.localTime = localTime;
} }
public LocalTime getLocalTimeAnnotated() { public LocalTime getStyleLocalTime() {
return localTimeAnnotated; return this.styleLocalTime;
}
public void setStyleLocalTime(LocalTime styleLocalTime) {
this.styleLocalTime = styleLocalTime;
}
public LocalTime getStyleLocalTimeWithFallbackPatterns() {
return this.styleLocalTimeWithFallbackPatterns;
} }
public void setLocalTimeAnnotated(LocalTime localTimeAnnotated) { public void setStyleLocalTimeWithFallbackPatterns(LocalTime styleLocalTimeWithFallbackPatterns) {
this.localTimeAnnotated = localTimeAnnotated; this.styleLocalTimeWithFallbackPatterns = styleLocalTimeWithFallbackPatterns;
} }
public LocalDateTime getLocalDateTime() { public LocalDateTime getLocalDateTime() {
return localDateTime; return this.localDateTime;
} }
public void setLocalDateTime(LocalDateTime localDateTime) { public void setLocalDateTime(LocalDateTime localDateTime) {
this.localDateTime = localDateTime; this.localDateTime = localDateTime;
} }
public LocalDateTime getLocalDateTimeAnnotated() { public LocalDateTime getStyleLocalDateTime() {
return localDateTimeAnnotated; return this.styleLocalDateTime;
}
public void setStyleLocalDateTime(LocalDateTime styleLocalDateTime) {
this.styleLocalDateTime = styleLocalDateTime;
}
public LocalDateTime getPatternLocalDateTime() {
return this.patternLocalDateTime;
} }
public void setLocalDateTimeAnnotated(LocalDateTime localDateTimeAnnotated) { public void setPatternLocalDateTime(LocalDateTime patternLocalDateTime) {
this.localDateTimeAnnotated = localDateTimeAnnotated; this.patternLocalDateTime = patternLocalDateTime;
} }
public LocalDateTime getDateTimeAnnotatedPattern() { public LocalDate getIsoLocalDate() {
return dateTimeAnnotatedPattern; return this.isoLocalDate;
} }
public void setDateTimeAnnotatedPattern(LocalDateTime dateTimeAnnotatedPattern) { public void setIsoLocalDate(LocalDate isoLocalDate) {
this.dateTimeAnnotatedPattern = dateTimeAnnotatedPattern; this.isoLocalDate = isoLocalDate;
} }
public LocalDate getIsoDate() { public LocalTime getIsoLocalTime() {
return isoDate; return this.isoLocalTime;
} }
public void setIsoDate(LocalDate isoDate) { public void setIsoLocalTime(LocalTime isoLocalTime) {
this.isoDate = isoDate; this.isoLocalTime = isoLocalTime;
} }
public LocalTime getIsoTime() { public LocalDateTime getIsoLocalDateTime() {
return isoTime; return this.isoLocalDateTime;
} }
public void setIsoTime(LocalTime isoTime) { public void setIsoLocalDateTime(LocalDateTime isoLocalDateTime) {
this.isoTime = isoTime; this.isoLocalDateTime = isoLocalDateTime;
} }
public LocalDateTime getIsoDateTime() { public LocalDateTime getIsoLocalDateTimeWithFallbackPatterns() {
return isoDateTime; return this.isoLocalDateTimeWithFallbackPatterns;
} }
public void setIsoDateTime(LocalDateTime isoDateTime) { public void setIsoLocalDateTimeWithFallbackPatterns(LocalDateTime isoLocalDateTimeWithFallbackPatterns) {
this.isoDateTime = isoDateTime; this.isoLocalDateTimeWithFallbackPatterns = isoLocalDateTimeWithFallbackPatterns;
} }
public Instant getInstant() { public Instant getInstant() {
return instant; return this.instant;
} }
public void setInstant(Instant instant) { public void setInstant(Instant instant) {
@ -572,7 +702,7 @@ public class DateTimeFormattingTests {
} }
public Period getPeriod() { public Period getPeriod() {
return period; return this.period;
} }
public void setPeriod(Period period) { public void setPeriod(Period period) {
@ -580,7 +710,7 @@ public class DateTimeFormattingTests {
} }
public Duration getDuration() { public Duration getDuration() {
return duration; return this.duration;
} }
public void setDuration(Duration duration) { public void setDuration(Duration duration) {
@ -588,7 +718,7 @@ public class DateTimeFormattingTests {
} }
public Year getYear() { public Year getYear() {
return year; return this.year;
} }
public void setYear(Year year) { public void setYear(Year year) {
@ -596,7 +726,7 @@ public class DateTimeFormattingTests {
} }
public Month getMonth() { public Month getMonth() {
return month; return this.month;
} }
public void setMonth(Month month) { public void setMonth(Month month) {
@ -604,7 +734,7 @@ public class DateTimeFormattingTests {
} }
public YearMonth getYearMonth() { public YearMonth getYearMonth() {
return yearMonth; return this.yearMonth;
} }
public void setYearMonth(YearMonth yearMonth) { public void setYearMonth(YearMonth yearMonth) {
@ -612,7 +742,7 @@ public class DateTimeFormattingTests {
} }
public MonthDay getMonthDay() { public MonthDay getMonthDay() {
return monthDay; return this.monthDay;
} }
public void setMonthDay(MonthDay monthDay) { public void setMonthDay(MonthDay monthDay) {
@ -620,7 +750,7 @@ public class DateTimeFormattingTests {
} }
public List<DateTimeBean> getChildren() { public List<DateTimeBean> getChildren() {
return children; return this.children;
} }
} }

Loading…
Cancel
Save