From 173323757935bbc76afc21b915b8bc5093a56937 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 23 Apr 2013 13:59:19 +0200 Subject: [PATCH] Initial support for JDK 8 Date-Time (JSR-310) This is largely derived from our existing Joda-Time support, with corresponding classes wherever possible. Issue: SPR-9641 --- .../datetime/standard/DateTimeContext.java | 85 ++++ .../standard/DateTimeContextHolder.java | 79 ++++ .../standard/DateTimeFormatterFactory.java | 211 +++++++++ .../DateTimeFormatterFactoryBean.java | 58 +++ .../standard/DateTimeFormatterRegistrar.java | 201 +++++++++ .../datetime/standard/InstantFormatter.java | 44 ++ ...eTimeFormatAnnotationFormatterFactory.java | 104 +++++ .../standard/TemporalAccessorParser.java | 90 ++++ .../standard/TemporalAccessorPrinter.java | 52 +++ .../datetime/standard/package-info.java | 4 + .../DefaultFormattingConversionService.java | 17 +- .../DateTimeFormatterFactoryBeanTests.java | 60 +++ .../DateTimeFormatterFactoryTests.java | 104 +++++ .../standard/DateTimeFormattingTests.java | 422 ++++++++++++++++++ 14 files changed, 1529 insertions(+), 2 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java create mode 100644 spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java create mode 100644 spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java create mode 100644 spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java create mode 100644 spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java new file mode 100644 index 0000000000..f5b64dcbc1 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.ZoneId; +import java.time.chrono.Chronology; +import java.time.format.DateTimeFormatter; + +/** + * A context that holds user-specific java.time (JSR-310) settings + * such as the user's Chronology (calendar system) and time zone. + * A {@code null} property value indicate the user has not specified a setting. + * + * @author Juergen Hoeller + * @since 4.0 + * @see DateTimeContextHolder + */ +public class DateTimeContext { + + private Chronology chronology; + + private ZoneId timeZone; + + + /** + * Set the user's chronology. + */ + public void setChronology(Chronology chronology) { + this.chronology = chronology; + } + + /** + * The user's chronology (calendar system), if any. + */ + public Chronology getChronology() { + return this.chronology; + } + + /** + * Set the user's timezone. + */ + public void setTimeZone(ZoneId timeZone) { + this.timeZone = timeZone; + } + + /** + * The user's timezone, if any. + */ + public ZoneId getTimeZone() { + return timeZone; + } + + + /** + * Get the DateTimeFormatter with the this context's settings + * applied to the base {@code formatter}. + * @param formatter the base formatter that establishes default + * formatting rules, generally context-independent + * @return the contextual DateTimeFormatter + */ + public DateTimeFormatter getFormatter(DateTimeFormatter formatter) { + if (this.chronology != null) { + formatter = formatter.withChronology(this.chronology); + } + if (this.timeZone != null) { + formatter = formatter.withZone(this.timeZone); + } + return formatter; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java new file mode 100644 index 0000000000..85febf16bd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.util.Locale; + +import org.springframework.core.NamedThreadLocal; + +/** + * A holder for a thread-local user {@link DateTimeContext}. + * + * @author Juergen Hoeller + * @since 4.0 + */ +public final class DateTimeContextHolder { + + private static final ThreadLocal dateTimeContextHolder = + new NamedThreadLocal("DateTime Context"); + + + /** + * Reset the DateTimeContext for the current thread. + */ + public static void resetDateTimeContext() { + dateTimeContextHolder.remove(); + } + + /** + * Associate the given DateTimeContext with the current thread. + * @param dateTimeContext the current DateTimeContext, + * or {@code null} to reset the thread-bound context + */ + public static void setDateTimeContext(DateTimeContext dateTimeContext) { + if (dateTimeContext == null) { + resetDateTimeContext(); + } + else { + dateTimeContextHolder.set(dateTimeContext); + } + } + + /** + * Return the DateTimeContext associated with the current thread, if any. + * @return the current DateTimeContext, or {@code null} if none + */ + public static DateTimeContext getDateTimeContext() { + return dateTimeContextHolder.get(); + } + + + /** + * Obtain a DateTimeFormatter with user-specific settings applied to the given base Formatter. + * @param formatter the base formatter that establishes default formatting rules + * (generally user independent) + * @param locale the current user locale (may be {@code null} if not known) + * @return the user-specific DateTimeFormatter + */ + public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, Locale locale) { + DateTimeFormatter formatterToUse = (locale != null ? formatter.withLocale(locale) : formatter); + DateTimeContext context = getDateTimeContext(); + return (context != null ? context.getFormatter(formatterToUse) : formatterToUse); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java new file mode 100644 index 0000000000..fba9f7f857 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.FormatStyle; +import java.util.TimeZone; + +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Factory that creates a JSR-310 {@link java.time.format.DateTimeFormatter}. + * + *

Formatters will be created using the defined {@link #setPattern pattern}, + * {@link #setIso ISO}, and xxxStyle methods (considered in that order). + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 4.0 + * @see #createDateTimeFormatter() + * @see #createDateTimeFormatter(DateTimeFormatter) + * @see #setPattern + * @see #setIso + * @see #setDateStyle + * @see #setTimeStyle + * @see #setDateTimeStyle + * @see DateTimeFormatterFactoryBean + */ +public class DateTimeFormatterFactory { + + private String pattern; + + private ISO iso; + + private FormatStyle dateStyle; + + private FormatStyle timeStyle; + + private TimeZone timeZone; + + + /** + * Create a new {@code DateTimeFormatterFactory} instance. + */ + public DateTimeFormatterFactory() { + } + + /** + * Create a new {@code DateTimeFormatterFactory} instance. + * @param pattern the pattern to use to format date values + */ + public DateTimeFormatterFactory(String pattern) { + this.pattern = pattern; + } + + + /** + * Set the pattern to use to format date values. + * @param pattern the format pattern + */ + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * Set the ISO format used to format date values. + * @param iso the ISO format + */ + public void setIso(ISO iso) { + this.iso = iso; + } + + /** + * Set the style to use for date types. + */ + public void setDateStyle(FormatStyle dateStyle) { + this.dateStyle = dateStyle; + } + + /** + * Set the style to use for time types. + */ + public void setTimeStyle(FormatStyle timeStyle) { + this.timeStyle = timeStyle; + } + + /** + * Set the style to use for date and time types. + */ + public void setDateTimeStyle(FormatStyle dateTimeStyle) { + this.dateStyle = dateTimeStyle; + this.timeStyle = dateTimeStyle; + } + + /** + * Set the two characters to use to format date values, in Joda-Time style. + *

The first character is used for the date style; the second is for + * the time style. Supported characters are: + *

    + *
  • 'S' = Small
  • + *
  • 'M' = Medium
  • + *
  • 'L' = Long
  • + *
  • 'F' = Full
  • + *
  • '-' = Omitted
  • + *
+ *

This method mimics the styles supported by Joda-Time. Note that + * JSR-310 natively favors {@link java.time.format.FormatStyle} as used for + * {@link #setDateStyle}, {@link #setTimeStyle} and {@link #setDateTimeStyle}. + * @param style two characters from the set {"S", "M", "L", "F", "-"} + */ + public void setStylePattern(String style) { + Assert.isTrue(style != null && style.length() == 2); + this.dateStyle = convertStyleCharacter(style.charAt(0)); + this.timeStyle = convertStyleCharacter(style.charAt(1)); + } + + private FormatStyle convertStyleCharacter(char c) { + switch (c) { + case 'S': return FormatStyle.SHORT; + case 'M': return FormatStyle.MEDIUM; + case 'L': return FormatStyle.LONG; + case 'F': return FormatStyle.FULL; + case '-': return null; + default: throw new IllegalArgumentException("Invalid style character '" + c + "'"); + } + } + + /** + * Set the {@code TimeZone} to normalize the date values into, if any. + * @param timeZone the time zone + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + + /** + * Create a new {@code DateTimeFormatter} using this factory. + *

If no specific pattern or style has been defined, + * {@link FormatStyle#MEDIUM medium date time format} will be used. + * @return a new date time formatter + * @see #createDateTimeFormatter(DateTimeFormatter) + */ + public DateTimeFormatter createDateTimeFormatter() { + return createDateTimeFormatter(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); + } + + /** + * Create a new {@code DateTimeFormatter} using this factory. + *

If no specific pattern or style has been defined, + * the supplied {@code fallbackFormatter} will be used. + * @param fallbackFormatter the fall-back formatter to use when no specific + * factory properties have been set (can be {@code null}). + * @return a new date time formatter + */ + public DateTimeFormatter createDateTimeFormatter(DateTimeFormatter fallbackFormatter) { + DateTimeFormatter dateTimeFormatter = null; + if (StringUtils.hasLength(this.pattern)) { + dateTimeFormatter = DateTimeFormatter.ofPattern(this.pattern); + } + else if (this.iso != null && this.iso != ISO.NONE) { + switch (this.iso) { + case DATE: + dateTimeFormatter = DateTimeFormatter.ISO_DATE; + break; + case TIME: + dateTimeFormatter = DateTimeFormatter.ISO_TIME; + break; + case DATE_TIME: + dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME; + break; + case NONE: + /* no-op */ + break; + default: + throw new IllegalStateException("Unsupported ISO format: " + this.iso); + } + } + else if (this.dateStyle != null && this.timeStyle != null) { + dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(this.dateStyle, this.timeStyle); + } + else if (this.dateStyle != null) { + dateTimeFormatter = DateTimeFormatter.ofLocalizedDate(this.dateStyle); + } + else if (this.timeStyle != null) { + dateTimeFormatter = DateTimeFormatter.ofLocalizedTime(this.timeStyle); + } + + if (dateTimeFormatter != null && this.timeZone != null) { + dateTimeFormatter = dateTimeFormatter.withZone(this.timeZone.toZoneId()); + } + return (dateTimeFormatter != null ? dateTimeFormatter : fallbackFormatter); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java new file mode 100644 index 0000000000..aa64799b68 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2013 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 + * + * http://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 org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * {@link FactoryBean} that creates a JSR-310 {@link java.time.format.DateTimeFormatter}. + * See the {@link DateTimeFormatterFactory base class} for configuration details. + * + * @author Juergen Hoeller + * @since 4.0 + * @see #setPattern + * @see #setIso + * @see #setDateStyle + * @see #setTimeStyle + * @see DateTimeFormatterFactory + */ +public class DateTimeFormatterFactoryBean extends DateTimeFormatterFactory + implements FactoryBean, InitializingBean { + + private DateTimeFormatter dateTimeFormatter; + + + public void afterPropertiesSet() { + this.dateTimeFormatter = createDateTimeFormatter(); + } + + public DateTimeFormatter getObject() { + return this.dateTimeFormatter; + } + + public Class getObjectType() { + return DateTimeFormatter.class; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java new file mode 100644 index 0000000000..e40be83479 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.format.FormatterRegistrar; +import org.springframework.format.FormatterRegistry; +import org.springframework.format.annotation.DateTimeFormat.ISO; + +/** + * Configures the JSR-310 java.time formatting system for use with Spring. + * + * @author Juergen Hoeller + * @author Phillip Webb + * @since 4.0 + * @see #setDateStyle + * @see #setTimeStyle + * @see #setDateTimeStyle + * @see #setUseIsoFormat + * @see org.springframework.format.FormatterRegistrar#registerFormatters + * @see org.springframework.format.datetime.DateFormatterRegistrar + * @see org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean + */ +public class DateTimeFormatterRegistrar implements FormatterRegistrar { + + private static enum Type {DATE, TIME, DATE_TIME} + + + /** + * User defined formatters. + */ + private final Map formatters = new HashMap(); + + /** + * Factories used when specific formatters have not been specified. + */ + private final Map factories; + + + public DateTimeFormatterRegistrar() { + this.factories = new HashMap(); + for (Type type : Type.values()) { + this.factories.put(type, new DateTimeFormatterFactory()); + } + } + + + /** + * Set whether standard ISO formatting should be applied to all date/time types. + * Default is "false" (no). + *

If set to "true", the "dateStyle", "timeStyle" and "dateTimeStyle" + * properties are effectively ignored. + */ + public void setUseIsoFormat(boolean useIsoFormat) { + this.factories.get(Type.DATE).setIso(useIsoFormat ? ISO.DATE : null); + this.factories.get(Type.TIME).setIso(useIsoFormat ? ISO.TIME : null); + this.factories.get(Type.DATE_TIME).setIso(useIsoFormat ? ISO.DATE_TIME : null); + } + + /** + * Set the default format style of {@link java.time.LocalDate} objects. + * Default is {@link java.time.format.FormatStyle#SHORT}. + */ + public void setDateStyle(FormatStyle dateStyle) { + this.factories.get(Type.DATE).setDateStyle(dateStyle); + } + + /** + * Set the default format style of {@link java.time.LocalTime} objects. + * Default is {@link java.time.format.FormatStyle#SHORT}. + */ + public void setTimeStyle(FormatStyle timeStyle) { + this.factories.get(Type.TIME).setTimeStyle(timeStyle); + } + + /** + * Set the default format style of {@link java.time.LocalDateTime} objects. + * Default is {@link java.time.format.FormatStyle#SHORT}. + */ + public void setDateTimeStyle(FormatStyle dateTimeStyle) { + this.factories.get(Type.DATE_TIME).setDateTimeStyle(dateTimeStyle); + } + + /** + * Set the formatter that will be used for objects representing date values. + *

This formatter will be used for the {@link org.joda.time.LocalDate} type. + * When specified, the {@link #setDateStyle dateStyle} and + * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @see #setTimeFormatter + * @see #setDateTimeFormatter + */ + public void setDateFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.DATE, formatter); + } + + /** + * Set the formatter that will be used for objects representing time values. + *

This formatter will be used for the {@link org.joda.time.LocalTime} type. + * When specified, the {@link #setTimeStyle timeStyle} and + * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @see #setDateFormatter + * @see #setDateTimeFormatter + */ + public void setTimeFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.TIME, formatter); + } + + /** + * Set the formatter that will be used for objects representing date and time values. + *

This formatter will be used for {@link org.joda.time.LocalDateTime}, {@link org.joda.time.ReadableInstant}, + * {@link java.util.Date} and {@link java.util.Calendar} types. + * When specified, the {@link #setDateTimeStyle dateTimeStyle} and + * {@link #setUseIsoFormat useIsoFormat} properties will be ignored. + * @param formatter the formatter to use + * @see #setDateFormatter + * @see #setTimeFormatter + */ + public void setDateTimeFormatter(DateTimeFormatter formatter) { + this.formatters.put(Type.DATE_TIME, formatter); + } + + + public void registerFormatters(FormatterRegistry registry) { + DateTimeFormatter dateFormatter = getFormatter(Type.DATE); + DateTimeFormatter timeFormatter = getFormatter(Type.TIME); + DateTimeFormatter dateTimeFormatter = getFormatter(Type.DATE_TIME); + + registry.addFormatterForFieldType(LocalDate.class, + new TemporalAccessorPrinter(dateFormatter), + new TemporalAccessorParser(LocalDate.class, dateFormatter)); + + registry.addFormatterForFieldType(LocalTime.class, + new TemporalAccessorPrinter(timeFormatter), + new TemporalAccessorParser(LocalTime.class, timeFormatter)); + + registry.addFormatterForFieldType(LocalDateTime.class, + new TemporalAccessorPrinter(dateTimeFormatter), + new TemporalAccessorParser(LocalDateTime.class, dateTimeFormatter)); + + registry.addFormatterForFieldType(ZonedDateTime.class, + new TemporalAccessorPrinter(dateTimeFormatter), + new TemporalAccessorParser(ZonedDateTime.class, dateTimeFormatter)); + + registry.addFormatterForFieldType(OffsetDateTime.class, + new TemporalAccessorPrinter(dateTimeFormatter), + new TemporalAccessorParser(OffsetDateTime.class, dateTimeFormatter)); + + registry.addFormatterForFieldType(OffsetTime.class, + new TemporalAccessorPrinter(timeFormatter), + new TemporalAccessorParser(OffsetTime.class, timeFormatter)); + + registry.addFormatterForFieldType(Instant.class, new InstantFormatter()); + + registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory()); + } + + private DateTimeFormatter getFormatter(Type type) { + DateTimeFormatter formatter = this.formatters.get(type); + if (formatter != null) { + return formatter; + } + DateTimeFormatter fallbackFormatter = getFallbackFormatter(type); + return this.factories.get(type).createDateTimeFormatter(fallbackFormatter); + } + + private DateTimeFormatter getFallbackFormatter(Type type) { + switch (type) { + case DATE: return DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT); + case TIME: return DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT); + default: return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java new file mode 100644 index 0000000000..a60e5e78eb --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/InstantFormatter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.text.ParseException; +import java.time.Instant; +import java.util.Locale; + +import org.springframework.format.Formatter; + +/** + * {@link Formatter} implementation for a JSR-310 {@link java.time.Instant}, + * following JSR-310's parsing rules for an Instant (that is, not using a + * configurable {@link java.time.format.DateTimeFormatter}). + * + * @author Juergen Hoeller + * @since 4.0 + * @see java.time.Instant#parse + */ +public class InstantFormatter implements Formatter { + + public Instant parse(String text, Locale locale) throws ParseException { + return Instant.parse(text); + } + + public String print(Instant object, Locale locale) { + return object.toString(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java new file mode 100644 index 0000000000..f16ee4f574 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.format.AnnotationFormatterFactory; +import org.springframework.format.Parser; +import org.springframework.format.Printer; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.util.StringValueResolver; + +/** + * Formats fields annotated with the {@link DateTimeFormat} annotation using + * the JSR-310 java.time package in JDK 8. + * + * @author Juergen Hoeller + * @since 4.0 + * @see org.springframework.format.annotation.DateTimeFormat + */ +public class Jsr310DateTimeFormatAnnotationFormatterFactory + implements AnnotationFormatterFactory, EmbeddedValueResolverAware { + + private static final Set> FIELD_TYPES; + static { + // Create the set of field types that may be annotated with @DateTimeFormat. + Set> fieldTypes = new HashSet>(8); + fieldTypes.add(LocalDate.class); + fieldTypes.add(LocalTime.class); + fieldTypes.add(LocalDateTime.class); + fieldTypes.add(ZonedDateTime.class); + fieldTypes.add(OffsetDateTime.class); + fieldTypes.add(OffsetTime.class); + FIELD_TYPES = Collections.unmodifiableSet(fieldTypes); + } + + + private StringValueResolver embeddedValueResolver; + + + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + protected String resolveEmbeddedValue(String value) { + return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); + } + + + public final Set> getFieldTypes() { + return FIELD_TYPES; + } + + public Printer getPrinter(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatter formatter = getFormatter(annotation, fieldType); + return new TemporalAccessorPrinter(formatter); + } + + @SuppressWarnings("unchecked") + public Parser getParser(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatter formatter = getFormatter(annotation, fieldType); + return new TemporalAccessorParser((Class) fieldType, formatter); + } + + /** + * Factory method used to create a {@link org.joda.time.format.DateTimeFormatter}. + * @param annotation the format annotation for the field + * @param fieldType the type of field + * @return a {@link org.joda.time.format.DateTimeFormatter} instance + */ + protected DateTimeFormatter getFormatter(DateTimeFormat annotation, Class fieldType) { + DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + factory.setStylePattern(resolveEmbeddedValue(annotation.style())); + factory.setIso(annotation.iso()); + factory.setPattern(resolveEmbeddedValue(annotation.pattern())); + return factory.createDateTimeFormatter(); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java new file mode 100644 index 0000000000..6e742b01c4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.text.ParseException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.Locale; + +import org.springframework.format.Parser; + +/** + * {@link Parser} implementation for a JSR-310 {@link java.time.temporal.TemporalAccessor}, + * using a {@link java.time.format.DateTimeFormatter}) (the contextual one, if available). + * + * @author Juergen Hoeller + * @since 4.0 + * @see DateTimeContextHolder#getFormatter + * @see java.time.LocalDate#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.LocalTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.LocalDateTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.ZonedDateTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.OffsetDateTime#parse(CharSequence, java.time.format.DateTimeFormatter) + * @see java.time.OffsetTime#parse(CharSequence, java.time.format.DateTimeFormatter) + */ +public final class TemporalAccessorParser implements Parser { + + private final Class temporalAccessorType; + + private final DateTimeFormatter formatter; + + + /** + * Create a new TemporalAccessorParser for the given TemporalAccessor type. + * @param temporalAccessorType the specific TemporalAccessor class + * (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, OffsetDateTime, OffsetTime) + * @param formatter the base DateTimeFormatter instance + */ + public TemporalAccessorParser(Class temporalAccessorType, DateTimeFormatter formatter) { + this.temporalAccessorType = temporalAccessorType; + this.formatter = formatter; + } + + + public TemporalAccessor parse(String text, Locale locale) throws ParseException { + DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale); + if (LocalDate.class.equals(this.temporalAccessorType)) { + return LocalDate.parse(text, formatterToUse); + } + else if (LocalTime.class.equals(this.temporalAccessorType)) { + return LocalTime.parse(text, formatterToUse); + } + else if (LocalDateTime.class.equals(this.temporalAccessorType)) { + return LocalDateTime.parse(text, formatterToUse); + } + else if (ZonedDateTime.class.equals(this.temporalAccessorType)) { + return ZonedDateTime.parse(text, formatterToUse); + } + else if (OffsetDateTime.class.equals(this.temporalAccessorType)) { + return OffsetDateTime.parse(text, formatterToUse); + } + else if (OffsetTime.class.equals(this.temporalAccessorType)) { + return OffsetTime.parse(text, formatterToUse); + } + else { + throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType); + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java new file mode 100644 index 0000000000..c6c1fed593 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorPrinter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.temporal.TemporalAccessor; +import java.util.Locale; + +import org.springframework.format.Printer; + +/** + * {@link Printer} implementation for a JSR-310 {@link java.time.temporal.TemporalAccessor}, + * using a {@link java.time.format.DateTimeFormatter}) (the contextual one, if available). + * + * @author Juergen Hoeller + * @since 4.0 + * @see DateTimeContextHolder#getFormatter + * @see java.time.format.DateTimeFormatter#format(java.time.temporal.TemporalAccessor) + */ +public final class TemporalAccessorPrinter implements Printer { + + private final DateTimeFormatter formatter; + + + /** + * Create a new TemporalAccessorPrinter. + * @param formatter the base DateTimeFormatter instance + */ + public TemporalAccessorPrinter(DateTimeFormatter formatter) { + this.formatter = formatter; + } + + + public String print(TemporalAccessor partial, Locale locale) { + return DateTimeContextHolder.getFormatter(this.formatter, locale).format(partial); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java new file mode 100644 index 0000000000..31c142dcd6 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java @@ -0,0 +1,4 @@ +/** + * Integration with the JSR-310 java.time package in JDK 8. + */ +package org.springframework.format.datetime.standard; diff --git a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java index 5ed55dc0eb..6670071d50 100644 --- a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -20,6 +20,7 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.FormatterRegistry; import org.springframework.format.datetime.DateFormatterRegistrar; import org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar; +import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar; import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; import org.springframework.util.ClassUtils; import org.springframework.util.StringValueResolver; @@ -34,13 +35,18 @@ import org.springframework.util.StringValueResolver; * {@link DefaultConversionService#addDefaultConverters addDefaultConverters} method. * * @author Chris Beams + * @author Juergen Hoeller * @since 3.1 */ public class DefaultFormattingConversionService extends FormattingConversionService { + private static final boolean jsr310Present = ClassUtils.isPresent( + "java.time.LocalDate", DefaultFormattingConversionService.class.getClassLoader()); + private static final boolean jodaTimePresent = ClassUtils.isPresent( "org.joda.time.LocalDate", DefaultFormattingConversionService.class.getClassLoader()); + /** * Create a new {@code DefaultFormattingConversionService} with the set of * {@linkplain DefaultConversionService#addDefaultConverters default converters} and @@ -71,13 +77,14 @@ public class DefaultFormattingConversionService extends FormattingConversionServ * @param registerDefaultFormatters whether to register default formatters */ public DefaultFormattingConversionService(StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) { - this.setEmbeddedValueResolver(embeddedValueResolver); + setEmbeddedValueResolver(embeddedValueResolver); DefaultConversionService.addDefaultConverters(this); if (registerDefaultFormatters) { addDefaultFormatters(this); } } + /** * Add formatters appropriate for most environments, including number formatters and a Joda-Time * date formatter if Joda-Time is present on the classpath. @@ -85,10 +92,16 @@ public class DefaultFormattingConversionService extends FormattingConversionServ */ public static void addDefaultFormatters(FormatterRegistry formatterRegistry) { formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + if (jsr310Present) { + // just handling JSR-310 specific date and time types + new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry); + } if (jodaTimePresent) { + // handles Joda-specific types as well as Date, Calendar, Long new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry); } else { + // regular DateFormat-based Date, Calendar, Long converters new DateFormatterRegistrar().registerFormatters(formatterRegistry); } } diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java new file mode 100644 index 0000000000..536921b387 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBeanTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.FormatStyle; + +import org.junit.Test; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * @author Phillip Webb + * @author Sam Brannen + */ +public class DateTimeFormatterFactoryBeanTests { + + private DateTimeFormatterFactoryBean factory = new DateTimeFormatterFactoryBean(); + + @Test + public void isSingleton() throws Exception { + assertThat(factory.isSingleton(), is(true)); + } + + @Test + @SuppressWarnings("rawtypes") + public void getObjectType() throws Exception { + assertThat(factory.getObjectType(), is(equalTo((Class) DateTimeFormatter.class))); + } + + @Test + public void getObject() throws Exception { + factory.afterPropertiesSet(); + assertThat(factory.getObject().toString(), is(equalTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()))); + } + + @Test + public void getObjectIsAlwaysSingleton() throws Exception { + factory.afterPropertiesSet(); + DateTimeFormatter formatter = factory.getObject(); + assertThat(formatter.toString(), is(equalTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()))); + factory.setStylePattern("LL"); + assertThat(factory.getObject(), is(sameInstance(formatter))); + } +} diff --git a/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java new file mode 100644 index 0000000000..1b979e88ab --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Test; + +import org.springframework.format.annotation.DateTimeFormat.ISO; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * @author Phillip Webb + * @author Sam Brannen + */ +public class DateTimeFormatterFactoryTests { + + // Potential test timezone, both have daylight savings on October 21st + private static final TimeZone ZURICH = TimeZone.getTimeZone("Europe/Zurich"); + private static final TimeZone NEW_YORK = TimeZone.getTimeZone("America/New_York"); + + // Ensure that we are testing against a timezone other than the default. + private static final TimeZone TEST_TIMEZONE = ZURICH.equals(TimeZone.getDefault()) ? NEW_YORK : ZURICH; + + + private DateTimeFormatterFactory factory = new DateTimeFormatterFactory(); + + private LocalDateTime dateTime = LocalDateTime.of(2009, 10, 21, 12, 10, 00, 00); + + + @Test + public void createDateTimeFormatter() throws Exception { + assertThat(factory.createDateTimeFormatter().toString(), is(equalTo(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).toString()))); + } + + @Test + public void createDateTimeFormatterWithPattern() throws Exception { + factory = new DateTimeFormatterFactory("yyyyMMddHHmmss"); + DateTimeFormatter formatter = factory.createDateTimeFormatter(); + assertThat(formatter.format(dateTime), is("20091021121000")); + } + + @Test + public void createDateTimeFormatterWithNullFallback() throws Exception { + DateTimeFormatter formatter = factory.createDateTimeFormatter(null); + assertThat(formatter, is(nullValue())); + } + + @Test + public void createDateTimeFormatterWithFallback() throws Exception { + DateTimeFormatter fallback = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG); + DateTimeFormatter formatter = factory.createDateTimeFormatter(fallback); + assertThat(formatter, is(sameInstance(fallback))); + } + + @Test + public void createDateTimeFormatterInOrderOfPropertyPriority() throws Exception { + factory.setStylePattern("SS"); + assertThat(applyLocale(factory.createDateTimeFormatter()).format(dateTime), is("10/21/09 12:10 PM")); + + factory.setIso(ISO.DATE); + assertThat(applyLocale(factory.createDateTimeFormatter()).format(dateTime), is("2009-10-21")); + + factory.setPattern("yyyyMMddHHmmss"); + assertThat(factory.createDateTimeFormatter().format(dateTime), is("20091021121000")); + } + + @Test + public void createDateTimeFormatterWithTimeZone() throws Exception { + factory.setPattern("yyyyMMddHHmmss Z"); + factory.setTimeZone(TEST_TIMEZONE); + ZoneId dateTimeZone = TEST_TIMEZONE.toZoneId(); + ZonedDateTime dateTime = ZonedDateTime.of(2009, 10, 21, 12, 10, 00, 00, dateTimeZone); + String offset = (TEST_TIMEZONE.equals(NEW_YORK) ? "-0400" : "+0200"); + assertThat(factory.createDateTimeFormatter().format(dateTime), is("20091021121000 " + offset)); + } + + private DateTimeFormatter applyLocale(DateTimeFormatter dateTimeFormatter) { + return dateTimeFormatter.withLocale(Locale.US); + } + +} 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 new file mode 100644 index 0000000000..112ac986bc --- /dev/null +++ b/spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java @@ -0,0 +1,422 @@ +/* + * Copyright 2002-2013 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 + * + * http://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.lang.reflect.Method; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.format.annotation.DateTimeFormat.ISO; +import org.springframework.format.support.FormattingConversionService; +import org.springframework.validation.DataBinder; + +import static org.junit.Assert.*; + +/** + * @author Keith Donald + * @author Juergen Hoeller + * @author Phillip Webb + */ +public class DateTimeFormattingTests { + + private FormattingConversionService conversionService; + + private DataBinder binder; + + @Before + public void setUp() { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + setUp(registrar); + } + + private void setUp(DateTimeFormatterRegistrar registrar) { + conversionService = new FormattingConversionService(); + DefaultConversionService.addDefaultConverters(conversionService); + + registrar.registerFormatters(conversionService); + + DateTimeBean bean = new DateTimeBean(); + bean.getChildren().add(new DateTimeBean()); + binder = new DataBinder(bean); + binder.setConversionService(conversionService); + + LocaleContextHolder.setLocale(Locale.US); + DateTimeContext context = new DateTimeContext(); + context.setTimeZone(ZoneId.of("-05:00")); + DateTimeContextHolder.setDateTimeContext(context); + } + + @After + public void tearDown() { + LocaleContextHolder.setLocale(null); + DateTimeContextHolder.setDateTimeContext(null); + } + + @Test + public void testBindLocalDate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "10/31/09"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("10/31/09", binder.getBindingResult().getFieldValue("localDate")); + } + + @Test + public void testBindLocalDateWithSpecificStyle() throws Exception { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateStyle(FormatStyle.LONG); + setUp(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "October 31, 2009"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("October 31, 2009", binder.getBindingResult().getFieldValue("localDate")); + } + + @Test + public void testBindLocalDateWithSpecificFormatter() throws Exception { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); + setUp(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", "20091031"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("20091031", binder.getBindingResult().getFieldValue("localDate")); + } + + @Test + public void testBindLocalDateArray() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDate", new String[]{"10/31/09"}); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + } + + @Test + public void testBindLocalDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("Oct 31, 2009", binder.getBindingResult().getFieldValue("localDateAnnotated")); + } + + @Test + public void testBindLocalDateAnnotatedWithError() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct -31, 2009"); + binder.bind(propertyValues); + assertEquals(1, binder.getBindingResult().getFieldErrorCount("localDateAnnotated")); + assertEquals("Oct -31, 2009", binder.getBindingResult().getFieldValue("localDateAnnotated")); + } + + @Test + public void testBindNestedLocalDateAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("children[0].localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("Oct 31, 2009", binder.getBindingResult().getFieldValue("children[0].localDateAnnotated")); + } + + @Test + public void testBindLocalDateAnnotatedWithDirectFieldAccess() { + binder.initDirectFieldAccess(); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct 31, 2009"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("Oct 31, 2009", binder.getBindingResult().getFieldValue("localDateAnnotated")); + } + + @Test + public void testBindLocalDateAnnotatedWithDirectFieldAccessAndError() { + binder.initDirectFieldAccess(); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateAnnotated", "Oct -31, 2009"); + binder.bind(propertyValues); + assertEquals(1, binder.getBindingResult().getFieldErrorCount("localDateAnnotated")); + assertEquals("Oct -31, 2009", binder.getBindingResult().getFieldValue("localDateAnnotated")); + } + + @Test + public void testBindLocalTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "12:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("12:00 PM", binder.getBindingResult().getFieldValue("localTime")); + } + + @Test + public void testBindLocalTimeWithSpecificStyle() throws Exception { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setTimeStyle(FormatStyle.MEDIUM); + setUp(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "12:00:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("12:00:00 PM", binder.getBindingResult().getFieldValue("localTime")); + } + + @Test + public void testBindLocalTimeWithSpecificFormatter() throws Exception { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setTimeFormatter(DateTimeFormatter.ofPattern("HHmmss")); + setUp(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTime", "130000"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("130000", binder.getBindingResult().getFieldValue("localTime")); + } + + @Test + public void testBindLocalTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localTimeAnnotated", "12:00:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("12:00:00 PM", binder.getBindingResult().getFieldValue("localTimeAnnotated")); + } + + @Test + public void testBindLocalDateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", "10/31/09 12:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("10/31/09 12:00 PM", binder.getBindingResult().getFieldValue("localDateTime")); + } + + @Test + public void testBindLocalDateTimeAnnotated() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTimeAnnotated", "Oct 31, 2009 12:00:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("Oct 31, 2009 12:00:00 PM", binder.getBindingResult().getFieldValue("localDateTimeAnnotated")); + } + + @Test + public void testBindDateTimeWithSpecificStyle() throws Exception { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateTimeStyle(FormatStyle.MEDIUM); + setUp(registrar); + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("localDateTime", "Oct 31, 2009 12:00:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("Oct 31, 2009 12:00:00 PM", binder.getBindingResult().getFieldValue("localDateTime")); + Method testMethod = LocalVariableTableParameterNameDiscoverer.class.getMethod("getParameterNames", Method.class); + System.out.println(testMethod.getParameters()[0].getName()); + System.out.println(new LocalVariableTableParameterNameDiscoverer().getParameterNames(testMethod)[0]); + } + + @Test + public void testBindDateTimeAnnotatedPattern() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("dateTimeAnnotatedPattern", "10/31/09 12:00 PM"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("10/31/09 12:00 PM", binder.getBindingResult().getFieldValue("dateTimeAnnotatedPattern")); + } + + @Test + public void testBindISODate() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDate", "2009-10-31"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("2009-10-31", binder.getBindingResult().getFieldValue("isoDate")); + } + + @Test + public void testBindISOTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoTime", "12:00:00.000-05:00"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("12:00:00", binder.getBindingResult().getFieldValue("isoTime")); + } + + @Test + public void testBindISODateTime() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("isoDateTime", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("2009-10-31T12:00:00", binder.getBindingResult().getFieldValue("isoDateTime")); + } + + @Test + public void testBindInstant() { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + propertyValues.add("instant", "2009-10-31T12:00:00.000Z"); + binder.bind(propertyValues); + assertEquals(0, binder.getBindingResult().getErrorCount()); + assertEquals("2009-10-31T12:00Z", binder.getBindingResult().getFieldValue("instant")); + } + + + @SuppressWarnings("unused") + public static class DateTimeBean { + + private LocalDate localDate; + + @DateTimeFormat(style="M-") + private LocalDate localDateAnnotated; + + private LocalTime localTime; + + @DateTimeFormat(style="-M") + private LocalTime localTimeAnnotated; + + private LocalDateTime localDateTime; + + @DateTimeFormat(style="MM") + private LocalDateTime localDateTimeAnnotated; + + @DateTimeFormat(pattern="M/d/yy h:mm a") + private LocalDateTime dateTimeAnnotatedPattern; + + @DateTimeFormat(iso=ISO.DATE) + private LocalDate isoDate; + + @DateTimeFormat(iso=ISO.TIME) + private LocalTime isoTime; + + @DateTimeFormat(iso=ISO.DATE_TIME) + private LocalDateTime isoDateTime; + + private Instant instant; + + private final List children = new ArrayList(); + + public LocalDate getLocalDate() { + return localDate; + } + + public void setLocalDate(LocalDate localDate) { + this.localDate = localDate; + } + + public LocalDate getLocalDateAnnotated() { + return localDateAnnotated; + } + + public void setLocalDateAnnotated(LocalDate localDateAnnotated) { + this.localDateAnnotated = localDateAnnotated; + } + + public LocalTime getLocalTime() { + return localTime; + } + + public void setLocalTime(LocalTime localTime) { + this.localTime = localTime; + } + + public LocalTime getLocalTimeAnnotated() { + return localTimeAnnotated; + } + + public void setLocalTimeAnnotated(LocalTime localTimeAnnotated) { + this.localTimeAnnotated = localTimeAnnotated; + } + + public LocalDateTime getLocalDateTime() { + return localDateTime; + } + + public void setLocalDateTime(LocalDateTime localDateTime) { + this.localDateTime = localDateTime; + } + + public LocalDateTime getLocalDateTimeAnnotated() { + return localDateTimeAnnotated; + } + + public void setLocalDateTimeAnnotated(LocalDateTime localDateTimeAnnotated) { + this.localDateTimeAnnotated = localDateTimeAnnotated; + } + + public LocalDateTime getDateTimeAnnotatedPattern() { + return dateTimeAnnotatedPattern; + } + + public void setDateTimeAnnotatedPattern(LocalDateTime dateTimeAnnotatedPattern) { + this.dateTimeAnnotatedPattern = dateTimeAnnotatedPattern; + } + + public LocalDate getIsoDate() { + return isoDate; + } + + public void setIsoDate(LocalDate isoDate) { + this.isoDate = isoDate; + } + + public LocalTime getIsoTime() { + return isoTime; + } + + public void setIsoTime(LocalTime isoTime) { + this.isoTime = isoTime; + } + + public LocalDateTime getIsoDateTime() { + return isoDateTime; + } + + public void setIsoDateTime(LocalDateTime isoDateTime) { + this.isoDateTime = isoDateTime; + } + + public Instant getInstant() { + return instant; + } + + public void setInstant(Instant instant) { + this.instant = instant; + } + + public List getChildren() { + return children; + } + } + +} -- GitLab