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 * https://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 // 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(" (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 }