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:
048 *        <ul>
049 *           <li><code>setUseLocaleFormat(true)</code></li>
050 *        </ul>
051 *    </li>
052 *    <li>Using the SHORT date format for a specified Locale, configure using:
053 *        <ul>
054 *           <li><code>setLocale(Locale)</code></li>
055 *        </ul>
056 *    </li>
057 *    <li>Using the specified date pattern(s) for the default Locale, configure using:
058 *        <ul>
059 *           <li>Either <code>setPattern(String)</code> or
060 *                      <code>setPatterns(String[])</code></li>
061 *        </ul>
062 *    </li>
063 *    <li>Using the specified date pattern(s) for a specified Locale, configure using:
064 *        <ul>
065 *           <li><code>setPattern(String)</code> or
066 *                    <code>setPatterns(String[]) and...</code></li>
067 *           <li><code>setLocale(Locale)</code></li>
068 *        </ul>
069 *    </li>
070 *    <li>If none of the above are configured the
071 *        <code>toDate(String)</code> method is used to convert
072 *        from String to Date and the Dates's
073 *        <code>toString()</code> method used to convert from
074 *        Date to String.</li>
075 * </ul>
076 *
077 * <p>
078 * The <b>Time Zone</b> to use with the date format can be specified
079 * using the {@link #setTimeZone(TimeZone)} method.
080 *
081 * @version $Id$
082 * @since 1.8.0
083 */
084public abstract class DateTimeConverter extends AbstractConverter {
085
086    private String[] patterns;
087    private String displayPatterns;
088    private Locale locale;
089    private TimeZone timeZone;
090    private boolean useLocaleFormat;
091
092
093    // ----------------------------------------------------------- Constructors
094
095    /**
096     * Construct a Date/Time <i>Converter</i> that throws a
097     * <code>ConversionException</code> if an error occurs.
098     */
099    public DateTimeConverter() {
100        super();
101    }
102
103    /**
104     * Construct a Date/Time <i>Converter</i> that returns a default
105     * value if an error occurs.
106     *
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(final Object defaultValue) {
112        super(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(final 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(final 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(final 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(final 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(final String[] patterns) {
198        this.patterns = patterns;
199        if (patterns != null && patterns.length > 1) {
200            final StringBuilder buffer = new StringBuilder();
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    @Override
228    protected String convertToString(final Object value) throws Throwable {
229
230        Date date = null;
231        if (value instanceof Date) {
232            date = (Date)value;
233        } else if (value instanceof Calendar) {
234            date = ((Calendar)value).getTime();
235        } else if (value instanceof Long) {
236            date = new Date(((Long)value).longValue());
237        }
238
239        String result = null;
240        if (useLocaleFormat && date != null) {
241            DateFormat format = null;
242            if (patterns != null && patterns.length > 0) {
243                format = getFormat(patterns[0]);
244            } else {
245                format = getFormat(locale, timeZone);
246            }
247            logFormat("Formatting", format);
248            result = format.format(date);
249            if (log().isDebugEnabled()) {
250                log().debug("    Converted  to String using format '" + result + "'");
251            }
252        } else {
253            result = value.toString();
254            if (log().isDebugEnabled()) {
255                log().debug("    Converted  to String using toString() '" + result + "'");
256             }
257        }
258        return result;
259    }
260
261    /**
262     * Convert the input object into a Date object of the
263     * specified type.
264     * <p>
265     * This method handles conversions between the following
266     * types:
267     * <ul>
268     *     <li><code>java.util.Date</code></li>
269     *     <li><code>java.util.Calendar</code></li>
270     *     <li><code>java.sql.Date</code></li>
271     *     <li><code>java.sql.Time</code></li>
272     *     <li><code>java.sql.Timestamp</code></li>
273     * </ul>
274     *
275     * It also handles conversion from a <code>String</code> to
276     * any of the above types.
277     * <p>
278     *
279     * For <code>String</code> conversion, if the converter has been configured
280     * with one or more patterns (using <code>setPatterns()</code>), then
281     * the conversion is attempted with each of the specified patterns.
282     * Otherwise the default <code>DateFormat</code> for the default locale
283     * (and <i>style</i> if configured) will be used.
284     *
285     * @param <T> The desired target type of the conversion.
286     * @param targetType Data type to which this value should be converted.
287     * @param value The input value to be converted.
288     * @return The converted value.
289     * @throws Exception if conversion cannot be performed successfully
290     */
291    @Override
292    protected <T> T convertToType(final Class<T> targetType, final Object value) throws Exception {
293
294        final Class<?> sourceType = value.getClass();
295
296        // Handle java.sql.Timestamp
297        if (value instanceof java.sql.Timestamp) {
298
299            // ---------------------- JDK 1.3 Fix ----------------------
300            // N.B. Prior to JDK 1.4 the Timestamp's getTime() method
301            //      didn't include the milliseconds. The following code
302            //      ensures it works consistently accross JDK versions
303            final java.sql.Timestamp timestamp = (java.sql.Timestamp)value;
304            long timeInMillis = ((timestamp.getTime() / 1000) * 1000);
305            timeInMillis += timestamp.getNanos() / 1000000;
306            // ---------------------- JDK 1.3 Fix ----------------------
307            return toDate(targetType, timeInMillis);
308        }
309
310        // Handle Date (includes java.sql.Date & java.sql.Time)
311        if (value instanceof Date) {
312            final Date date = (Date)value;
313            return toDate(targetType, date.getTime());
314        }
315
316        // Handle Calendar
317        if (value instanceof Calendar) {
318            final Calendar calendar = (Calendar)value;
319            return toDate(targetType, calendar.getTime().getTime());
320        }
321
322        // Handle Long
323        if (value instanceof Long) {
324            final Long longObj = (Long)value;
325            return toDate(targetType, longObj.longValue());
326        }
327
328        // Convert all other types to String & handle
329        final String stringValue = value.toString().trim();
330        if (stringValue.length() == 0) {
331            return handleMissing(targetType);
332        }
333
334        // Parse the Date/Time
335        if (useLocaleFormat) {
336            Calendar calendar = null;
337            if (patterns != null && patterns.length > 0) {
338                calendar = parse(sourceType, targetType, stringValue);
339            } else {
340                final DateFormat format = getFormat(locale, timeZone);
341                calendar = parse(sourceType, targetType, stringValue, format);
342            }
343            if (Calendar.class.isAssignableFrom(targetType)) {
344                return targetType.cast(calendar);
345            } else {
346                return toDate(targetType, calendar.getTime().getTime());
347            }
348        }
349
350        // Default String conversion
351        return toDate(targetType, stringValue);
352
353    }
354
355    /**
356     * Convert a long value to the specified Date type for this
357     * <i>Converter</i>.
358     * <p>
359     *
360     * This method handles conversion to the following types:
361     * <ul>
362     *     <li><code>java.util.Date</code></li>
363     *     <li><code>java.util.Calendar</code></li>
364     *     <li><code>java.sql.Date</code></li>
365     *     <li><code>java.sql.Time</code></li>
366     *     <li><code>java.sql.Timestamp</code></li>
367     * </ul>
368     *
369     * @param <T> The target type
370     * @param type The Date type to convert to
371     * @param value The long value to convert.
372     * @return The converted date value.
373     */
374    private <T> T toDate(final Class<T> type, final long value) {
375
376        // java.util.Date
377        if (type.equals(Date.class)) {
378            return type.cast(new Date(value));
379        }
380
381        // java.sql.Date
382        if (type.equals(java.sql.Date.class)) {
383            return type.cast(new java.sql.Date(value));
384        }
385
386        // java.sql.Time
387        if (type.equals(java.sql.Time.class)) {
388            return type.cast(new java.sql.Time(value));
389        }
390
391        // java.sql.Timestamp
392        if (type.equals(java.sql.Timestamp.class)) {
393            return type.cast(new java.sql.Timestamp(value));
394        }
395
396        // java.util.Calendar
397        if (type.equals(Calendar.class)) {
398            Calendar calendar = null;
399            if (locale == null && timeZone == null) {
400                calendar = Calendar.getInstance();
401            } else if (locale == null) {
402                calendar = Calendar.getInstance(timeZone);
403            } else if (timeZone == null) {
404                calendar = Calendar.getInstance(locale);
405            } else {
406                calendar = Calendar.getInstance(timeZone, locale);
407            }
408            calendar.setTime(new Date(value));
409            calendar.setLenient(false);
410            return type.cast(calendar);
411        }
412
413        final String msg = toString(getClass()) + " cannot handle conversion to '"
414                   + toString(type) + "'";
415        if (log().isWarnEnabled()) {
416            log().warn("    " + msg);
417        }
418        throw new ConversionException(msg);
419    }
420
421    /**
422     * Default String to Date conversion.
423     * <p>
424     * This method handles conversion from a String to the following types:
425     * <ul>
426     *     <li><code>java.sql.Date</code></li>
427     *     <li><code>java.sql.Time</code></li>
428     *     <li><code>java.sql.Timestamp</code></li>
429     * </ul>
430     * <p>
431     * <strong>N.B.</strong> No default String conversion
432     * mechanism is provided for <code>java.util.Date</code>
433     * and <code>java.util.Calendar</code> type.
434     *
435     * @param <T> The target type
436     * @param type The date type to convert to
437     * @param value The String value to convert.
438     * @return The converted Number value.
439     */
440    private <T> T toDate(final Class<T> type, final String value) {
441        // java.sql.Date
442        if (type.equals(java.sql.Date.class)) {
443            try {
444                return type.cast(java.sql.Date.valueOf(value));
445            } catch (final IllegalArgumentException e) {
446                throw new ConversionException(
447                        "String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date");
448            }
449        }
450
451        // java.sql.Time
452        if (type.equals(java.sql.Time.class)) {
453            try {
454                return type.cast(java.sql.Time.valueOf(value));
455            } catch (final IllegalArgumentException e) {
456                throw new ConversionException(
457                        "String must be in JDBC format [HH:mm:ss] to create a java.sql.Time");
458            }
459        }
460
461        // java.sql.Timestamp
462        if (type.equals(java.sql.Timestamp.class)) {
463            try {
464                return type.cast(java.sql.Timestamp.valueOf(value));
465            } catch (final IllegalArgumentException e) {
466                throw new ConversionException(
467                        "String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] " +
468                        "to create a java.sql.Timestamp");
469            }
470        }
471
472        final String msg = toString(getClass()) + " does not support default String to '"
473                   + toString(type) + "' conversion.";
474        if (log().isWarnEnabled()) {
475            log().warn("    " + msg);
476            log().warn("    (N.B. Re-configure Converter or use alternative implementation)");
477        }
478        throw new ConversionException(msg);
479    }
480
481    /**
482     * Return a <code>DateFormat</code> for the Locale.
483     * @param locale The Locale to create the Format with (may be null)
484     * @param timeZone The Time Zone create the Format with (may be null)
485     *
486     * @return A Date Format.
487     */
488    protected DateFormat getFormat(final Locale locale, final TimeZone timeZone) {
489        DateFormat format = null;
490        if (locale == null) {
491            format = DateFormat.getDateInstance(DateFormat.SHORT);
492        } else {
493            format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
494        }
495        if (timeZone != null) {
496            format.setTimeZone(timeZone);
497        }
498        return format;
499    }
500
501    /**
502     * Create a date format for the specified pattern.
503     *
504     * @param pattern The date pattern
505     * @return The DateFormat
506     */
507    private DateFormat getFormat(final String pattern) {
508        final DateFormat format = new SimpleDateFormat(pattern);
509        if (timeZone != null) {
510            format.setTimeZone(timeZone);
511        }
512        return format;
513    }
514
515    /**
516     * Parse a String date value using the set of patterns.
517     *
518     * @param sourceType The type of the value being converted
519     * @param targetType The type to convert the value to.
520     * @param value The String date value.
521     *
522     * @return The converted Date object.
523     * @throws Exception if an error occurs parsing the date.
524     */
525    private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value) throws Exception {
526        Exception firstEx = null;
527        for (String pattern : patterns) {
528            try {
529                final DateFormat format = getFormat(pattern);
530                final Calendar calendar = parse(sourceType, targetType, value, format);
531                return calendar;
532            } catch (final Exception ex) {
533                if (firstEx == null) {
534                    firstEx = ex;
535                }
536            }
537        }
538        if (patterns.length > 1) {
539            throw new ConversionException("Error converting '" + toString(sourceType) + "' to '" + toString(targetType)
540                    + "' using  patterns '" + displayPatterns + "'");
541        } else {
542            throw firstEx;
543        }
544    }
545
546    /**
547     * Parse a String into a <code>Calendar</code> object
548     * using the specified <code>DateFormat</code>.
549     *
550     * @param sourceType The type of the value being converted
551     * @param targetType The type to convert the value to
552     * @param value The String date value.
553     * @param format The DateFormat to parse the String value.
554     *
555     * @return The converted Calendar object.
556     * @throws ConversionException if the String cannot be converted.
557     */
558    private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value, final DateFormat format) {
559        logFormat("Parsing", format);
560        format.setLenient(false);
561        final ParsePosition pos = new ParsePosition(0);
562        final Date parsedDate = format.parse(value, pos); // ignore the result (use the Calendar)
563        if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedDate == null) {
564            String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
565            if (format instanceof SimpleDateFormat) {
566                msg += " using pattern '" + ((SimpleDateFormat)format).toPattern() + "'";
567            }
568            if (log().isDebugEnabled()) {
569                log().debug("    " + msg);
570            }
571            throw new ConversionException(msg);
572        }
573        final Calendar calendar = format.getCalendar();
574        return calendar;
575    }
576
577    /**
578     * Provide a String representation of this date/time converter.
579     *
580     * @return A String representation of this date/time converter
581     */
582    @Override
583    public String toString() {
584        final StringBuilder buffer = new StringBuilder();
585        buffer.append(toString(getClass()));
586        buffer.append("[UseDefault=");
587        buffer.append(isUseDefault());
588        buffer.append(", UseLocaleFormat=");
589        buffer.append(useLocaleFormat);
590        if (displayPatterns != null) {
591            buffer.append(", Patterns={");
592            buffer.append(displayPatterns);
593            buffer.append('}');
594        }
595        if (locale != null) {
596            buffer.append(", Locale=");
597            buffer.append(locale);
598        }
599        if (timeZone != null) {
600            buffer.append(", TimeZone=");
601            buffer.append(timeZone);
602        }
603        buffer.append(']');
604        return buffer.toString();
605    }
606
607    /**
608     * Log the <code>DateFormat</code> creation.
609     * @param action The action the format is being used for
610     * @param format The Date format
611     */
612    private void logFormat(final String action, final DateFormat format) {
613        if (log().isDebugEnabled()) {
614            final StringBuilder buffer = new StringBuilder(45);
615            buffer.append("    ");
616            buffer.append(action);
617            buffer.append(" with Format");
618            if (format instanceof SimpleDateFormat) {
619                buffer.append("[");
620                buffer.append(((SimpleDateFormat)format).toPattern());
621                buffer.append("]");
622            }
623            buffer.append(" for ");
624            if (locale == null) {
625                buffer.append("default locale");
626            } else {
627                buffer.append("locale[");
628                buffer.append(locale);
629                buffer.append("]");
630            }
631            if (timeZone != null) {
632                buffer.append(", TimeZone[");
633                buffer.append(timeZone);
634                buffer.append("]");
635            }
636            log().debug(buffer.toString());
637        }
638    }
639}