    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.beanutils2.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.beanutils2.ConversionException;
    030import org.apache.commons.beanutils2.locale.BaseLocaleConverter;
    031import org.apache.commons.beanutils2.locale.LocaleConverter;
    032import org.apache.commons.logging.Log;
    033import org.apache.commons.logging.LogFactory;
    034
    035/**
    036 * Standard {@link org.apache.commons.beanutils2.locale.LocaleConverter} implementation that converts an incoming locale-sensitive String into a
    037 * {@link java.util.Date} object, optionally using a default value or throwing a {@link org.apache.commons.beanutils2.ConversionException} if a conversion error
    038 * occurs.
    039 *
    040 * @param <D> The Date type.
    041 */
    042public class DateLocaleConverter<D extends Date> extends BaseLocaleConverter<D> {
    043
    044    /**
    045     * Builds instances of {@link DateLocaleConverter}.
    046     *
    047     * @param <B> The builder type.
    048     * @param <D> The Date type.
    049     */
    050    public static class Builder<B extends Builder<B, D>, D extends Date> extends BaseLocaleConverter.Builder<B, D> {
    051
    052        /** Should the date conversion be lenient? */
    053        private boolean lenient;
    054
    055        /**
    056         * Gets a new instance.
    057         * <p>
    058         * Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default
    059         * locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion.
    060         * </p>
    061         *
    062         * @return a new instance.
    063         */
    064        @Override
    065        public DateLocaleConverter<D> get() {
    066            return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient);
    067        }
    068
    069        /**
    070         * Tests whether date formatting is lenient.
    071         *
    072         * @return true if the {@code DateFormat} used for formatting is lenient
    073         * @see java.text.DateFormat#isLenient()
    074         */
    075        public boolean isLenient() {
    076            return lenient;
    077        }
    078
    079        /**
    080         * Sets the leniency policy.
    081         *
    082         * @param lenient the leniency policy.
    083         * @return {@code this} instance.
    084         */
    085        public B setLenient(final boolean lenient) {
    086            this.lenient = lenient;
    087            return asThis();
    088        }
    089
    090    }
    091
    092    /**
    093     * Default Pattern Characters
    094     */
    095    private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();
    096
    097    /** All logging goes through this logger */
    098    private static final Log LOG = LogFactory.getLog(DateLocaleConverter.class);
    099
    100    /**
    101     * Constructs a new builder.
    102     *
    103     * @param <B> The builder type.
    104     * @param <D> The Date type.
    105     * @return a new builder.
    106     */
    107    @SuppressWarnings("unchecked")
    108    public static <B extends Builder<B, D>, D extends Date> B builder() {
    109        return (B) new Builder<>();
    110    }
    111
    112    /**
    113     * This method is called at class initialization time to define the value for constant member DEFAULT_PATTERN_CHARS. All other methods needing this data
    114     * should just read that constant.
    115     */
    116    private static String initDefaultChars() {
    117        return new DateFormatSymbols(Locale.US).getLocalPatternChars();
    118    }
    119
    120    /** Should the date conversion be lenient? */
    121    private final boolean isLenient;
    122
    123    /**
    124     * Constructs a new instance.
    125     *
    126     * @param defaultValue default value.
    127     * @param locale       locale.
    128     * @param pattern      pattern.
    129     * @param useDefault   use the default.
    130     * @param locPattern   localized pattern.
    131     * @param lenient      leniency policy.
    132     */
    133    protected DateLocaleConverter(final D defaultValue, final Locale locale, final String pattern, final boolean useDefault, final boolean locPattern,
    134            final boolean lenient) {
    135        super(defaultValue, locale, pattern, useDefault, locPattern);
    136        this.isLenient = lenient;
    137    }
    138
    139    /**
    140     * Converts a pattern from a localized format to the default format.
    141     *
    142     * @param locale           The locale
    143     * @param localizedPattern The pattern in 'local' symbol format
    144     * @return pattern in 'default' symbol format
    145     */
    146    private String convertLocalizedPattern(final String localizedPattern, final Locale locale) {
    147        if (localizedPattern == null) {
    148            return null;
    149        }
    150
    151        // Note that this is a little obtuse.
    152        // However, it is the best way that anyone can come up with
    153        // that works with some 1.4 series JVM.
    154
    155        // Get the symbols for the localized pattern
    156        final DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
    157        final String localChars = localizedSymbols.getLocalPatternChars();
    158
    159        if (DEFAULT_PATTERN_CHARS.equals(localChars)) {
    160            return localizedPattern;
    161        }
    162
    163        // Convert the localized pattern to default
    164        String convertedPattern = null;
    165        try {
    166            convertedPattern = convertPattern(localizedPattern, localChars, DEFAULT_PATTERN_CHARS);
    167        } catch (final Exception ex) {
    168            if (LOG.isDebugEnabled()) {
    169                LOG.debug("Converting pattern '" + localizedPattern + "' for " + locale, ex);
    170            }
    171        }
    172        return convertedPattern;
    173    }
    174
    175    /**
    176     * Converts a Pattern from one character set to another.
    177     */
    178    private String convertPattern(final String pattern, final String fromChars, final String toChars) {
    179        final StringBuilder converted = new StringBuilder();
    180        boolean quoted = false;
    181
    182        for (int i = 0; i < pattern.length(); ++i) {
    183            char thisChar = pattern.charAt(i);
    184            if (quoted) {
    185                if (thisChar == '\'') {
    186                    quoted = false;
    187                }
    188            } else if (thisChar == '\'') {
    189                quoted = true;
    190            } else if (thisChar >= 'a' && thisChar <= 'z' || thisChar >= 'A' && thisChar <= 'Z') {
    191                final int index = fromChars.indexOf(thisChar);
    192                if (index == -1) {
    193                    throw new IllegalArgumentException("Illegal pattern character '" + thisChar + "'");
    194                }
    195                thisChar = toChars.charAt(index);
    196            }
    197            converted.append(thisChar);
    198        }
    199
    200        if (quoted) {
    201            throw new IllegalArgumentException("Unfinished quote in pattern");
    202        }
    203
    204        return converted.toString();
    205    }
    206
    207    /**
    208     * Tests whether date formatting is lenient.
    209     *
    210     * @return true if the {@code DateFormat} used for formatting is lenient
    211     * @see java.text.DateFormat#isLenient()
    212     */
    213    public boolean isLenient() {
    214        return isLenient;
    215    }
    216
    217    /**
    218     * Convert the specified locale-sensitive input object into an output object of the specified type.
    219     *
    220     * @param value   The input object to be converted
    221     * @param pattern The pattern is used for the conversion
    222     * @return the converted Date value
    223     * @throws ConversionException if conversion cannot be performed successfully
    224     * @throws ParseException      if an error occurs parsing
    225     */
    226    @Override
    227    protected D parse(final Object value, String pattern) throws ParseException {
    228        // Handle Date
    229        if (value instanceof Date) {
    230            return (D) value;
    231        }
    232
    233        // Handle Calendar
    234        if (value instanceof Calendar) {
    235            return (D) ((Calendar) value).getTime();
    236        }
    237
    238        if (localizedPattern) {
    239            pattern = convertLocalizedPattern(pattern, locale);
    240        }
    241
    242        // Create Formatter - use default if pattern is null
    243        final DateFormat formatter = pattern == null ? DateFormat.getDateInstance(DateFormat.SHORT, locale) : new SimpleDateFormat(pattern, locale);
    244        formatter.setLenient(isLenient);
    245
    246        // Parse the Date
    247        final ParsePosition pos = new ParsePosition(0);
    248        final String strValue = value.toString();
    249        final Object parsedValue = formatter.parseObject(strValue, pos);
    250        if (pos.getErrorIndex() > -1) {
    251            throw ConversionException.format("Error parsing date '%s' at position = %s", value, pos.getErrorIndex());
    252        }
    253        if (pos.getIndex() < strValue.length()) {
    254            throw ConversionException.format("Date '%s' contains unparsed characters from position = %s", value, pos.getIndex());
    255        }
    256
    257        return (D) parsedValue;
    258    }
    259
    260}