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.text.DateFormat;
020import java.text.ParsePosition;
021import java.text.SimpleDateFormat;
022import java.util.Calendar;
023import java.util.Date;
024import java.util.Locale;
025import java.util.TimeZone;
026
027import org.apache.commons.beanutils.ConversionException;
028
029/**
030 * {@link org.apache.commons.beanutils.Converter} implementaion
031 * that handles conversion to and from <b>date/time</b> objects.
032 * <p>
033 * This implementation handles conversion for the following
034 * <i>date/time</i> types.
035 * <ul>
036 *     <li><code>java.util.Date</code></li>
037 *     <li><code>java.util.Calendar</code></li>
038 *     <li><code>java.sql.Date</code></li>
039 *     <li><code>java.sql.Time</code></li>
040 *     <li><code>java.sql.Timestamp</code></li>
041 * </ul>
042 *
043 * <h3>String Conversions (to and from)</h3>
044 * This class provides a number of ways in which date/time
045 * conversions to/from Strings can be achieved:
046 * <ul>
047 *    <li>Using the SHORT date format for the default Locale, configure using:</li>
048 *        <ul>
049 *           <li><code>setUseLocaleFormat(true)</code></li>
050 *        </ul>
051 *    <li>Using the SHORT date format for a specified Locale, configure using:</li>
052 *        <ul>
053 *           <li><code>setLocale(Locale)</code></li>
054 *        </ul>
055 *    <li>Using the specified date pattern(s) for the default Locale, configure using:</li>
056 *        <ul>
057 *           <li>Either <code>setPattern(String)</code> or
058 *                      <code>setPatterns(String[])</code></li>
059 *        </ul>
060 *    <li>Using the specified date pattern(s) for a specified Locale, configure using:</li>
061 *        <ul>
062 *           <li><code>setPattern(String)</code> or
063 *                    <code>setPatterns(String[]) and...</code></li>
064 *           <li><code>setLocale(Locale)</code></li>
065 *        </ul>
066 *    <li>If none of the above are configured the
067 *        <code>toDate(String)</code> method is used to convert
068 *        from String to Date and the Dates's
069 *        <code>toString()</code> method used to convert from
070 *        Date to String.</li>
071 * </ul>
072 *
073 * <p>
074 * The <b>Time Zone</b> to use with the date format can be specified
075 * using the <code>setTimeZone()</code> method.
076 *
077 * @version $Id$
078 * @since 1.8.0
079 */
080public abstract class DateTimeConverter extends AbstractConverter {
081
082    private String[] patterns;
083    private String displayPatterns;
084    private Locale locale;
085    private TimeZone timeZone;
086    private boolean useLocaleFormat;
087
088
089    // ----------------------------------------------------------- Constructors
090
091    /**
092     * Construct a Date/Time <i>Converter</i> that throws a
093     * <code>ConversionException</code> if an error occurs.
094     */
095    public DateTimeConverter() {
096        super();
097    }
098
099    /**
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}