    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     */
    017    package org.apache.commons.beanutils.converters;
    018
    019    import java.util.Date;
    020    import java.util.Locale;
    021    import java.util.Calendar;
    022    import java.util.TimeZone;
    023    import java.text.DateFormat;
    024    import java.text.SimpleDateFormat;
    025    import java.text.ParsePosition;
    026    import 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     */
    079    public 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    }