1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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
95
96 public DateTimeConverter() {
97 }
98
99
100
101
102
103
104 public DateTimeConverter(final D defaultValue) {
105 super(defaultValue);
106 }
107
108
109
110
111
112
113
114
115
116
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
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
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
196 if (value instanceof java.sql.Timestamp) {
197
198
199
200
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
209 if (value instanceof Date) {
210 final Date date = (Date) value;
211 return toDate(targetType, date.getTime());
212 }
213
214
215 if (value instanceof Calendar) {
216 final Calendar calendar = (Calendar) value;
217 return toDate(targetType, calendar.getTime().getTime());
218 }
219
220
221 if (value instanceof Long) {
222 final Long longObj = (Long) value;
223 return toDate(targetType, longObj.longValue());
224 }
225
226
227 if (value instanceof LocalDate) {
228 final LocalDate date = (LocalDate) value;
229 return toDate(targetType, date.atStartOfDay(getZoneId()).toInstant().toEpochMilli());
230 }
231
232
233 if (value instanceof LocalDateTime) {
234 final LocalDateTime date = (LocalDateTime) value;
235 return toDate(targetType, date.atZone(getZoneId()).toInstant().toEpochMilli());
236 }
237
238
239 if (value instanceof ZonedDateTime) {
240 final ZonedDateTime date = (ZonedDateTime) value;
241 return toDate(targetType, date.toInstant().toEpochMilli());
242 }
243
244
245 if (value instanceof OffsetDateTime) {
246 final OffsetDateTime date = (OffsetDateTime) value;
247 return toDate(targetType, date.toInstant().toEpochMilli());
248 }
249
250
251 final String stringValue = toTrim(value);
252 if (stringValue.isEmpty()) {
253 return handleMissing(targetType);
254 }
255
256
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
272 return toDate(targetType, stringValue);
273 }
274
275
276
277
278
279
280
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
297
298
299
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
311
312
313
314 public Locale getLocale() {
315 return locale;
316 }
317
318
319
320
321
322
323
324 public String[] getPatterns() {
325 return patterns.clone();
326 }
327
328
329
330
331
332
333 public TimeZone getTimeZone() {
334 return timeZone;
335 }
336
337
338
339
340
341
342 private ZoneId getZoneId() {
343 return timeZone == null ? ZoneId.systemDefault() : timeZone.toZoneId();
344 }
345
346
347
348
349
350
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
382
383
384
385
386
387
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
411
412
413
414
415
416
417
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);
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
442
443
444
445 public void setLocale(final Locale locale) {
446 this.locale = locale;
447 setUseLocaleFormat(true);
448 }
449
450
451
452
453
454
455
456 public void setPattern(final String pattern) {
457 setPatterns(new String[] { pattern });
458 }
459
460
461
462
463
464
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
476
477
478
479 public void setTimeZone(final TimeZone timeZone) {
480 this.timeZone = timeZone;
481 }
482
483
484
485
486
487
488 public void setUseLocaleFormat(final boolean useLocaleFormat) {
489 this.useLocaleFormat = useLocaleFormat;
490 }
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513 private <T> T toDate(final Class<T> type, final long value) {
514
515 if (type.equals(Date.class)) {
516 return type.cast(new Date(value));
517 }
518
519
520 if (type.equals(java.sql.Date.class)) {
521 return type.cast(new java.sql.Date(value));
522 }
523
524
525 if (type.equals(java.sql.Time.class)) {
526 return type.cast(new java.sql.Time(value));
527 }
528
529
530 if (type.equals(java.sql.Timestamp.class)) {
531 return type.cast(new java.sql.Timestamp(value));
532 }
533
534
535 if (type.equals(LocalDate.class)) {
536 final LocalDate localDate = Instant.ofEpochMilli(value).atZone(getZoneId()).toLocalDate();
537 return type.cast(localDate);
538 }
539
540
541 if (type.equals(LocalDateTime.class)) {
542 final LocalDateTime localDateTime = Instant.ofEpochMilli(value).atZone(getZoneId()).toLocalDateTime();
543 return type.cast(localDateTime);
544 }
545
546
547 if (type.equals(ZonedDateTime.class)) {
548 final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), getZoneId());
549 return type.cast(zonedDateTime);
550 }
551
552
553 if (type.equals(OffsetDateTime.class)) {
554 final OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochMilli(value), getZoneId());
555 return type.cast(offsetDateTime);
556 }
557
558
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
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599 private <T> T toDate(final Class<T> type, final String value) {
600
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
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
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
637
638
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 }