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.util.Date;
020import java.util.Locale;
021import java.util.Calendar;
022import java.util.TimeZone;
023import java.text.DateFormat;
024import java.text.SimpleDateFormat;
025import java.text.ParsePosition;
026import 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 */
079public 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}