diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/DateTimeAdapter.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/DateTimeAdapter.java index 9ba1ff76033d..ec1a72cfc78c 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/DateTimeAdapter.java +++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/util/DateTimeAdapter.java @@ -18,6 +18,7 @@ import jakarta.xml.bind.annotation.adapters.XmlAdapter; +import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -39,7 +40,7 @@ public String marshal(Date date) throws Exception { @Override public Date unmarshal(String date) throws Exception { - final ZonedDateTime zonedDateTime = ZonedDateTime.parse(date, DATE_TIME_FORMATTER); + final ZonedDateTime zonedDateTime = LocalDateTime.parse(date, DATE_TIME_FORMATTER).atZone(ZoneId.systemDefault()); return Date.from(zonedDateTime.toInstant()); } diff --git a/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/test/java/org/apache/nifi/web/api/dto/util/TestDateTimeAdapter.java b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/test/java/org/apache/nifi/web/api/dto/util/TestDateTimeAdapter.java new file mode 100644 index 000000000000..4682f954a99e --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/test/java/org/apache/nifi/web/api/dto/util/TestDateTimeAdapter.java @@ -0,0 +1,105 @@ +/* + * 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.web.api.dto.util; + +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Date; +import java.util.TimeZone; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestDateTimeAdapter { + + private static final DateTimeAdapter DEFAULT_TIME_ZONE_ADAPTER = new DateTimeAdapter(); + // Month, day, hour, minute, and second values are distinct for verification. + private static final long TEST_DATE_TIME_MILLISECONDS = 1767323096000L; // Represents 2026-01-02T03:04:56Z + private static final Date TEST_DATE = new Date(TEST_DATE_TIME_MILLISECONDS); + private static final DateTimeFormatter TEST_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss z") + .withZone(ZoneId.systemDefault()); + private static final String TEST_DATE_TIME = TEST_DATE_TIME_FORMATTER.format(TEST_DATE.toInstant()); + + // Verify round-trip consistency for both marshal-then-unmarshal and unmarshal-then-marshal operations. + @Test + public void testMarshalThenUnmarshal() throws Exception { + assertEquals(TEST_DATE, DEFAULT_TIME_ZONE_ADAPTER.unmarshal(DEFAULT_TIME_ZONE_ADAPTER.marshal(TEST_DATE))); + } + + @Test + public void testUnmarshalThenMarshal() throws Exception { + assertEquals(TEST_DATE_TIME, DEFAULT_TIME_ZONE_ADAPTER.marshal(DEFAULT_TIME_ZONE_ADAPTER.unmarshal(TEST_DATE_TIME))); + } + + // Verify behavior after switching the JVM default time zone before invoking the adapter. + @Test + public void testMarshalAcrossTimeZones() throws Exception { + assertAll( + () -> assertMarshalInTimeZone("UTC", "01/02/2026 03:04:56 UTC"), + () -> assertMarshalInTimeZone("Asia/Shanghai", "01/02/2026 11:04:56 CST"), + () -> assertMarshalInTimeZone("America/Chicago", "01/01/2026 21:04:56 CST"), + () -> assertMarshalInTimeZone("Asia/Kolkata", "01/02/2026 08:34:56 IST"), + () -> assertMarshalInTimeZone("America/St_Johns", "01/01/2026 23:34:56 NST"), + () -> assertMarshalInTimeZone("America/New_York", "01/01/2026 22:04:56 EST"), + () -> assertMarshalInTimeZone("Asia/Tokyo", "01/02/2026 12:04:56 JST"), + () -> assertMarshalInTimeZone("Australia/Adelaide", "01/02/2026 13:34:56 ACDT"), + () -> assertMarshalInTimeZone("Pacific/Auckland", "01/02/2026 16:04:56 NZDT"), + () -> assertMarshalInTimeZone("Europe/Berlin", "01/02/2026 04:04:56 CET") + ); + } + + @Test + public void testUnmarshalAcrossTimeZones() throws Exception { + assertAll( + () -> assertUnmarshalInTimeZone("UTC", "01/02/2026 03:04:56 UTC"), + () -> assertUnmarshalInTimeZone("Asia/Shanghai", "01/02/2026 11:04:56 CST"), + () -> assertUnmarshalInTimeZone("America/Chicago", "01/01/2026 21:04:56 CST"), + () -> assertUnmarshalInTimeZone("Asia/Kolkata", "01/02/2026 08:34:56 IST"), + () -> assertUnmarshalInTimeZone("America/St_Johns", "01/01/2026 23:34:56 NST"), + () -> assertUnmarshalInTimeZone("America/New_York", "01/01/2026 22:04:56 EST"), + () -> assertUnmarshalInTimeZone("Asia/Tokyo", "01/02/2026 12:04:56 JST"), + () -> assertUnmarshalInTimeZone("Australia/Adelaide", "01/02/2026 13:34:56 ACDT"), + () -> assertUnmarshalInTimeZone("Pacific/Auckland", "01/02/2026 16:04:56 NZDT"), + () -> assertUnmarshalInTimeZone("Europe/Berlin", "01/02/2026 04:04:56 CET") + ); + } + + private static void assertMarshalInTimeZone(final String timeZoneId, final String expectedDateTime) throws Exception { + final TimeZone originalTimeZone = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId)); + final DateTimeAdapter dateTimeAdapter = new DateTimeAdapter(); + assertEquals(expectedDateTime, dateTimeAdapter.marshal(TEST_DATE)); + } finally { + TimeZone.setDefault(originalTimeZone); + } + } + + private static void assertUnmarshalInTimeZone(final String timeZoneId, final String dateTime) throws Exception { + final TimeZone originalTimeZone = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId)); + final DateTimeAdapter dateTimeAdapter = new DateTimeAdapter(); + assertEquals(TEST_DATE, dateTimeAdapter.unmarshal(dateTime)); + } finally { + TimeZone.setDefault(originalTimeZone); + } + } + +}