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           * Constructs a new instance.
57           */
58          public Builder() {
59              // empty
60          }
61  
62          /**
63           * Gets a new instance.
64           * <p>
65           * Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default
66           * locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion.
67           * </p>
68           *
69           * @return a new instance.
70           */
71          @Override
72          public DateLocaleConverter<D> get() {
73              return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient);
74          }
75  
76          /**
77           * Tests whether date formatting is lenient.
78           *
79           * @return true if the {@code DateFormat} used for formatting is lenient
80           * @see java.text.DateFormat#isLenient()
81           */
82          public boolean isLenient() {
83              return lenient;
84          }
85  
86          /**
87           * Sets the leniency policy.
88           *
89           * @param lenient the leniency policy.
90           * @return {@code this} instance.
91           */
92          public B setLenient(final boolean lenient) {
93              this.lenient = lenient;
94              return asThis();
95          }
96  
97      }
98  
99      /**
100      * Default Pattern Characters
101      */
102     private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();
103 
104     /** All logging goes through this logger */
105     private static final Log LOG = LogFactory.getLog(DateLocaleConverter.class);
106 
107     /**
108      * Constructs a new builder.
109      *
110      * @param <B> The builder type.
111      * @param <D> The Date type.
112      * @return a new builder.
113      */
114     @SuppressWarnings("unchecked")
115     public static <B extends Builder<B, D>, D extends Date> B builder() {
116         return (B) new Builder<>();
117     }
118 
119     /**
120      * This method is called at class initialization time to define the value for constant member DEFAULT_PATTERN_CHARS. All other methods needing this data
121      * should just read that constant.
122      */
123     private static String initDefaultChars() {
124         return new DateFormatSymbols(Locale.US).getLocalPatternChars();
125     }
126 
127     /** Should the date conversion be lenient? */
128     private final boolean isLenient;
129 
130     /**
131      * Constructs a new instance.
132      *
133      * @param defaultValue default value.
134      * @param locale       locale.
135      * @param pattern      pattern.
136      * @param useDefault   use the default.
137      * @param locPattern   localized pattern.
138      * @param lenient      leniency policy.
139      */
140     protected DateLocaleConverter(final D defaultValue, final Locale locale, final String pattern, final boolean useDefault, final boolean locPattern,
141             final boolean lenient) {
142         super(defaultValue, locale, pattern, useDefault, locPattern);
143         this.isLenient = lenient;
144     }
145 
146     /**
147      * Converts a pattern from a localized format to the default format.
148      *
149      * @param locale           The locale
150      * @param localizedPattern The pattern in 'local' symbol format
151      * @return pattern in 'default' symbol format
152      */
153     private String convertLocalizedPattern(final String localizedPattern, final Locale locale) {
154         if (localizedPattern == null) {
155             return null;
156         }
157 
158         // Note that this is a little obtuse.
159         // However, it is the best way that anyone can come up with
160         // that works with some 1.4 series JVM.
161 
162         // Get the symbols for the localized pattern
163         final DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
164         final String localChars = localizedSymbols.getLocalPatternChars();
165 
166         if (DEFAULT_PATTERN_CHARS.equals(localChars)) {
167             return localizedPattern;
168         }
169 
170         // Convert the localized pattern to default
171         String convertedPattern = null;
172         try {
173             convertedPattern = convertPattern(localizedPattern, localChars, DEFAULT_PATTERN_CHARS);
174         } catch (final Exception ex) {
175             if (LOG.isDebugEnabled()) {
176                 LOG.debug("Converting pattern '" + localizedPattern + "' for " + locale, ex);
177             }
178         }
179         return convertedPattern;
180     }
181 
182     /**
183      * Converts a Pattern from one character set to another.
184      */
185     private String convertPattern(final String pattern, final String fromChars, final String toChars) {
186         final StringBuilder converted = new StringBuilder();
187         boolean quoted = false;
188 
189         for (int i = 0; i < pattern.length(); ++i) {
190             char thisChar = pattern.charAt(i);
191             if (quoted) {
192                 if (thisChar == '\'') {
193                     quoted = false;
194                 }
195             } else if (thisChar == '\'') {
196                 quoted = true;
197             } else if (thisChar >= 'a' && thisChar <= 'z' || thisChar >= 'A' && thisChar <= 'Z') {
198                 final int index = fromChars.indexOf(thisChar);
199                 if (index == -1) {
200                     throw new IllegalArgumentException("Illegal pattern character '" + thisChar + "'");
201                 }
202                 thisChar = toChars.charAt(index);
203             }
204             converted.append(thisChar);
205         }
206 
207         if (quoted) {
208             throw new IllegalArgumentException("Unfinished quote in pattern");
209         }
210 
211         return converted.toString();
212     }
213 
214     /**
215      * Tests whether date formatting is lenient.
216      *
217      * @return true if the {@code DateFormat} used for formatting is lenient
218      * @see java.text.DateFormat#isLenient()
219      */
220     public boolean isLenient() {
221         return isLenient;
222     }
223 
224     /**
225      * Convert the specified locale-sensitive input object into an output object of the specified type.
226      *
227      * @param value   The input object to be converted
228      * @param pattern The pattern is used for the conversion
229      * @return the converted Date value
230      * @throws ConversionException if conversion cannot be performed successfully
231      * @throws ParseException      if an error occurs parsing
232      */
233     @Override
234     protected D parse(final Object value, String pattern) throws ParseException {
235         // Handle Date
236         if (value instanceof Date) {
237             return (D) value;
238         }
239 
240         // Handle Calendar
241         if (value instanceof Calendar) {
242             return (D) ((Calendar) value).getTime();
243         }
244 
245         if (localizedPattern) {
246             pattern = convertLocalizedPattern(pattern, locale);
247         }
248 
249         // Create Formatter - use default if pattern is null
250         final DateFormat formatter = pattern == null ? DateFormat.getDateInstance(DateFormat.SHORT, locale) : new SimpleDateFormat(pattern, locale);
251         formatter.setLenient(isLenient);
252 
253         // Parse the Date
254         final ParsePosition pos = new ParsePosition(0);
255         final String strValue = value.toString();
256         final Object parsedValue = formatter.parseObject(strValue, pos);
257         if (pos.getErrorIndex() > -1) {
258             throw ConversionException.format("Error parsing date '%s' at position = %s", value, pos.getErrorIndex());
259         }
260         if (pos.getIndex() < strValue.length()) {
261             throw ConversionException.format("Date '%s' contains unparsed characters from position = %s", value, pos.getIndex());
262         }
263 
264         return (D) parsedValue;
265     }
266 
267 }