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 }