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.text.DateFormat;
20  import java.text.ParsePosition;
21  import java.text.SimpleDateFormat;
22  import java.util.Calendar;
23  import java.util.Date;
24  import java.util.Locale;
25  import java.util.TimeZone;
26  
27  import org.apache.commons.beanutils.ConversionException;
28  
29  /**
30   * {@link org.apache.commons.beanutils.Converter} implementaion
31   * that handles conversion to and from <b>date/time</b> objects.
32   * <p>
33   * This implementation handles conversion for the following
34   * <i>date/time</i> types.
35   * <ul>
36   *     <li><code>java.util.Date</code></li>
37   *     <li><code>java.util.Calendar</code></li>
38   *     <li><code>java.sql.Date</code></li>
39   *     <li><code>java.sql.Time</code></li>
40   *     <li><code>java.sql.Timestamp</code></li>
41   * </ul>
42   *
43   * <h3>String Conversions (to and from)</h3>
44   * This class provides a number of ways in which date/time
45   * conversions to/from Strings can be achieved:
46   * <ul>
47   *    <li>Using the SHORT date format for the default Locale, configure using:</li>
48   *        <ul>
49   *           <li><code>setUseLocaleFormat(true)</code></li>
50   *        </ul>
51   *    <li>Using the SHORT date format for a specified Locale, configure using:</li>
52   *        <ul>
53   *           <li><code>setLocale(Locale)</code></li>
54   *        </ul>
55   *    <li>Using the specified date pattern(s) for the default Locale, configure using:</li>
56   *        <ul>
57   *           <li>Either <code>setPattern(String)</code> or
58   *                      <code>setPatterns(String[])</code></li>
59   *        </ul>
60   *    <li>Using the specified date pattern(s) for a specified Locale, configure using:</li>
61   *        <ul>
62   *           <li><code>setPattern(String)</code> or
63   *                    <code>setPatterns(String[]) and...</code></li>
64   *           <li><code>setLocale(Locale)</code></li>
65   *        </ul>
66   *    <li>If none of the above are configured the
67   *        <code>toDate(String)</code> method is used to convert
68   *        from String to Date and the Dates's
69   *        <code>toString()</code> method used to convert from
70   *        Date to String.</li>
71   * </ul>
72   *
73   * <p>
74   * The <b>Time Zone</b> to use with the date format can be specified
75   * using the <code>setTimeZone()</code> method.
76   *
77   * @version $Id$
78   * @since 1.8.0
79   */
80  public abstract class DateTimeConverter extends AbstractConverter {
81  
82      private String[] patterns;
83      private String displayPatterns;
84      private Locale locale;
85      private TimeZone timeZone;
86      private boolean useLocaleFormat;
87  
88  
89      // ----------------------------------------------------------- Constructors
90  
91      /**
92       * Construct a Date/Time <i>Converter</i> that throws a
93       * <code>ConversionException</code> if an error occurs.
94       */
95      public DateTimeConverter() {
96          super();
97      }
98  
99      /**
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 }