| /* |
| * 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 |
| * |
| * https://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.time.format.DateTimeParseException; |
| import java.time.temporal.TemporalAccessor; |
| 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 <strong>date/time</strong> objects. |
| * <p> |
| * This implementation handles conversion for the following <em>date/time</em> types. |
| * <ul> |
| * <li>{@link java.util.Date}</li> |
| * <li>{@link java.util.Calendar}</li> |
| * <li>{@link java.time.LocalDate}</li> |
| * <li>{@link java.time.LocalDateTime}</li> |
| * <li>{@link java.time.OffsetDateTime}</li> |
| * <li>{@link java.time.ZonedDateTime}</li> |
| * <li>{@link java.sql.Date}</li> |
| * <li>{@link java.sql.Time}</li> |
| * <li>{@link java.sql.Timestamp}</li> |
| * </ul> |
| * |
| * <h2>String Conversions (to and from)</h2> 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 <strong>Time Zone</strong> to use with the date format can be specified using the {@link #setTimeZone(TimeZone)} method. |
| * |
| * @param <D> The default value type. |
| * @since 1.8.0 |
| */ |
| public abstract class DateTimeConverter<D> extends AbstractConverter<D> { |
| |
| private String[] patterns; |
| private String displayPatterns; |
| private Locale locale; |
| private TimeZone timeZone; |
| private boolean useLocaleFormat; |
| |
| /** |
| * Constructs a Date/Time <em>Converter</em> that throws a {@code ConversionException} if an error occurs. |
| */ |
| public DateTimeConverter() { |
| } |
| |
| /** |
| * Constructs a Date/Time <em>Converter</em> 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 D defaultValue) { |
| super(defaultValue); |
| } |
| |
| /** |
| * Convert an input Date/Calendar object into a String. |
| * <p> |
| * <strong>N.B.</strong>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 <em>style</em> if configured) will be used. |
| * |
| * @param value The input value to be converted |
| * @return The converted String value. |
| * @throws IllegalArgumentException if an error occurs converting to a String |
| */ |
| @Override |
| protected String convertToString(final Object value) { |
| 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()); |
| } else if (value instanceof TemporalAccessor) { |
| // Backstop for other TemporalAccessor implementations. |
| date = Date.from(Instant.from((TemporalAccessor) value)); |
| } else if (value instanceof Instant) { |
| date = Date.from((Instant) value); |
| } |
| 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>{@link java.util.Date}</li> |
| * <li>{@link java.util.Calendar}</li> |
| * <li>{@link java.time.LocalDate}</li> |
| * <li>{@link java.time.LocalDateTime}</li> |
| * <li>{@link java.time.OffsetDateTime}</li> |
| * <li>{@link java.time.Instant}</li> |
| * <li>{@link java.time.ZonedDateTime}</li> |
| * <li>{@link java.sql.Date}</li> |
| * <li>{@link java.sql.Time}</li> |
| * <li>{@link 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 <em>style</em> 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 Thrown 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) { |
| // 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 = Math.floorDiv(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()); |
| } |
| if (value instanceof Instant) { |
| final Instant date = (Instant) value; |
| return toDate(targetType, date.toEpochMilli()); |
| } |
| // Convert all other types to String & handle |
| final String stringValue = toTrim(value); |
| if (stringValue.isEmpty()) { |
| 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); |
| } |
| |
| /** |
| * Gets 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 = locale == null ? new SimpleDateFormat(pattern) : new SimpleDateFormat(pattern, locale); |
| if (timeZone != null) { |
| format.setTimeZone(timeZone); |
| } |
| return format; |
| } |
| |
| /** |
| * Gets the Locale for the <em>Converter</em> (or {@code null} if none specified). |
| * |
| * @return The locale to use for conversion |
| */ |
| public Locale getLocale() { |
| return locale; |
| } |
| |
| /** |
| * Gets the date format patterns used to convert dates to/from a {@link String} (or {@code null} if none specified). |
| * |
| * @see SimpleDateFormat |
| * @return Array of format patterns. |
| */ |
| public String[] getPatterns() { |
| return patterns.clone(); |
| } |
| |
| /** |
| * Gets the Time Zone to use when converting dates (or {@code null} if none specified. |
| * |
| * @return The Time Zone. |
| */ |
| public TimeZone getTimeZone() { |
| return timeZone; |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| /** |
| * 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()); |
| } |
| } |
| |
| /** |
| * 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 Thrown 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 { |
| return parse(sourceType, targetType, value, getFormat(pattern)); |
| } catch (final Exception ex) { |
| if (firstEx == null) { |
| firstEx = ex; |
| } |
| } |
| } |
| if (patterns.length > 1) { |
| throw ConversionException.format("Error converting '%s' to '%s' using patterns '%s'", toString(sourceType), toString(targetType), displayPatterns); |
| } |
| if (firstEx != null) { |
| throw firstEx; |
| } |
| return null; |
| } |
| |
| /** |
| * 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) |
| final int errorIndex = pos.getErrorIndex(); |
| if (errorIndex >= 0 || pos.getIndex() != value.length() || parsedDate == null) { |
| String msg = "Error converting '" + toString(sourceType) + "' to '" + toString(targetType) + "'"; |
| if (format instanceof SimpleDateFormat) { |
| final SimpleDateFormat simpleFormat = (SimpleDateFormat) format; |
| msg += String.format(" using pattern '%s', localized pattern '%s', errorIndex %,d, calendar type %s, this %s", simpleFormat.toPattern(), |
| simpleFormat.toLocalizedPattern(), errorIndex, format.getCalendar().getClass().getSimpleName(), this); |
| } |
| if (log().isDebugEnabled()) { |
| log().debug(" " + msg); |
| } |
| throw new ConversionException(msg); |
| } |
| return format.getCalendar(); |
| } |
| |
| /** |
| * Sets the Locale for the <em>Converter</em>. |
| * |
| * @param locale The Locale. |
| */ |
| public void setLocale(final Locale locale) { |
| this.locale = locale; |
| setUseLocaleFormat(true); |
| } |
| |
| /** |
| * Sets a date format pattern to use to convert dates to/from a {@link String}. |
| * |
| * @see SimpleDateFormat |
| * @param pattern The format pattern. |
| */ |
| public void setPattern(final String pattern) { |
| setPatterns(new String[] { pattern }); |
| } |
| |
| /** |
| * Sets the date format patterns to use to convert dates to/from a {@link String}. |
| * |
| * @see SimpleDateFormat |
| * @param patterns Array of format patterns. |
| */ |
| public void setPatterns(final String[] patterns) { |
| this.patterns = patterns != null ? patterns.clone() : null; |
| if (this.patterns != null && this.patterns.length > 1) { |
| displayPatterns = String.join(", ", this.patterns); |
| } |
| setUseLocaleFormat(true); |
| } |
| |
| /** |
| * Sets the Time Zone to use when converting dates. |
| * |
| * @param timeZone The Time Zone. |
| */ |
| public void setTimeZone(final TimeZone timeZone) { |
| this.timeZone = timeZone; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * Convert a long value to the specified Date type for this <em>Converter</em>. |
| * <p> |
| * |
| * This method handles conversion to the following types: |
| * <ul> |
| * <li>{@link java.util.Date}</li> |
| * <li>{@link java.util.Calendar}</li> |
| * <li>{@link java.time.LocalDate}</li> |
| * <li>{@link java.time.LocalDateTime}</li> |
| * <li>{@link java.time.ZonedDateTime}</li> |
| * <li>{@link java.sql.Date}</li> |
| * <li>{@link java.sql.Time}</li> |
| * <li>{@link 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.time.Instant |
| if (type.equals(Instant.class)) { |
| return type.cast(Instant.ofEpochMilli(value)); |
| } |
| // 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>{@link java.sql.Date}</li> |
| * <li>{@link java.sql.Time}</li> |
| * <li>{@link java.sql.Timestamp}</li> |
| * <li>{@link java.time.Instant}</li> |
| * </ul> |
| * <p> |
| * <strong>N.B.</strong> No default String conversion mechanism is provided for {@link java.util.Date} and {@link 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"); |
| } |
| } |
| // java.time.Instant |
| if (type.equals(Instant.class)) { |
| try { |
| return type.cast(Instant.parse(value)); |
| } catch (final DateTimeParseException ex) { |
| throw new ConversionException("String must be in ISO-8601 format to create a java.time.Instant"); |
| } |
| } |
| final String msg = toString(getClass()) + " does not support default String to '" + toString(type) + "' conversion."; |
| if (log().isWarnEnabled()) { |
| log().warn(" " + msg); |
| log().warn(" (Re-configure Converter or use alternative implementation)"); |
| } |
| throw new ConversionException(msg); |
| } |
| |
| /** |
| * 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(); |
| } |
| } |