View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.beanutils.converters;
18  
19  import java.util.Date;
20  import java.util.Locale;
21  import java.util.Calendar;
22  import java.util.TimeZone;
23  import java.text.DateFormat;
24  import java.text.SimpleDateFormat;
25  import java.text.ParsePosition;
26  import org.apache.commons.beanutils.ConversionException;
27  
28  /***
29   * {@link org.apache.commons.beanutils.Converter} implementaion
30   * that handles conversion to and from <b>date/time</b> objects.
31   * <p>
32   * This implementation handles conversion for the following
33   * <i>date/time</i> types.
34   * <ul>
35   *     <li><code>java.util.Date</code></li>
36   *     <li><code>java.util.Calendar</code></li>
37   *     <li><code>java.sql.Date</code></li>
38   *     <li><code>java.sql.Time</code></li>
39   *     <li><code>java.sql.Timestamp</code></li>
40   * </ul>
41   *
42   * <h3>String Conversions (to and from)</h3>
43   * This class provides a number of ways in which date/time
44   * conversions to/from Strings can be achieved:
45   * <ul>
46   *    <li>Using the SHORT date format for the default Locale, configure using:</li>
47   *        <ul>
48   *           <li><code>setUseLocaleFormat(true)</code></li>
49   *        </ul>
50   *    <li>Using the SHORT date format for a specified Locale, configure using:</li>
51   *        <ul>
52   *           <li><code>setLocale(Locale)</code></li>
53   *        </ul>
54   *    <li>Using the specified date pattern(s) for the default Locale, configure using:</li>
55   *        <ul>
56   *           <li>Either <code>setPattern(String)</code> or
57   *                      <code>setPatterns(String[])</code></li>
58   *        </ul>
59   *    <li>Using the specified date pattern(s) for a specified Locale, configure using:</li>
60   *        <ul>
61   *           <li><code>setPattern(String)</code> or
62   *                    <code>setPatterns(String[]) and...</code></li>
63   *           <li><code>setLocale(Locale)</code></li>
64   *        </ul>
65   *    <li>If none of the above are configured the
66   *        <code>toDate(String)</code> method is used to convert
67   *        from String to Date and the Dates's
68   *        <code>toString()</code> method used to convert from
69   *        Date to String.</li>
70   * </ul>
71   *
72   * <p>
73   * The <b>Time Zone</b> to use with the date format can be specified
74   * using the <code>setTimeZone()</code> method.
75   *
76   * @version $Revision: 555845 $ $Date: 2007-07-13 03:52:05 +0100 (Fri, 13 Jul 2007) $
77   * @since 1.8.0
78   */
79  public class DateTimeConverter extends AbstractConverter {
80  
81      private String[] patterns;
82      private String displayPatterns;
83      private Locale locale;
84      private TimeZone timeZone;
85      private boolean useLocaleFormat;
86  
87  
88      // ----------------------------------------------------------- Constructors
89  
90      /***
91       * Construct a Date/Time <i>Converter</i> that throws a
92       * <code>ConversionException</code> if an error occurs.
93       *
94       * @param defaultType The default type this <code>Converter</code>
95       * handles
96       */
97      public DateTimeConverter(Class defaultType) {
98          super(defaultType);
99      }
100 
101     /***
102      * Construct a Date/Time <i>Converter</i> that returns a default
103      * value if an error occurs.
104      *
105      * @param defaultType The default type this <code>Converter</code>
106      * handles
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(Class defaultType, Object defaultValue) {
112         super(defaultType, 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(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(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(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(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(String[] patterns) {
198         this.patterns = patterns;
199         if (patterns != null && patterns.length > 1) {
200             StringBuffer buffer = new StringBuffer();
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     protected String convertToString(Object value) throws Throwable {
228 
229         Date date = null;
230         if (value instanceof Date) {
231             date = (Date)value;
232         } else if (value instanceof Calendar) {
233             date = ((Calendar)value).getTime();
234         } else if (value instanceof Long) {
235             date = new Date(((Long)value).longValue());
236         }
237 
238         String result = null;
239         if (useLocaleFormat && date != null) {
240             DateFormat format = null;
241             if (patterns != null && patterns.length > 0) {
242                 format = getFormat(patterns[0]);
243             } else {
244                 format = getFormat(locale, timeZone);
245             }
246             logFormat("Formatting", format);
247             result = format.format(date);
248             if (log().isDebugEnabled()) {
249                 log().debug("    Converted  to String using format '" + result + "'");
250             }
251         } else {
252             result = value.toString();
253             if (log().isDebugEnabled()) {
254                 log().debug("    Converted  to String using toString() '" + result + "'");
255              }
256         }
257         return result;
258     }
259 
260     /***
261      * Convert the input object into a Date object of the
262      * specified type.
263      * <p>
264      * This method handles conversions between the following
265      * types:
266      * <ul>
267      *     <li><code>java.util.Date</code></li>
268      *     <li><code>java.util.Calendar</code></li>
269      *     <li><code>java.sql.Date</code></li>
270      *     <li><code>java.sql.Time</code></li>
271      *     <li><code>java.sql.Timestamp</code></li>
272      * </ul>
273      *
274      * It also handles conversion from a <code>String</code> to
275      * any of the above types.
276      * <p>
277      *
278      * For <code>String</code> conversion, if the converter has been configured
279      * with one or more patterns (using <code>setPatterns()</code>), then
280      * the conversion is attempted with each of the specified patterns.
281      * Otherwise the default <code>DateFormat</code> for the default locale
282      * (and <i>style</i> if configured) will be used.
283      *
284      * @param targetType Data type to which this value should be converted.
285      * @param value The input value to be converted.
286      * @return The converted value.
287      * @throws Exception if conversion cannot be performed successfully
288      */
289     protected Object convertToType(Class targetType, Object value) throws Exception {
290 
291         Class sourceType = value.getClass();
292 
293         // Handle java.sql.Timestamp
294         if (value instanceof java.sql.Timestamp) {
295 
296             // ---------------------- JDK 1.3 Fix ----------------------
297             // N.B. Prior to JDK 1.4 the Timestamp's getTime() method
298             //      didn't include the milliseconds. The following code
299             //      ensures it works consistently accross JDK versions
300             java.sql.Timestamp timestamp = (java.sql.Timestamp)value;
301             long timeInMillis = ((timestamp.getTime() / 1000) * 1000);
302             timeInMillis += timestamp.getNanos() / 1000000;
303             // ---------------------- JDK 1.3 Fix ----------------------
304             return toDate(targetType, timeInMillis);
305         }
306 
307         // Handle Date (includes java.sql.Date & java.sql.Time)
308         if (value instanceof Date) {
309             Date date = (Date)value;
310             return toDate(targetType, date.getTime());
311         }
312 
313         // Handle Calendar
314         if (value instanceof Calendar) {
315             Calendar calendar = (Calendar)value;
316             return toDate(targetType, calendar.getTime().getTime());
317         }
318 
319         // Handle Long
320         if (value instanceof Long) {
321             Long longObj = (Long)value;
322             return toDate(targetType, longObj.longValue());
323         }
324 
325         // Convert all other types to String & handle
326         String stringValue = value.toString().trim();
327         if (stringValue.length() == 0) {
328             return handleMissing(targetType);
329         }
330 
331         // Parse the Date/Time
332         if (useLocaleFormat) {
333             Calendar calendar = null;
334             if (patterns != null && patterns.length > 0) {
335                 calendar = parse(sourceType, targetType, stringValue);
336             } else {
337                 DateFormat format = getFormat(locale, timeZone);
338                 calendar = parse(sourceType, targetType, stringValue, format);
339             }
340             if (Calendar.class.isAssignableFrom(targetType)) {
341                 return calendar;
342             } else {
343                 return toDate(targetType, calendar.getTime().getTime());
344             }
345         }
346 
347         // Default String conversion
348         return toDate(targetType, stringValue);
349 
350     }
351 
352     /***
353      * Convert a long value to the specified Date type for this
354      * <i>Converter</i>.
355      * <p>
356      *
357      * This method handles conversion to the following types:
358      * <ul>
359      *     <li><code>java.util.Date</code></li>
360      *     <li><code>java.util.Calendar</code></li>
361      *     <li><code>java.sql.Date</code></li>
362      *     <li><code>java.sql.Time</code></li>
363      *     <li><code>java.sql.Timestamp</code></li>
364      * </ul>
365      *
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 Object toDate(Class type, long value) {
371 
372         // java.util.Date
373         if (type.equals(Date.class)) {
374             return new Date(value);
375         }
376 
377         // java.sql.Date
378         if (type.equals(java.sql.Date.class)) {
379             return new java.sql.Date(value);
380         }
381 
382         // java.sql.Time
383         if (type.equals(java.sql.Time.class)) {
384             return new java.sql.Time(value);
385         }
386 
387         // java.sql.Timestamp
388         if (type.equals(java.sql.Timestamp.class)) {
389             return 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 calendar;
407         }
408 
409         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 type The Number type to convert to
432      * @param value The String value to convert.
433      * @return The converted Number value.
434      */
435     private Object toDate(Class type, String value) {
436         // java.sql.Date
437         if (type.equals(java.sql.Date.class)) {
438             try {
439                 return java.sql.Date.valueOf(value);
440             } catch (IllegalArgumentException e) {
441                 throw new ConversionException(
442                         "String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date");
443             }
444         }
445 
446         // java.sql.Time
447         if (type.equals(java.sql.Time.class)) {
448             try {
449                 return java.sql.Time.valueOf(value);
450             } catch (IllegalArgumentException e) {
451                 throw new ConversionException(
452                         "String must be in JDBC format [HH:mm:ss] to create a java.sql.Time");
453             }
454         }
455 
456         // java.sql.Timestamp
457         if (type.equals(java.sql.Timestamp.class)) {
458             try {
459                 return java.sql.Timestamp.valueOf(value);
460             } catch (IllegalArgumentException e) {
461                 throw new ConversionException(
462                         "String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] " +
463                         "to create a java.sql.Timestamp");
464             }
465         }
466 
467         String msg = toString(getClass()) + " does not support default String to '"
468                    + toString(type) + "' conversion.";
469         if (log().isWarnEnabled()) {
470             log().warn("    " + msg);
471             log().warn("    (N.B. Re-configure Converter or use alternative implementation)");
472         }
473         throw new ConversionException(msg);
474     }
475 
476     /***
477      * Return a <code>DateFormat<code> for the Locale.
478      * @param locale The Locale to create the Format with (may be null)
479      * @param timeZone The Time Zone create the Format with (may be null)
480      *
481      * @return A Date Format.
482      */
483     protected DateFormat getFormat(Locale locale, TimeZone timeZone) {
484         DateFormat format = null;
485         if (locale == null) {
486             format = DateFormat.getDateInstance(DateFormat.SHORT);
487         } else {
488             format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
489         }
490         if (timeZone != null) {
491             format.setTimeZone(timeZone);
492         }
493         return format;
494     }
495 
496     /***
497      * Create a date format for the specified pattern.
498      *
499      * @param pattern The date pattern
500      * @return The DateFormat
501      */
502     private DateFormat getFormat(String pattern) {
503         DateFormat format = new SimpleDateFormat(pattern);
504         if (timeZone != null) {
505             format.setTimeZone(timeZone);
506         }
507         return format;
508     }
509 
510     /***
511      * Parse a String date value using the set of patterns.
512      *
513      * @param sourceType The type of the value being converted
514      * @param targetType The type to convert the value to.
515      * @param value The String date value.
516      *
517      * @return The converted Date object.
518      * @throws Exception if an error occurs parsing the date.
519      */
520     private Calendar parse(Class sourceType, Class targetType, String value) throws Exception {
521         Exception firstEx = null;
522         for (int i = 0; i < patterns.length; i++) {
523             try {
524                 DateFormat format = getFormat(patterns[i]);
525                 Calendar calendar = parse(sourceType, targetType, value, format);
526                 return calendar;
527             } catch (Exception ex) {
528                 if (firstEx == null) {
529                     firstEx = ex;
530                 }
531             }
532         }
533         if (patterns.length > 1) {
534             throw new ConversionException("Error converting '" + toString(sourceType) + "' to '" + toString(targetType)
535                     + "' using  patterns '" + displayPatterns + "'");
536         } else {
537             throw firstEx;
538         }
539     }
540 
541     /***
542      * Parse a String into a <code>Calendar</code> object
543      * using the specified <code>DateFormat</code>.
544      *
545      * @param sourceType The type of the value being converted
546      * @param targetType The type to convert the value to
547      * @param value The String date value.
548      * @param format The DateFormat to parse the String value.
549      *
550      * @return The converted Calendar object.
551      * @throws ConversionException if the String cannot be converted.
552      */
553     private Calendar parse(Class sourceType, Class targetType, String value, DateFormat format) {
554         logFormat("Parsing", format);
555         format.setLenient(false);
556         ParsePosition pos = new ParsePosition(0);
557         Date parsedDate = format.parse(value, pos); // ignore the result (use the Calendar)
558         if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedDate == null) {
559             String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
560             if (format instanceof SimpleDateFormat) {
561                 msg += " using pattern '" + ((SimpleDateFormat)format).toPattern() + "'";
562             }
563             if (log().isDebugEnabled()) {
564                 log().debug("    " + msg);
565             }
566             throw new ConversionException(msg);
567         }
568         Calendar calendar = format.getCalendar();
569         return calendar;
570     }
571 
572     /***
573      * Provide a String representation of this date/time converter.
574      *
575      * @return A String representation of this date/time converter
576      */
577     public String toString() {
578         StringBuffer buffer = new StringBuffer();
579         buffer.append(toString(getClass()));
580         buffer.append("[UseDefault=");
581         buffer.append(isUseDefault());
582         buffer.append(", UseLocaleFormat=");
583         buffer.append(useLocaleFormat);
584         if (displayPatterns != null) {
585             buffer.append(", Patterns={");
586             buffer.append(displayPatterns);
587             buffer.append('}');
588         }
589         if (locale != null) {
590             buffer.append(", Locale=");
591             buffer.append(locale);
592         }
593         if (timeZone != null) {
594             buffer.append(", TimeZone=");
595             buffer.append(timeZone);
596         }
597         buffer.append(']');
598         return buffer.toString();
599     }
600 
601     /***
602      * Log the <code>DateFormat<code> creation.
603      * @param action The action the format is being used for
604      * @param format The Date format
605      */
606     private void logFormat(String action, DateFormat format) {
607         if (log().isDebugEnabled()) {
608             StringBuffer buffer = new StringBuffer(45);
609             buffer.append("    ");
610             buffer.append(action);
611             buffer.append(" with Format");
612             if (format instanceof SimpleDateFormat) {
613                 buffer.append("[");
614                 buffer.append(((SimpleDateFormat)format).toPattern());
615                 buffer.append("]");
616             }
617             buffer.append(" for ");
618             if (locale == null) {
619                 buffer.append("default locale");
620             } else {
621                 buffer.append("locale[");
622                 buffer.append(locale);
623                 buffer.append("]");
624             }
625             if (timeZone != null) {
626                 buffer.append(", TimeZone[");
627                 buffer.append(timeZone);
628                 buffer.append("]");
629             }
630             log().debug(buffer.toString());
631         }
632     }
633 }