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}