001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.beanutils2.converters; 018 019import java.text.DateFormat; 020import java.text.ParsePosition; 021import java.text.SimpleDateFormat; 022import java.time.Instant; 023import java.time.LocalDate; 024import java.time.LocalDateTime; 025import java.time.OffsetDateTime; 026import java.time.ZoneId; 027import java.time.ZonedDateTime; 028import java.time.temporal.TemporalAccessor; 029import java.util.Calendar; 030import java.util.Date; 031import java.util.Locale; 032import java.util.TimeZone; 033 034import org.apache.commons.beanutils2.ConversionException; 035 036/** 037 * {@link org.apache.commons.beanutils2.Converter} implementation that handles conversion to and from <strong>date/time</strong> objects. 038 * <p> 039 * This implementation handles conversion for the following <em>date/time</em> types. 040 * <ul> 041 * <li>{@link java.util.Date}</li> 042 * <li>{@link java.util.Calendar}</li> 043 * <li>{@link java.time.LocalDate}</li> 044 * <li>{@link java.time.LocalDateTime}</li> 045 * <li>{@link java.time.OffsetDateTime}</li> 046 * <li>{@link java.time.ZonedDateTime}</li> 047 * <li>{@link java.sql.Date}</li> 048 * <li>{@link java.sql.Time}</li> 049 * <li>{@link java.sql.Timestamp}</li> 050 * </ul> 051 * 052 * <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: 053 * <ul> 054 * <li>Using the SHORT date format for the default Locale, configure using: 055 * <ul> 056 * <li>{@code setUseLocaleFormat(true)}</li> 057 * </ul> 058 * </li> 059 * <li>Using the SHORT date format for a specified Locale, configure using: 060 * <ul> 061 * <li>{@code setLocale(Locale)}</li> 062 * </ul> 063 * </li> 064 * <li>Using the specified date pattern(s) for the default Locale, configure using: 065 * <ul> 066 * <li>Either {@code setPattern(String)} or {@code setPatterns(String[])}</li> 067 * </ul> 068 * </li> 069 * <li>Using the specified date pattern(s) for a specified Locale, configure using: 070 * <ul> 071 * <li>{@code setPattern(String)} or {@code setPatterns(String[]) and...}</li> 072 * <li>{@code setLocale(Locale)}</li> 073 * </ul> 074 * </li> 075 * <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 076 * used to convert from Date to String.</li> 077 * </ul> 078 * 079 * <p> 080 * The <strong>Time Zone</strong> to use with the date format can be specified using the {@link #setTimeZone(TimeZone)} method. 081 * 082 * @param <D> The default value type. 083 * @since 1.8.0 084 */ 085public abstract class DateTimeConverter<D> extends AbstractConverter<D> { 086 087 private String[] patterns; 088 private String displayPatterns; 089 private Locale locale; 090 private TimeZone timeZone; 091 private boolean useLocaleFormat; 092 093 /** 094 * Constructs a Date/Time <em>Converter</em> that throws a {@code ConversionException} if an error occurs. 095 */ 096 public DateTimeConverter() { 097 } 098 099 /** 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}