DateLocaleConverter.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.beanutils2.locale.converters;
import java.text.DateFormat;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import org.apache.commons.beanutils2.ConversionException;
import org.apache.commons.beanutils2.locale.BaseLocaleConverter;
import org.apache.commons.beanutils2.locale.LocaleConverter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Standard {@link org.apache.commons.beanutils2.locale.LocaleConverter} implementation that converts an incoming locale-sensitive String into a
* {@link java.util.Date} object, optionally using a default value or throwing a {@link org.apache.commons.beanutils2.ConversionException} if a conversion error
* occurs.
*
* @param <D> The Date type.
*/
public class DateLocaleConverter<D extends Date> extends BaseLocaleConverter<D> {
/**
* Builds instances of {@link DateLocaleConverter}.
*
* @param <B> The builder type.
* @param <D> The Date type.
*/
public static class Builder<B extends Builder<B, D>, D extends Date> extends BaseLocaleConverter.Builder<B, D> {
/** Should the date conversion be lenient? */
private boolean lenient;
/**
* Gets a new instance.
* <p>
* Defaults construct a {@link LocaleConverter} that will throw a {@link ConversionException} if a conversion error occurs. The locale is the default
* locale for this instance of the Java Virtual Machine and an unlocalized pattern is used for the conversion.
* </p>
*
* @return a new instance.
*/
@Override
public DateLocaleConverter<D> get() {
return new DateLocaleConverter<>(defaultValue, locale, pattern, useDefault || defaultValue != null, localizedPattern, lenient);
}
/**
* Tests whether date formatting is lenient.
*
* @return true if the {@code DateFormat} used for formatting is lenient
* @see java.text.DateFormat#isLenient()
*/
public boolean isLenient() {
return lenient;
}
/**
* Sets the leniency policy.
*
* @param lenient the leniency policy.
* @return {@code this} instance.
*/
public B setLenient(final boolean lenient) {
this.lenient = lenient;
return asThis();
}
}
/**
* Default Pattern Characters
*/
private static final String DEFAULT_PATTERN_CHARS = DateLocaleConverter.initDefaultChars();
/** All logging goes through this logger */
private static final Log LOG = LogFactory.getLog(DateLocaleConverter.class);
/**
* Constructs a new builder.
*
* @param <B> The builder type.
* @param <D> The Date type.
* @return a new builder.
*/
@SuppressWarnings("unchecked")
public static <B extends Builder<B, D>, D extends Date> B builder() {
return (B) new Builder<>();
}
/**
* This method is called at class initialization time to define the value for constant member DEFAULT_PATTERN_CHARS. All other methods needing this data
* should just read that constant.
*/
private static String initDefaultChars() {
return new DateFormatSymbols(Locale.US).getLocalPatternChars();
}
/** Should the date conversion be lenient? */
private final boolean isLenient;
/**
* Constructs a new instance.
*
* @param defaultValue default value.
* @param locale locale.
* @param pattern pattern.
* @param useDefault use the default.
* @param locPattern localized pattern.
* @param lenient leniency policy.
*/
protected DateLocaleConverter(final D defaultValue, final Locale locale, final String pattern, final boolean useDefault, final boolean locPattern,
final boolean lenient) {
super(defaultValue, locale, pattern, useDefault, locPattern);
this.isLenient = lenient;
}
/**
* Converts a pattern from a localized format to the default format.
*
* @param locale The locale
* @param localizedPattern The pattern in 'local' symbol format
* @return pattern in 'default' symbol format
*/
private String convertLocalizedPattern(final String localizedPattern, final Locale locale) {
if (localizedPattern == null) {
return null;
}
// Note that this is a little obtuse.
// However, it is the best way that anyone can come up with
// that works with some 1.4 series JVM.
// Get the symbols for the localized pattern
final DateFormatSymbols localizedSymbols = new DateFormatSymbols(locale);
final String localChars = localizedSymbols.getLocalPatternChars();
if (DEFAULT_PATTERN_CHARS.equals(localChars)) {
return localizedPattern;
}
// Convert the localized pattern to default
String convertedPattern = null;
try {
convertedPattern = convertPattern(localizedPattern, localChars, DEFAULT_PATTERN_CHARS);
} catch (final Exception ex) {
if (LOG.isDebugEnabled()) {
LOG.debug("Converting pattern '" + localizedPattern + "' for " + locale, ex);
}
}
return convertedPattern;
}
/**
* Converts a Pattern from one character set to another.
*/
private String convertPattern(final String pattern, final String fromChars, final String toChars) {
final StringBuilder converted = new StringBuilder();
boolean quoted = false;
for (int i = 0; i < pattern.length(); ++i) {
char thisChar = pattern.charAt(i);
if (quoted) {
if (thisChar == '\'') {
quoted = false;
}
} else if (thisChar == '\'') {
quoted = true;
} else if (thisChar >= 'a' && thisChar <= 'z' || thisChar >= 'A' && thisChar <= 'Z') {
final int index = fromChars.indexOf(thisChar);
if (index == -1) {
throw new IllegalArgumentException("Illegal pattern character '" + thisChar + "'");
}
thisChar = toChars.charAt(index);
}
converted.append(thisChar);
}
if (quoted) {
throw new IllegalArgumentException("Unfinished quote in pattern");
}
return converted.toString();
}
/**
* Tests whether date formatting is lenient.
*
* @return true if the {@code DateFormat} used for formatting is lenient
* @see java.text.DateFormat#isLenient()
*/
public boolean isLenient() {
return isLenient;
}
/**
* Convert the specified locale-sensitive input object into an output object of the specified type.
*
* @param value The input object to be converted
* @param pattern The pattern is used for the conversion
* @return the converted Date value
* @throws ConversionException if conversion cannot be performed successfully
* @throws ParseException if an error occurs parsing
*/
@Override
protected D parse(final Object value, String pattern) throws ParseException {
// Handle Date
if (value instanceof Date) {
return (D) value;
}
// Handle Calendar
if (value instanceof Calendar) {
return (D) ((Calendar) value).getTime();
}
if (localizedPattern) {
pattern = convertLocalizedPattern(pattern, locale);
}
// Create Formatter - use default if pattern is null
final DateFormat formatter = pattern == null ? DateFormat.getDateInstance(DateFormat.SHORT, locale) : new SimpleDateFormat(pattern, locale);
formatter.setLenient(isLenient);
// Parse the Date
final ParsePosition pos = new ParsePosition(0);
final String strValue = value.toString();
final Object parsedValue = formatter.parseObject(strValue, pos);
if (pos.getErrorIndex() > -1) {
throw ConversionException.format("Error parsing date '%s' at position = %s", value, pos.getErrorIndex());
}
if (pos.getIndex() < strValue.length()) {
throw ConversionException.format("Date '%s' contains unparsed characters from position = %s", value, pos.getIndex());
}
return (D) parsedValue;
}
}