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