blob: 49c07f78a0bcfdf48d4cec9504cc5cb84915518c [file] [log] [blame]
/*
* 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.converters;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import org.apache.commons.beanutils2.ConversionException;
/**
* {@link org.apache.commons.beanutils2.Converter} implementation
* that handles conversion to and from <b>date/time</b> objects.
* <p>
* This implementation handles conversion for the following
* <i>date/time</i> types.
* <ul>
* <li>{@code java.util.Date}</li>
* <li>{@code java.util.Calendar}</li>
* <li>{@code java.time.LocalDate}</li>
* <li>{@code java.time.LocalDateTime}</li>
* <li>{@code java.time.OffsetDateTime}</li>
* <li>{@code java.time.ZonedDateTime}</li>
* <li>{@code java.sql.Date}</li>
* <li>{@code java.sql.Time}</li>
* <li>{@code java.sql.Timestamp}</li>
* </ul>
*
* <h3>String Conversions (to and from)</h3>
* This class provides a number of ways in which date/time
* conversions to/from Strings can be achieved:
* <ul>
* <li>Using the SHORT date format for the default Locale, configure using:
* <ul>
* <li>{@code setUseLocaleFormat(true)}</li>
* </ul>
* </li>
* <li>Using the SHORT date format for a specified Locale, configure using:
* <ul>
* <li>{@code setLocale(Locale)}</li>
* </ul>
* </li>
* <li>Using the specified date pattern(s) for the default Locale, configure using:
* <ul>
* <li>Either {@code setPattern(String)} or
* {@code setPatterns(String[])}</li>
* </ul>
* </li>
* <li>Using the specified date pattern(s) for a specified Locale, configure using:
* <ul>
* <li>{@code setPattern(String)} or
* {@code setPatterns(String[]) and...}</li>
* <li>{@code setLocale(Locale)}</li>
* </ul>
* </li>
* <li>If none of the above are configured the
* {@code toDate(String)} method is used to convert
* from String to Date and the Dates's
* {@code toString()} method used to convert from
* Date to String.</li>
* </ul>
*
* <p>
* The <b>Time Zone</b> to use with the date format can be specified
* using the {@link #setTimeZone(TimeZone)} method.
*
* @since 1.8.0
*/
public abstract class DateTimeConverter extends AbstractConverter {
private String[] patterns;
private String displayPatterns;
private Locale locale;
private TimeZone timeZone;
private boolean useLocaleFormat;
/**
* Construct a Date/Time <i>Converter</i> that throws a
* {@code ConversionException} if an error occurs.
*/
public DateTimeConverter() {
super();
}
/**
* Construct a Date/Time <i>Converter</i> that returns a default
* value if an error occurs.
*
* @param defaultValue The default value to be returned
* if the value to be converted is missing or an error
* occurs converting the value.
*/
public DateTimeConverter(final Object defaultValue) {
super(defaultValue);
}
/**
* Indicate whether conversion should use a format/pattern or not.
*
* @param useLocaleFormat {@code true} if the format
* for the locale should be used, otherwise {@code false}
*/
public void setUseLocaleFormat(final boolean useLocaleFormat) {
this.useLocaleFormat = useLocaleFormat;
}
/**
* Return the Time Zone to use when converting dates
* (or {@code null} if none specified.
*
* @return The Time Zone.
*/
public TimeZone getTimeZone() {
return timeZone;
}
/**
* Set the Time Zone to use when converting dates.
*
* @param timeZone The Time Zone.
*/
public void setTimeZone(final TimeZone timeZone) {
this.timeZone = timeZone;
}
/**
* Return the Locale for the <i>Converter</i>
* (or {@code null} if none specified).
*
* @return The locale to use for conversion
*/
public Locale getLocale() {
return locale;
}
/**
* Set the Locale for the <i>Converter</i>.
*
* @param locale The Locale.
*/
public void setLocale(final Locale locale) {
this.locale = locale;
setUseLocaleFormat(true);
}
/**
* Set a date format pattern to use to convert
* dates to/from a {@code java.lang.String}.
*
* @see SimpleDateFormat
* @param pattern The format pattern.
*/
public void setPattern(final String pattern) {
setPatterns(new String[] {pattern});
}
/**
* Return the date format patterns used to convert
* dates to/from a {@code java.lang.String}
* (or {@code null} if none specified).
*
* @see SimpleDateFormat
* @return Array of format patterns.
*/
public String[] getPatterns() {
return patterns;
}
/**
* Set the date format patterns to use to convert
* dates to/from a {@code java.lang.String}.
*
* @see SimpleDateFormat
* @param patterns Array of format patterns.
*/
public void setPatterns(final String[] patterns) {
this.patterns = patterns;
if (patterns != null && patterns.length > 1) {
final StringBuilder buffer = new StringBuilder();
for (int i = 0; i < patterns.length; i++) {
if (i > 0) {
buffer.append(", ");
}
buffer.append(patterns[i]);
}
displayPatterns = buffer.toString();
}
setUseLocaleFormat(true);
}
/**
* Convert an input Date/Calendar object into a String.
* <p>
* <b>N.B.</b>If the converter has been configured to with
* one or more patterns (using {@code setPatterns()}), then
* the first pattern will be used to format the date into a String.
* Otherwise the default {@code DateFormat} for the default locale
* (and <i>style</i> if configured) will be used.
*
* @param value The input value to be converted
* @return the converted String value.
* @throws Throwable if an error occurs converting to a String
*/
@Override
protected String convertToString(final Object value) throws Throwable {
Date date = null;
if (value instanceof Date) {
date = (Date)value;
} else if (value instanceof Calendar) {
date = ((Calendar)value).getTime();
} else if (value instanceof Long) {
date = new Date(((Long)value).longValue());
} else if (value instanceof LocalDateTime) {
date = java.sql.Timestamp.valueOf(((LocalDateTime)value));
} else if (value instanceof LocalDate) {
date = java.sql.Date.valueOf(((LocalDate)value));
} else if (value instanceof ZonedDateTime) {
date = Date.from(((ZonedDateTime)value).toInstant());
} else if (value instanceof OffsetDateTime) {
date = Date.from(((OffsetDateTime)value).toInstant());
}
String result = null;
if (useLocaleFormat && date != null) {
DateFormat format = null;
if (patterns != null && patterns.length > 0) {
format = getFormat(patterns[0]);
} else {
format = getFormat(locale, timeZone);
}
logFormat("Formatting", format);
result = format.format(date);
if (log().isDebugEnabled()) {
log().debug(" Converted to String using format '" + result + "'");
}
} else {
result = value.toString();
if (log().isDebugEnabled()) {
log().debug(" Converted to String using toString() '" + result + "'");
}
}
return result;
}
/**
* Convert the input object into a Date object of the
* specified type.
* <p>
* This method handles conversions between the following
* types:
* <ul>
* <li>{@code java.util.Date}</li>
* <li>{@code java.util.Calendar}</li>
* <li>{@code java.time.LocalDate}</li>
* <li>{@code java.time.LocalDateTime}</li>
* <li>{@code java.time.OffsetDateTime}</li>
* <li>{@code java.time.ZonedDateTime}</li>
* <li>{@code java.sql.Date}</li>
* <li>{@code java.sql.Time}</li>
* <li>{@code java.sql.Timestamp}</li>
* </ul>
*
* It also handles conversion from a {@code String} to
* any of the above types.
* <p>
*
* For {@code String} conversion, if the converter has been configured
* with one or more patterns (using {@code setPatterns()}), then
* the conversion is attempted with each of the specified patterns.
* Otherwise the default {@code DateFormat} for the default locale
* (and <i>style</i> if configured) will be used.
*
* @param <T> The desired target type of the conversion.
* @param targetType Data type to which this value should be converted.
* @param value The input value to be converted.
* @return The converted value.
* @throws Exception if conversion cannot be performed successfully
*/
@Override
protected <T> T convertToType(final Class<T> targetType, final Object value) throws Exception {
final Class<?> sourceType = value.getClass();
// Handle java.sql.Timestamp
if (value instanceof java.sql.Timestamp) {
// N.B. Prior to JDK 1.4 the Timestamp's getTime() method
// didn't include the milliseconds. The following code
// ensures it works consistently across JDK versions
final java.sql.Timestamp timestamp = (java.sql.Timestamp)value;
long timeInMillis = ((timestamp.getTime() / 1000) * 1000);
timeInMillis += timestamp.getNanos() / 1000000;
return toDate(targetType, timeInMillis);
}
// Handle Date (includes java.sql.Date & java.sql.Time)
if (value instanceof Date) {
final Date date = (Date)value;
return toDate(targetType, date.getTime());
}
// Handle Calendar
if (value instanceof Calendar) {
final Calendar calendar = (Calendar)value;
return toDate(targetType, calendar.getTime().getTime());
}
// Handle Long
if (value instanceof Long) {
final Long longObj = (Long)value;
return toDate(targetType, longObj.longValue());
}
// Handle LocalDate
if (value instanceof LocalDate) {
final LocalDate date = (LocalDate)value;
return toDate(targetType, date.atStartOfDay(getZoneId()).toInstant().toEpochMilli());
}
// Handle LocalDateTime
if (value instanceof LocalDateTime) {
final LocalDateTime date = (LocalDateTime)value;
return toDate(targetType, date.atZone(getZoneId()).toInstant().toEpochMilli());
}
// Handle ZonedDateTime
if (value instanceof ZonedDateTime) {
final ZonedDateTime date = (ZonedDateTime)value;
return toDate(targetType, date.toInstant().toEpochMilli());
}
// Handle OffsetDateTime
if (value instanceof OffsetDateTime) {
final OffsetDateTime date = (OffsetDateTime)value;
return toDate(targetType, date.toInstant().toEpochMilli());
}
// Convert all other types to String & handle
final String stringValue = value.toString().trim();
if (stringValue.length() == 0) {
return handleMissing(targetType);
}
// Parse the Date/Time
if (useLocaleFormat) {
Calendar calendar = null;
if (patterns != null && patterns.length > 0) {
calendar = parse(sourceType, targetType, stringValue);
} else {
final DateFormat format = getFormat(locale, timeZone);
calendar = parse(sourceType, targetType, stringValue, format);
}
if (Calendar.class.isAssignableFrom(targetType)) {
return targetType.cast(calendar);
}
return toDate(targetType, calendar.getTime().getTime());
}
// Default String conversion
return toDate(targetType, stringValue);
}
/**
* Convert a long value to the specified Date type for this
* <i>Converter</i>.
* <p>
*
* This method handles conversion to the following types:
* <ul>
* <li>{@code java.util.Date}</li>
* <li>{@code java.util.Calendar}</li>
* <li>{@code java.time.LocalDate}</li>
* <li>{@code java.time.LocalDateTime}</li>
* <li>{@code java.time.ZonedDateTime}</li>
* <li>{@code java.sql.Date}</li>
* <li>{@code java.sql.Time}</li>
* <li>{@code java.sql.Timestamp}</li>
* </ul>
*
* @param <T> The target type
* @param type The Date type to convert to
* @param value The long value to convert.
* @return The converted date value.
*/
private <T> T toDate(final Class<T> type, final long value) {
// java.util.Date
if (type.equals(Date.class)) {
return type.cast(new Date(value));
}
// java.sql.Date
if (type.equals(java.sql.Date.class)) {
return type.cast(new java.sql.Date(value));
}
// java.sql.Time
if (type.equals(java.sql.Time.class)) {
return type.cast(new java.sql.Time(value));
}
// java.sql.Timestamp
if (type.equals(java.sql.Timestamp.class)) {
return type.cast(new java.sql.Timestamp(value));
}
// java.time.LocalDateTime
if (type.equals(LocalDate.class)) {
final LocalDate localDate = Instant.ofEpochMilli(value).atZone(getZoneId()).toLocalDate();
return type.cast(localDate);
}
// java.time.LocalDateTime
if (type.equals(LocalDateTime.class)) {
final LocalDateTime localDateTime = Instant.ofEpochMilli(value).atZone(getZoneId()).toLocalDateTime();
return type.cast(localDateTime);
}
// java.time.ZonedDateTime
if (type.equals(ZonedDateTime.class)) {
final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), getZoneId());
return type.cast(zonedDateTime);
}
// java.time.OffsetDateTime
if (type.equals(OffsetDateTime.class)) {
final OffsetDateTime offsetDateTime = OffsetDateTime.ofInstant(Instant.ofEpochMilli(value), getZoneId());
return type.cast(offsetDateTime);
}
// java.util.Calendar
if (type.equals(Calendar.class)) {
Calendar calendar = null;
if (locale == null && timeZone == null) {
calendar = Calendar.getInstance();
} else if (locale == null) {
calendar = Calendar.getInstance(timeZone);
} else if (timeZone == null) {
calendar = Calendar.getInstance(locale);
} else {
calendar = Calendar.getInstance(timeZone, locale);
}
calendar.setTime(new Date(value));
calendar.setLenient(false);
return type.cast(calendar);
}
final String msg = toString(getClass()) + " cannot handle conversion to '"
+ toString(type) + "'";
if (log().isWarnEnabled()) {
log().warn(" " + msg);
}
throw new ConversionException(msg);
}
/**
* Default String to Date conversion.
* <p>
* This method handles conversion from a String to the following types:
* <ul>
* <li>{@code java.sql.Date}</li>
* <li>{@code java.sql.Time}</li>
* <li>{@code java.sql.Timestamp}</li>
* </ul>
* <p>
* <strong>N.B.</strong> No default String conversion
* mechanism is provided for {@code java.util.Date}
* and {@code java.util.Calendar} type.
*
* @param <T> The target type
* @param type The date type to convert to
* @param value The String value to convert.
* @return The converted Number value.
*/
private <T> T toDate(final Class<T> type, final String value) {
// java.sql.Date
if (type.equals(java.sql.Date.class)) {
try {
return type.cast(java.sql.Date.valueOf(value));
} catch (final IllegalArgumentException e) {
throw new ConversionException(
"String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date");
}
}
// java.sql.Time
if (type.equals(java.sql.Time.class)) {
try {
return type.cast(java.sql.Time.valueOf(value));
} catch (final IllegalArgumentException e) {
throw new ConversionException(
"String must be in JDBC format [HH:mm:ss] to create a java.sql.Time");
}
}
// java.sql.Timestamp
if (type.equals(java.sql.Timestamp.class)) {
try {
return type.cast(java.sql.Timestamp.valueOf(value));
} catch (final IllegalArgumentException e) {
throw new ConversionException(
"String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] " +
"to create a java.sql.Timestamp");
}
}
final String msg = toString(getClass()) + " does not support default String to '"
+ toString(type) + "' conversion.";
if (log().isWarnEnabled()) {
log().warn(" " + msg);
log().warn(" (N.B. Re-configure Converter or use alternative implementation)");
}
throw new ConversionException(msg);
}
/**
* Return a {@code DateFormat} for the Locale.
* @param locale The Locale to create the Format with (may be null)
* @param timeZone The Time Zone create the Format with (may be null)
*
* @return A Date Format.
*/
protected DateFormat getFormat(final Locale locale, final TimeZone timeZone) {
DateFormat format = null;
if (locale == null) {
format = DateFormat.getDateInstance(DateFormat.SHORT);
} else {
format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
}
if (timeZone != null) {
format.setTimeZone(timeZone);
}
return format;
}
/**
* Create a date format for the specified pattern.
*
* @param pattern The date pattern
* @return The DateFormat
*/
private DateFormat getFormat(final String pattern) {
final DateFormat format = new SimpleDateFormat(pattern);
if (timeZone != null) {
format.setTimeZone(timeZone);
}
return format;
}
/**
* Parse a String date value using the set of patterns.
*
* @param sourceType The type of the value being converted
* @param targetType The type to convert the value to.
* @param value The String date value.
*
* @return The converted Date object.
* @throws Exception if an error occurs parsing the date.
*/
private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value) throws Exception {
Exception firstEx = null;
for (final String pattern : patterns) {
try {
final DateFormat format = getFormat(pattern);
final Calendar calendar = parse(sourceType, targetType, value, format);
return calendar;
} catch (final Exception ex) {
if (firstEx == null) {
firstEx = ex;
}
}
}
if (patterns.length > 1) {
throw new ConversionException("Error converting '" + toString(sourceType) + "' to '" + toString(targetType)
+ "' using patterns '" + displayPatterns + "'");
}
throw firstEx;
}
/**
* Parse a String into a {@code Calendar} object
* using the specified {@code DateFormat}.
*
* @param sourceType The type of the value being converted
* @param targetType The type to convert the value to
* @param value The String date value.
* @param format The DateFormat to parse the String value.
*
* @return The converted Calendar object.
* @throws ConversionException if the String cannot be converted.
*/
private Calendar parse(final Class<?> sourceType, final Class<?> targetType, final String value, final DateFormat format) {
logFormat("Parsing", format);
format.setLenient(false);
final ParsePosition pos = new ParsePosition(0);
final Date parsedDate = format.parse(value, pos); // ignore the result (use the Calendar)
if (pos.getErrorIndex() >= 0 || pos.getIndex() != value.length() || parsedDate == null) {
String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'";
if (format instanceof SimpleDateFormat) {
msg += " using pattern '" + ((SimpleDateFormat)format).toPattern() + "'";
}
if (log().isDebugEnabled()) {
log().debug(" " + msg);
}
throw new ConversionException(msg);
}
final Calendar calendar = format.getCalendar();
return calendar;
}
/**
* Provide a String representation of this date/time converter.
*
* @return A String representation of this date/time converter
*/
@Override
public String toString() {
final StringBuilder buffer = new StringBuilder();
buffer.append(toString(getClass()));
buffer.append("[UseDefault=");
buffer.append(isUseDefault());
buffer.append(", UseLocaleFormat=");
buffer.append(useLocaleFormat);
if (displayPatterns != null) {
buffer.append(", Patterns={");
buffer.append(displayPatterns);
buffer.append('}');
}
if (locale != null) {
buffer.append(", Locale=");
buffer.append(locale);
}
if (timeZone != null) {
buffer.append(", TimeZone=");
buffer.append(timeZone);
}
buffer.append(']');
return buffer.toString();
}
/**
* Log the {@code DateFormat} creation.
* @param action The action the format is being used for
* @param format The Date format
*/
private void logFormat(final String action, final DateFormat format) {
if (log().isDebugEnabled()) {
final StringBuilder buffer = new StringBuilder(45);
buffer.append(" ");
buffer.append(action);
buffer.append(" with Format");
if (format instanceof SimpleDateFormat) {
buffer.append("[");
buffer.append(((SimpleDateFormat)format).toPattern());
buffer.append("]");
}
buffer.append(" for ");
if (locale == null) {
buffer.append("default locale");
} else {
buffer.append("locale[");
buffer.append(locale);
buffer.append("]");
}
if (timeZone != null) {
buffer.append(", TimeZone[");
buffer.append(timeZone);
buffer.append("]");
}
log().debug(buffer.toString());
}
}
/**
* Gets the {@code java.time.ZoneId</code> from the <code>java.util.Timezone}
* set or use the system default if no time zone is set.
* @return the {@code ZoneId}
*/
private ZoneId getZoneId() {
return timeZone == null ? ZoneId.systemDefault() : timeZone.toZoneId();
}
}