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 * https://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 }