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.beanutils2.locale.converters; 019 020import java.text.DateFormat; 021import java.text.DateFormatSymbols; 022import java.text.ParseException; 023import java.text.ParsePosition; 024import java.text.SimpleDateFormat; 025import java.util.Calendar; 026import java.util.Date; 027import java.util.Locale; 028 029import org.apache.commons.beanutils2.ConversionException; 030import org.apache.commons.beanutils2.locale.BaseLocaleConverter; 031import org.apache.commons.beanutils2.locale.LocaleConverter; 032import org.apache.commons.logging.Log; 033import org.apache.commons.logging.LogFactory; 034 035/** 036 * Standard {@link org.apache.commons.beanutils2.locale.LocaleConverter} implementation that converts an incoming locale-sensitive String into a 037 * {@link java.util.Date} object, optionally using a default value or throwing a {@link org.apache.commons.beanutils2.ConversionException} if a conversion error 038 * occurs. 039 * 040 * @param <D> The Date type. 041 */ 042public class DateLocaleConverter<D extends Date> extends BaseLocaleConverter<D> { 043 044 /** 045 * Builds instances of {@link DateLocaleConverter}. 046 * 047 * @param <B> The builder type. 048 * @param <D> The Date type. 049 */ 050 public static class Builder<B extends Builder<B, D>, D extends Date> extends BaseLocaleConverter.Builder<B, D> { 051 052 /** Should the date conversion be lenient? */ 053 private boolean lenient; 054 055 /** 056 * Constructs a new instance. 057 */ 058 public Builder() { 059 // empty 060 } 061 062 /** 063 * Gets a new instance. 064 * <p> 065 * Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default 066 * locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion. 067 * </p> 068 * 069 * @return a new instance. 070 */ 071 @Override 072 public DateLocaleConverter<D> get() { 073 return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient); 074 } 075 076 /** 077 * Tests whether date formatting is lenient. 078 * 079 * @return true if the {@code DateFormat} used for formatting is lenient 080 * @see java.text.DateFormat#isLenient() 081 */ 082 public boolean isLenient() { 083 return lenient; 084 } 085 086 /** 087 * Sets the leniency policy. 088 * 089 * @param lenient the leniency policy. 090 * @return {@code this} instance. 091 */ 092 public B setLenient(final boolean lenient) { 093 this.lenient = lenient; 094 return asThis(); 095 } 096 097 } 098 099 /** 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}