From c6066973bdced8c88ee52911a11d635904bb2329 Mon Sep 17 00:00:00 2001 From: Pasqual Koschmieder Date: Tue, 8 Aug 2023 13:13:00 +0200 Subject: [PATCH] feat: add object serializers for all kinds of time types --- .../defaults/object/DefaultObjectMapper.java | 27 ++ .../serializers/TimeObjectSerializer.java | 268 ++++++++++++++++++ .../rpc/object/DefaultObjectMapperTest.java | 118 ++++++++ 3 files changed, 413 insertions(+) create mode 100644 driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/serializers/TimeObjectSerializer.java diff --git a/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/DefaultObjectMapper.java b/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/DefaultObjectMapper.java index d05aa2d164..e2a31477ce 100644 --- a/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/DefaultObjectMapper.java +++ b/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/DefaultObjectMapper.java @@ -37,6 +37,7 @@ import eu.cloudnetservice.driver.network.rpc.defaults.object.serializers.OptionalObjectSerializer; import eu.cloudnetservice.driver.network.rpc.defaults.object.serializers.PathObjectSerializer; import eu.cloudnetservice.driver.network.rpc.defaults.object.serializers.PatternObjectSerializer; +import eu.cloudnetservice.driver.network.rpc.defaults.object.serializers.TimeObjectSerializer; import eu.cloudnetservice.driver.network.rpc.defaults.object.serializers.UUIDObjectSerializer; import eu.cloudnetservice.driver.network.rpc.exception.MissingObjectSerializerException; import eu.cloudnetservice.driver.network.rpc.object.ObjectMapper; @@ -45,6 +46,18 @@ import java.lang.reflect.Type; import java.nio.file.Path; import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -132,6 +145,20 @@ public class DefaultObjectMapper implements ObjectMapper { .put(NavigableMap.class, MapObjectSerializer.of(TreeMap::new)) .put(ConcurrentNavigableMap.class, MapObjectSerializer.of(ConcurrentSkipListMap::new)) // ==== object data class types ==== + // java.time classes + .put(Year.class, TimeObjectSerializer.YEAR_SERIALIZER) + .put(Period.class, TimeObjectSerializer.PERIOD_SERIALIZER) + .put(ZoneId.class, TimeObjectSerializer.ZONE_ID_SERIALIZER) + .put(Instant.class, TimeObjectSerializer.INSTANT_SERIALIZER) + .put(Duration.class, TimeObjectSerializer.DURATION_SERIALIZER) + .put(MonthDay.class, TimeObjectSerializer.MONTH_DAY_SERIALIZER) + .put(LocalDate.class, TimeObjectSerializer.LOCAL_DATE_SERIALIZER) + .put(LocalTime.class, TimeObjectSerializer.LOCAL_TIME_SERIALIZER) + .put(YearMonth.class, TimeObjectSerializer.YEAR_MONTH_SERIALIZER) + .put(OffsetTime.class, TimeObjectSerializer.OFFSET_TIME_SERIALIZER) + .put(LocalDateTime.class, TimeObjectSerializer.LOCAL_DATE_TIME_SERIALIZER) + .put(ZonedDateTime.class, TimeObjectSerializer.ZONED_DATE_TIME_SERIALIZER) + .put(OffsetDateTime.class, TimeObjectSerializer.OFFSET_DATE_TIME_SERIALIZER) // data classes .put(Path.class, new PathObjectSerializer()) .put(DataBuf.class, new DataBufObjectSerializer()) diff --git a/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/serializers/TimeObjectSerializer.java b/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/serializers/TimeObjectSerializer.java new file mode 100644 index 0000000000..2089b14f09 --- /dev/null +++ b/driver/src/main/java/eu/cloudnetservice/driver/network/rpc/defaults/object/serializers/TimeObjectSerializer.java @@ -0,0 +1,268 @@ +/* + * Copyright 2019-2023 CloudNetService team & contributors + * + * Licensed 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 eu.cloudnetservice.driver.network.rpc.defaults.object.serializers; + +import eu.cloudnetservice.driver.network.rpc.object.ObjectSerializer; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +/** + * Collection of object serializers for all kinds of java.time classes. Calendar systems other than ISO-8601 are not + * supported (such as the Japanese calendar). + * + * @since 4.0 + */ +public final class TimeObjectSerializer { + + /** + * Serializer for a year. + * + * @see Year + */ + public static final ObjectSerializer YEAR_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> Year.of(dataBuf.readInt()), + (dataBuf, year) -> dataBuf.writeInt(year.getValue()) + ); + + /** + * Serializer for a year and month mapping. + * + * @see YearMonth + */ + public static final ObjectSerializer YEAR_MONTH_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> YearMonth.of(dataBuf.readInt(), dataBuf.readByte()), + (dataBuf, yearMonth) -> { + dataBuf.writeInt(yearMonth.getYear()); + dataBuf.writeByte((byte) yearMonth.getMonthValue()); + } + ); + + /** + * Serializer for a day in a month. + * + * @see MonthDay + */ + public static final ObjectSerializer MONTH_DAY_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> MonthDay.of(dataBuf.readByte(), dataBuf.readByte()), + (dataBuf, monthDay) -> { + dataBuf.writeByte((byte) monthDay.getMonthValue()); + dataBuf.writeByte((byte) monthDay.getDayOfMonth()); + } + ); + + /** + * A serializer for a standard zone region or any zone offset. Custom implementations are not supported. + * + * @see ZoneId + */ + public static final ObjectSerializer ZONE_ID_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> ZoneId.of(dataBuf.readString()), + (dataBuf, zoneId) -> dataBuf.writeString(zoneId.getId()) + ); + + /** + * A serializer for a duration. + * + * @see Duration + */ + public static final ObjectSerializer DURATION_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> Duration.ofSeconds(dataBuf.readLong(), dataBuf.readInt()), + (dataBuf, duration) -> { + dataBuf.writeLong(duration.getSeconds()); + dataBuf.writeInt(duration.getNano()); + } + ); + + /** + * A serializer for a period. + * + * @see Period + */ + public static final ObjectSerializer PERIOD_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> Period.of(dataBuf.readInt(), dataBuf.readInt(), dataBuf.readInt()), + (dataBuf, period) -> { + dataBuf.writeInt(period.getYears()); + dataBuf.writeInt(period.getMonths()); + dataBuf.writeInt(period.getDays()); + } + ); + + /** + * A serializer for an instant. + * + * @see Instant + */ + public static final ObjectSerializer INSTANT_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> Instant.ofEpochSecond(dataBuf.readLong(), dataBuf.readInt()), + (dataBuf, instant) -> { + dataBuf.writeLong(instant.getEpochSecond()); + dataBuf.writeInt(instant.getNano()); + } + ); + + /** + * A serializer for a local date. + * + * @see LocalDate + */ + public static final ObjectSerializer LOCAL_DATE_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> LocalDate.of(dataBuf.readInt(), dataBuf.readByte(), dataBuf.readByte()), + (dataBuf, localDate) -> { + dataBuf.writeInt(localDate.getYear()); + dataBuf.writeByte((byte) localDate.getMonthValue()); + dataBuf.writeByte((byte) localDate.getDayOfMonth()); + } + ); + + /** + * A serializer for a local time. + * + * @see LocalTime + */ + public static final ObjectSerializer LOCAL_TIME_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> { + var hour = dataBuf.readByte(); + if (hour < 0) { + return LocalTime.of(~hour, 0); + } + + var minute = dataBuf.readByte(); + if (minute < 0) { + return LocalTime.of(hour, ~minute); + } + + var second = dataBuf.readByte(); + if (second < 0) { + return LocalTime.of(hour, minute, ~second); + } + + var nano = dataBuf.readInt(); + return LocalTime.of(hour, minute, second, nano); + }, + (dataBuf, localTime) -> { + var nano = localTime.getNano(); + var second = localTime.getSecond(); + var minute = localTime.getMinute(); + var hour = localTime.getHour(); + + if (nano == 0) { + if (second == 0) { + if (minute == 0) { + dataBuf.writeByte((byte) ~hour); + } else { + dataBuf.writeByte((byte) hour); + dataBuf.writeByte((byte) ~minute); + } + } else { + dataBuf.writeByte((byte) hour); + dataBuf.writeByte((byte) minute); + dataBuf.writeByte((byte) ~second); + } + } else { + dataBuf.writeByte((byte) hour); + dataBuf.writeByte((byte) minute); + dataBuf.writeByte((byte) second); + dataBuf.writeInt(nano); + } + } + ); + + /** + * A serializer for a local date and a local time mapping. + * + * @see LocalDateTime + */ + public static final ObjectSerializer LOCAL_DATE_TIME_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> { + var date = dataBuf.readObject(LocalDate.class); + var time = dataBuf.readObject(LocalTime.class); + return LocalDateTime.of(date, time); + }, + (dataBuf, localDateTime) -> { + dataBuf.writeObject(localDateTime.toLocalDate()); + dataBuf.writeObject(localDateTime.toLocalTime()); + } + ); + + /** + * A serializer for a date and time mapping in a specific time zone. Only the standard zone id implementations (zone + * offset and zone region) are supported. + * + * @see ZonedDateTime + */ + public static final ObjectSerializer ZONED_DATE_TIME_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> { + var zoneId = dataBuf.readObject(ZoneId.class); + var dateTime = dataBuf.readObject(LocalDateTime.class); + return ZonedDateTime.of(dateTime, zoneId); + }, + (dataBuf, zonedDateTime) -> { + dataBuf.writeObject(zonedDateTime.getZone()); + dataBuf.writeObject(zonedDateTime.toLocalDateTime()); + } + ); + + /** + * A serializer for a time with a zone offset. + * + * @see OffsetTime + */ + public static final ObjectSerializer OFFSET_TIME_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> { + var time = dataBuf.readObject(LocalTime.class); + var offset = ZoneOffset.ofTotalSeconds(dataBuf.readInt()); + return OffsetTime.of(time, offset); + }, + (dataBuf, offsetTime) -> { + dataBuf.writeObject(offsetTime.toLocalTime()); + dataBuf.writeInt(offsetTime.getOffset().getTotalSeconds()); + } + ); + + /** + * A serializer for a time and date mapping with a zone offset. + * + * @see OffsetDateTime + */ + public static final ObjectSerializer OFFSET_DATE_TIME_SERIALIZER = FunctionalObjectSerializer.of( + dataBuf -> { + var dateTime = dataBuf.readObject(LocalDateTime.class); + var offset = ZoneOffset.ofTotalSeconds(dataBuf.readInt()); + return OffsetDateTime.of(dateTime, offset); + }, + (dataBuf, offsetTime) -> { + dataBuf.writeObject(offsetTime.toLocalDateTime()); + dataBuf.writeInt(offsetTime.getOffset().getTotalSeconds()); + } + ); + + private TimeObjectSerializer() { + throw new UnsupportedOperationException(); + } +} diff --git a/driver/src/test/java/eu/cloudnetservice/driver/network/rpc/object/DefaultObjectMapperTest.java b/driver/src/test/java/eu/cloudnetservice/driver/network/rpc/object/DefaultObjectMapperTest.java index 8e74cd3f91..fc8ba3cd20 100644 --- a/driver/src/test/java/eu/cloudnetservice/driver/network/rpc/object/DefaultObjectMapperTest.java +++ b/driver/src/test/java/eu/cloudnetservice/driver/network/rpc/object/DefaultObjectMapperTest.java @@ -32,6 +32,20 @@ import eu.cloudnetservice.driver.service.ThreadSnapshot; import io.leangen.geantyref.TypeFactory; import java.lang.reflect.Type; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -144,6 +158,96 @@ static Stream dataClassProvider() { ); } + static Stream timeClassProvider() { + return Stream.of( + // Year + Arguments.of(Year.MIN_VALUE), + Arguments.of(Year.MAX_VALUE), + Arguments.of(Year.of(1902)), + Arguments.of(Year.of(-57889)), + Arguments.of(Year.now()), + // Period + Arguments.of(Period.ZERO), + Arguments.of(Period.ofDays(15)), + Arguments.of(Period.ofWeeks(178)), + Arguments.of(Period.ofYears(199943)), + Arguments.of(Period.of(12234, 9990, 22345)), + // ZoneId + Arguments.of(ZoneOffset.UTC), + Arguments.of(ZoneOffset.MIN), + Arguments.of(ZoneOffset.MAX), + Arguments.of(ZoneOffset.ofHours(5)), + Arguments.of(ZoneOffset.ofHours(-3)), + Arguments.of(ZoneId.of("Europe/Berlin")), + Arguments.of(ZoneId.of("America/New_York")), + Arguments.of(ZoneId.of("GMT+5")), + Arguments.of(ZoneId.of("UTC-2")), + // Instant + Arguments.of(Instant.MIN), + Arguments.of(Instant.MAX), + Arguments.of(Instant.EPOCH), + Arguments.of(Instant.now()), + Arguments.of(Instant.now().minusSeconds(500)), + // Duration + Arguments.of(Duration.ZERO), + Arguments.of(Duration.ofDays(5)), + Arguments.of(Duration.ofMinutes(50000)), + Arguments.of(Duration.ofHours(999999999)), + Arguments.of(Duration.ofSeconds(120000)), + // MonthDay + Arguments.of(MonthDay.of(12, 5)), + Arguments.of(MonthDay.of(9, 18)), + Arguments.of(MonthDay.of(1, 1)), + Arguments.of(MonthDay.of(6, 30)), + Arguments.of(MonthDay.of(3, 1)), + // LocalDate + Arguments.of(LocalDate.EPOCH), + Arguments.of(LocalDate.MIN), + Arguments.of(LocalDate.MAX), + Arguments.of(LocalDate.now()), + Arguments.of(LocalDate.of(2003, 9, 18)), + Arguments.of(LocalDate.of(1999, 12, 31)), + Arguments.of(LocalDate.of(1989, 11, 9)), + Arguments.of(LocalDate.of(1871, 1, 18)), + // LocalTime + Arguments.of(LocalTime.MIN), + Arguments.of(LocalTime.MAX), + Arguments.of(LocalTime.MIDNIGHT), + Arguments.of(LocalTime.NOON), + Arguments.of(LocalTime.now()), + Arguments.of(LocalTime.of(11, 11)), + Arguments.of(LocalTime.of(16, 12, 34, 667)), + // YearMonth + Arguments.of(YearMonth.now()), + Arguments.of(YearMonth.of(1961, 8)), + Arguments.of(YearMonth.of(1929, 10)), + Arguments.of(YearMonth.of(2024, 11)), + // OffsetTime + Arguments.of(OffsetTime.MIN), + Arguments.of(OffsetTime.MAX), + Arguments.of(OffsetTime.now()), + Arguments.of(OffsetTime.of(LocalTime.now(), ZoneOffset.UTC)), + Arguments.of(OffsetTime.of(LocalTime.now(), ZoneOffset.ofHours(5))), + Arguments.of(OffsetTime.of(LocalTime.now(), ZoneOffset.ofHours(-3))), + // LocalDateTime + Arguments.of(LocalDateTime.MIN), + Arguments.of(LocalDateTime.MAX), + Arguments.of(LocalDateTime.now()), + Arguments.of(LocalDateTime.of(1945, 8, 6, 8, 16, 2)), + Arguments.of(LocalDateTime.of(1945, 8, 9, 11, 2)), + // ZonedDateTime + Arguments.of(ZonedDateTime.now()), + Arguments.of(ZonedDateTime.of(LocalDateTime.of(2034, 6, 6, 12, 5), ZoneId.of("UTC-2"))), + Arguments.of(ZonedDateTime.of(LocalDateTime.of(2011, 3, 11, 14, 46, 23), ZoneId.of("GMT+9"))), + // OffsetDateTime + Arguments.of(OffsetDateTime.MIN), + Arguments.of(OffsetDateTime.MAX), + Arguments.of(OffsetDateTime.now()), + Arguments.of(OffsetDateTime.of(LocalDateTime.of(2058, 8, 15, 23, 12), ZoneOffset.ofHours(-5))), + Arguments.of(OffsetDateTime.of(LocalDateTime.of(1986, 4, 26, 1, 23, 44), ZoneOffset.ofHours(3))) + ); + } + @BeforeAll static void setupBootInjectionLayer() { TestInjectionLayerConfigurator.loadAutoconfigureBindings(); @@ -255,4 +359,18 @@ void testByteArrayWriting() { Assertions.assertNotNull(result); Assertions.assertArrayEquals(bytes, result); } + + @Order(80) + @ParameterizedTest + @MethodSource("timeClassProvider") + void testTimeClassSerialization(Object timeInstance) { + var mapper = new DefaultObjectMapper(); + var buf = DataBuf.empty(); + + mapper.writeObject(buf, timeInstance); + var result = mapper.readObject(buf, timeInstance.getClass()); + + Assertions.assertNotNull(result); + Assertions.assertEquals(timeInstance, result); + } }