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