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
*