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 * Gets a new instance. 057 * <p> 058 * Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default 059 * locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion. 060 * </p> 061 * 062 * @return a new instance. 063 */ 064 @Override 065 public DateLocaleConverter<D> get() { 066 return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient); 067 } 068 069 /** 070 * Tests whether date formatting is lenient. 071 * 072 * @return true if the {@code DateFormat} used for formatting is lenient 073 * @see java.text.DateFormat#isLenient() 074 */ 075 public boolean isLenient() { 076 return lenient; 077 } 078 079 /** 080 * Sets the leniency policy. 081 * 082 * @param lenient the leniency policy. 083 * @return {@code this} instance. 084 */ 085 public B setLenient(final boolean lenient) { 086 this.lenient = lenient; 087 return asThis(); 088 } 089 090 } 091 092 /** 093 * Default Pattern Characters 094 */ 095 private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars(); 096 097 /** All logging goes through this logger */ 098 private static final Log LOG = LogFactory.getLog(DateLocaleConverter.class); 099 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}