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