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