From b78396508faa768c84a82a2e02a95dd95ea333bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Chomczyk?= Date: Wed, 28 Feb 2024 12:45:01 +0100 Subject: [PATCH] Add serdes support for arrays and maps; Add generic type retrieval for enums --- cory-core/pom.xml | 9 ++ .../moe/rafal/cory/serdes/PacketPacker.java | 7 ++ .../moe/rafal/cory/serdes/PacketUnpacker.java | 9 +- .../cory/serdes/PacketUnpackingException.java | 25 +++++ cory-serdes-msgpack/pom.xml | 12 +-- .../cory/serdes/MessagePackPacketPacker.java | 47 +++++++- .../serdes/MessagePackPacketPackerUtils.java | 102 ++++++++++++++++++ .../serdes/MessagePackPacketUnpacker.java | 61 ++++++++++- .../serdes/MessagePackPacketPackerTests.java | 38 ++++++- .../MessagePackPacketUnpackerTests.java | 26 ++++- 10 files changed, 322 insertions(+), 14 deletions(-) create mode 100644 cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpackingException.java create mode 100644 cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPackerUtils.java diff --git a/cory-core/pom.xml b/cory-core/pom.xml index 234aecd..51363a9 100644 --- a/cory-core/pom.xml +++ b/cory-core/pom.xml @@ -15,4 +15,13 @@ UTF-8 + + + com.pivovarit + throwing-function + 1.5.1 + compile + + + \ No newline at end of file diff --git a/cory-core/src/main/java/moe/rafal/cory/serdes/PacketPacker.java b/cory-core/src/main/java/moe/rafal/cory/serdes/PacketPacker.java index 883cde2..9149682 100644 --- a/cory-core/src/main/java/moe/rafal/cory/serdes/PacketPacker.java +++ b/cory-core/src/main/java/moe/rafal/cory/serdes/PacketPacker.java @@ -21,12 +21,15 @@ import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.Map; import java.util.UUID; public interface PacketPacker extends Closeable { PacketPacker packArrayHeader(int value) throws IOException; + PacketPacker packArray(V[] value) throws IOException; + PacketPacker packBinaryHeader(int value) throws IOException; PacketPacker packPayload(byte[] value) throws IOException; @@ -49,6 +52,8 @@ public interface PacketPacker extends Closeable { PacketPacker packDouble(Double value) throws IOException; + PacketPacker packMap(Map value) throws IOException; + PacketPacker packMapHeader(int value) throws IOException; PacketPacker packInstant(Instant value) throws IOException; @@ -57,6 +62,8 @@ public interface PacketPacker extends Closeable { PacketPacker packEnum(Enum value) throws IOException; + PacketPacker packAuto(T value) throws IOException; + PacketPacker packNil() throws IOException; PacketPacker flush() throws IOException; diff --git a/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpacker.java b/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpacker.java index 219fd3e..42e116e 100644 --- a/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpacker.java +++ b/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpacker.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.Map; import java.util.UUID; public interface PacketUnpacker extends Closeable { @@ -29,6 +30,8 @@ public interface PacketUnpacker extends Closeable { int unpackArrayHeader() throws IOException; + V[] unpackArray() throws IOException; + int unpackBinaryHeader() throws IOException; byte[] unpackPayload() throws IOException; @@ -53,11 +56,15 @@ public interface PacketUnpacker extends Closeable { int unpackMapHeader() throws IOException; + Map unpackMap() throws IOException; + Instant unpackInstant() throws IOException; Duration unpackDuration() throws IOException; - > T unpackEnum(Class expectedType) throws IOException; + > T unpackEnum() throws IOException; + + T unpackAuto() throws IOException; boolean hasNext() throws IOException; diff --git a/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpackingException.java b/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpackingException.java new file mode 100644 index 0000000..41035da --- /dev/null +++ b/cory-core/src/main/java/moe/rafal/cory/serdes/PacketUnpackingException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 cory + * + * 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 moe.rafal.cory.serdes; + +public class PacketUnpackingException extends IllegalStateException { + + PacketUnpackingException(String message) { + super(message); + } +} diff --git a/cory-serdes-msgpack/pom.xml b/cory-serdes-msgpack/pom.xml index b0d44e8..8955644 100644 --- a/cory-serdes-msgpack/pom.xml +++ b/cory-serdes-msgpack/pom.xml @@ -16,18 +16,18 @@ - - org.msgpack - msgpack-core - 0.9.8 - compile - com.pivovarit throwing-function 1.5.1 compile + + org.msgpack + msgpack-core + 0.9.8 + compile + moe.rafal cory-core diff --git a/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPacker.java b/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPacker.java index 4976c01..2ad08a6 100644 --- a/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPacker.java +++ b/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPacker.java @@ -17,10 +17,14 @@ package moe.rafal.cory.serdes; +import static moe.rafal.cory.serdes.MessagePackPacketPackerUtils.PACKET_PACKER_BY_BOXED_TYPE; +import static moe.rafal.cory.serdes.MessagePackPacketPackerUtils.getBoxedType; + import com.pivovarit.function.ThrowingBiConsumer; import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.Map; import java.util.UUID; import org.msgpack.core.MessageBufferPacker; import org.msgpack.core.MessagePacker; @@ -39,6 +43,16 @@ public PacketPacker packArrayHeader(int value) throws IOException { return this; } + @Override + public PacketPacker packArray(final V[] value) throws IOException { + return packOrNil(value, (packer, val) -> { + packer.packArrayHeader(val.length); + for (final V currentValue : value) { + this.packAuto(currentValue); + } + }); + } + @Override public PacketPacker packBinaryHeader(int value) throws IOException { underlyingPacker.packBinaryHeader(value); @@ -105,6 +119,17 @@ public PacketPacker packMapHeader(int value) throws IOException { return this; } + @Override + public PacketPacker packMap(final Map value) throws IOException { + return packOrNil(value, (packer, val) -> { + packer.packMapHeader(val.size()); + for (final Map.Entry entry : value.entrySet()) { + this.packAuto(entry.getKey()); + this.packAuto(entry.getValue()); + } + }); + } + @Override public PacketPacker packInstant(Instant value) throws IOException { return packOrNil(value, (packer, val) -> packer.packString(val.toString())); @@ -117,7 +142,27 @@ public PacketPacker packDuration(Duration value) throws IOException { @Override public PacketPacker packEnum(Enum value) throws IOException { - return packOrNil(value, (packer, val) -> packer.packString(val.name())); + return packOrNil(value, (packer, val) -> { + packer.packString(val.getDeclaringClass().getName()); + packer.packString(val.name()); + }); + } + + @Override + public @SuppressWarnings("unchecked") PacketPacker packAuto(final T value) throws IOException { + return packOrNil(value, (packer, val) -> { + final Class rawType = val.getClass(); + final Class type = getBoxedType(val.getClass()); + if (!rawType.isEnum()) { + this.packString(type.getName()); + } + + final ThrowingBiConsumer packerFunction = + (ThrowingBiConsumer) PACKET_PACKER_BY_BOXED_TYPE.get( + type + ); + packerFunction.accept(this, val); + }); } @Override diff --git a/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPackerUtils.java b/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPackerUtils.java new file mode 100644 index 0000000..d8edb6d --- /dev/null +++ b/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketPackerUtils.java @@ -0,0 +1,102 @@ +/* + * Copyright 2023 cory + * + * 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 moe.rafal.cory.serdes; + +import static java.lang.String.format; + +import com.pivovarit.function.ThrowingBiConsumer; +import com.pivovarit.function.ThrowingFunction; +import java.io.IOException; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +final class MessagePackPacketPackerUtils { + + static final Map, Class> PRIMITIVE_TO_BOXED_TYPE; + static final Map< + Class, ThrowingBiConsumer> PACKET_PACKER_BY_BOXED_TYPE; + static final Map< + Class, ThrowingFunction> PACKET_UNPACKER_BY_BOXED_TYPE; + + static { + PRIMITIVE_TO_BOXED_TYPE = new HashMap<>(); + PRIMITIVE_TO_BOXED_TYPE.put(boolean.class, Boolean.class); + PRIMITIVE_TO_BOXED_TYPE.put(int.class, Integer.class); + PRIMITIVE_TO_BOXED_TYPE.put(byte.class, Byte.class); + PRIMITIVE_TO_BOXED_TYPE.put(long.class, Long.class); + PRIMITIVE_TO_BOXED_TYPE.put(short.class, Short.class); + PRIMITIVE_TO_BOXED_TYPE.put(float.class, Float.class); + PRIMITIVE_TO_BOXED_TYPE.put(double.class, Double.class); + PACKET_PACKER_BY_BOXED_TYPE = new HashMap<>(); + PACKET_PACKER_BY_BOXED_TYPE.put(String.class, (packer, value) -> packer.packString((String) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Boolean.class, (packer, value) -> packer.packBoolean((Boolean) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Integer.class, (packer, value) -> packer.packInt((Integer) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Byte.class, (packer, value) -> packer.packByte((Byte) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Long.class, (packer, value) -> packer.packLong((Long) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(UUID.class, (packer, value) -> packer.packUUID((UUID) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Short.class, (packer, value) -> packer.packShort((Short) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Float.class, (packer, value) -> packer.packFloat((Float) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Double.class, (packer, value) -> packer.packDouble((Double) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Instant.class, (packer, value) -> packer.packInstant((Instant) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Duration.class, (packer, value) -> packer.packDuration((Duration) value)); + PACKET_PACKER_BY_BOXED_TYPE.put(Enum.class, (packer, value) -> packer.packEnum((Enum) value)); + PACKET_UNPACKER_BY_BOXED_TYPE = new HashMap<>(); + PACKET_UNPACKER_BY_BOXED_TYPE.put(String.class, PacketUnpacker::unpackString); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Boolean.class, PacketUnpacker::unpackBoolean); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Integer.class, PacketUnpacker::unpackInt); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Byte.class, PacketUnpacker::unpackByte); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Long.class, PacketUnpacker::unpackLong); + PACKET_UNPACKER_BY_BOXED_TYPE.put(UUID.class, PacketUnpacker::unpackUUID); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Short.class, PacketUnpacker::unpackShort); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Float.class, PacketUnpacker::unpackFloat); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Double.class, PacketUnpacker::unpackDouble); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Instant.class, PacketUnpacker::unpackInstant); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Duration.class, PacketUnpacker::unpackDuration); + PACKET_UNPACKER_BY_BOXED_TYPE.put(Enum.class, PacketUnpacker::unpackEnum); + } + + private MessagePackPacketPackerUtils() { + + } + + static Class getClassByNameOrThrow(final String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException exception) { + throw new PacketUnpackingException(format( + "Could not find class by name %s", + className + )); + } + } + + static Class getBoxedType(final Class type) { + if (type.isPrimitive()) { + return PRIMITIVE_TO_BOXED_TYPE.get(type); + } + + if (type.isEnum()) { + return Enum.class; + } + + return type; + } +} diff --git a/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketUnpacker.java b/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketUnpacker.java index 36fa66f..6a11d9d 100644 --- a/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketUnpacker.java +++ b/cory-serdes-msgpack/src/main/java/moe/rafal/cory/serdes/MessagePackPacketUnpacker.java @@ -17,12 +17,16 @@ package moe.rafal.cory.serdes; +import static moe.rafal.cory.serdes.MessagePackPacketPackerUtils.PACKET_UNPACKER_BY_BOXED_TYPE; +import static moe.rafal.cory.serdes.MessagePackPacketPackerUtils.getClassByNameOrThrow; import static org.msgpack.core.MessageFormat.NIL; import com.pivovarit.function.ThrowingFunction; import java.io.IOException; import java.time.Duration; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import java.util.UUID; import org.msgpack.core.MessageUnpacker; @@ -44,6 +48,18 @@ public int unpackArrayHeader() throws IOException { return underlyingUnpacker.unpackArrayHeader(); } + @Override + public @SuppressWarnings("unchecked") V[] unpackArray() throws IOException { + final int length = underlyingUnpacker.unpackArrayHeader(); + + final V[] result = (V[]) new Object[length]; + for (int index = 0; index < length; index++) { + result[index] = unpackAuto(); + } + + return result; + } + @Override public int unpackBinaryHeader() throws IOException { return underlyingUnpacker.unpackBinaryHeader(); @@ -110,6 +126,22 @@ public int unpackMapHeader() throws IOException { return underlyingUnpacker.unpackMapHeader(); } + @Override + public Map unpackMap() + throws IOException { + final int length = unpackMapHeader(); + + final Map result = new HashMap<>(length); + for (int index = 0; index < length; index++) { + result.put( + unpackAuto(), + unpackAuto() + ); + } + + return result; + } + @Override public Instant unpackInstant() throws IOException { return unpackOrNil(unpacker -> Instant.parse(unpacker.unpackString())); @@ -121,8 +153,33 @@ public Duration unpackDuration() throws IOException { } @Override - public > T unpackEnum(Class expectedType) throws IOException { - return unpackOrNil(unpacker -> Enum.valueOf(expectedType, unpacker.unpackString())); + public @SuppressWarnings("unchecked") > T unpackEnum() throws IOException { + if (hasNextNilValue()) { + return null; + } + + final String className = unpackString(); + final Class type = getClassByNameOrThrow(className); + if (type.isEnum()) { + return unpackOrNil(unpacker -> Enum.valueOf((Class) type, unpacker.unpackString())); + } + + return null; + } + + @Override + public @SuppressWarnings("unchecked") T unpackAuto() throws IOException { + if (hasNextNilValue()) { + return null; + } + + final String className = underlyingUnpacker.unpackString(); + final Class type = getClassByNameOrThrow(className); + final ThrowingFunction unpackerFunction = + (ThrowingFunction) PACKET_UNPACKER_BY_BOXED_TYPE.get( + type + ); + return unpackerFunction.apply(this); } @Override diff --git a/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketPackerTests.java b/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketPackerTests.java index 11d79c9..6d7cf8c 100644 --- a/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketPackerTests.java +++ b/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketPackerTests.java @@ -254,7 +254,7 @@ private static Set getDurationSubjects() { void packEnumTest(GameState value) throws IOException { packValueAndAssertThatContains(packetPacker, PacketPacker::packEnum, - packetUnpacker -> packetUnpacker.unpackEnum(GameState.class), value); + PacketUnpacker::unpackEnum, value); } @Test @@ -263,12 +263,46 @@ void packEnumWithNullValueTest() throws IOException { packer.packDuration(null); try (PacketUnpacker unpacker = MessagePackPacketUnpackerFactory.INSTANCE.producePacketUnpacker( packer.toBinaryArray())) { - assertThat(unpacker.unpackEnum(GameState.class)) + assertThat((GameState) unpacker.unpackEnum()) .isNull(); } } } + @Test + void packAutoTest() throws IOException { + try (PacketPacker packer = MessagePackPacketPackerFactory.INSTANCE.producePacketPacker()) { + packer.packAuto(10); + packer.packAuto("test_string"); + packer.packAuto(AWAITING); + try (PacketUnpacker unpacker = MessagePackPacketUnpackerFactory.INSTANCE.producePacketUnpacker( + packer.toBinaryArray())) { + assertThat(unpacker.unpackString()) + .isEqualTo(Integer.class.getName()); + assertThat(unpacker.unpackInt()) + .isEqualTo(10); + assertThat(unpacker.unpackString()) + .isEqualTo(String.class.getName()); + assertThat(unpacker.unpackString()) + .isEqualTo("test_string"); + assertThat((GameState) unpacker.unpackEnum()) + .isEqualTo(AWAITING); + } + } + } + + @Test + void packAutoWithNullValueTest() throws IOException { + try (PacketPacker packer = MessagePackPacketPackerFactory.INSTANCE.producePacketPacker()) { + packer.packAuto(null); + try (PacketUnpacker unpacker = MessagePackPacketUnpackerFactory.INSTANCE.producePacketUnpacker( + packer.toBinaryArray())) { + assertThat(unpacker.hasNextNilValue()) + .isTrue(); + } + } + } + private static Set getEnumSubjects() { return Set.of(AWAITING, COUNTING, RUNNING); } diff --git a/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketUnpackerTests.java b/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketUnpackerTests.java index 99a40ac..b10696f 100644 --- a/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketUnpackerTests.java +++ b/cory-serdes-msgpack/src/test/java/moe/rafal/cory/serdes/MessagePackPacketUnpackerTests.java @@ -35,6 +35,7 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.Instant; +import java.util.Map; import java.util.Set; import java.util.UUID; import moe.rafal.cory.subject.GameState; @@ -80,6 +81,16 @@ void unpackArrayHeaderTest(int value) throws IOException { PacketUnpacker::unpackArrayHeader, value); } + @Test + void unpackArrayTest() throws IOException { + final String[] value = new String[] {"test_string_1", "test_string_2", "test_string_3"}; + unpackValueAndAssertThatEqualTo( + PacketPacker::packArray, + PacketUnpacker::unpackArray, + value + ); + } + @ValueSource(ints = {40, 50, 60}) @ParameterizedTest void unpackBinaryHeaderTest(int value) throws IOException { @@ -175,6 +186,17 @@ void unpackMapHeaderTest(int value) throws IOException { PacketUnpacker::unpackInt, value); } + @Test + void unpackMapTest() throws IOException { + final Map value = Map.of("test_key_1", "test_value_1", "test_key_2", + "test_value_2", "test_key_3", "test_value_3"); + unpackValueAndAssertThatEqualTo( + PacketPacker::packMap, + PacketUnpacker::unpackMap, + value + ); + } + @MethodSource("getInstantSubjects") @ParameterizedTest void unpackInstantTest(Instant value) throws IOException { @@ -231,7 +253,7 @@ private static Set getDurationSubjects() { void unpackEnumTest(GameState value) throws IOException { unpackValueAndAssertThatEqualTo( PacketPacker::packEnum, - packetUnpacker -> packetUnpacker.unpackEnum(GameState.class), value); + PacketUnpacker::unpackEnum, value); } @Test @@ -240,7 +262,7 @@ void unpackEnumWithNullValueTest() throws IOException { packer.packEnum(null); try (PacketUnpacker unpacker = MessagePackPacketUnpackerFactory.INSTANCE.producePacketUnpacker( packer.toBinaryArray())) { - assertThat(unpacker.unpackEnum(GameState.class)) + assertThat((GameState) unpacker.unpackEnum()) .isNull(); } }