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