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