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