diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/LocalTimeValueType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/LocalTimeValueType.java index beb3df8cc4..b86da2fd0b 100644 --- a/cayenne-server/src/main/java/org/apache/cayenne/access/types/LocalTimeValueType.java +++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/LocalTimeValueType.java @@ -20,7 +20,13 @@ package org.apache.cayenne.access.types; import java.sql.Time; +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; /** * @since 4.0 @@ -39,12 +45,17 @@ public Class getValueType() { @Override public LocalTime toJavaObject(Time value) { - return value.toLocalTime(); + Instant instant = Instant.ofEpochMilli(value.getTime()); + OffsetDateTime offsetDateTime = instant.atOffset(ZoneId.systemDefault().getRules().getOffset(instant)); + return LocalTime.from(offsetDateTime); } @Override public Time fromJavaObject(LocalTime object) { - return Time.valueOf(object); + LocalDateTime epochDateTime = object.atDate(LocalDate.ofEpochDay(0)); + ZonedDateTime zonedDateTime = epochDateTime.atZone(ZoneId.systemDefault()); + long epochMillis = zonedDateTime.toInstant().toEpochMilli(); + return new Time(epochMillis); } @Override diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/types/LocalTimeValueTypeTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/types/LocalTimeValueTypeTest.java new file mode 100644 index 0000000000..d3071afc9b --- /dev/null +++ b/cayenne-server/src/test/java/org/apache/cayenne/access/types/LocalTimeValueTypeTest.java @@ -0,0 +1,137 @@ +/***************************************************************** + * 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 + * + * https://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.cayenne.access.types; + +import org.junit.Test; + +import java.sql.Time; +import java.time.Instant; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.temporal.ChronoField; +import java.util.Arrays; +import java.util.List; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +public class LocalTimeValueTypeTest { + + private static final LocalTimeValueType valueType = new LocalTimeValueType(); + + @Test + public void testToJavaObject() { + // UTC 17:50:23.123 + long utcMillis = 64223123; + long systemMillis = Instant.ofEpochMilli(utcMillis) + .atZone(ZoneId.systemDefault()) + .get(ChronoField.MILLI_OF_DAY); + + Time time = new Time(utcMillis); + LocalTime localTime = valueType.toJavaObject(time); + + assertEquals(systemMillis, localTime.get(ChronoField.MILLI_OF_DAY)); + } + + @Test + public void testFromJavaObject() { + // UTC 17:50:23.123 + long utcMillis = 64223123; + long systemMillis = Instant.ofEpochMilli(utcMillis) + .atZone(ZoneId.systemDefault()) + .getLong(ChronoField.MILLI_OF_DAY); + + LocalTime localTime = LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(systemMillis)); + Time time = valueType.fromJavaObject(localTime); + + assertEquals(utcMillis, time.getTime()); + } + + @Test + public void testToJavaObjectFromJavaObject() { + // UTC 17:50:23.123 + long utcMillis = 64223123; + + Time time = new Time(utcMillis); + LocalTime localTime = valueType.toJavaObject(time); + Time newTime = valueType.fromJavaObject(localTime); + + assertEquals(time, newTime); + } + + @Test + public void testToJavaObject_isBackwardCompatible() { + // UTC 17:50:23.123 + long utcMillis = 64223123; + + Time time = new Time(utcMillis); + LocalTime impreciseLocalTime = time.toLocalTime(); + LocalTime localTime = valueType.toJavaObject(time); + + assertEquals(impreciseLocalTime.toSecondOfDay(), localTime.get(ChronoField.SECOND_OF_DAY)); + } + + @Test + public void testFromJavaObject_isBackwardCompatible() { + // UTC 17:50:23.123 + long utcMillis = 64223123; + long systemMillis = Instant.ofEpochMilli(utcMillis) + .atZone(ZoneId.systemDefault()) + .getLong(ChronoField.MILLI_OF_DAY); + + LocalTime localTime = LocalTime.ofNanoOfDay(TimeUnit.MILLISECONDS.toNanos(systemMillis)); + Time impreciseTime = Time.valueOf(localTime); + Time time = valueType.fromJavaObject(localTime); + + assertEquals(TimeUnit.MILLISECONDS.toSeconds(impreciseTime.getTime()), + TimeUnit.MILLISECONDS.toSeconds(time.getTime())); + } + + @Test + public void testToJavaObjectFromJavaObject_changeTimeZone() { + TimeZone originalTimeZone = TimeZone.getDefault(); + + try { + // UTC 17:50:23.123 + long utcMillis = 64223123; + + Time time = new Time(utcMillis); + LocalTime localTime = valueType.toJavaObject(time); + TimeZone.setDefault(getOtherTimeZone(originalTimeZone)); + Time newTime = valueType.fromJavaObject(localTime); + + assertNotEquals(time, newTime); + } finally { + TimeZone.setDefault(originalTimeZone); + } + } + + private static TimeZone getOtherTimeZone(TimeZone timeZone) { + List timeZones = Arrays.stream(TimeZone.getAvailableIDs()) + .map(TimeZone::getTimeZone) + .distinct() + .filter(tz -> tz.getRawOffset() != timeZone.getRawOffset()) + .collect(Collectors.toList()); + return timeZones.get(0); + } +}