diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverter.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverter.java index 63f2633998fd..40b8ae191bc6 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverter.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverter.java @@ -49,7 +49,7 @@ public LocalDate convertField(final Object field, final Optional pattern return localDate; } case Date date -> { - return date.toLocalDate(); + return ofInstant(Instant.ofEpochMilli(date.getTime())); } case java.util.Date date -> { final Instant instant = date.toInstant(); diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java index 3c84ad8d3a6d..cf8aa118d40d 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java @@ -64,7 +64,7 @@ public LocalDateTime convertField(final Object field, final Optional pat return localDateTime; } case Timestamp timestamp -> { - return timestamp.toLocalDateTime(); + return ofInstant(timestamp.toInstant()); } case Date date -> { // java.sql.Date and java.sql.Time do not support the toInstant() method so using getTime() is required diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverter.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverter.java index 65b9c92d34b2..7e0be549cb10 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverter.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverter.java @@ -23,7 +23,6 @@ import java.sql.Clob; import java.sql.Timestamp; import java.time.Instant; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -58,10 +57,7 @@ public String convertField(final Object field, final Optional pattern, f return Long.toString(timestamp.getTime()); } final DateTimeFormatter formatter = DateTimeFormatterRegistry.getDateTimeFormatter(pattern.get()); - final LocalDateTime localDateTime = timestamp.toLocalDateTime(); - - // Convert LocalDateTime to ZonedDateTime using system default zone to support offsets in Date Time Formatter - final ZonedDateTime dateTime = ZonedDateTime.of(localDateTime, ZoneId.systemDefault()); + final ZonedDateTime dateTime = timestamp.toInstant().atZone(ZoneId.systemDefault()); return formatter.format(dateTime); } case Date date -> { diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverter.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverter.java index aa8f5b48b230..6bd7e013bba3 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverter.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverter.java @@ -20,6 +20,8 @@ import java.sql.Timestamp; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Optional; /** @@ -40,6 +42,11 @@ class ObjectTimestampFieldConverter implements FieldConverter @Override public Timestamp convertField(final Object field, final Optional pattern, final String name) { final LocalDateTime localDateTime = CONVERTER.convertField(field, pattern, name); - return localDateTime == null ? null : Timestamp.valueOf(localDateTime); + if (localDateTime == null) { + return null; + } + + final ZonedDateTime zonedDateTime = localDateTime.atZone(ZoneId.systemDefault()); + return Timestamp.from(zonedDateTime.toInstant()); } } diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java index 8e02b4a1d1e5..4ccbf2fbf511 100644 --- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java +++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/util/DataTypeUtils.java @@ -200,7 +200,12 @@ public static Object convertType( case DATE: final FieldConverter localDateConverter = StandardFieldConverterRegistry.getRegistry().getFieldConverter(LocalDate.class); final LocalDate localDate = localDateConverter.convertField(value, dateFormat, fieldName); - return localDate == null ? null : Date.valueOf(localDate); + if (localDate == null) { + return null; + } + + final ZonedDateTime zonedDate = localDate.atStartOfDay(ZoneId.systemDefault()); + return new Date(zonedDate.toInstant().toEpochMilli()); case DECIMAL: return toBigDecimal(value, fieldName); case DOUBLE: diff --git a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverterTest.java b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverterTest.java new file mode 100644 index 000000000000..03ff0b661ae8 --- /dev/null +++ b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectLocalDateFieldConverterTest.java @@ -0,0 +1,65 @@ +/* + * 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.nifi.serialization.record.field; + +import org.junit.jupiter.api.Test; + +import java.sql.Date; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ObjectLocalDateFieldConverterTest { + + private static final ObjectLocalDateFieldConverter CONVERTER = new ObjectLocalDateFieldConverter(); + private static final String FIELD_NAME = LocalDate.class.getSimpleName(); + + @Test + void testConvertFieldNull() { + assertNull(CONVERTER.convertField(null, Optional.empty(), FIELD_NAME)); + } + + @Test + void testConvertFieldLocalDate() { + final LocalDate input = LocalDate.of(2025, 5, 25); + assertEquals(input, CONVERTER.convertField(input, Optional.empty(), FIELD_NAME)); + } + + @Test + void testConvertFieldSqlDateModernYear() { + final LocalDate localDate = LocalDate.of(2025, 5, 25); + final ZonedDateTime zonedDate = localDate.atStartOfDay(ZoneId.systemDefault()); + final Date sqlDate = new Date(zonedDate.toInstant().toEpochMilli()); + final LocalDate result = CONVERTER.convertField(sqlDate, Optional.empty(), FIELD_NAME); + + assertEquals(localDate, result); + } + + @Test + void testConvertFieldSqlDateYearOneIsProlepticGregorian() { + final LocalDate yearOne = LocalDate.of(1, 1, 1); + final ZonedDateTime zonedDate = yearOne.atStartOfDay(ZoneId.systemDefault()); + final Date sqlDate = new Date(zonedDate.toInstant().toEpochMilli()); + final LocalDate result = CONVERTER.convertField(sqlDate, Optional.empty(), FIELD_NAME); + + assertEquals(yearOne, result); + } +} diff --git a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverterTest.java b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverterTest.java index e73cc490f4ac..bc1732832c3a 100644 --- a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverterTest.java +++ b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectStringFieldConverterTest.java @@ -24,8 +24,11 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.time.zone.ZoneRules; import java.util.Date; +import java.util.Locale; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -158,6 +161,17 @@ void testConvertFieldObjectArrayEmpty() { assertEquals(EMPTY_ARRAY_STRING, string); } + @Test + void testConvertFieldTimestampYearOneIsProlepticGregorian() { + final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0); + final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault()); + final Timestamp timestamp = Timestamp.from(zonedYearOne.toInstant()); + final String formatted = CONVERTER.convertField(timestamp, Optional.of(DEFAULT_PATTERN), FIELD_NAME); + final String expected = DateTimeFormatter.ofPattern(DEFAULT_PATTERN, Locale.ROOT).format(yearOne); + + assertEquals(expected, formatted); + } + private String getDateTimeZoneOffset() { final Timestamp inputTimestamp = Timestamp.valueOf(DATE_TIME_DEFAULT); final LocalDateTime inputLocalDateTime = inputTimestamp.toLocalDateTime(); diff --git a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverterTest.java b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverterTest.java index 2d92724c2bd9..2628aabd3c82 100644 --- a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverterTest.java +++ b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/ObjectTimestampFieldConverterTest.java @@ -162,6 +162,26 @@ public void testConvertFieldStringFormatCustomZoneOffsetCoordinatedUniversalTime assertEquals(expected, timestamp); } + @Test + public void testConvertFieldStringYearOneIsProlepticGregorian() { + final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0); + final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault()); + final Timestamp timestamp = CONVERTER.convertField("0001-01-01 12:00:00", DEFAULT_PATTERN, FIELD_NAME); + final Timestamp expected = Timestamp.from(zonedYearOne.toInstant()); + + assertEquals(expected, timestamp); + } + + @Test + public void testConvertFieldLocalDateTimeYearOneIsProlepticGregorian() { + final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0); + final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault()); + final Timestamp timestamp = CONVERTER.convertField(yearOne, DEFAULT_PATTERN, FIELD_NAME); + final Timestamp expected = Timestamp.from(zonedYearOne.toInstant()); + + assertEquals(expected, timestamp); + } + private Timestamp getDateTimeCoordinatedUniversalTime() { final Timestamp dateTime = Timestamp.valueOf(DATE_TIME_DEFAULT); final LocalDateTime localDateTime = dateTime.toLocalDateTime(); diff --git a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java index 1aeabfc527f0..b923c9d8771e 100644 --- a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java +++ b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java @@ -19,9 +19,11 @@ import org.junit.jupiter.api.Test; +import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Optional; @@ -100,4 +102,21 @@ public void testWithDateFormatMicrosecondPrecision() { final LocalDateTime result = converter.convertField(MICROS_TIMESTAMP_LONG, Optional.of("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"), FIELD_NAME); assertEquals(LOCAL_DATE_TIME_MICROS_PRECISION, result); } + + @Test + public void testConvertTimestampYearOneIsProlepticGregorian() { + final LocalDateTime yearOne = LocalDateTime.of(1, 1, 1, 12, 0, 0); + final ZonedDateTime zonedYearOne = yearOne.atZone(ZoneId.systemDefault()); + final Timestamp timestamp = Timestamp.from(zonedYearOne.toInstant()); + final LocalDateTime result = converter.convertField(timestamp, Optional.empty(), FIELD_NAME); + + assertEquals(yearOne, result); + } + + @Test + public void testConvertStringYearOneIsProlepticGregorian() { + final LocalDateTime result = converter.convertField("0001-01-01 12:00:00", Optional.of("yyyy-MM-dd HH:mm:ss"), FIELD_NAME); + final LocalDateTime expected = LocalDateTime.of(1, 1, 1, 12, 0, 0); + assertEquals(expected, result); + } } diff --git a/nifi-extension-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java b/nifi-extension-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java index eeed128c7a63..4a453cfc6f42 100644 --- a/nifi-extension-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java +++ b/nifi-extension-bundles/nifi-extension-utils/nifi-record-utils/nifi-avro-record-utils/src/main/java/org/apache/nifi/avro/AvroTypeUtil.java @@ -63,6 +63,7 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.sql.Blob; +import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; import java.time.Duration; @@ -1153,11 +1154,16 @@ private static Object normalizeValue(final Object value, final Schema avroSchema final String logicalName = logicalType.getName(); if (LOGICAL_TYPE_DATE.equals(logicalName)) { // date logical name means that the value is number of days since Jan 1, 1970 - // Handle both Integer (legacy) and LocalDate (newer Avro libraries) - if (value instanceof LocalDate localDate) { - return java.sql.Date.valueOf(localDate); + // Handle both Integer (legacy) and LocalDate (newer Avro libraries). + final LocalDate localDate; + if (value instanceof LocalDate ld) { + localDate = ld; + } else { + localDate = LocalDate.ofEpochDay((int) value); } - return java.sql.Date.valueOf(LocalDate.ofEpochDay((int) value)); + + final ZonedDateTime zonedDate = localDate.atStartOfDay(ZoneId.systemDefault()); + return new Date(zonedDate.toInstant().toEpochMilli()); } else if (LOGICAL_TYPE_TIME_MILLIS.equals(logicalName)) { // time-millis logical name means that the value is number of milliseconds since midnight. // Handle both Integer (legacy) and LocalTime (newer Avro libraries)