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 * https://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.beanutils.converters; 018 019import java.text.DateFormat; 020import java.text.ParsePosition; 021import java.text.SimpleDateFormat; 022import java.util.Calendar; 023import java.util.Date; 024import java.util.Locale; 025import java.util.TimeZone; 026 027import org.apache.commons.beanutils.ConversionException; 028 029/** 030 * {@link org.apache.commons.beanutils.Converter} implementation 031 * that handles conversion to and from <strong>date/time</strong> objects. 032 * <p> 033 * This implementation handles conversion for the following 034 * <em>date/time</em> types. 035 * </p> 036 * <ul> 037 * <li><code>java.util.Date</code></li> 038 * <li><code>java.util.Calendar</code></li> 039 * <li><code>java.sql.Date</code></li> 040 * <li><code>java.sql.Time</code></li> 041 * <li><code>java.sql.Timestamp</code></li> 042 * </ul> 043 * 044 * <h2>String Conversions (to and from)</h2> 045 * This class provides a number of ways in which date/time 046 * conversions to/from Strings can be achieved: 047 * <ul> 048 * <li>Using the SHORT date format for the default Locale, configure using: 049 * <ul> 050 * <li><code>setUseLocaleFormat(true)</code></li> 051 * </ul> 052 * </li> 053 * <li>Using the SHORT date format for a specified Locale, configure using: 054 * <ul> 055 * <li><code>setLocale(Locale)</code></li> 056 * </ul> 057 * </li> 058 * <li>Using the specified date pattern(s) for the default Locale, configure using: 059 * <ul> 060 * <li>Either <code>setPattern(String)</code> or 061 * <code>setPatterns(String[])</code></li> 062 * </ul> 063 * </li> 064 * <li>Using the specified date pattern(s) for a specified Locale, configure using: 065 * <ul> 066 * <li><code>setPattern(String)</code> or 067 * <code>setPatterns(String[]) and...</code></li> 068 * <li><code>setLocale(Locale)</code></li> 069 * </ul> 070 * </li> 071 * <li>If none of the above are configured the 072 * <code>toDate(String)</code> method is used to convert 073 * from String to Date and the Dates's 074 * <code>toString()</code> method used to convert from 075 * Date to String.</li> 076 * </ul> 077 * <p> 078 * The <strong>Time Zone</strong> to use with the date format can be specified 079 * using the <code>setTimeZone()</code> method. 080 * </p> 081 * 082 * @since 1.8.0 083 */ 084public abstract class DateTimeConverter extends AbstractConverter { 085 086 private String[] patterns; 087 private String displayPatterns; 088 private Locale locale; 089 private TimeZone timeZone; 090 private boolean useLocaleFormat; 091 092 /** 093 * Construct a Date/Time <em>Converter</em> that throws a 094 * <code>ConversionException</code> if an error occurs. 095 */ 096 public DateTimeConverter() { 097 } 098 099 /** 100 * Construct a Date/Time <em>Converter</em> that returns a default 101 * value if an error occurs. 102 * 103 * @param defaultValue The default value to be returned 104 * if the value to be converted is missing or an error 105 * occurs converting the value. 106 */ 107 public DateTimeConverter(final Object defaultValue) { 108 super(defaultValue); 109 } 110 111 /** 112 * Convert an input Date/Calendar object into a String. 113 * <p> 114 * <strong>N.B.</strong>If the converter has been configured to with 115 * one or more patterns (using <code>setPatterns()</code>), then 116 * the first pattern will be used to format the date into a String. 117 * Otherwise the default <code>DateFormat</code> for the default locale 118 * (and <em>style</em> if configured) will be used. 119 * 120 * @param value The input value to be converted 121 * @return the converted String value. 122 * @throws Throwable if an error occurs converting to a String 123 */ 124 @Override 125 protected String convertToString(final Object value) throws Throwable { 126 127 Date date = null; 128 if (value instanceof Date) { 129 date = (Date)value; 130 } else if (value instanceof Calendar) { 131 date = ((Calendar)value).getTime(); 132 } else if (value instanceof Long) { 133 date = new Date(((Long)value).longValue()); 134 } 135 136 String result = null; 137 if (useLocaleFormat && date != null) { 138 DateFormat format = null; 139 if (patterns != null && patterns.length > 0) { 140 format = getFormat(patterns[0]); 141 } else { 142 format = getFormat(locale, timeZone); 143 } 144 logFormat("Formatting", format); 145 result = format.format(date); 146 if (log().isDebugEnabled()) { 147 log().debug(" Converted to String using format '" + result + "'"); 148 } 149 } else { 150 result = value.toString(); 151 if (log().isDebugEnabled()) { 152 log().debug(" Converted to String using toString() '" + result + "'"); 153 } 154 } 155 return result; 156 } 157 158 /** 159 * Convert the input object into a Date object of the 160 * specified type. 161 * <p> 162 * This method handles conversions between the following 163 * types: 164 * <ul> 165 * <li><code>java.util.Date</code></li> 166 * <li><code>java.util.Calendar</code></li> 167 * <li><code>java.sql.Date</code></li> 168 * <li><code>java.sql.Time</code></li> 169 * <li><code>java.sql.Timestamp</code></li> 170 * </ul> 171 * 172 * It also handles conversion from a <code>String</code> to 173 * any of the above types. 174 * <p> 175 * 176 * For <code>String</code> conversion, if the converter has been configured 177 * with one or more patterns (using <code>setPatterns()</code>), then 178 * the conversion is attempted with each of the specified patterns. 179 * Otherwise the default <code>DateFormat</code> for the default locale 180 * (and <em>style</em> if configured) will be used. 181 * 182 * @param <T> The desired target type of the conversion. 183 * @param targetType Data type to which this value should be converted. 184 * @param value The input value to be converted. 185 * @return The converted value. 186 * @throws Exception if conversion cannot be performed successfully 187 */ 188 @Override 189 protected <T> T convertToType(final Class<T> targetType, final Object value) throws Exception { 190 191 final Class<?> sourceType = value.getClass(); 192 193 // Handle java.sql.Timestamp 194 if (value instanceof java.sql.Timestamp) { 195 196 // Prior to JDK 1.4 the Timestamp's getTime() method 197 // didn't include the milliseconds. The following code 198 // ensures it works consistently accross JDK versions 199 final java.sql.Timestamp timestamp = (java.sql.Timestamp)value; 200 long timeInMillis = timestamp.getTime() / 1000 * 1000; 201 timeInMillis += timestamp.getNanos() / 1000000; 202 return toDate(targetType, timeInMillis); 203 } 204 205 // Handle Date (includes java.sql.Date & java.sql.Time) 206 if (value instanceof Date) { 207 final Date date = (Date)value; 208 return toDate(targetType, date.getTime()); 209 } 210 211 // Handle Calendar 212 if (value instanceof Calendar) { 213 final Calendar calendar = (Calendar)value; 214 return toDate(targetType, calendar.getTime().getTime()); 215 } 216 217 // Handle Long 218 if (value instanceof Long) { 219 final Long longObj = (Long)value; 220 return toDate(targetType, longObj.longValue()); 221 } 222 223 // Convert all other types to String & handle 224 final String stringValue = value.toString().trim(); 225 if (stringValue.length() == 0) { 226 return handleMissing(targetType); 227 } 228 229 // Parse the Date/Time 230 if (useLocaleFormat) { 231 Calendar calendar = null; 232 if (patterns != null && patterns.length > 0) { 233 calendar = parse(sourceType, targetType, stringValue); 234 } else { 235 final DateFormat format = getFormat(locale, timeZone); 236 calendar = parse(sourceType, targetType, stringValue, format); 237 } 238 if (Calendar.class.isAssignableFrom(targetType)) { 239 return targetType.cast(calendar); 240 } 241 return toDate(targetType, calendar.getTime().getTime()); 242 } 243 244 // Default String conversion 245 return toDate(targetType, stringValue); 246 247 } 248 249 /** 250 * Return a <code>DateFormat</code> for the Locale. 251 * @param locale The Locale to create the Format with (may be null) 252 * @param timeZone The Time Zone create the Format with (may be null) 253 * @return A Date Format. 254 */ 255 protected DateFormat getFormat(final Locale locale, final TimeZone timeZone) { 256 DateFormat format = null; 257 if (locale == null) { 258 format = DateFormat.getDateInstance(DateFormat.SHORT); 259 } else { 260 format = DateFormat.getDateInstance(DateFormat.SHORT, locale); 261 } 262 if (timeZone != null) { 263 format.setTimeZone(timeZone); 264 } 265 return format; 266 } 267 268 /** 269 * Create a date format for the specified pattern. 270 * 271 * @param pattern The date pattern 272 * @return The DateFormat 273 */ 274 private DateFormat getFormat(final String pattern) { 275 final DateFormat format = new SimpleDateFormat(pattern); 276 if (timeZone != null) { 277 format.setTimeZone(timeZone); 278 } 279 return format; 280 } 281 282 /** 283 * Return the Locale for the <em>Converter</em> 284 * (or <code>null</code> if none specified). 285 * 286 * @return The locale to use for conversion 287 */ 288 public Locale getLocale() { 289 return locale; 290 } 291 292 /** 293 * Return the date format patterns used to convert 294 * dates to/from a <code>java.lang.String</code> 295 * (or <code>null</code> if none specified). 296 * 297 * @see SimpleDateFormat 298 * @return Array of format patterns. 299 */ 300 public String[] getPatterns() { 301 return patterns; 302 } 303 304 /** 305 * Return the Time Zone to use when converting dates 306 * (or <code>null</code> if none specified. 307 * 308 * @return The Time Zone. 309 */ 310 public TimeZone getTimeZone() { 311 return timeZone; 312 } 313 314 /** 315 * Log the <code>DateFormat<code> creation. 316 * @param action The action the format is being used for 317 * @param format The Date format 318 */ 319 private void logFormat(final String action, final DateFormat format) { 320 if (log().isDebugEnabled()) { 321 final StringBuilder buffer = new StringBuilder(45); 322 buffer.append(" "); 323 buffer.append(action); 324 buffer.append(" with Format"); 325 if (format instanceof SimpleDateFormat) { 326 buffer.append("["); 327 buffer.append(((SimpleDateFormat)format).toPattern()); 328 buffer.append("]"); 329 } 330 buffer.append(" for "); 331 if (locale == null) { 332 buffer.append("default locale"); 333 } else { 334 buffer.append("locale["); 335 buffer.append(locale); 336 buffer.append("]"); 337 } 338 if (timeZone != null) { 339 buffer.append(", TimeZone["); 340 buffer.append(timeZone); 341 buffer.append("]"); 342 } 343 log().debug(buffer.toString()); 344 } 345 } 346 347 /** 348 * Parse a String date value using the set of patterns. 349 * 350 * @param sourceType The type of the value being converted 351 * @param targetType The type to convert the value to. 352 * @param value The String date value. 353 * @return The converted Date object. 354 * @throws Exception if an error occurs parsing the date. 355 */ 356 private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value) throws Exception { 357 Exception firstEx = null; 358 for (final String pattern : patterns) { 359 try { 360 final DateFormat format = getFormat(pattern); 361 return parse(sourceType, targetType, value, format); 362 } catch (final Exception ex) { 363 if (firstEx == null) { 364 firstEx = ex; 365 } 366 } 367 } 368 if (patterns.length > 1) { 369 throw new ConversionException("Error converting '" + toString(sourceType) + "' to '" + toString(targetType) 370 + "' using patterns '" + displayPatterns + "'"); 371 } 372 throw firstEx; 373 } 374 375 /** 376 * Parse a String into a <code>Calendar</code> object 377 * using the specified <code>DateFormat</code>. 378 * 379 * @param sourceType The type of the value being converted 380 * @param targetType The type to convert the value to 381 * @param value The String date value. 382 * @param format The DateFormat to parse the String value. 383 * @return The converted Calendar object. 384 * @throws ConversionException if the String cannot be converted. 385 */ 386 private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value, final DateFormat format) { 387 logFormat("Parsing", format); 388 format.setLenient(false); 389 final ParsePosition pos = new ParsePosition(0); 390 final Date parsedDate = format.parse(value, pos); // ignore the result (use the Calendar) 391 if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedDate == null) { 392 String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'"; 393 if (format instanceof SimpleDateFormat) { 394 msg += " using pattern '" + ((SimpleDateFormat)format).toPattern() + "'"; 395 } 396 if (log().isDebugEnabled()) { 397 log().debug(" " + msg); 398 } 399 throw new ConversionException(msg); 400 } 401 return format.getCalendar(); 402 } 403 404 /** 405 * Set the Locale for the <em>Converter</em>. 406 * 407 * @param locale The Locale. 408 */ 409 public void setLocale(final Locale locale) { 410 this.locale = locale; 411 setUseLocaleFormat(true); 412 } 413 414 /** 415 * Set a date format pattern to use to convert 416 * dates to/from a <code>java.lang.String</code>. 417 * 418 * @see SimpleDateFormat 419 * @param pattern The format pattern. 420 */ 421 public void setPattern(final String pattern) { 422 setPatterns(new String[] {pattern}); 423 } 424 425 /** 426 * Set the date format patterns to use to convert 427 * dates to/from a <code>java.lang.String</code>. 428 * 429 * @see SimpleDateFormat 430 * @param patterns Array of format patterns. 431 */ 432 public void setPatterns(final String[] patterns) { 433 this.patterns = patterns; 434 if (patterns != null && patterns.length > 1) { 435 final String buffer = String.join(", ", patterns); 436 displayPatterns = buffer; 437 } 438 setUseLocaleFormat(true); 439 } 440 441 /** 442 * Set the Time Zone to use when converting dates. 443 * 444 * @param timeZone The Time Zone. 445 */ 446 public void setTimeZone(final TimeZone timeZone) { 447 this.timeZone = timeZone; 448 } 449 450 /** 451 * Indicate whether conversion should use a format/pattern or not. 452 * 453 * @param useLocaleFormat <code>true</code> if the format 454 * for the locale should be used, otherwise <code>false</code> 455 */ 456 public void setUseLocaleFormat(final boolean useLocaleFormat) { 457 this.useLocaleFormat = useLocaleFormat; 458 } 459 460 /** 461 * Convert a long value to the specified Date type for this 462 * <em>Converter</em>. 463 * <p> 464 * 465 * This method handles conversion to the following types: 466 * <ul> 467 * <li><code>java.util.Date</code></li> 468 * <li><code>java.util.Calendar</code></li> 469 * <li><code>java.sql.Date</code></li> 470 * <li><code>java.sql.Time</code></li> 471 * <li><code>java.sql.Timestamp</code></li> 472 * </ul> 473 * 474 * @param <T> The target type 475 * @param type The Date type to convert to 476 * @param value The long value to convert. 477 * @return The converted date value. 478 */ 479 private <T> T toDate(final Class<T> type, final long value) { 480 481 // java.util.Date 482 if (type.equals(Date.class)) { 483 return type.cast(new Date(value)); 484 } 485 486 // java.sql.Date 487 if (type.equals(java.sql.Date.class)) { 488 return type.cast(new java.sql.Date(value)); 489 } 490 491 // java.sql.Time 492 if (type.equals(java.sql.Time.class)) { 493 return type.cast(new java.sql.Time(value)); 494 } 495 496 // java.sql.Timestamp 497 if (type.equals(java.sql.Timestamp.class)) { 498 return type.cast(new java.sql.Timestamp(value)); 499 } 500 501 // java.util.Calendar 502 if (type.equals(Calendar.class)) { 503 Calendar calendar = null; 504 if (locale == null && timeZone == null) { 505 calendar = Calendar.getInstance(); 506 } else if (locale == null) { 507 calendar = Calendar.getInstance(timeZone); 508 } else if (timeZone == null) { 509 calendar = Calendar.getInstance(locale); 510 } else { 511 calendar = Calendar.getInstance(timeZone, locale); 512 } 513 calendar.setTime(new Date(value)); 514 calendar.setLenient(false); 515 return type.cast(calendar); 516 } 517 518 final String msg = toString(getClass()) + " cannot handle conversion to '" 519 + toString(type) + "'"; 520 if (log().isWarnEnabled()) { 521 log().warn(" " + msg); 522 } 523 throw new ConversionException(msg); 524 } 525 526 /** 527 * Default String to Date conversion. 528 * <p> 529 * This method handles conversion from a String to the following types: 530 * <ul> 531 * <li><code>java.sql.Date</code></li> 532 * <li><code>java.sql.Time</code></li> 533 * <li><code>java.sql.Timestamp</code></li> 534 * </ul> 535 * <p> 536 * <strong>N.B.</strong> No default String conversion 537 * mechanism is provided for <code>java.util.Date</code> 538 * and <code>java.util.Calendar</code> type. 539 * 540 * @param <T> The target type 541 * @param type The date type to convert to 542 * @param value The String value to convert. 543 * @return The converted Number value. 544 */ 545 private <T> T toDate(final Class<T> type, final String value) { 546 // java.sql.Date 547 if (type.equals(java.sql.Date.class)) { 548 try { 549 return type.cast(java.sql.Date.valueOf(value)); 550 } catch (final IllegalArgumentException e) { 551 throw new ConversionException( 552 "String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date"); 553 } 554 } 555 556 // java.sql.Time 557 if (type.equals(java.sql.Time.class)) { 558 try { 559 return type.cast(java.sql.Time.valueOf(value)); 560 } catch (final IllegalArgumentException e) { 561 throw new ConversionException( 562 "String must be in JDBC format [HH:mm:ss] to create a java.sql.Time"); 563 } 564 } 565 566 // java.sql.Timestamp 567 if (type.equals(java.sql.Timestamp.class)) { 568 try { 569 return type.cast(java.sql.Timestamp.valueOf(value)); 570 } catch (final IllegalArgumentException e) { 571 throw new ConversionException( 572 "String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] " + 573 "to create a java.sql.Timestamp"); 574 } 575 } 576 577 final String msg = toString(getClass()) + " does not support default String to '" 578 + toString(type) + "' conversion."; 579 if (log().isWarnEnabled()) { 580 log().warn(" " + msg); 581 log().warn(" (Re-configure Converter or use alternative implementation)"); 582 } 583 throw new ConversionException(msg); 584 } 585 586 /** 587 * Provide a String representation of this date/time converter. 588 * 589 * @return A String representation of this date/time converter 590 */ 591 @Override 592 public String toString() { 593 final StringBuilder buffer = new StringBuilder(); 594 buffer.append(toString(getClass())); 595 buffer.append("[UseDefault="); 596 buffer.append(isUseDefault()); 597 buffer.append(", UseLocaleFormat="); 598 buffer.append(useLocaleFormat); 599 if (displayPatterns != null) { 600 buffer.append(", Patterns={"); 601 buffer.append(displayPatterns); 602 buffer.append('}'); 603 } 604 if (locale != null) { 605 buffer.append(", Locale="); 606 buffer.append(locale); 607 } 608 if (timeZone != null) { 609 buffer.append(", TimeZone="); 610 buffer.append(timeZone); 611 } 612 buffer.append(']'); 613 return buffer.toString(); 614 } 615}