diff --git a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java index 3e7952df8..a04d8375f 100644 --- a/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java +++ b/src/main/java/org/apache/commons/beanutils2/converters/DateTimeConverter.java @@ -22,10 +22,16 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; +import java.time.format.ResolverStyle; +import java.time.format.SignStyle; +import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.util.Calendar; import java.util.Date; @@ -85,6 +91,23 @@ */ public abstract class DateTimeConverter extends AbstractConverter { + /** Strict validator for the JDBC {@code java.sql.Date} escape format, rejecting out-of-range fields that {@code valueOf} would roll over. */ + private static final DateTimeFormatter SQL_DATE_FORMAT = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-').appendValue(ChronoField.MONTH_OF_YEAR).appendLiteral('-').appendValue(ChronoField.DAY_OF_MONTH).toFormatter() + .withResolverStyle(ResolverStyle.STRICT); + + /** Strict validator for the JDBC {@code java.sql.Time} escape format, rejecting out-of-range fields that {@code valueOf} would roll over. */ + private static final DateTimeFormatter SQL_TIME_FORMAT = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY).appendLiteral(':') + .appendValue(ChronoField.MINUTE_OF_HOUR).appendLiteral(':').appendValue(ChronoField.SECOND_OF_MINUTE).toFormatter() + .withResolverStyle(ResolverStyle.STRICT); + + /** Strict validator for the JDBC {@code java.sql.Timestamp} escape format, rejecting out-of-range fields that {@code valueOf} would roll over. */ + private static final DateTimeFormatter SQL_TIMESTAMP_FORMAT = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-').appendValue(ChronoField.MONTH_OF_YEAR).appendLiteral('-').appendValue(ChronoField.DAY_OF_MONTH).appendLiteral(' ') + .appendValue(ChronoField.HOUR_OF_DAY).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR).appendLiteral(':') + .appendValue(ChronoField.SECOND_OF_MINUTE).optionalStart().appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true).optionalEnd().toFormatter() + .withResolverStyle(ResolverStyle.STRICT); + private String[] patterns; private String displayPatterns; private Locale locale; @@ -579,6 +602,9 @@ private T toDate(final Class type, final long value) { *
  • {@link java.time.Instant}
  • * *

    + * For the {@code java.sql} types the String must be in the JDBC escape format and validation is strict: out-of-range fields (for example + * {@code 2006-02-31} or {@code 25:70:90}) are rejected with a {@link ConversionException} instead of being rolled over. + *

    * N.B. No default String conversion mechanism is provided for {@link java.util.Date} and {@link java.util.Calendar} type. * * @param The target type @@ -590,25 +616,31 @@ private T toDate(final Class type, final String value) { // java.sql.Date if (type.equals(java.sql.Date.class)) { try { + LocalDate.parse(value, SQL_DATE_FORMAT); 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"); + } catch (final IllegalArgumentException | DateTimeParseException e) { + throw new ConversionException( + "String must be in JDBC format [yyyy-MM-dd] to create a java.sql.Date; validation is strict, out-of-range fields are rejected"); } } // java.sql.Time if (type.equals(java.sql.Time.class)) { try { + LocalTime.parse(value, SQL_TIME_FORMAT); 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"); + } catch (final IllegalArgumentException | DateTimeParseException e) { + throw new ConversionException( + "String must be in JDBC format [HH:mm:ss] to create a java.sql.Time; validation is strict, out-of-range fields are rejected"); } } // java.sql.Timestamp if (type.equals(java.sql.Timestamp.class)) { try { + LocalDateTime.parse(value, SQL_TIMESTAMP_FORMAT); 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"); + } catch (final IllegalArgumentException | DateTimeParseException e) { + throw new ConversionException("String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] to create a java.sql.Timestamp; " + + "validation is strict, out-of-range fields are rejected"); } } // java.time.Instant diff --git a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java index 18cbe3eb5..b65905f90 100644 --- a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlDateConverterTest.java @@ -80,6 +80,10 @@ public void testDefaultStringToTypeConvert() { // Invalid String --> java.sql.Date Conversion invalidConversion(converter, "01/01/2006"); + + // Out-of-range fields must be rejected, not silently rolled over (2006-02-31 -> 2006-03-03) + invalidConversion(converter, "2006-02-31"); + invalidConversion(converter, "2006-13-01"); } /** diff --git a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java index 918290c73..010b1f0ad 100644 --- a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimeConverterTest.java @@ -77,6 +77,10 @@ public void testDefaultStringToTypeConvert() { // Invalid String --> java.sql.Time Conversion invalidConversion(converter, "15:36"); + + // Out-of-range fields must be rejected, not silently rolled over (25:70:90 -> 02:11:30) + invalidConversion(converter, "25:70:90"); + invalidConversion(converter, "-1:-1:-1"); } /** diff --git a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java index 902735e42..7829c9386 100644 --- a/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java +++ b/src/test/java/org/apache/commons/beanutils2/sql/converters/SqlTimestampConverterTest.java @@ -88,6 +88,10 @@ public void testDefaultStringToTypeConvert() { invalidConversion(converter, "2006/09/21 15:36:01.0"); invalidConversion(converter, "2006-10-22"); invalidConversion(converter, "15:36:01"); + + // Out-of-range fields must be rejected, not silently rolled over (2006-02-31 25:70:90 -> 2006-03-04 02:11:30) + invalidConversion(converter, "2006-02-31 15:36:01.0"); + invalidConversion(converter, "2006-10-23 25:70:90.0"); } /**