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 org.apache.commons.beanutils.ConversionException;
021import org.apache.commons.beanutils.locale.BaseLocaleConverter;
022import org.apache.commons.logging.LogFactory;
023import org.apache.commons.logging.Log;
024
025import java.text.ParseException;
026import java.text.ParsePosition;
027import java.text.SimpleDateFormat;
028import java.text.DateFormat;
029import java.text.DateFormatSymbols;
030import java.util.Locale;
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 * @author Yauheny Mikulski
042 * @author Michael Szlapa
043 */
044
045public class DateLocaleConverter extends BaseLocaleConverter {
046
047    // ----------------------------------------------------- Instance Variables
048
049    /** All logging goes through this logger */
050    private Log log = LogFactory.getLog(DateLocaleConverter.class);
051
052    /** Should the date conversion be lenient? */
053    boolean isLenient = false;
054
055    /** 
056     * Default Pattern Characters 
057     * 
058     */
059    private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();
060
061    // ----------------------------------------------------------- Constructors
062
063    /**
064     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
065     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
066     * if a conversion error occurs. The locale is the default locale for
067     * this instance of the Java Virtual Machine and an unlocalized pattern is used
068     * for the convertion.
069     *
070     */
071    public DateLocaleConverter() {
072
073        this(false);
074    }
075
076    /**
077     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
078     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
079     * if a conversion error occurs. The locale is the default locale for
080     * this instance of the Java Virtual Machine.
081     *
082     * @param locPattern    Indicate whether the pattern is localized or not
083     */
084    public DateLocaleConverter(boolean locPattern) {
085
086        this(Locale.getDefault(), locPattern);
087    }
088
089    /**
090     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
091     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
092     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
093     *
094     * @param locale        The locale
095     */
096    public DateLocaleConverter(Locale locale) {
097
098        this(locale, false);
099    }
100
101    /**
102     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
103     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
104     * if a conversion error occurs.
105     *
106     * @param locale        The locale
107     * @param locPattern    Indicate whether the pattern is localized or not
108     */
109    public DateLocaleConverter(Locale locale, boolean locPattern) {
110
111        this(locale, (String) null, locPattern);
112    }
113
114    /**
115     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
116     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
117     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
118     *
119     * @param locale        The locale
120     * @param pattern       The convertion pattern
121     */
122    public DateLocaleConverter(Locale locale, String pattern) {
123
124        this(locale, pattern, false);
125    }
126
127    /**
128     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
129     * that will throw a {@link org.apache.commons.beanutils.ConversionException}
130     * if a conversion error occurs.
131     *
132     * @param locale        The locale
133     * @param pattern       The convertion pattern
134     * @param locPattern    Indicate whether the pattern is localized or not
135     */
136    public DateLocaleConverter(Locale locale, String pattern, boolean locPattern) {
137
138        super(locale, pattern, locPattern);
139    }
140
141    /**
142     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
143     * that will return the specified default value
144     * if a conversion error occurs. The locale is the default locale for
145     * this instance of the Java Virtual Machine and an unlocalized pattern is used
146     * for the convertion.
147     *
148     * @param defaultValue  The default value to be returned
149     */
150    public DateLocaleConverter(Object defaultValue) {
151
152        this(defaultValue, false);
153    }
154
155    /**
156     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
157     * that will return the specified default value
158     * if a conversion error occurs. The locale is the default locale for
159     * this instance of the Java Virtual Machine.
160     *
161     * @param defaultValue  The default value to be returned
162     * @param locPattern    Indicate whether the pattern is localized or not
163     */
164    public DateLocaleConverter(Object defaultValue, boolean locPattern) {
165
166        this(defaultValue, Locale.getDefault(), locPattern);
167    }
168
169    /**
170     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
171     * that will return the specified default value
172     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
173     *
174     * @param defaultValue  The default value to be returned
175     * @param locale        The locale
176     */
177    public DateLocaleConverter(Object defaultValue, Locale locale) {
178
179        this(defaultValue, locale, false);
180    }
181
182    /**
183     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
184     * that will return the specified default value
185     * if a conversion error occurs.
186     *
187     * @param defaultValue  The default value to be returned
188     * @param locale        The locale
189     * @param locPattern    Indicate whether the pattern is localized or not
190     */
191    public DateLocaleConverter(Object defaultValue, Locale locale, boolean locPattern) {
192
193        this(defaultValue, locale, null, locPattern);
194    }
195
196
197    /**
198     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
199     * that will return the specified default value
200     * if a conversion error occurs. An unlocalized pattern is used for the convertion.
201     *
202     * @param defaultValue  The default value to be returned
203     * @param locale        The locale
204     * @param pattern       The convertion pattern
205     */
206    public DateLocaleConverter(Object defaultValue, Locale locale, String pattern) {
207
208        this(defaultValue, locale, pattern, false);
209    }
210
211    /**
212     * Create a {@link org.apache.commons.beanutils.locale.LocaleConverter} 
213     * that will return the specified default value
214     * if a conversion error occurs.
215     *
216     * @param defaultValue  The default value to be returned
217     * @param locale        The locale
218     * @param pattern       The convertion pattern
219     * @param locPattern    Indicate whether the pattern is localized or not
220     */
221    public DateLocaleConverter(Object defaultValue, Locale locale, String pattern, boolean locPattern) {
222
223        super(defaultValue, locale, pattern, locPattern);
224    }
225
226    // --------------------------------------------------------- Methods
227    
228    /**
229     * Returns whether date formatting is lenient.
230     *
231     * @return true if the <code>DateFormat</code> used for formatting is lenient
232     * @see java.text.DateFormat#isLenient
233     */
234    public boolean isLenient() {
235        return isLenient;
236    }
237    
238    /**
239     * Specify whether or not date-time parsing should be lenient.
240     * 
241     * @param lenient true if the <code>DateFormat</code> used for formatting should be lenient
242     * @see java.text.DateFormat#setLenient
243     */
244    public void setLenient(boolean lenient) {
245        isLenient = lenient;
246    }
247
248    // --------------------------------------------------------- Methods
249
250    /**
251     * Convert the specified locale-sensitive input object into an output object of the
252     * specified type.
253     *
254     * @param value The input object to be converted
255     * @param pattern The pattern is used for the convertion
256     * @return the converted Date value
257     *
258     * @exception org.apache.commons.beanutils.ConversionException 
259     * if conversion cannot be performed successfully
260     * @throws ParseException if an error occurs parsing
261     */
262    protected Object parse(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         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        ParsePosition pos = new ParsePosition(0);
286        String strValue = value.toString();
287        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(String localizedPattern, 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         DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
319         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 (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(String pattern, String fromChars, String toChars) {
341
342        StringBuffer converted = new StringBuffer();
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                    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        DateFormatSymbols defaultSymbols = new DateFormatSymbols(Locale.US);
381        return defaultSymbols.getLocalPatternChars();
382    }
383
384}