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    
018    package org.apache.commons.beanutils.locale.converters;
019    
020    import org.apache.commons.beanutils.ConversionException;
021    import org.apache.commons.beanutils.locale.BaseLocaleConverter;
022    import org.apache.commons.logging.LogFactory;
023    import org.apache.commons.logging.Log;
024    
025    import java.text.ParseException;
026    import java.text.ParsePosition;
027    import java.text.SimpleDateFormat;
028    import java.text.DateFormat;
029    import java.text.DateFormatSymbols;
030    import 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    
045    public 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    }