    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} implementation
    031 * that handles conversion to and from <strong>date/time</strong> objects.
    032 * <p>
    033 * This implementation handles conversion for the following
    034 * <em>date/time</em> types.
    035 * </p>
    036 * <ul>
    037 *     <li><code>java.util.Date</code></li>
    038 *     <li><code>java.util.Calendar</code></li>
    039 *     <li><code>java.sql.Date</code></li>
    040 *     <li><code>java.sql.Time</code></li>
    041 *     <li><code>java.sql.Timestamp</code></li>
    042 * </ul>
    043 *
    044 * <h2>String Conversions (to and from)</h2>
    045 * This class provides a number of ways in which date/time
    046 * conversions to/from Strings can be achieved:
    047 * <ul>
    048 *    <li>Using the SHORT date format for the default Locale, configure using:
    049 *        <ul>
    050 *           <li><code>setUseLocaleFormat(true)</code></li>
    051 *        </ul>
    052 *    </li>
    053 *    <li>Using the SHORT date format for a specified Locale, configure using:
    054 *        <ul>
    055 *           <li><code>setLocale(Locale)</code></li>
    056 *        </ul>
    057 *    </li>
    058 *    <li>Using the specified date pattern(s) for the default Locale, configure using:
    059 *        <ul>
    060 *           <li>Either <code>setPattern(String)</code> or
    061 *                      <code>setPatterns(String[])</code></li>
    062 *        </ul>
    063 *    </li>
    064 *    <li>Using the specified date pattern(s) for a specified Locale, configure using:
    065 *        <ul>
    066 *           <li><code>setPattern(String)</code> or
    067 *                    <code>setPatterns(String[]) and...</code></li>
    068 *           <li><code>setLocale(Locale)</code></li>
    069 *        </ul>
    070 *    </li>
    071 *    <li>If none of the above are configured the
    072 *        <code>toDate(String)</code> method is used to convert
    073 *        from String to Date and the Dates's
    074 *        <code>toString()</code> method used to convert from
    075 *        Date to String.</li>
    076 * </ul>
    077 * <p>
    078 * The <strong>Time Zone</strong> to use with the date format can be specified
    079 * using the <code>setTimeZone()</code> method.
    080 * </p>
    081 *
    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     * Construct a Date/Time <em>Converter</em> that throws a
    094     * <code>ConversionException</code> if an error occurs.
    095     */
    096    public DateTimeConverter() {
    097    }
    098
    099    /**
    100     * Construct a Date/Time <em>Converter</em> 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     * Convert an input Date/Calendar object into a String.
    113     * <p>
    114     * <strong>N.B.</strong>If the converter has been configured to with
    115     * one or more patterns (using <code>setPatterns()</code>), then
    116     * the first pattern will be used to format the date into a String.
    117     * Otherwise the default <code>DateFormat</code> for the default locale
    118     * (and <em>style</em> if configured) will be used.
    119     *
    120     * @param value The input value to be converted
    121     * @return the converted String value.
    122     * @throws Throwable if an error occurs converting to a String
    123     */
    124    @Override
    125    protected String convertToString(final Object value) throws Throwable {
    126
    127        Date date = null;
    128        if (value instanceof Date) {
    129            date = (Date)value;
    130        } else if (value instanceof Calendar) {
    131            date = ((Calendar)value).getTime();
    132        } else if (value instanceof Long) {
    133            date = new Date(((Long)value).longValue());
    134        }
    135
    136        String result = null;
    137        if (useLocaleFormat && date != null) {
    138            DateFormat format = null;
    139            if (patterns != null && patterns.length > 0) {
    140                format = getFormat(patterns[0]);
    141            } else {
    142                format = getFormat(locale, timeZone);
    143            }
    144            logFormat("Formatting", format);
    145            result = format.format(date);
    146            if (log().isDebugEnabled()) {
    147                log().debug("    Converted  to String using format '" + result + "'");
    148            }
    149        } else {
    150            result = value.toString();
    151            if (log().isDebugEnabled()) {
    152                log().debug("    Converted  to String using toString() '" + result + "'");
    153             }
    154        }
    155        return result;
    156    }
    157
    158    /**
    159     * Convert the input object into a Date object of the
    160     * specified type.
    161     * <p>
    162     * This method handles conversions between the following
    163     * types:
    164     * <ul>
    165     *     <li><code>java.util.Date</code></li>
    166     *     <li><code>java.util.Calendar</code></li>
    167     *     <li><code>java.sql.Date</code></li>
    168     *     <li><code>java.sql.Time</code></li>
    169     *     <li><code>java.sql.Timestamp</code></li>
    170     * </ul>
    171     *
    172     * It also handles conversion from a <code>String</code> to
    173     * any of the above types.
    174     * <p>
    175     *
    176     * For <code>String</code> conversion, if the converter has been configured
    177     * with one or more patterns (using <code>setPatterns()</code>), then
    178     * the conversion is attempted with each of the specified patterns.
    179     * Otherwise the default <code>DateFormat</code> for the default locale
    180     * (and <em>style</em> if configured) will be used.
    181     *
    182     * @param <T> The desired target type of the conversion.
    183     * @param targetType Data type to which this value should be converted.
    184     * @param value The input value to be converted.
    185     * @return The converted value.
    186     * @throws Exception if conversion cannot be performed successfully
    187     */
    188    @Override
    189    protected <T> T convertToType(final Class<T> targetType, final Object value) throws Exception {
    190
    191        final Class<?> sourceType = value.getClass();
    192
    193        // Handle java.sql.Timestamp
    194        if (value instanceof java.sql.Timestamp) {
    195
    196            // N.B. Prior to JDK 1.4 the Timestamp's getTime() method
    197            //      didn't include the milliseconds. The following code
    198            //      ensures it works consistently accross JDK versions
    199            final java.sql.Timestamp timestamp = (java.sql.Timestamp)value;
    200            long timeInMillis = timestamp.getTime() / 1000 * 1000;
    201            timeInMillis += timestamp.getNanos() / 1000000;
    202            return toDate(targetType, timeInMillis);
    203        }
    204
    205        // Handle Date (includes java.sql.Date & java.sql.Time)
    206        if (value instanceof Date) {
    207            final Date date = (Date)value;
    208            return toDate(targetType, date.getTime());
    209        }
    210
    211        // Handle Calendar
    212        if (value instanceof Calendar) {
    213            final Calendar calendar = (Calendar)value;
    214            return toDate(targetType, calendar.getTime().getTime());
    215        }
    216
    217        // Handle Long
    218        if (value instanceof Long) {
    219            final Long longObj = (Long)value;
    220            return toDate(targetType, longObj.longValue());
    221        }
    222
    223        // Convert all other types to String & handle
    224        final String stringValue = value.toString().trim();
    225        if (stringValue.length() == 0) {
    226            return handleMissing(targetType);
    227        }
    228
    229        // Parse the Date/Time
    230        if (useLocaleFormat) {
    231            Calendar calendar = null;
    232            if (patterns != null && patterns.length > 0) {
    233                calendar = parse(sourceType, targetType, stringValue);
    234            } else {
    235                final DateFormat format = getFormat(locale, timeZone);
    236                calendar = parse(sourceType, targetType, stringValue, format);
    237            }
    238            if (Calendar.class.isAssignableFrom(targetType)) {
    239                return targetType.cast(calendar);
    240            }
    241            return toDate(targetType, calendar.getTime().getTime());
    242        }
    243
    244        // Default String conversion
    245        return toDate(targetType, stringValue);
    246
    247    }
    248
    249    /**
    250     * Return a <code>DateFormat</code> for the Locale.
    251     * @param locale The Locale to create the Format with (may be null)
    252     * @param timeZone The Time Zone create the Format with (may be null)
    253     * @return A Date Format.
    254     */
    255    protected DateFormat getFormat(final Locale locale, final TimeZone timeZone) {
    256        DateFormat format = null;
    257        if (locale == null) {
    258            format = DateFormat.getDateInstance(DateFormat.SHORT);
    259        } else {
    260            format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
    261        }
    262        if (timeZone != null) {
    263            format.setTimeZone(timeZone);
    264        }
    265        return format;
    266    }
    267
    268    /**
    269     * Create a date format for the specified pattern.
    270     *
    271     * @param pattern The date pattern
    272     * @return The DateFormat
    273     */
    274    private DateFormat getFormat(final String pattern) {
    275        final DateFormat format = new SimpleDateFormat(pattern);
    276        if (timeZone != null) {
    277            format.setTimeZone(timeZone);
    278        }
    279        return format;
    280    }
    281
    282    /**
    283     * Return the Locale for the <em>Converter</em>
    284     * (or <code>null</code> if none specified).
    285     *
    286     * @return The locale to use for conversion
    287     */
    288    public Locale getLocale() {
    289        return locale;
    290    }
    291
    292    /**
    293     * Return the date format patterns used to convert
    294     * dates to/from a <code>java.lang.String</code>
    295     * (or <code>null</code> if none specified).
    296     *
    297     * @see SimpleDateFormat
    298     * @return Array of format patterns.
    299     */
    300    public String[] getPatterns() {
    301        return patterns;
    302    }
    303
    304    /**
    305     * Return the Time Zone to use when converting dates
    306     * (or <code>null</code> if none specified.
    307     *
    308     * @return The Time Zone.
    309     */
    310    public TimeZone getTimeZone() {
    311        return timeZone;
    312    }
    313
    314    /**
    315     * Log the <code>DateFormat<code> creation.
    316     * @param action The action the format is being used for
    317     * @param format The Date format
    318     */
    319    private void logFormat(final String action, final DateFormat format) {
    320        if (log().isDebugEnabled()) {
    321            final StringBuilder buffer = new StringBuilder(45);
    322            buffer.append("    ");
    323            buffer.append(action);
    324            buffer.append(" with Format");
    325            if (format instanceof SimpleDateFormat) {
    326                buffer.append("[");
    327                buffer.append(((SimpleDateFormat)format).toPattern());
    328                buffer.append("]");
    329            }
    330            buffer.append(" for ");
    331            if (locale == null) {
    332                buffer.append("default locale");
    333            } else {
    334                buffer.append("locale[");
    335                buffer.append(locale);
    336                buffer.append("]");
    337            }
    338            if (timeZone != null) {
    339                buffer.append(", TimeZone[");
    340                buffer.append(timeZone);
    341                buffer.append("]");
    342            }
    343            log().debug(buffer.toString());
    344        }
    345    }
    346
    347    /**
    348     * Parse a String date value using the set of patterns.
    349     *
    350     * @param sourceType The type of the value being converted
    351     * @param targetType The type to convert the value to.
    352     * @param value The String date value.
    353     * @return The converted Date object.
    354     * @throws Exception if an error occurs parsing the date.
    355     */
    356    private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value) throws Exception {
    357        Exception firstEx = null;
    358        for (final String pattern : patterns) {
    359            try {
    360                final DateFormat format = getFormat(pattern);
    361                return parse(sourceType, targetType, value, format);
    362            } catch (final Exception ex) {
    363                if (firstEx == null) {
    364                    firstEx = ex;
    365                }
    366            }
    367        }
    368        if (patterns.length > 1) {
    369            throw new ConversionException("Error converting '" + toString(sourceType) + "' to '" + toString(targetType)
    370                    + "' using  patterns '" + displayPatterns + "'");
    371        }
    372        throw firstEx;
    373    }
    374
    375    /**
    376     * Parse a String into a <code>Calendar</code> object
    377     * using the specified <code>DateFormat</code>.
    378     *
    379     * @param sourceType The type of the value being converted
    380     * @param targetType The type to convert the value to
    381     * @param value The String date value.
    382     * @param format The DateFormat to parse the String value.
    383     * @return The converted Calendar object.
    384     * @throws ConversionException if the String cannot be converted.
    385     */
    386    private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value, final DateFormat format) {
    387        logFormat("Parsing", format);
    388        format.setLenient(false);
    389        final ParsePosition pos = new ParsePosition(0);
    390        final Date parsedDate = format.parse(value, pos); // ignore the result (use the Calendar)
    391        if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedDate == null) {
    392            String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
    393            if (format instanceof SimpleDateFormat) {
    394                msg += " using pattern '" + ((SimpleDateFormat)format).toPattern() + "'";
    395            }
    396            if (log().isDebugEnabled()) {
    397                log().debug("    " + msg);
    398            }
    399            throw new ConversionException(msg);
    400        }
    401        return format.getCalendar();
    402    }
    403
    404    /**
    405     * Set the Locale for the <em>Converter</em>.
    406     *
    407     * @param locale The Locale.
    408     */
    409    public void setLocale(final Locale locale) {
    410        this.locale = locale;
    411        setUseLocaleFormat(true);
    412    }
    413
    414    /**
    415     * Set a date format pattern to use to convert
    416     * dates to/from a <code>java.lang.String</code>.
    417     *
    418     * @see SimpleDateFormat
    419     * @param pattern The format pattern.
    420     */
    421    public void setPattern(final String pattern) {
    422        setPatterns(new String[] {pattern});
    423    }
    424
    425    /**
    426     * Set the date format patterns to use to convert
    427     * dates to/from a <code>java.lang.String</code>.
    428     *
    429     * @see SimpleDateFormat
    430     * @param patterns Array of format patterns.
    431     */
    432    public void setPatterns(final String[] patterns) {
    433        this.patterns = patterns;
    434        if (patterns != null && patterns.length > 1) {
    435            final String buffer = String.join(", ", patterns);
    436            displayPatterns = buffer;
    437        }
    438        setUseLocaleFormat(true);
    439    }
    440
    441    /**
    442     * Set the Time Zone to use when converting dates.
    443     *
    444     * @param timeZone The Time Zone.
    445     */
    446    public void setTimeZone(final TimeZone timeZone) {
    447        this.timeZone = timeZone;
    448    }
    449
    450    /**
    451     * Indicate whether conversion should use a format/pattern or not.
    452     *
    453     * @param useLocaleFormat <code>true</code> if the format
    454     * for the locale should be used, otherwise <code>false</code>
    455     */
    456    public void setUseLocaleFormat(final boolean useLocaleFormat) {
    457        this.useLocaleFormat = useLocaleFormat;
    458    }
    459
    460    /**
    461     * Convert a long value to the specified Date type for this
    462     * <em>Converter</em>.
    463     * <p>
    464     *
    465     * This method handles conversion to the following types:
    466     * <ul>
    467     *     <li><code>java.util.Date</code></li>
    468     *     <li><code>java.util.Calendar</code></li>
    469     *     <li><code>java.sql.Date</code></li>
    470     *     <li><code>java.sql.Time</code></li>
    471     *     <li><code>java.sql.Timestamp</code></li>
    472     * </ul>
    473     *
    474     * @param <T> The target type
    475     * @param type The Date type to convert to
    476     * @param value The long value to convert.
    477     * @return The converted date value.
    478     */
    479    private <T> T toDate(final Class<T> type, final long value) {
    480
    481        // java.util.Date
    482        if (type.equals(Date.class)) {
    483            return type.cast(new Date(value));
    484        }
    485
    486        // java.sql.Date
    487        if (type.equals(java.sql.Date.class)) {
    488            return type.cast(new java.sql.Date(value));
    489        }
    490
    491        // java.sql.Time
    492        if (type.equals(java.sql.Time.class)) {
    493            return type.cast(new java.sql.Time(value));
    494        }
    495
    496        // java.sql.Timestamp
    497        if (type.equals(java.sql.Timestamp.class)) {
    498            return type.cast(new java.sql.Timestamp(value));
    499        }
    500
    501        // java.util.Calendar
    502        if (type.equals(Calendar.class)) {
    503            Calendar calendar = null;
    504            if (locale == null && timeZone == null) {
    505                calendar = Calendar.getInstance();
    506            } else if (locale == null) {
    507                calendar = Calendar.getInstance(timeZone);
    508            } else if (timeZone == null) {
    509                calendar = Calendar.getInstance(locale);
    510            } else {
    511                calendar = Calendar.getInstance(timeZone, locale);
    512            }
    513            calendar.setTime(new Date(value));
    514            calendar.setLenient(false);
    515            return type.cast(calendar);
    516        }
    517
    518        final String msg = toString(getClass()) + " cannot handle conversion to '"
    519                   + toString(type) + "'";
    520        if (log().isWarnEnabled()) {
    521            log().warn("    " + msg);
    522        }
    523        throw new ConversionException(msg);
    524    }
    525
    526    /**
    527     * Default String to Date conversion.
    528     * <p>
    529     * This method handles conversion from a String to the following types:
    530     * <ul>
    531     *     <li><code>java.sql.Date</code></li>
    532     *     <li><code>java.sql.Time</code></li>
    533     *     <li><code>java.sql.Timestamp</code></li>
    534     * </ul>
    535     * <p>
    536     * <strong>N.B.</strong> No default String conversion
    537     * mechanism is provided for <code>java.util.Date</code>
    538     * and <code>java.util.Calendar</code> type.
    539     *
    540     * @param <T> The target type
    541     * @param type The date type to convert to
    542     * @param value The String value to convert.
    543     * @return The converted Number value.
    544     */
    545    private <T> T toDate(final Class<T> type, final String value) {
    546        // java.sql.Date
    547        if (type.equals(java.sql.Date.class)) {
    548            try {
    549                return type.cast(java.sql.Date.valueOf(value));
    550            } catch (final IllegalArgumentException e) {
    551                throw new ConversionException(
    552                        "String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date");
    553            }
    554        }
    555
    556        // java.sql.Time
    557        if (type.equals(java.sql.Time.class)) {
    558            try {
    559                return type.cast(java.sql.Time.valueOf(value));
    560            } catch (final IllegalArgumentException e) {
    561                throw new ConversionException(
    562                        "String must be in JDBC format [HH:mm:ss] to create a java.sql.Time");
    563            }
    564        }
    565
    566        // java.sql.Timestamp
    567        if (type.equals(java.sql.Timestamp.class)) {
    568            try {
    569                return type.cast(java.sql.Timestamp.valueOf(value));
    570            } catch (final IllegalArgumentException e) {
    571                throw new ConversionException(
    572                        "String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] " +
    573                        "to create a java.sql.Timestamp");
    574            }
    575        }
    576
    577        final String msg = toString(getClass()) + " does not support default String to '"
    578                   + toString(type) + "' conversion.";
    579        if (log().isWarnEnabled()) {
    580            log().warn("    " + msg);
    581            log().warn("    (N.B. Re-configure Converter or use alternative implementation)");
    582        }
    583        throw new ConversionException(msg);
    584    }
    585
    586    /**
    587     * Provide a String representation of this date/time converter.
    588     *
    589     * @return A String representation of this date/time converter
    590     */
    591    @Override
    592    public String toString() {
    593        final StringBuilder buffer = new StringBuilder();
    594        buffer.append(toString(getClass()));
    595        buffer.append("[UseDefault=");
    596        buffer.append(isUseDefault());
    597        buffer.append(", UseLocaleFormat=");
    598        buffer.append(useLocaleFormat);
    599        if (displayPatterns != null) {
    600            buffer.append(", Patterns={");
    601            buffer.append(displayPatterns);
    602            buffer.append('}');
    603        }
    604        if (locale != null) {
    605            buffer.append(", Locale=");
    606            buffer.append(locale);
    607        }
    608        if (timeZone != null) {
    609            buffer.append(", TimeZone=");
    610            buffer.append(timeZone);
    611        }
    612        buffer.append(']');
    613        return buffer.toString();
    614    }
    615}