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 *      https://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            // 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("    (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}