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         * 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}