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