View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.beanutils2.converters;
18  
19  import java.text.DateFormat;
20  import java.text.ParsePosition;
21  import java.text.SimpleDateFormat;
22  import java.time.Instant;
23  import java.time.LocalDate;
24  import java.time.LocalDateTime;
25  import java.time.OffsetDateTime;
26  import java.time.ZoneId;
27  import java.time.ZonedDateTime;
28  import java.time.temporal.TemporalAccessor;
29  import java.util.Calendar;
30  import java.util.Date;
31  import java.util.Locale;
32  import java.util.TimeZone;
33  
34  import org.apache.commons.beanutils2.ConversionException;
35  
36  /**
37   * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from <strong>date/time</strong> objects.
38   * <p>
39   * This implementation handles conversion for the following <em>date/time</em> types.
40   * <ul>
41   * <li>{@link java.util.Date}</li>
42   * <li>{@link java.util.Calendar}</li>
43   * <li>{@link java.time.LocalDate}</li>
44   * <li>{@link java.time.LocalDateTime}</li>
45   * <li>{@link java.time.OffsetDateTime}</li>
46   * <li>{@link java.time.ZonedDateTime}</li>
47   * <li>{@link java.sql.Date}</li>
48   * <li>{@link java.sql.Time}</li>
49   * <li>{@link java.sql.Timestamp}</li>
50   * </ul>
51   *
52   * <h2>String Conversions (to and from)</h2> This class provides a number of ways in which date/time conversions to/from Strings can be achieved:
53   * <ul>
54   * <li>Using the SHORT date format for the default Locale, configure using:
55   * <ul>
56   * <li>{@code setUseLocaleFormat(true)}</li>
57   * </ul>
58   * </li>
59   * <li>Using the SHORT date format for a specified Locale, configure using:
60   * <ul>
61   * <li>{@code setLocale(Locale)}</li>
62   * </ul>
63   * </li>
64   * <li>Using the specified date pattern(s) for the default Locale, configure using:
65   * <ul>
66   * <li>Either {@code setPattern(String)} or {@code setPatterns(String[])}</li>
67   * </ul>
68   * </li>
69   * <li>Using the specified date pattern(s) for a specified Locale, configure using:
70   * <ul>
71   * <li>{@code setPattern(String)} or {@code setPatterns(String[]) and...}</li>
72   * <li>{@code setLocale(Locale)}</li>
73   * </ul>
74   * </li>
75   * <li>If none of the above are configured the {@code toDate(String)} method is used to convert from String to Date and the Dates's {@code toString()} method
76   * used to convert from Date to String.</li>
77   * </ul>
78   *
79   * <p>
80   * The <strong>Time Zone</strong> to use with the date format can be specified using the {@link #setTimeZone(TimeZone)} method.
81   *
82   * @param <D> The default value type.
83   * @since 1.8.0
84   */
85  public abstract class DateTimeConverter<D> extends AbstractConverter<D> {
86  
87      private String[] patterns;
88      private String displayPatterns;
89      private Locale locale;
90      private TimeZone timeZone;
91      private boolean useLocaleFormat;
92  
93      /**
94       * Constructs a Date/Time <em>Converter</em> that throws a {@code ConversionException} if an error occurs.
95       */
96      public DateTimeConverter() {
97      }
98  
99      /**
100      * Constructs a Date/Time <em>Converter</em> that returns a default value if an error occurs.
101      *
102      * @param defaultValue The default value to be returned if the value to be converted is missing or an error occurs converting the value.
103      */
104     public DateTimeConverter(final D defaultValue) {
105         super(defaultValue);
106     }
107 
108     /**
109      * Convert an input Date/Calendar object into a String.
110      * <p>
111      * <strong>N.B.</strong>If the converter has been configured to with one or more patterns (using {@code setPatterns()}), then the first pattern will be used
112      * to format the date into a String. Otherwise the default {@code DateFormat} for the default locale (and <em>style</em> if configured) will be used.
113      *
114      * @param value The input value to be converted
115      * @return the converted String value.
116      * @throws IllegalArgumentException if an error occurs converting to a String
117      */
118     @Override
119     protected String convertToString(final Object value) {
120         Date date = null;
121         if (value instanceof Date) {
122             date = (Date) value;
123         } else if (value instanceof Calendar) {
124             date = ((Calendar) value).getTime();
125         } else if (value instanceof Long) {
126             date = new Date(((Long) value).longValue());
127         } else if (value instanceof LocalDateTime) {
128             date = java.sql.Timestamp.valueOf((LocalDateTime) value);
129         } else if (value instanceof LocalDate) {
130             date = java.sql.Date.valueOf((LocalDate) value);
131         } else if (value instanceof ZonedDateTime) {
132             date = Date.from(((ZonedDateTime) value).toInstant());
133         } else if (value instanceof OffsetDateTime) {
134             date = Date.from(((OffsetDateTime) value).toInstant());
135         } else if (value instanceof TemporalAccessor) {
136             // Backstop for other TemporalAccessor implementations.
137             date = Date.from(Instant.from((TemporalAccessor) value));
138         }
139 
140         String result = null;
141         if (useLocaleFormat && date != null) {
142             DateFormat format = null;
143             if (patterns != null && patterns.length > 0) {
144                 format = getFormat(patterns[0]);
145             } else {
146                 format = getFormat(locale, timeZone);
147             }
148             logFormat("Formatting", format);
149             result = format.format(date);
150             if (log().isDebugEnabled()) {
151                 log().debug("    Converted  to String using format '" + result + "'");
152             }
153         } else {
154             result = value.toString();
155             if (log().isDebugEnabled()) {
156                 log().debug("    Converted  to String using toString() '" + result + "'");
157             }
158         }
159         return result;
160     }
161 
162     /**
163      * Convert the input object into a Date object of the specified type.
164      * <p>
165      * This method handles conversions between the following types:
166      * <ul>
167      * <li>{@link java.util.Date}</li>
168      * <li>{@link java.util.Calendar}</li>
169      * <li>{@link java.time.LocalDate}</li>
170      * <li>{@link java.time.LocalDateTime}</li>
171      * <li>{@link java.time.OffsetDateTime}</li>
172      * <li>{@link java.time.ZonedDateTime}</li>
173      * <li>{@link java.sql.Date}</li>
174      * <li>{@link java.sql.Time}</li>
175      * <li>{@link java.sql.Timestamp}</li>
176      * </ul>
177      *
178      * It also handles conversion from a {@code String} to any of the above types.
179      * <p>
180      *
181      * For {@code String} conversion, if the converter has been configured with one or more patterns (using {@code setPatterns()}), then the conversion is
182      * attempted with each of the specified patterns. Otherwise the default {@code DateFormat} for the default locale (and <em>style</em> if configured) will be
183      * used.
184      *
185      * @param <T>        The desired target type of the conversion.
186      * @param targetType Data type to which this value should be converted.
187      * @param value      The input value to be converted.
188      * @return The converted value.
189      * @throws Exception if conversion cannot be performed successfully
190      */
191     @Override
192     protected <T> T convertToType(final Class<T> targetType, final Object value) throws Exception {
193         final Class<?> sourceType = value.getClass();
194 
195         // Handle java.sql.Timestamp
196         if (value instanceof java.sql.Timestamp) {
197 
198             // N.B. Prior to JDK 1.4 the Timestamp's getTime() method
199             // didn't include the milliseconds. The following code
200             // ensures it works consistently across JDK versions
201             final java.sql.Timestamp timestamp = (java.sql.Timestamp) value;
202             long timeInMillis = timestamp.getTime() / 1000 * 1000;
203             timeInMillis += timestamp.getNanos() / 1000000;
204 
205             return toDate(targetType, timeInMillis);
206         }
207 
208         // Handle Date (includes java.sql.Date & java.sql.Time)
209         if (value instanceof Date) {
210             final Date date = (Date) value;
211             return toDate(targetType, date.getTime());
212         }
213 
214         // Handle Calendar
215         if (value instanceof Calendar) {
216             final Calendar calendar = (Calendar) value;
217             return toDate(targetType, calendar.getTime().getTime());
218         }
219 
220         // Handle Long
221         if (value instanceof Long) {
222             final Long longObj = (Long) value;
223             return toDate(targetType, longObj.longValue());
224         }
225 
226         // Handle LocalDate
227         if (value instanceof LocalDate) {
228             final LocalDate date = (LocalDate) value;
229             return toDate(targetType, date.atStartOfDay(getZoneId()).toInstant().toEpochMilli());
230         }
231 
232         // Handle LocalDateTime
233         if (value instanceof LocalDateTime) {
234             final LocalDateTime date = (LocalDateTime) value;
235             return toDate(targetType, date.atZone(getZoneId()).toInstant().toEpochMilli());
236         }
237 
238         // Handle ZonedDateTime
239         if (value instanceof ZonedDateTime) {
240             final ZonedDateTime date = (ZonedDateTime) value;
241             return toDate(targetType, date.toInstant().toEpochMilli());
242         }
243 
244         // Handle OffsetDateTime
245         if (value instanceof OffsetDateTime) {
246             final OffsetDateTime date = (OffsetDateTime) value;
247             return toDate(targetType, date.toInstant().toEpochMilli());
248         }
249 
250         // Convert all other types to String & handle
251         final String stringValue = toTrim(value);
252         if (stringValue.isEmpty()) {
253             return handleMissing(targetType);
254         }
255 
256         // Parse the Date/Time
257         if (useLocaleFormat) {
258             Calendar calendar = null;
259             if (patterns != null && patterns.length > 0) {
260                 calendar = parse(sourceType, targetType, stringValue);
261             } else {
262                 final DateFormat format = getFormat(locale, timeZone);
263                 calendar = parse(sourceType, targetType, stringValue, format);
264             }
265             if (Calendar.class.isAssignableFrom(targetType)) {
266                 return targetType.cast(calendar);
267             }
268             return toDate(targetType, calendar.getTime().getTime());
269         }
270 
271         // Default String conversion
272         return toDate(targetType, stringValue);
273     }
274 
275     /**
276      * Gets a {@code DateFormat} for the Locale.
277      *
278      * @param locale   The Locale to create the Format with (may be null)
279      * @param timeZone The Time Zone create the Format with (may be null)
280      * @return A Date Format.
281      */
282     protected DateFormat getFormat(final Locale locale, final TimeZone timeZone) {
283         DateFormat format = null;
284         if (locale == null) {
285             format = DateFormat.getDateInstance(DateFormat.SHORT);
286         } else {
287             format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
288         }
289         if (timeZone != null) {
290             format.setTimeZone(timeZone);
291         }
292         return format;
293     }
294 
295     /**
296      * Create a date format for the specified pattern.
297      *
298      * @param pattern The date pattern
299      * @return The DateFormat
300      */
301     private DateFormat getFormat(final String pattern) {
302         final DateFormat format = new SimpleDateFormat(pattern);
303         if (timeZone != null) {
304             format.setTimeZone(timeZone);
305         }
306         return format;
307     }
308 
309     /**
310      * Gets the Locale for the <em>Converter</em> (or {@code null} if none specified).
311      *
312      * @return The locale to use for conversion
313      */
314     public Locale getLocale() {
315         return locale;
316     }
317 
318     /**
319      * Gets the date format patterns used to convert dates to/from a {@link String} (or {@code null} if none specified).
320      *
321      * @see SimpleDateFormat
322      * @return Array of format patterns.
323      */
324     public String[] getPatterns() {
325         return patterns.clone();
326     }
327 
328     /**
329      * Gets the Time Zone to use when converting dates (or {@code null} if none specified.
330      *
331      * @return The Time Zone.
332      */
333     public TimeZone getTimeZone() {
334         return timeZone;
335     }
336 
337     /**
338      * Gets the {@code java.time.ZoneId</code> from the <code>java.util.Timezone} set or use the system default if no time zone is set.
339      *
340      * @return the {@code ZoneId}
341      */
342     private ZoneId getZoneId() {
343         return timeZone == null ? ZoneId.systemDefault() : timeZone.toZoneId();
344     }
345 
346     /**
347      * Log the {@code DateFormat} creation.
348      *
349      * @param action The action the format is being used for
350      * @param format The Date format
351      */
352     private void logFormat(final String action, final DateFormat format) {
353         if (log().isDebugEnabled()) {
354             final StringBuilder buffer = new StringBuilder(45);
355             buffer.append("    ");
356             buffer.append(action);
357             buffer.append(" with Format");
358             if (format instanceof SimpleDateFormat) {
359                 buffer.append("[");
360                 buffer.append(((SimpleDateFormat) format).toPattern());
361                 buffer.append("]");
362             }
363             buffer.append(" for ");
364             if (locale == null) {
365                 buffer.append("default locale");
366             } else {
367                 buffer.append("locale[");
368                 buffer.append(locale);
369                 buffer.append("]");
370             }
371             if (timeZone != null) {
372                 buffer.append(", TimeZone[");
373                 buffer.append(timeZone);
374                 buffer.append("]");
375             }
376             log().debug(buffer.toString());
377         }
378     }
379 
380     /**
381      * Parse a String date value using the set of patterns.
382      *
383      * @param sourceType The type of the value being converted
384      * @param targetType The type to convert the value to.
385      * @param value      The String date value.
386      * @return The converted Date object.
387      * @throws Exception if an error occurs parsing the date.
388      */
389     private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value) throws Exception {
390         Exception firstEx = null;
391         for (final String pattern : patterns) {
392             try {
393                 return parse(sourceType, targetType, value, getFormat(pattern));
394             } catch (final Exception ex) {
395                 if (firstEx == null) {
396                     firstEx = ex;
397                 }
398             }
399         }
400         if (patterns.length > 1) {
401             throw ConversionException.format("Error converting '%s' to '%s' using  patterns '%s'", toString(sourceType), toString(targetType), displayPatterns);
402         }
403         if (firstEx != null) {
404             throw firstEx;
405         }
406         return null;
407     }
408 
409     /**
410      * Parse a String into a {@code Calendar} object using the specified {@code DateFormat}.
411      *
412      * @param sourceType The type of the value being converted
413      * @param targetType The type to convert the value to
414      * @param value      The String date value.
415      * @param format     The DateFormat to parse the String value.
416      * @return The converted Calendar object.
417      * @throws ConversionException if the String cannot be converted.
418      */
419     private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value, final DateFormat format) {
420         logFormat("Parsing", format);
421         format.setLenient(false);
422         final ParsePosition pos = new ParsePosition(0);
423         final Date parsedDate = format.parse(value, pos); // ignore the result (use the Calendar)
424         final int errorIndex = pos.getErrorIndex();
425         if (errorIndex >= 0 || pos.getIndex() != value.length() || parsedDate == null) {
426             String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
427             if (format instanceof SimpleDateFormat) {
428                 final SimpleDateFormat simpleFormat = (SimpleDateFormat) format;
429                 msg += String.format(" using pattern '%s', localized pattern '%s', errorIndex %,d, calendar type %s, this %s", simpleFormat.toPattern(),
430                         simpleFormat.toLocalizedPattern(), errorIndex, format.getCalendar().getClass().getSimpleName(), this);
431             }
432             if (log().isDebugEnabled()) {
433                 log().debug("    " + msg);
434             }
435             throw new ConversionException(msg);
436         }
437         return format.getCalendar();
438     }
439 
440     /**
441      * Sets the Locale for the <em>Converter</em>.
442      *
443      * @param locale The Locale.
444      */
445     public void setLocale(final Locale locale) {
446         this.locale = locale;
447         setUseLocaleFormat(true);
448     }
449 
450     /**
451      * Sets a date format pattern to use to convert dates to/from a {@link String}.
452      *
453      * @see SimpleDateFormat
454      * @param pattern The format pattern.
455      */
456     public void setPattern(final String pattern) {
457         setPatterns(new String[] { pattern });
458     }
459 
460     /**
461      * Sets the date format patterns to use to convert dates to/from a {@link String}.
462      *
463      * @see SimpleDateFormat
464      * @param patterns Array of format patterns.
465      */
466     public void setPatterns(final String[] patterns) {
467         this.patterns = patterns != null ? patterns.clone() : null;
468         if (this.patterns != null && this.patterns.length > 1) {
469             displayPatterns = String.join(", ", this.patterns);
470         }
471         setUseLocaleFormat(true);
472     }
473 
474     /**
475      * Sets the Time Zone to use when converting dates.
476      *
477      * @param timeZone The Time Zone.
478      */
479     public void setTimeZone(final TimeZone timeZone) {
480         this.timeZone = timeZone;
481     }
482 
483     /**
484      * Indicate whether conversion should use a format/pattern or not.
485      *
486      * @param useLocaleFormat {@code true} if the format for the locale should be used, otherwise {@code false}
487      */
488     public void setUseLocaleFormat(final boolean useLocaleFormat) {
489         this.useLocaleFormat = useLocaleFormat;
490     }
491 
492     /**
493      * Convert a long value to the specified Date type for this <em>Converter</em>.
494      * <p>
495      *
496      * This method handles conversion to the following types:
497      * <ul>
498      * <li>{@link java.util.Date}</li>
499      * <li>{@link java.util.Calendar}</li>
500      * <li>{@link java.time.LocalDate}</li>
501      * <li>{@link java.time.LocalDateTime}</li>
502      * <li>{@link java.time.ZonedDateTime}</li>
503      * <li>{@link java.sql.Date}</li>
504      * <li>{@link java.sql.Time}</li>
505      * <li>{@link java.sql.Timestamp}</li>
506      * </ul>
507      *
508      * @param <T>   The target type
509      * @param type  The Date type to convert to
510      * @param value The long value to convert.
511      * @return The converted date value.
512      */
513     private <T> T toDate(final Class<T> type, final long value) {
514         // java.util.Date
515         if (type.equals(Date.class)) {
516             return type.cast(new Date(value));
517         }
518 
519         // java.sql.Date
520         if (type.equals(java.sql.Date.class)) {
521             return type.cast(new java.sql.Date(value));
522         }
523 
524         // java.sql.Time
525         if (type.equals(java.sql.Time.class)) {
526             return type.cast(new java.sql.Time(value));
527         }
528 
529         // java.sql.Timestamp
530         if (type.equals(java.sql.Timestamp.class)) {
531             return type.cast(new java.sql.Timestamp(value));
532         }
533 
534         // java.time.LocalDateTime
535         if (type.equals(LocalDate.class)) {
536             final LocalDate localDate = Instant.ofEpochMilli(value).atZone(getZoneId()).toLocalDate();
537             return type.cast(localDate);
538         }
539 
540         // java.time.LocalDateTime
541         if (type.equals(LocalDateTime.class)) {
542             final LocalDateTime localDateTime = Instant.ofEpochMilli(value).atZone(getZoneId()).toLocalDateTime();
543             return type.cast(localDateTime);
544         }
545 
546         // java.time.ZonedDateTime
547         if (type.equals(ZonedDateTime.class)) {
548             final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), getZoneId());
549             return type.cast(zonedDateTime);
550         }
551 
552         // java.time.OffsetDateTime
553         if (type.equals(OffsetDateTime.class)) {
554             final OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochMilli(value), getZoneId());
555             return type.cast(offsetDateTime);
556         }
557 
558         // java.util.Calendar
559         if (type.equals(Calendar.class)) {
560             Calendar calendar = null;
561             if (locale == null && timeZone == null) {
562                 calendar = Calendar.getInstance();
563             } else if (locale == null) {
564                 calendar = Calendar.getInstance(timeZone);
565             } else if (timeZone == null) {
566                 calendar = Calendar.getInstance(locale);
567             } else {
568                 calendar = Calendar.getInstance(timeZone, locale);
569             }
570             calendar.setTime(new Date(value));
571             calendar.setLenient(false);
572             return type.cast(calendar);
573         }
574 
575         final String msg = toString(getClass()) + " cannot handle conversion to '" + toString(type) + "'";
576         if (log().isWarnEnabled()) {
577             log().warn("    " + msg);
578         }
579         throw new ConversionException(msg);
580     }
581 
582     /**
583      * Default String to Date conversion.
584      * <p>
585      * This method handles conversion from a String to the following types:
586      * <ul>
587      * <li>{@link java.sql.Date}</li>
588      * <li>{@link java.sql.Time}</li>
589      * <li>{@link java.sql.Timestamp}</li>
590      * </ul>
591      * <p>
592      * <strong>N.B.</strong> No default String conversion mechanism is provided for {@link java.util.Date} and {@link java.util.Calendar} type.
593      *
594      * @param <T>   The target type
595      * @param type  The date type to convert to
596      * @param value The String value to convert.
597      * @return The converted Number value.
598      */
599     private <T> T toDate(final Class<T> type, final String value) {
600         // java.sql.Date
601         if (type.equals(java.sql.Date.class)) {
602             try {
603                 return type.cast(java.sql.Date.valueOf(value));
604             } catch (final IllegalArgumentException e) {
605                 throw new ConversionException("String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date");
606             }
607         }
608 
609         // java.sql.Time
610         if (type.equals(java.sql.Time.class)) {
611             try {
612                 return type.cast(java.sql.Time.valueOf(value));
613             } catch (final IllegalArgumentException e) {
614                 throw new ConversionException("String must be in JDBC format [HH:mm:ss] to create a java.sql.Time");
615             }
616         }
617 
618         // java.sql.Timestamp
619         if (type.equals(java.sql.Timestamp.class)) {
620             try {
621                 return type.cast(java.sql.Timestamp.valueOf(value));
622             } catch (final IllegalArgumentException e) {
623                 throw new ConversionException("String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] " + "to create a java.sql.Timestamp");
624             }
625         }
626 
627         final String msg = toString(getClass()) + " does not support default String to '" + toString(type) + "' conversion.";
628         if (log().isWarnEnabled()) {
629             log().warn("    " + msg);
630             log().warn("    (N.B. Re-configure Converter or use alternative implementation)");
631         }
632         throw new ConversionException(msg);
633     }
634 
635     /**
636      * Provide a String representation of this date/time converter.
637      *
638      * @return A String representation of this date/time converter
639      */
640     @Override
641     public String toString() {
642         final StringBuilder buffer = new StringBuilder();
643         buffer.append(toString(getClass()));
644         buffer.append("[UseDefault=");
645         buffer.append(isUseDefault());
646         buffer.append(", UseLocaleFormat=");
647         buffer.append(useLocaleFormat);
648         if (displayPatterns != null) {
649             buffer.append(", Patterns={");
650             buffer.append(displayPatterns);
651             buffer.append('}');
652         }
653         if (locale != null) {
654             buffer.append(", Locale=");
655             buffer.append(locale);
656         }
657         if (timeZone != null) {
658             buffer.append(", TimeZone=");
659             buffer.append(timeZone);
660         }
661         buffer.append(']');
662         return buffer.toString();
663     }
664 }