View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.beanutils2.locale.converters;
19  
20  import java.text.DateFormat;
21  import java.text.DateFormatSymbols;
22  import java.text.ParseException;
23  import java.text.ParsePosition;
24  import java.text.SimpleDateFormat;
25  import java.util.Calendar;
26  import java.util.Date;
27  import java.util.Locale;
28  
29  import org.apache.commons.beanutils2.ConversionException;
30  import org.apache.commons.beanutils2.locale.BaseLocaleConverter;
31  import org.apache.commons.beanutils2.locale.LocaleConverter;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  
35  /**
36   * Standard {@link org.apache.commons.beanutils2.locale.LocaleConverter} implementation that converts an incoming locale-sensitive String into a
37   * {@link java.util.Date} object, optionally using a default value or throwing a {@link org.apache.commons.beanutils2.ConversionException} if a conversion error
38   * occurs.
39   *
40   * @param <D> The Date type.
41   */
42  public class DateLocaleConverter<D extends Date> extends BaseLocaleConverter<D> {
43  
44      /**
45       * Builds instances of {@link DateLocaleConverter}.
46       *
47       * @param <B> The builder type.
48       * @param <D> The Date type.
49       */
50      public static class Builder<B extends Builder<B, D>, D extends Date> extends BaseLocaleConverter.Builder<B, D> {
51  
52          /** Should the date conversion be lenient? */
53          private boolean lenient;
54  
55          /**
56           * Gets a new instance.
57           * <p>
58           * Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default
59           * locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion.
60           * </p>
61           *
62           * @return a new instance.
63           */
64          @Override
65          public DateLocaleConverter<D> get() {
66              return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient);
67          }
68  
69          /**
70           * Tests whether date formatting is lenient.
71           *
72           * @return true if the {@code DateFormat} used for formatting is lenient
73           * @see java.text.DateFormat#isLenient()
74           */
75          public boolean isLenient() {
76              return lenient;
77          }
78  
79          /**
80           * Sets the leniency policy.
81           *
82           * @param lenient the leniency policy.
83           * @return {@code this} instance.
84           */
85          public B setLenient(final boolean lenient) {
86              this.lenient = lenient;
87              return asThis();
88          }
89  
90      }
91  
92      /**
93       * Default Pattern Characters
94       */
95      private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();
96  
97      /** All logging goes through this logger */
98      private static final Log LOG = LogFactory.getLog(DateLocaleConverter.class);
99  
100     /**
101      * Constructs a new builder.
102      *
103      * @param <B> The builder type.
104      * @param <D> The Date type.
105      * @return a new builder.
106      */
107     @SuppressWarnings("unchecked")
108     public static <B extends Builder<B, D>, D extends Date> B builder() {
109         return (B) new Builder<>();
110     }
111 
112     /**
113      * This method is called at class initialization time to define the value for constant member DEFAULT_PATTERN_CHARS. All other methods needing this data
114      * should just read that constant.
115      */
116     private static String initDefaultChars() {
117         return new DateFormatSymbols(Locale.US).getLocalPatternChars();
118     }
119 
120     /** Should the date conversion be lenient? */
121     private final boolean isLenient;
122 
123     /**
124      * Constructs a new instance.
125      *
126      * @param defaultValue default value.
127      * @param locale       locale.
128      * @param pattern      pattern.
129      * @param useDefault   use the default.
130      * @param locPattern   localized pattern.
131      * @param lenient      leniency policy.
132      */
133     protected DateLocaleConverter(final D defaultValue, final Locale locale, final String pattern, final boolean useDefault, final boolean locPattern,
134             final boolean lenient) {
135         super(defaultValue, locale, pattern, useDefault, locPattern);
136         this.isLenient = lenient;
137     }
138 
139     /**
140      * Converts a pattern from a localized format to the default format.
141      *
142      * @param locale           The locale
143      * @param localizedPattern The pattern in 'local' symbol format
144      * @return pattern in 'default' symbol format
145      */
146     private String convertLocalizedPattern(final String localizedPattern, final Locale locale) {
147         if (localizedPattern == null) {
148             return null;
149         }
150 
151         // Note that this is a little obtuse.
152         // However, it is the best way that anyone can come up with
153         // that works with some 1.4 series JVM.
154 
155         // Get the symbols for the localized pattern
156         final DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
157         final String localChars = localizedSymbols.getLocalPatternChars();
158 
159         if (DEFAULT_PATTERN_CHARS.equals(localChars)) {
160             return localizedPattern;
161         }
162 
163         // Convert the localized pattern to default
164         String convertedPattern = null;
165         try {
166             convertedPattern = convertPattern(localizedPattern, localChars, DEFAULT_PATTERN_CHARS);
167         } catch (final Exception ex) {
168             if (LOG.isDebugEnabled()) {
169                 LOG.debug("Converting pattern '" + localizedPattern + "' for " + locale, ex);
170             }
171         }
172         return convertedPattern;
173     }
174 
175     /**
176      * Converts a Pattern from one character set to another.
177      */
178     private String convertPattern(final String pattern, final String fromChars, final String toChars) {
179         final StringBuilder converted = new StringBuilder();
180         boolean quoted = false;
181 
182         for (int i = 0; i < pattern.length(); ++i) {
183             char thisChar = pattern.charAt(i);
184             if (quoted) {
185                 if (thisChar == '\'') {
186                     quoted = false;
187                 }
188             } else if (thisChar == '\'') {
189                 quoted = true;
190             } else if (thisChar >= 'a' && thisChar <= 'z' || thisChar >= 'A' && thisChar <= 'Z') {
191                 final int index = fromChars.indexOf(thisChar);
192                 if (index == -1) {
193                     throw new IllegalArgumentException("Illegal pattern character '" + thisChar + "'");
194                 }
195                 thisChar = toChars.charAt(index);
196             }
197             converted.append(thisChar);
198         }
199 
200         if (quoted) {
201             throw new IllegalArgumentException("Unfinished quote in pattern");
202         }
203 
204         return converted.toString();
205     }
206 
207     /**
208      * Tests whether date formatting is lenient.
209      *
210      * @return true if the {@code DateFormat} used for formatting is lenient
211      * @see java.text.DateFormat#isLenient()
212      */
213     public boolean isLenient() {
214         return isLenient;
215     }
216 
217     /**
218      * Convert the specified locale-sensitive input object into an output object of the specified type.
219      *
220      * @param value   The input object to be converted
221      * @param pattern The pattern is used for the conversion
222      * @return the converted Date value
223      * @throws ConversionException if conversion cannot be performed successfully
224      * @throws ParseException      if an error occurs parsing
225      */
226     @Override
227     protected D parse(final Object value, String pattern) throws ParseException {
228         // Handle Date
229         if (value instanceof Date) {
230             return (D) value;
231         }
232 
233         // Handle Calendar
234         if (value instanceof Calendar) {
235             return (D) ((Calendar) value).getTime();
236         }
237 
238         if (localizedPattern) {
239             pattern = convertLocalizedPattern(pattern, locale);
240         }
241 
242         // Create Formatter - use default if pattern is null
243         final DateFormat formatter = pattern == null ? DateFormat.getDateInstance(DateFormat.SHORT, locale) : new SimpleDateFormat(pattern, locale);
244         formatter.setLenient(isLenient);
245 
246         // Parse the Date
247         final ParsePosition pos = new ParsePosition(0);
248         final String strValue = value.toString();
249         final Object parsedValue = formatter.parseObject(strValue, pos);
250         if (pos.getErrorIndex() > -1) {
251             throw ConversionException.format("Error parsing date '%s' at position = %s", value, pos.getErrorIndex());
252         }
253         if (pos.getIndex() < strValue.length()) {
254             throw ConversionException.format("Date '%s' contains unparsed characters from position = %s", value, pos.getIndex());
255         }
256 
257         return (D) parsedValue;
258     }
259 
260 }