diff --git a/pom.xml b/pom.xml index b45bb43b67f..52c9dccee24 100644 --- a/pom.xml +++ b/pom.xml @@ -452,8 +452,9 @@ + - java9+ + java-9-up [9,) @@ -462,6 +463,32 @@ -Xmx512m --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.time.chrono=ALL-UNNAMED + + + + + org.moditect + moditect-maven-plugin + + + add-module-infos + + + + + static java.sql; + *; + + + + + + + + + java15 diff --git a/src/main/java/org/apache/commons/lang3/time/DateUtils.java b/src/main/java/org/apache/commons/lang3/time/DateUtils.java index 7ba125706ee..5fcf9cde2ab 100644 --- a/src/main/java/org/apache/commons/lang3/time/DateUtils.java +++ b/src/main/java/org/apache/commons/lang3/time/DateUtils.java @@ -16,8 +16,12 @@ */ package org.apache.commons.lang3.time; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.text.ParseException; import java.text.ParsePosition; +import java.time.Instant; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.Date; import java.util.Iterator; @@ -202,6 +206,28 @@ private enum ModifyType { * A month range, the week starting on Monday. */ public static final int RANGE_MONTH_MONDAY = 6; + /** + * The {@link Class} for {@link java.sql.Timestamp}. + */ + private static final Class timestampClass; + /** + * The {@link Method} for {@link java.sql.Timestamp#getNanos()}. + */ + private static final Method timestampGetNanosMethod; + + static { + Class clazz; + Method method; + try { + clazz = Class.forName("java.sql.Timestamp"); + method = clazz.getMethod("getNanos"); + } catch (ClassNotFoundException | NoSuchMethodException ex) { + clazz = null; + method = null; + } + timestampClass = clazz; + timestampGetNanosMethod = method; + } /** * Adds to a date returning a new object. @@ -1625,6 +1651,61 @@ public static Calendar toCalendar(final Date date, final TimeZone tz) { return c; } + /** + * Converts a {@link Date} into a {@link LocalDateTime}, using the default time zone. + * @param date the date to convert to a LocalDateTime + * @return the created LocalDateTime + * @throws NullPointerException if {@code date} is null + * @since 3.18 + */ + public static LocalDateTime toLocalDateTime(final Date date) { + return toLocalDateTime(date, TimeZone.getDefault()); + } + + /** + * Converts a {@link Date} into a {@link LocalDateTime} + * @param date the date to convert to a LocalDateTime + * @param tz the time zone of the {@code date} + * @return the created LocalDateTime + * @throws NullPointerException if {@code date} is null + * @since 3.18 + */ + public static LocalDateTime toLocalDateTime(final Date date, final TimeZone tz) { + Objects.requireNonNull(date, "date"); + Objects.requireNonNull(tz, "tz"); + final LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(date.getTime()), tz.toZoneId()); + if (isTimestamp(date)) { + return localDateTime.withNano(extractNanosFromSqlTimestamp(date)); + } + return localDateTime; + } + + /** + * Get the nanosecond part in the {@link java.sql.Timestamp} object. without requiring the java.sql module. + * + * @param date The date is a {@link java.sql.Timestamp} object. + * If it is not a {@link java.sql.Timestamp} object, + * it will return 0. + * @return The nanosecond part of the {@link java.sql.Timestamp} object. + */ + private static int extractNanosFromSqlTimestamp(Date date) { + if (timestampClass == null) { + return 0; + } + try { + return (int) timestampGetNanosMethod.invoke(date); + } catch (IllegalAccessException | InvocationTargetException e) { + return 0; + } + } + + /** + * Check to see if obj is an instance of {@link java.sql.Timestamp} without requiring the java.sql module. + */ + private static boolean isTimestamp(final Date date) { + return timestampClass != null && timestampClass.isAssignableFrom(date.getClass()); + } + /** * Truncates a date, leaving the field specified as the most * significant field. diff --git a/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java b/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java index d6fba80f3cf..129631a3453 100644 --- a/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java @@ -28,6 +28,8 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; @@ -35,12 +37,16 @@ import java.util.Locale; import java.util.NoSuchElementException; import java.util.TimeZone; +import java.util.stream.Stream; import org.apache.commons.lang3.AbstractLangTest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junitpioneer.jupiter.DefaultLocale; import org.junitpioneer.jupiter.ReadsDefaultLocale; import org.junitpioneer.jupiter.WritesDefaultLocale; @@ -1285,6 +1291,107 @@ public void testToCalendarWithTimeZoneNull() { assertThrows(NullPointerException.class, () -> DateUtils.toCalendar(date1, null)); } + private static Stream dateConversionProvider() { + return Stream.of( + Arguments.of( + java.sql.Date.valueOf("2000-01-01"), + LocalDateTime.of(2000, 1, 1, 0, 0, 0) + ), + Arguments.of( + java.sql.Date.valueOf("1970-01-01"), + LocalDateTime.of(1970, 1, 1, 0, 0, 0) + ), + Arguments.of( + java.sql.Time.valueOf("12:30:45"), + LocalDateTime.of(1970, 1, 1, 12, 30, 45) + ), + Arguments.of( + java.sql.Time.valueOf("23:59:59"), + LocalDateTime.of(1970, 1, 1, 23, 59, 59) + ), + Arguments.of( + java.sql.Timestamp.valueOf("2000-01-01 12:30:45.123456789"), + LocalDateTime.of(2000, 1, 1, 12, 30, 45, 123_456_789) + ), + Arguments.of( + java.sql.Timestamp.valueOf("2000-01-01 12:30:45.987654321"), + LocalDateTime.of(2000, 1, 1, 12, 30, 45, 987_654_321) + ) + ); + } + + private static Stream dateWithTimeZoneProvider() { + return Stream.of( + Arguments.of( + java.sql.Timestamp.valueOf("2000-01-01 12:30:45"), + TimeZone.getTimeZone("America/New_York"), + LocalDateTime.ofInstant( + java.sql.Timestamp.valueOf("2000-01-01 12:30:45").toInstant(), + TimeZone.getTimeZone("America/New_York").toZoneId() + ) + ), + Arguments.of( + java.sql.Timestamp.valueOf("2023-03-12 02:30:00"), + TimeZone.getTimeZone("America/New_York"), + LocalDateTime.ofInstant( + java.sql.Timestamp.valueOf("2023-03-12 02:30:00").toInstant(), + TimeZone.getTimeZone("America/New_York").toZoneId() + ) + ), + Arguments.of( + Date.from(LocalDateTime.of(2023, 1, 1, 0, 0) + .atOffset(ZoneOffset.UTC) + .toInstant()), + TimeZone.getTimeZone("America/New_York"), + LocalDateTime.of(2022, 12, 31, 19, 0) + ), + Arguments.of( + Date.from(LocalDateTime.of(2023, 3, 12, 7, 0) + .atOffset(ZoneOffset.UTC) + .toInstant()), + TimeZone.getTimeZone("America/New_York"), + LocalDateTime.of(2023, 3, 12, 3, 0) + ), + Arguments.of( + Date.from(LocalDateTime.of(2023, 1, 1, 0, 0) + .atOffset(ZoneOffset.UTC) + .toInstant()), + TimeZone.getTimeZone("Pacific/Kiritimati"), + LocalDateTime.of(2023, 1, 1, 14, 0) + ) + ); + } + + @ParameterizedTest + @MethodSource("dateConversionProvider") + void testToLocalDateTimeWithDate(final Date sqlDate, final LocalDateTime expected) { + final LocalDateTime result = DateUtils.toLocalDateTime(sqlDate); + assertNotNull(result); + assertEquals(expected, result); + } + + @ParameterizedTest + @MethodSource("dateWithTimeZoneProvider") + void testToLocalDateTimeWithDate( + final Date date, + final TimeZone timeZone, + final LocalDateTime expected) { + final LocalDateTime result; + if (timeZone != null) { + result = DateUtils.toLocalDateTime(date, timeZone); + } else { + result = DateUtils.toLocalDateTime(date); + } + assertEquals(expected, result); + } + + @Test + void shouldThrowNullPointerExceptionWhenDateIsNull() { + assertThrows(NullPointerException.class, () -> DateUtils.toLocalDateTime(null)); + assertThrows(NullPointerException.class, () -> DateUtils.toLocalDateTime(null, TimeZone.getDefault())); + assertThrows(NullPointerException.class, () -> DateUtils.toLocalDateTime(new Date(), null)); + } + /** * Tests various values with the trunc method *