    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
    018package org.apache.commons.beanutils.locale.converters;
    019
    020import java.text.DateFormat;
    021import java.text.DateFormatSymbols;
    022import java.text.ParseException;
    023import java.text.ParsePosition;
    024import java.text.SimpleDateFormat;
    025import java.util.Calendar;
    026import java.util.Date;
    027import java.util.Locale;
    028
    029import org.apache.commons.beanutils.ConversionException;
    030import org.apache.commons.beanutils.locale.BaseLocaleConverter;
    031import org.apache.commons.logging.Log;
    032import org.apache.commons.logging.LogFactory;
    033
    034/**
    035 * <p>Standard {@link org.apache.commons.beanutils.locale.LocaleConverter}
    036 * implementation that converts an incoming
    037 * locale-sensitive String into a <code>java.util.Date</code> object,
    038 * optionally using a default value or throwing a
    039 * {@link org.apache.commons.beanutils.ConversionException}
    040 * if a conversion error occurs.</p>
    041 *
    042 */
    043
    044public class DateLocaleConverter extends BaseLocaleConverter {
    045
    046    /**
    047     * Default Pattern Characters
    048     *
    049     */
    050    private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();
    051
    052    /**
    053     * This method is called at class initialization time to define the
    054     * value for constant member DEFAULT_PATTERN_CHARS. All other methods needing
    055     * this data should just read that constant.
    056     */
    057    private static String initDefaultChars() {
    058        final DateFormatSymbols defaultSymbols = new DateFormatSymbols(Locale.US);
    059        return defaultSymbols.getLocalPatternChars();
    060    }
    061
    062    /** All logging goes through this logger */
    063    private final Log log = LogFactory.getLog(DateLocaleConverter.class);
    064
    065    /** Should the date conversion be lenient? */
    066    boolean isLenient;
    067
    068    /**
    069     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    070     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
    071     * if a conversion error occurs. The locale is the default locale for
    072     * this instance of the Java Virtual Machine and an unlocalized pattern is used
    073     * for the convertion.
    074     *
    075     */
    076    public DateLocaleConverter() {
    077
    078        this(false);
    079    }
    080
    081    /**
    082     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    083     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
    084     * if a conversion error occurs. The locale is the default locale for
    085     * this instance of the Java Virtual Machine.
    086     *
    087     * @param locPattern    Indicate whether the pattern is localized or not
    088     */
    089    public DateLocaleConverter(final boolean locPattern) {
    090
    091        this(Locale.getDefault(), locPattern);
    092    }
    093
    094    /**
    095     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    096     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
    097     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
    098     *
    099     * @param locale        The locale
    100     */
    101    public DateLocaleConverter(final Locale locale) {
    102
    103        this(locale, false);
    104    }
    105
    106    /**
    107     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    108     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
    109     * if a conversion error occurs.
    110     *
    111     * @param locale        The locale
    112     * @param locPattern    Indicate whether the pattern is localized or not
    113     */
    114    public DateLocaleConverter(final Locale locale, final boolean locPattern) {
    115
    116        this(locale, (String) null, locPattern);
    117    }
    118
    119    /**
    120     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    121     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
    122     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
    123     *
    124     * @param locale        The locale
    125     * @param pattern       The convertion pattern
    126     */
    127    public DateLocaleConverter(final Locale locale, final String pattern) {
    128
    129        this(locale, pattern, false);
    130    }
    131
    132    /**
    133     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    134     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
    135     * if a conversion error occurs.
    136     *
    137     * @param locale        The locale
    138     * @param pattern       The convertion pattern
    139     * @param locPattern    Indicate whether the pattern is localized or not
    140     */
    141    public DateLocaleConverter(final Locale locale, final String pattern, final boolean locPattern) {
    142
    143        super(locale, pattern, locPattern);
    144    }
    145
    146    /**
    147     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    148     * that will return the specified default value
    149     * if a conversion error occurs. The locale is the default locale for
    150     * this instance of the Java Virtual Machine and an unlocalized pattern is used
    151     * for the convertion.
    152     *
    153     * @param defaultValue  The default value to be returned
    154     */
    155    public DateLocaleConverter(final Object defaultValue) {
    156
    157        this(defaultValue, false);
    158    }
    159
    160    /**
    161     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    162     * that will return the specified default value
    163     * if a conversion error occurs. The locale is the default locale for
    164     * this instance of the Java Virtual Machine.
    165     *
    166     * @param defaultValue  The default value to be returned
    167     * @param locPattern    Indicate whether the pattern is localized or not
    168     */
    169    public DateLocaleConverter(final Object defaultValue, final boolean locPattern) {
    170
    171        this(defaultValue, Locale.getDefault(), locPattern);
    172    }
    173
    174    /**
    175     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    176     * that will return the specified default value
    177     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
    178     *
    179     * @param defaultValue  The default value to be returned
    180     * @param locale        The locale
    181     */
    182    public DateLocaleConverter(final Object defaultValue, final Locale locale) {
    183
    184        this(defaultValue, locale, false);
    185    }
    186
    187    /**
    188     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    189     * that will return the specified default value
    190     * if a conversion error occurs.
    191     *
    192     * @param defaultValue  The default value to be returned
    193     * @param locale        The locale
    194     * @param locPattern    Indicate whether the pattern is localized or not
    195     */
    196    public DateLocaleConverter(final Object defaultValue, final Locale locale, final boolean locPattern) {
    197
    198        this(defaultValue, locale, null, locPattern);
    199    }
    200
    201    /**
    202     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    203     * that will return the specified default value
    204     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
    205     *
    206     * @param defaultValue  The default value to be returned
    207     * @param locale        The locale
    208     * @param pattern       The convertion pattern
    209     */
    210    public DateLocaleConverter(final Object defaultValue, final Locale locale, final String pattern) {
    211
    212        this(defaultValue, locale, pattern, false);
    213    }
    214
    215    /**
    216     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter}
    217     * that will return the specified default value
    218     * if a conversion error occurs.
    219     *
    220     * @param defaultValue  The default value to be returned
    221     * @param locale        The locale
    222     * @param pattern       The convertion pattern
    223     * @param locPattern    Indicate whether the pattern is localized or not
    224     */
    225    public DateLocaleConverter(final Object defaultValue, final Locale locale, final String pattern, final boolean locPattern) {
    226
    227        super(defaultValue, locale, pattern, locPattern);
    228    }
    229
    230    /**
    231      * Convert a pattern from a localized format to the default format.
    232      *
    233      * @param locale   The locale
    234      * @param localizedPattern The pattern in 'local' symbol format
    235      * @return pattern in 'default' symbol format
    236      */
    237     private String convertLocalizedPattern(final String localizedPattern, final Locale locale) {
    238
    239         if (localizedPattern == null) {
    240            return null;
    241         }
    242
    243         // Note that this is a little obtuse.
    244         // However, it is the best way that anyone can come up with
    245         // that works with some 1.4 series JVM.
    246
    247         // Get the symbols for the localized pattern
    248         final DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
    249         final String localChars = localizedSymbols.getLocalPatternChars();
    250
    251         if (DEFAULT_PATTERN_CHARS.equals(localChars)) {
    252             return localizedPattern;
    253         }
    254
    255         // Convert the localized pattern to default
    256         String convertedPattern = null;
    257         try {
    258             convertedPattern = convertPattern(localizedPattern,
    259                                                localChars,
    260                                                DEFAULT_PATTERN_CHARS);
    261         } catch (final Exception ex) {
    262             log.debug("Converting pattern '" + localizedPattern + "' for " + locale, ex);
    263         }
    264         return convertedPattern;
    265    }
    266
    267    /**
    268     * <p>Converts a Pattern from one character set to another.</p>
    269     */
    270    private String convertPattern(final String pattern, final String fromChars, final String toChars) {
    271
    272        final StringBuilder converted = new StringBuilder();
    273        boolean quoted = false;
    274
    275        for (int i = 0; i < pattern.length(); ++i) {
    276            char thisChar = pattern.charAt(i);
    277            if (quoted) {
    278                if (thisChar == '\'') {
    279                    quoted = false;
    280                }
    281            } else if (thisChar == '\'') {
    282               quoted = true;
    283            } else if (thisChar >= 'a' && thisChar <= 'z' ||
    284                       thisChar >= 'A' && thisChar <= 'Z') {
    285                final int index = fromChars.indexOf(thisChar );
    286                if (index == -1) {
    287                    throw new IllegalArgumentException(
    288                        "Illegal pattern character '" + thisChar + "'");
    289                }
    290                thisChar = toChars.charAt(index);
    291            }
    292            converted.append(thisChar);
    293        }
    294
    295        if (quoted) {
    296            throw new IllegalArgumentException("Unfinished quote in pattern");
    297        }
    298
    299        return converted.toString();
    300    }
    301
    302     /**
    303     * Returns whether date formatting is lenient.
    304     *
    305     * @return true if the <code>DateFormat</code> used for formatting is lenient
    306     * @see java.text.DateFormat#isLenient
    307     */
    308    public boolean isLenient() {
    309        return isLenient;
    310    }
    311
    312    /**
    313     * Convert the specified locale-sensitive input object into an output object of the
    314     * specified type.
    315     *
    316     * @param value The input object to be converted
    317     * @param pattern The pattern is used for the convertion
    318     * @return the converted Date value
    319     * @throws org.apache.commons.beanutils.ConversionException
    320     * if conversion cannot be performed successfully
    321     * @throws ParseException if an error occurs parsing
    322     */
    323    @Override
    324    protected Object parse(final Object value, String pattern) throws ParseException {
    325
    326        // Handle Date
    327        if (value instanceof Date) {
    328            return value;
    329        }
    330
    331        // Handle Calendar
    332        if (value instanceof Calendar) {
    333            return ((Calendar) value).getTime();
    334        }
    335
    336         if (locPattern) {
    337             pattern = convertLocalizedPattern(pattern, locale);
    338         }
    339
    340         // Create Formatter - use default if pattern is null
    341         final DateFormat formatter = pattern == null ? DateFormat.getDateInstance(DateFormat.SHORT, locale)
    342                                                : new SimpleDateFormat(pattern, locale);
    343         formatter.setLenient(isLenient);
    344
    345         // Parse the Date
    346        final ParsePosition pos = new ParsePosition(0);
    347        final String strValue = value.toString();
    348        final Object parsedValue = formatter.parseObject(strValue, pos);
    349        if (pos.getErrorIndex() > -1) {
    350            throw new ConversionException("Error parsing date '" + value +
    351                    "' at position="+ pos.getErrorIndex());
    352        }
    353        if (pos.getIndex() < strValue.length()) {
    354            throw new ConversionException("Date '" + value +
    355                    "' contains unparsed characters from position=" + pos.getIndex());
    356        }
    357
    358        return parsedValue;
    359     }
    360
    361    /**
    362     * Specify whether or not date-time parsing should be lenient.
    363     *
    364     * @param lenient true if the <code>DateFormat</code> used for formatting should be lenient
    365     * @see java.text.DateFormat#setLenient
    366     */
    367    public void setLenient(final boolean lenient) {
    368        isLenient = lenient;
    369    }
    370
    371}