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    }