From 3fc0efca5f2c0e45a70d9f16094cbe25dcd33d9a Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Thu, 21 Sep 2023 18:35:19 -0400 Subject: [PATCH] Refactored completely for version 2.0.0 --- README.md | 9 +- pom.xml | 27 ++- .../com/andrewlalis/record_net/IOUtil.java | 209 ++++++++++++++++++ .../andrewlalis/record_net/RecordInfo.java | 23 ++ .../record_net/RecordMappedSerializer.java | 107 +++++++++ .../record_net/RecordSerializer.java | 10 + .../record_net/UnknownMessageIdException.java | 10 + .../UnsupportedMessageTypeException.java | 16 ++ src/main/java/module-info.java | 3 + .../java/nl/andrewl/record_net/Message.java | 21 -- .../nl/andrewl/record_net/MessageReader.java | 19 -- .../record_net/MessageTypeSerializer.java | 35 --- .../record_net/MessageTypeSerializerImpl.java | 18 -- .../nl/andrewl/record_net/MessageUtils.java | 95 -------- .../nl/andrewl/record_net/MessageWriter.java | 16 -- .../nl/andrewl/record_net/Serializer.java | 156 ------------- .../nl/andrewl/record_net/package-info.java | 5 - .../util/ExtendedDataInputStream.java | 141 ------------ .../util/ExtendedDataOutputStream.java | 179 --------------- .../java/nl/andrewl/record_net/util/Pair.java | 8 - .../util/RecordMessageTypeSerializer.java | 129 ----------- .../nl/andrewl/record_net/util/Triple.java | 9 - .../andrewl/record_net/util/package-info.java | 4 - .../andrewlalis/record_net/IOUtilTest.java | 39 ++++ .../RecordMappedSerializerTest.java | 62 ++++++ .../andrewl/record_net/MessageUtilsTest.java | 29 --- .../RecordMessageTypeSerializerTest.java | 36 --- .../nl/andrewl/record_net/SerializerTest.java | 29 --- .../andrewl/record_net/msg/ChatMessage.java | 9 - .../util/ExtendedDataOutputStreamTest.java | 35 --- 30 files changed, 505 insertions(+), 983 deletions(-) create mode 100644 src/main/java/com/andrewlalis/record_net/IOUtil.java create mode 100644 src/main/java/com/andrewlalis/record_net/RecordInfo.java create mode 100644 src/main/java/com/andrewlalis/record_net/RecordMappedSerializer.java create mode 100644 src/main/java/com/andrewlalis/record_net/RecordSerializer.java create mode 100644 src/main/java/com/andrewlalis/record_net/UnknownMessageIdException.java create mode 100644 src/main/java/com/andrewlalis/record_net/UnsupportedMessageTypeException.java create mode 100644 src/main/java/module-info.java delete mode 100644 src/main/java/nl/andrewl/record_net/Message.java delete mode 100644 src/main/java/nl/andrewl/record_net/MessageReader.java delete mode 100644 src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java delete mode 100644 src/main/java/nl/andrewl/record_net/MessageTypeSerializerImpl.java delete mode 100644 src/main/java/nl/andrewl/record_net/MessageUtils.java delete mode 100644 src/main/java/nl/andrewl/record_net/MessageWriter.java delete mode 100644 src/main/java/nl/andrewl/record_net/Serializer.java delete mode 100644 src/main/java/nl/andrewl/record_net/package-info.java delete mode 100644 src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java delete mode 100644 src/main/java/nl/andrewl/record_net/util/ExtendedDataOutputStream.java delete mode 100644 src/main/java/nl/andrewl/record_net/util/Pair.java delete mode 100644 src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java delete mode 100644 src/main/java/nl/andrewl/record_net/util/Triple.java delete mode 100644 src/main/java/nl/andrewl/record_net/util/package-info.java create mode 100644 src/test/java/com/andrewlalis/record_net/IOUtilTest.java create mode 100644 src/test/java/com/andrewlalis/record_net/RecordMappedSerializerTest.java delete mode 100644 src/test/java/nl/andrewl/record_net/MessageUtilsTest.java delete mode 100644 src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java delete mode 100644 src/test/java/nl/andrewl/record_net/SerializerTest.java delete mode 100644 src/test/java/nl/andrewl/record_net/msg/ChatMessage.java delete mode 100644 src/test/java/nl/andrewl/record_net/util/ExtendedDataOutputStreamTest.java diff --git a/README.md b/README.md index 796395e..a2cadff 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ record-net gives you the advantages of reflection, without the runtime costs. By Here's an example of how you can use record-net: ```java -import nl.andrewl.record_net.Message; -import nl.andrewl.record_net.Serializer; +import com.andrewlalis.record_net.Message; +import com.andrewlalis.record_net.impl.TypeMappedSerializer; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -18,10 +18,11 @@ class Example { long timestamp, String username, String msg - ) implements Message {} + ) implements Message { + } public static void main(String[] args) throws IOException { - var ser = new Serializer(); + var ser = new TypeMappedSerializer(); ser.registerType(1, ChatMessage.class); var socket = new Socket("127.0.0.1", 8081); var bOut = new ByteArrayOutputStream(); diff --git a/pom.xml b/pom.xml index 13049e4..82f833d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,13 +4,16 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - nl.andrewl + com.andrewlalis record-net - 1.3.4 + 2.0.0 + Record-Net + A simple library for reading and writing records deterministically and efficiently. + https://github.com/andrewlalis/record-net - 17 - 17 + 21 + 21 UTF-8 @@ -19,7 +22,7 @@ org.junit.jupiter junit-jupiter-api - 5.8.2 + 5.9.2 test @@ -27,7 +30,7 @@ org.junit.jupiter junit-jupiter-engine - 5.8.2 + 5.9.2 test @@ -40,4 +43,16 @@ https://maven.pkg.github.com/andrewlalis/record-net + + + https://github.com/andrewlalis/record-net + + + + + MIT License + https://github.com/andrewlalis/record-net/blob/main/LICENSE + repo + + \ No newline at end of file diff --git a/src/main/java/com/andrewlalis/record_net/IOUtil.java b/src/main/java/com/andrewlalis/record_net/IOUtil.java new file mode 100644 index 0000000..38c7225 --- /dev/null +++ b/src/main/java/com/andrewlalis/record_net/IOUtil.java @@ -0,0 +1,209 @@ +package com.andrewlalis.record_net; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.UUID; + +public final class IOUtil { + private IOUtil() {} + + public static Object readPrimitive(Class type, DataInputStream dIn) throws IOException { + if (type.equals(Integer.class) || type.equals(int.class)) return dIn.readInt(); + if (type.equals(Short.class) || type.equals(short.class)) return dIn.readShort(); + if (type.equals(Byte.class) || type.equals(byte.class)) return dIn.readByte(); + if (type.equals(Character.class) || type.equals(char.class)) return dIn.readChar(); + if (type.equals(Long.class) || type.equals(long.class)) return dIn.readLong(); + if (type.equals(Float.class) || type.equals(float.class)) return dIn.readFloat(); + if (type.equals(Double.class) || type.equals(double.class)) return dIn.readDouble(); + if (type.equals(Boolean.class) || type.equals(boolean.class)) return dIn.readBoolean(); + throw new IllegalArgumentException("Type " + type.getSimpleName() + " is not primitive."); + } + + public static void writePrimitive(Object obj, DataOutputStream dOut) throws IOException { + switch (obj) { + case Integer n -> dOut.writeInt(n); + case Short n -> dOut.writeShort(n); + case Byte n -> dOut.writeByte(n); + case Character c -> dOut.writeChar(c); + case Long n -> dOut.writeLong(n); + case Float n -> dOut.writeFloat(n); + case Double n -> dOut.writeDouble(n); + case Boolean b -> dOut.writeBoolean(b); + default -> throw new IllegalArgumentException("Type " + obj.getClass().getSimpleName() + " is not primitive."); + } + } + + public static String readString(DataInputStream dIn) throws IOException { + return dIn.readUTF(); + } + + public static void writeString(String s, DataOutputStream dOut) throws IOException { + dOut.writeUTF(s); + } + + public static UUID readUUID(DataInputStream dIn) throws IOException { + long n1 = dIn.readLong(); + long n2 = dIn.readLong(); + return new UUID(n1, n2); + } + + public static void writeUUID(UUID uuid, DataOutputStream dOut) throws IOException { + dOut.writeLong(uuid.getMostSignificantBits()); + dOut.writeLong(uuid.getLeastSignificantBits()); + } + + @SuppressWarnings("unchecked") + public static > T readEnum(Class type, DataInputStream dIn) throws IOException { + if (!type.isEnum()) throw new IllegalArgumentException("Type must be an enum."); + int ordinal = dIn.readInt(); + if (ordinal == -1) return null; + return (T) type.getEnumConstants()[ordinal]; + } + + public static void writeEnum(Enum value, DataOutputStream dOut) throws IOException { + if (value == null) { + dOut.writeInt(-1); + } else { + dOut.writeInt(value.ordinal()); + } + } + + public static Object readPrimitiveArray(Class type, DataInputStream dIn) throws IOException { + final var cType = type.getComponentType(); + if (cType.equals(byte.class)) return readByteArray(dIn); + if (cType.equals(short.class)) return readShortArray(dIn); + if (cType.equals(int.class)) return readIntArray(dIn); + if (cType.equals(long.class)) return readLongArray(dIn); + if (cType.equals(float.class)) return readFloatArray(dIn); + if (cType.equals(double.class)) return readDoubleArray(dIn); + if (cType.equals(boolean.class)) return readBooleanArray(dIn); + if (cType.equals(char.class)) return readCharArray(dIn); + throw new IllegalArgumentException("Type " + type + " is not a primitive array."); + } + + public static void writePrimitiveArray(Object array, DataOutputStream dOut) throws IOException { + switch (array) { + case byte[] a -> writeByteArray(a, dOut); + case short[] a -> writeShortArray(a, dOut); + case int[] a -> writeIntArray(a, dOut); + case long[] a -> writeLongArray(a, dOut); + case float[] a -> writeFloatArray(a, dOut); + case double[] a -> writeDoubleArray(a, dOut); + case boolean[] a -> writeBooleanArray(a, dOut); + case char[] a -> writeCharArray(a, dOut); + default -> throw new IllegalArgumentException(array.getClass() + " is not a primitive array."); + } + } + + public static byte[] readByteArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + byte[] array = new byte[length]; + dIn.readFully(array); + return array; + } + + public static void writeByteArray(byte[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeByte(element); + } + + public static short[] readShortArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + short[] array = new short[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readShort(); + } + return array; + } + + public static void writeShortArray(short[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeShort(element); + } + + public static int[] readIntArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + int[] array = new int[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readInt(); + } + return array; + } + + public static void writeIntArray(int[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeInt(element); + } + + public static long[] readLongArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + long[] array = new long[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readLong(); + } + return array; + } + + public static void writeLongArray(long[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeLong(element); + } + + public static float[] readFloatArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + float[] array = new float[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readFloat(); + } + return array; + } + + public static void writeFloatArray(float[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeFloat(element); + } + + public static double[] readDoubleArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + double[] array = new double[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readDouble(); + } + return array; + } + + public static void writeDoubleArray(double[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeDouble(element); + } + + public static boolean[] readBooleanArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + boolean[] array = new boolean[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readBoolean(); + } + return array; + } + + public static void writeBooleanArray(boolean[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeBoolean(element); + } + + public static char[] readCharArray(DataInputStream dIn) throws IOException { + int length = dIn.readInt(); + char[] array = new char[length]; + for (int i = 0; i < length; i++) { + array[i] = dIn.readChar(); + } + return array; + } + + public static void writeCharArray(char[] array, DataOutputStream dOut) throws IOException { + dOut.writeInt(array.length); + for (var element : array) dOut.writeChar(element); + } +} diff --git a/src/main/java/com/andrewlalis/record_net/RecordInfo.java b/src/main/java/com/andrewlalis/record_net/RecordInfo.java new file mode 100644 index 0000000..5af4af0 --- /dev/null +++ b/src/main/java/com/andrewlalis/record_net/RecordInfo.java @@ -0,0 +1,23 @@ +package com.andrewlalis.record_net; + +import java.lang.reflect.Constructor; +import java.lang.reflect.RecordComponent; + +record RecordInfo(RecordComponent[] components, Constructor constructor) { + public static RecordInfo forType(Class type) { + if (!type.isRecord()) throw new IllegalArgumentException(type + " is not a record."); + RecordComponent[] c = type.getRecordComponents(); + Class[] paramTypes = new Class[c.length]; + for (int i = 0; i < c.length; i++) { + paramTypes[i] = c[i].getType(); + } + try { + Constructor ctor = type.getDeclaredConstructor(paramTypes); + return new RecordInfo<>(c, ctor); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/src/main/java/com/andrewlalis/record_net/RecordMappedSerializer.java b/src/main/java/com/andrewlalis/record_net/RecordMappedSerializer.java new file mode 100644 index 0000000..8dbbcb0 --- /dev/null +++ b/src/main/java/com/andrewlalis/record_net/RecordMappedSerializer.java @@ -0,0 +1,107 @@ +package com.andrewlalis.record_net; + +import java.io.*; +import java.lang.reflect.Array; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class RecordMappedSerializer implements RecordSerializer { + private final Map> messageTypes = new HashMap<>(); + private final Map, Integer> messageTypeIds = new HashMap<>(); + private final Map, RecordInfo> messageRecordInfo = new HashMap<>(); + + public void registerType(int id, Class type) { + if (!type.isRecord()) throw new IllegalArgumentException("Only records are permitted."); + this.messageTypes.put(id, type); + this.messageTypeIds.put(type, id); + this.messageRecordInfo.put(type, RecordInfo.forType(type)); + } + + public boolean isTypeSupported(Class type) { + return messageTypeIds.containsKey(type); + } + + @Override + public Object readMessage(InputStream in) throws IOException { + var dIn = new DataInputStream(in); + int id = dIn.readInt(); + Class msgType = messageTypes.get(id); + if (msgType == null) throw new UnknownMessageIdException(id); + return readRawObject(dIn, msgType); + } + + private Object readRawObject(DataInputStream dIn, Class type) throws IOException { + if (messageRecordInfo.containsKey(type)) { + RecordInfo recordInfo = messageRecordInfo.get(type); + Object[] values = new Object[recordInfo.components().length]; + for (int i = 0; i < recordInfo.components().length; i++) { + values[i] = readRawObject(dIn, recordInfo.components()[i].getType()); + } + try { + return recordInfo.constructor().newInstance(values); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + if (type.isArray()) { + if (type.getComponentType().isPrimitive()) { + return IOUtil.readPrimitiveArray(type, dIn); + } else { + int length = dIn.readInt(); + Object[] array = new Object[length]; + for (int i = 0; i < length; i++) { + array[i] = readRawObject(dIn, type.getComponentType()); + } + return array; + } + } + if (type.isEnum()) { + return IOUtil.readEnum(type, dIn); + } + if (type.equals(UUID.class)) { + return IOUtil.readUUID(dIn); + } + return IOUtil.readPrimitive(type, dIn); + } + + @Override + public void writeMessage(Object msg, OutputStream out) throws IOException { + if (msg == null) throw new IllegalArgumentException("Cannot write a null message."); + if (!isTypeSupported(msg.getClass())) throw new UnsupportedMessageTypeException(msg.getClass()); + var dOut = new DataOutputStream(out); + int id = messageTypeIds.get(msg.getClass()); + dOut.writeInt(id); + writeRawObject(msg, dOut); + } + + private void writeRawObject(Object obj, DataOutputStream dOut) throws IOException { + final Class type = obj.getClass(); + if (messageRecordInfo.containsKey(type)) { + RecordInfo recordInfo = messageRecordInfo.get(type); + for (var component : recordInfo.components()) { + try { + writeRawObject(component.getAccessor().invoke(obj), dOut); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + } else if (type.isArray()) { + if (type.getComponentType().isPrimitive()) { + IOUtil.writePrimitiveArray(obj, dOut); + } else { + int length = Array.getLength(obj); + dOut.writeInt(length); + for (int i = 0; i < length; i++) { + writeRawObject(Array.get(obj, i), dOut); + } + } + } else if (type.isEnum()) { + IOUtil.writeEnum((Enum) obj, dOut); + } else if (type.equals(UUID.class)) { + IOUtil.writeUUID((UUID) obj, dOut); + } else { + IOUtil.writePrimitive(obj, dOut); + } + } +} diff --git a/src/main/java/com/andrewlalis/record_net/RecordSerializer.java b/src/main/java/com/andrewlalis/record_net/RecordSerializer.java new file mode 100644 index 0000000..f91b1fe --- /dev/null +++ b/src/main/java/com/andrewlalis/record_net/RecordSerializer.java @@ -0,0 +1,10 @@ +package com.andrewlalis.record_net; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface RecordSerializer { + Object readMessage(InputStream in) throws IOException; + void writeMessage(Object msg, OutputStream out) throws IOException; +} diff --git a/src/main/java/com/andrewlalis/record_net/UnknownMessageIdException.java b/src/main/java/com/andrewlalis/record_net/UnknownMessageIdException.java new file mode 100644 index 0000000..d155d22 --- /dev/null +++ b/src/main/java/com/andrewlalis/record_net/UnknownMessageIdException.java @@ -0,0 +1,10 @@ +package com.andrewlalis.record_net; + +public class UnknownMessageIdException extends RuntimeException { + public final int messageId; + + public UnknownMessageIdException(int messageId) { + super("Unknown record-net message id " + messageId); + this.messageId = messageId; + } +} diff --git a/src/main/java/com/andrewlalis/record_net/UnsupportedMessageTypeException.java b/src/main/java/com/andrewlalis/record_net/UnsupportedMessageTypeException.java new file mode 100644 index 0000000..459c5d0 --- /dev/null +++ b/src/main/java/com/andrewlalis/record_net/UnsupportedMessageTypeException.java @@ -0,0 +1,16 @@ +package com.andrewlalis.record_net; + +public class UnsupportedMessageTypeException extends RuntimeException { + public Class messageType; + public int messageId; + + public UnsupportedMessageTypeException(Class messageType) { + super("The message type " + messageType.getSimpleName() + " is not supported."); + this.messageType = messageType; + } + + public UnsupportedMessageTypeException(int messageId) { + super("The message with id " + messageId + " is not supported."); + this.messageId = messageId; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..3a4b7f8 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,3 @@ +module com.andrewlalis.record_net { + exports com.andrewlalis.record_net; +} \ No newline at end of file diff --git a/src/main/java/nl/andrewl/record_net/Message.java b/src/main/java/nl/andrewl/record_net/Message.java deleted file mode 100644 index 1c7fc7c..0000000 --- a/src/main/java/nl/andrewl/record_net/Message.java +++ /dev/null @@ -1,21 +0,0 @@ -package nl.andrewl.record_net; - -/** - * Represents any message which can be sent over the network. - *

- * All messages consist of a single byte type identifier, followed by a - * payload whose structure depends on the message. - *

- */ -public interface Message { - /** - * Convenience method to get the serializer for this message's type, using - * the static auto-generated set of serializers. - * @param The message type. - * @return The serializer to use to read and write messages of this type. - */ - @SuppressWarnings("unchecked") - default MessageTypeSerializer getTypeSerializer(Serializer serializer) { - return (MessageTypeSerializer) serializer.getTypeSerializer(this.getClass()); - } -} diff --git a/src/main/java/nl/andrewl/record_net/MessageReader.java b/src/main/java/nl/andrewl/record_net/MessageReader.java deleted file mode 100644 index a4130b8..0000000 --- a/src/main/java/nl/andrewl/record_net/MessageReader.java +++ /dev/null @@ -1,19 +0,0 @@ -package nl.andrewl.record_net; - -import nl.andrewl.record_net.util.ExtendedDataInputStream; - -import java.io.IOException; - -@FunctionalInterface -public interface MessageReader{ - /** - * Reads all of this message's properties from the given input stream. - *

- * The single byte type identifier has already been read. - *

- * @param in The input stream to read from. - * @return The message that was read. - * @throws IOException If an error occurs while reading. - */ - T read(ExtendedDataInputStream in) throws IOException; -} diff --git a/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java b/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java deleted file mode 100644 index 16bb5a2..0000000 --- a/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java +++ /dev/null @@ -1,35 +0,0 @@ -package nl.andrewl.record_net; - -import java.util.function.Function; - -/** - * A type serializer provides the basic components needed to read and write - * instances of the given message type. - * @param The message type. - */ -public interface MessageTypeSerializer { - /** - * Gets the class of the message type that this serializer handles. - * @return The message class. - */ - Class messageClass(); - - /** - * Gets a function that computes the size, in bytes, of messages of this - * serializer's type. - * @return A byte size function. - */ - Function byteSizeFunction(); - - /** - * Gets a component that can read messages from an input stream. - * @return The message reader. - */ - MessageReader reader(); - - /** - * Gets a component that can write messages to an output stream. - * @return The message writer. - */ - MessageWriter writer(); -} diff --git a/src/main/java/nl/andrewl/record_net/MessageTypeSerializerImpl.java b/src/main/java/nl/andrewl/record_net/MessageTypeSerializerImpl.java deleted file mode 100644 index 4d3a2a6..0000000 --- a/src/main/java/nl/andrewl/record_net/MessageTypeSerializerImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package nl.andrewl.record_net; - -import java.util.function.Function; - -/** - * Record containing the components needed to read and write a given message. - * @param The type of message. - * @param messageClass The class of the message. - * @param byteSizeFunction A function that computes the byte size of the message. - * @param reader A reader that can read messages from an input stream. - * @param writer A writer that write messages from an input stream. - */ -public record MessageTypeSerializerImpl( - Class messageClass, - Function byteSizeFunction, - MessageReader reader, - MessageWriter writer -) implements MessageTypeSerializer {} diff --git a/src/main/java/nl/andrewl/record_net/MessageUtils.java b/src/main/java/nl/andrewl/record_net/MessageUtils.java deleted file mode 100644 index d3eb1e0..0000000 --- a/src/main/java/nl/andrewl/record_net/MessageUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -package nl.andrewl.record_net; - -import java.nio.charset.StandardCharsets; -import java.util.UUID; - -/** - * Utility class which provides methods for serializing and deserializing complex - * data types. - */ -public class MessageUtils { - public static final int UUID_BYTES = 2 * Long.BYTES; - public static final int ENUM_BYTES = Integer.BYTES; - - /** - * Gets the number of bytes that the given string will occupy when it is - * serialized. - * @param s The string. This may be null. - * @return The number of bytes used to serialize the string. - */ - public static int getByteSize(String s) { - return Integer.BYTES + (s == null ? 0 : s.getBytes(StandardCharsets.UTF_8).length); - } - - /** - * Gets the number of bytes that all the given strings will occupy when - * serialized with a length-prefix encoding. - * @param strings The set of strings. - * @return The total byte size. - */ - public static int getByteSize(String... strings) { - int size = 0; - for (var s : strings) { - size += getByteSize(s); - } - return size; - } - - @SuppressWarnings("unchecked") - public static int getByteSize(Serializer serializer, T msg) { - if (msg == null) { - return 1; - } else { - MessageTypeSerializerImpl typeSerializer = (MessageTypeSerializerImpl) serializer.getTypeSerializer(msg.getClass()); - return 1 + typeSerializer.byteSizeFunction().apply(msg); - } - } - - public static int getByteSize(Serializer serializer, T[] items) { - int count = Integer.BYTES; - for (var item : items) { - count += getByteSize(serializer, item); - } - return count; - } - - public static int getByteSize(Serializer serializer, Object o) { - if (o instanceof Integer) { - return Integer.BYTES; - } else if (o instanceof Float) { - return Float.BYTES; - } else if (o instanceof Byte) { - return Byte.BYTES; - } else if (o instanceof Double) { - return Double.BYTES; - } else if (o instanceof Short) { - return Short.BYTES; - } else if (o instanceof Long) { - return Long.BYTES; - } else if (o instanceof Boolean) { - return 1; - } else if (o instanceof String) { - return getByteSize((String) o); - } else if (o instanceof UUID) { - return UUID_BYTES; - } else if (o instanceof Enum) { - return ENUM_BYTES; - } else if (o instanceof byte[]) { - return Integer.BYTES + ((byte[]) o).length; - } else if (o.getClass().isArray() && Message.class.isAssignableFrom(o.getClass().getComponentType())) { - return getByteSize(serializer, (Message[]) o); - } else if (o instanceof Message) { - return getByteSize(serializer, (Message) o); - } else { - throw new IllegalArgumentException("Unsupported object type: " + o.getClass().getSimpleName()); - } - } - - public static int getByteSize(Serializer serializer, Object... objects) { - int size = 0; - for (var o : objects) { - size += getByteSize(serializer, o); - } - return size; - } -} diff --git a/src/main/java/nl/andrewl/record_net/MessageWriter.java b/src/main/java/nl/andrewl/record_net/MessageWriter.java deleted file mode 100644 index b80decf..0000000 --- a/src/main/java/nl/andrewl/record_net/MessageWriter.java +++ /dev/null @@ -1,16 +0,0 @@ -package nl.andrewl.record_net; - -import nl.andrewl.record_net.util.ExtendedDataOutputStream; - -import java.io.IOException; - -@FunctionalInterface -public interface MessageWriter { - /** - * Writes this message to the given output stream. - * @param msg The message to write. - * @param out The output stream to write to. - * @throws IOException If an error occurs while writing. - */ - void write(T msg, ExtendedDataOutputStream out) throws IOException; -} diff --git a/src/main/java/nl/andrewl/record_net/Serializer.java b/src/main/java/nl/andrewl/record_net/Serializer.java deleted file mode 100644 index b3842f3..0000000 --- a/src/main/java/nl/andrewl/record_net/Serializer.java +++ /dev/null @@ -1,156 +0,0 @@ -package nl.andrewl.record_net; - -import nl.andrewl.record_net.util.ExtendedDataInputStream; -import nl.andrewl.record_net.util.ExtendedDataOutputStream; -import nl.andrewl.record_net.util.RecordMessageTypeSerializer; - -import java.io.*; -import java.util.HashMap; -import java.util.Map; - -/** - * This class is responsible for reading and writing messages from streams. It - * also defines the set of supported message types, and their associated byte - * identifiers, via the {@link Serializer#registerType(int, Class)} method. - */ -public class Serializer { - /** - * The mapping which defines each supported message type and the byte value - * used to identify it when reading and writing messages. - */ - private final Map> messageTypes = new HashMap<>(); - - private final Map, MessageTypeSerializer> messageTypeClasses = new HashMap<>(); - - /** - * An inverse of {@link Serializer#messageTypes} which is used to look up a - * message's byte value when you know the class of the message. - */ - private final Map, Byte> inverseMessageTypes = new HashMap<>(); - - /** - * Constructs a new serializer instance. - */ - public Serializer() {} - - /** - * Constructs a serializer using a predefined mapping of message types and - * their ids. - * @param messageTypes A map containing message types mapped to their ids. - */ - public Serializer(Map> messageTypes) { - messageTypes.forEach(this::registerType); - } - - /** - * Helper method for registering a message type serializer for a record - * class, using {@link RecordMessageTypeSerializer#generateForRecord(Serializer, Class)}. - * @param id The byte which will be used to identify messages of the given - * class. The value should from 0 to 127. - * @param messageClass The type of message associated with the given id. - */ - public synchronized void registerType(int id, Class messageClass) { - registerTypeSerializer(id, RecordMessageTypeSerializer.generateForRecord(this, messageClass)); - } - - /** - * Registers the given type serializer with the given id. - * @param id The id to use. - * @param typeSerializer The type serializer that will be associated with - * the given id. - * @param The message type. - */ - public synchronized void registerTypeSerializer(int id, MessageTypeSerializer typeSerializer) { - if (id < 0 || id > 127) throw new IllegalArgumentException("Invalid id."); - messageTypes.put((byte) id, typeSerializer); - inverseMessageTypes.put(typeSerializer, (byte) id); - messageTypeClasses.put(typeSerializer.messageClass(), typeSerializer); - } - - /** - * Gets the {@link MessageTypeSerializer} for the given message class. - * @param messageType The class of message to get the serializer for. - * @return The message type serializer. - * @param The type of message. - */ - @SuppressWarnings("unchecked") - public MessageTypeSerializer getTypeSerializer(Class messageType) { - return (MessageTypeSerializer) messageTypeClasses.get(messageType); - } - - /** - * Reads a message from the given input stream and returns it, or throws an - * exception if an error occurred while reading from the stream. - * @param i The input stream to read from. - * @return The message which was read. - * @throws IOException If an error occurs while reading, such as trying to - * read an unsupported message type, or if a message object could not be - * constructed for the incoming data. - */ - public Message readMessage(InputStream i) throws IOException { - return readMessage(new ExtendedDataInputStream(this, i)); - } - - public Message readMessage(ExtendedDataInputStream in) throws IOException { - byte typeId = in.readByte(); - var type = messageTypes.get(typeId); - if (type == null) { - throw new IOException("Unsupported message type: " + typeId); - } - try { - return type.reader().read(in); - } catch (IOException e) { - throw new IOException("Could not instantiate new message object of type " + type.getClass().getSimpleName(), e); - } - } - - /** - * Reads a message from the given byte array and returns it, or throws an - * exception if an error occurred while reading from the stream. - * @param data The data to read from. - * @return The message which was read. - * @throws IOException If an error occurs while reading, such as trying to - * read an unsupported message type, or if a message object could not be - * constructed for the incoming data. - */ - public Message readMessage(byte[] data) throws IOException { - return readMessage(new ByteArrayInputStream(data)); - } - - /** - * Writes a message to the given output stream. - * @param msg The message to write. - * @param o The output stream to write to. - * @param The message type. - * @throws IOException If an error occurs while writing, or if the message - * to write is not supported by this serializer. - */ - public void writeMessage(T msg, OutputStream o) throws IOException { - writeMessage(msg, new ExtendedDataOutputStream(this, o)); - } - - public void writeMessage(T msg, ExtendedDataOutputStream out) throws IOException { - Byte typeId = inverseMessageTypes.get(msg.getTypeSerializer(this)); - if (typeId == null) { - throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName()); - } - out.writeByte(typeId); - msg.getTypeSerializer(this).writer().write(msg, out); - out.flush(); - } - - /** - * Writes a message as a byte array. - * @param msg The message to write. - * @return The bytes that were written. - * @param The message type. - * @throws IOException If an error occurs while writing, or if the message - * to write is not supported by this serializer. - */ - public byte[] writeMessage(T msg) throws IOException { - int bytes = msg.getTypeSerializer(this).byteSizeFunction().apply(msg); - ByteArrayOutputStream out = new ByteArrayOutputStream(1 + bytes); - writeMessage(msg, out); - return out.toByteArray(); - } -} diff --git a/src/main/java/nl/andrewl/record_net/package-info.java b/src/main/java/nl/andrewl/record_net/package-info.java deleted file mode 100644 index a31b99e..0000000 --- a/src/main/java/nl/andrewl/record_net/package-info.java +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Main package containing notably the {@link nl.andrewl.record_net.Serializer} - * and other components that may be of use for more fine-grained control. - */ -package nl.andrewl.record_net; \ No newline at end of file diff --git a/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java b/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java deleted file mode 100644 index 768b8e0..0000000 --- a/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java +++ /dev/null @@ -1,141 +0,0 @@ -package nl.andrewl.record_net.util; - -import nl.andrewl.record_net.Message; -import nl.andrewl.record_net.MessageTypeSerializer; -import nl.andrewl.record_net.Serializer; - -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Array; -import java.nio.charset.StandardCharsets; -import java.util.UUID; - -/** - * An extended output stream which contains additional methods for reading more - * complex types that are used by the Concord system. - */ -public class ExtendedDataInputStream extends DataInputStream { - private final Serializer serializer; - - public ExtendedDataInputStream(Serializer serializer, InputStream in) { - super(in); - this.serializer = serializer; - } - - public ExtendedDataInputStream(Serializer serializer, byte[] data) { - this(serializer, new ByteArrayInputStream(data)); - } - - public String readString() throws IOException { - int length = super.readInt(); - if (length == -1) return null; - if (length == 0) return ""; - byte[] data = new byte[length]; - int read = super.read(data); - if (read != length) throw new IOException("Not all bytes of a string of length " + length + " could be read."); - return new String(data, StandardCharsets.UTF_8); - } - - public > T readEnum(Class e) throws IOException { - int ordinal = super.readInt(); - if (ordinal == -1) return null; - return e.getEnumConstants()[ordinal]; - } - - public UUID readUUID() throws IOException { - long a = super.readLong(); - long b = super.readLong(); - if (a == -1 && b == -1) { - return null; - } - return new UUID(a, b); - } - - public byte[] readByteArray() throws IOException { - int length = readInt(); - if (length < 0) return null; - byte[] array = new byte[length]; - int readCount = read(array, 0, length); - while (readCount < length) { - readCount += read(array, readCount, length - readCount); - } - return array; - } - - public int[] readIntArray() throws IOException { - int length = readInt(); - if (length < 0) return null; - int[] array = new int[length]; - for (int i = 0; i < length; i++) { - array[i] = readInt(); - } - return array; - } - - public float[] readFloatArray() throws IOException { - int length = readInt(); - if (length < 0) return null; - float[] array = new float[length]; - for (int i = 0; i < length; i++) { - array[i] = readFloat(); - } - return array; - } - - @SuppressWarnings("unchecked") - public T[] readArray(MessageTypeSerializer type) throws IOException { - int length = super.readInt(); - T[] array = (T[]) Array.newInstance(type.messageClass(), length); - for (int i = 0; i < length; i++) { - array[i] = type.reader().read(this); - } - return array; - } - - /** - * Reads an object from the stream that is of a certain expected type. - * @param type The type of object to read. - * @return The object that was read. - * @throws IOException If an error occurs while reading. - */ - @SuppressWarnings("unchecked") - public Object readObject(Class type) throws IOException { - if (type.equals(Integer.class) || type.equals(int.class)) { - return readInt(); - } else if (type.equals(Short.class) || type.equals(short.class)) { - return readShort(); - } else if (type.equals(Byte.class) || type.equals(byte.class)) { - return (byte) read(); - } else if (type.equals(Long.class) || type.equals(long.class)) { - return readLong(); - } else if (type.equals(Float.class) || type.equals(float.class)) { - return readFloat(); - } else if (type.equals(Double.class) || type.equals(double.class)) { - return readDouble(); - } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { - return readBoolean(); - } else if (type.equals(String.class)) { - return readString(); - } else if (type.equals(UUID.class)) { - return readUUID(); - } else if (type.isEnum()) { - return readEnum((Class>) type); - } else if (type.isAssignableFrom(byte[].class)) { - return readByteArray(); - } else if (type.isAssignableFrom(int[].class)) { - return readIntArray(); - } else if (type.isAssignableFrom(float[].class)) { - return readFloatArray(); - } else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) { - var messageType = RecordMessageTypeSerializer.get(serializer, (Class) type.getComponentType()); - return readArray(messageType); - } else if (Message.class.isAssignableFrom(type)) { - var messageType = RecordMessageTypeSerializer.get(serializer, (Class) type); - return messageType.reader().read(this); - } else { - throw new IOException("Unsupported object type: " + type.getSimpleName()); - } - } -} diff --git a/src/main/java/nl/andrewl/record_net/util/ExtendedDataOutputStream.java b/src/main/java/nl/andrewl/record_net/util/ExtendedDataOutputStream.java deleted file mode 100644 index acd3a00..0000000 --- a/src/main/java/nl/andrewl/record_net/util/ExtendedDataOutputStream.java +++ /dev/null @@ -1,179 +0,0 @@ -package nl.andrewl.record_net.util; - -import nl.andrewl.record_net.Message; -import nl.andrewl.record_net.Serializer; - -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.UUID; - -/** - * An extended version of {@link DataOutputStream} with some extra methods - * that help us to write more data. - */ -public class ExtendedDataOutputStream extends DataOutputStream { - private final Serializer serializer; - - public ExtendedDataOutputStream(Serializer serializer, OutputStream out) { - super(out); - this.serializer = serializer; - } - - /** - * Writes a string in length-prefixed form, where the 4-byte length of the - * string is written, followed by exactly that many bytes. If the string - * is null, then a length of -1 is written, and no bytes following it. - * @param s The string to write. - * @throws IOException If an error occurs while writing. - */ - public void writeString(String s) throws IOException { - if (s == null) { - writeInt(-1); - } else { - writeInt(s.length()); - write(s.getBytes(StandardCharsets.UTF_8)); - } - } - - public void writeStrings(String... strings) throws IOException { - for (var s : strings) { - writeString(s); - } - } - - /** - * Writes an enum value as a 4-byte integer using the enum's ordinal - * position, or -1 if the given value is null. - * @param value The value to write. - * @throws IOException If an error occurs while writing. - */ - public void writeEnum(Enum value) throws IOException { - if (value == null) { - writeInt(-1); - } else { - writeInt(value.ordinal()); - } - } - - /** - * Writes a UUID as a 16-byte value. If the given UUID is null, then -1 - * is written twice as two long (8 byte) values. - * @param uuid The value to write. - * @throws IOException If an error occurs while writing. - */ - public void writeUUID(UUID uuid) throws IOException { - if (uuid == null) { - writeLong(-1); - writeLong(-1); - } else { - writeLong(uuid.getMostSignificantBits()); - writeLong(uuid.getLeastSignificantBits()); - } - } - - /** - * Writes an array of messages using length-prefixed form. That is, we - * first write a 4-byte integer length that specifies how many items are in - * the array, followed by writing each element of the array. If the array - * is null, a length of -1 is written. - * @param array The array to write. - * @param The type of items in the array. - * @throws IOException If an error occurs while writing. - */ - public void writeArray(T[] array) throws IOException { - if (array == null) { - writeInt(-1); - } else { - writeInt(array.length); - for (var item : array) writeMessage(item); - } - } - - public void writeArray(byte[] array) throws IOException { - if (array == null) { - writeInt(-1); - } else { - writeInt(array.length); - write(array); - } - } - - public void writeArray(int[] array) throws IOException { - if (array == null) { - writeInt(-1); - } else { - writeInt(array.length); - for (var item : array) writeInt(item); - } - } - - public void writeArray(float[] array) throws IOException { - if (array == null) { - writeInt(-1); - } else { - writeInt(array.length); - for (var item : array) writeFloat(item); - } - } - - /** - * Writes a message using null-prefixed form. That is, we first write a - * boolean value which is false only if the message is null. Then, if the - * message is not null, we write it to the stream. - * @param msg The message to write. - * @param The type of the message. - * @throws IOException If an error occurs while writing. - */ - public void writeMessage(Message msg) throws IOException { - writeBoolean(msg != null); - if (msg != null) { - msg.getTypeSerializer(serializer).writer().write(msg, this); - } - } - - /** - * Writes an object to the stream. - * @param o The object to write. - * @param type The object's type. This is needed in case the object itself - * is null, which may be the case for some strings or ids. - * @throws IOException If an error occurs while writing, or if an - * unsupported object is supplied. - */ - public void writeObject(Object o, Class type) throws IOException { - if (type.equals(Integer.class) || type.equals(int.class)) { - writeInt((int) o); - } else if (type.equals(Short.class) || type.equals(short.class)) { - writeShort((short) o); - } else if (type.equals(Byte.class) || type.equals(byte.class)) { - writeByte((byte) o); - } else if (type.equals(Long.class) || type.equals(long.class)) { - writeLong((long) o); - } else if (type.equals(Float.class) || type.equals(float.class)) { - writeFloat((float) o); - } else if (type.equals(Double.class) || type.equals(double.class)) { - writeDouble((double) o); - } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { - writeBoolean((boolean) o); - } else if (type.equals(String.class)) { - writeString((String) o); - } else if (type.equals(UUID.class)) { - writeUUID((UUID) o); - } else if (type.isEnum()) { - writeEnum((Enum) o); - } else if (type.equals(byte[].class)) { - writeArray((byte[]) o); - } else if (type.equals(int[].class)) { - writeArray((int[]) o); - } else if (type.equals(float[].class)) { - writeArray((float[]) o); - } else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) { - writeArray((Message[]) o); - } else if (Message.class.isAssignableFrom(type)) { - writeMessage((Message) o); - } else { - throw new IOException("Unsupported object type: " + o.getClass().getSimpleName()); - } - } -} diff --git a/src/main/java/nl/andrewl/record_net/util/Pair.java b/src/main/java/nl/andrewl/record_net/util/Pair.java deleted file mode 100644 index edc2206..0000000 --- a/src/main/java/nl/andrewl/record_net/util/Pair.java +++ /dev/null @@ -1,8 +0,0 @@ -package nl.andrewl.record_net.util; - -/** - * Simple generic pair of two objects. - * @param The first object. - * @param The second object. - */ -public record Pair(A first, B second) {} diff --git a/src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java b/src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java deleted file mode 100644 index 45a5e16..0000000 --- a/src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java +++ /dev/null @@ -1,129 +0,0 @@ -package nl.andrewl.record_net.util; - -import nl.andrewl.record_net.*; - -import java.lang.reflect.Constructor; -import java.lang.reflect.RecordComponent; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - -/** - * Helper class that contains logic for generating {@link MessageTypeSerializerImpl} - * implementations for record classes. - */ -public class RecordMessageTypeSerializer { - /** - * An internal cache for storing generated type serializers. - */ - private static final Map, Serializer>, MessageTypeSerializer> generatedMessageTypes = new HashMap<>(); - - /** - * Gets the {@link MessageTypeSerializer} instance for a given message class, and - * generates a new implementation if none exists yet. - * @param serializer The serializer context to get a type serializer for. - * @param messageClass The class of the message to get a type for. - * @param The type of the message. - * @return The message type. - */ - @SuppressWarnings("unchecked") - public static MessageTypeSerializer get(Serializer serializer, Class messageClass) { - return (MessageTypeSerializer) generatedMessageTypes.computeIfAbsent( - new Pair<>(messageClass, serializer), - p -> generateForRecord(serializer, (Class) p.first()) - ); - } - - /** - * Generates a message type instance for a given class, using reflection to - * introspect the fields of the message. - *

- * Note that this only works for record-based messages. - *

- * @param serializer The serializer context to get a type serializer for. - * @param messageTypeClass The class of the message type. - * @param The type of the message. - * @return A message type instance. - */ - public static MessageTypeSerializerImpl generateForRecord(Serializer serializer, Class messageTypeClass) { - RecordComponent[] components = messageTypeClass.getRecordComponents(); - if (components == null) throw new IllegalArgumentException("Cannot generate a MessageTypeSerializer for non-record class " + messageTypeClass.getSimpleName()); - Constructor constructor; - try { - constructor = messageTypeClass.getDeclaredConstructor(Arrays.stream(components) - .map(RecordComponent::getType).toArray(Class[]::new)); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException(e); - } - return new MessageTypeSerializerImpl<>( - messageTypeClass, - generateByteSizeFunction(serializer, components), - generateReader(constructor), - generateWriter(components) - ); - } - - /** - * Generates a function implementation that counts the byte size of a - * message based on the message's record component types. - * @param serializer The serializer context to generate a function for. - * @param components The list of components that make up the message. - * @param The message type. - * @return A function that computes the byte size of a message of the given - * type. - */ - private static Function generateByteSizeFunction(Serializer serializer, RecordComponent[] components) { - return msg -> { - int size = 0; - for (var component : components) { - try { - size += MessageUtils.getByteSize(serializer, component.getAccessor().invoke(msg)); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException(e); - } - } - return size; - }; - } - - /** - * Generates a message reader for the given message constructor method. It - * will try to read objects from the input stream according to the - * parameters of the canonical constructor of a message record. - * @param constructor The canonical constructor of the message record. - * @param The message type. - * @return A message reader for the given type. - */ - private static MessageReader generateReader(Constructor constructor) { - return in -> { - Object[] values = new Object[constructor.getParameterCount()]; - for (int i = 0; i < values.length; i++) { - values[i] = in.readObject(constructor.getParameterTypes()[i]); - } - try { - return constructor.newInstance(values); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException(e); - } - }; - } - - /** - * Generates a message writer for the given message record components. - * @param components The record components to write. - * @param The type of message. - * @return The message writer for the given type. - */ - private static MessageWriter generateWriter(RecordComponent[] components) { - return (msg, out) -> { - for (var component: components) { - try { - out.writeObject(component.getAccessor().invoke(msg), component.getType()); - } catch (ReflectiveOperationException e) { - throw new IllegalStateException(e); - } - } - }; - } -} diff --git a/src/main/java/nl/andrewl/record_net/util/Triple.java b/src/main/java/nl/andrewl/record_net/util/Triple.java deleted file mode 100644 index ea8dc63..0000000 --- a/src/main/java/nl/andrewl/record_net/util/Triple.java +++ /dev/null @@ -1,9 +0,0 @@ -package nl.andrewl.record_net.util; - -/** - * Simple generic triple of objects. - * @param
The first object. - * @param The second object. - * @param The third object. - */ -public record Triple (A first, B second, C third) {} diff --git a/src/main/java/nl/andrewl/record_net/util/package-info.java b/src/main/java/nl/andrewl/record_net/util/package-info.java deleted file mode 100644 index 2d9bf2f..0000000 --- a/src/main/java/nl/andrewl/record_net/util/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Contains some useful one-off utility classes. - */ -package nl.andrewl.record_net.util; \ No newline at end of file diff --git a/src/test/java/com/andrewlalis/record_net/IOUtilTest.java b/src/test/java/com/andrewlalis/record_net/IOUtilTest.java new file mode 100644 index 0000000..8748a3c --- /dev/null +++ b/src/test/java/com/andrewlalis/record_net/IOUtilTest.java @@ -0,0 +1,39 @@ +package com.andrewlalis.record_net; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class IOUtilTest { + @Test + public void testReadPrimitive() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream dOut = new DataOutputStream(baos); + dOut.writeInt(42); + dOut.writeShort(25565); + dOut.writeByte(35); + dOut.writeChar(234); + dOut.writeLong(234843209243L); + dOut.writeFloat(3.14f); + dOut.writeDouble(2.17); + dOut.writeBoolean(true); + dOut.writeBoolean(false); + byte[] data = baos.toByteArray(); + + DataInputStream dIn = new DataInputStream(new ByteArrayInputStream(data)); + assertEquals(42, IOUtil.readPrimitive(Integer.class, dIn)); + assertEquals((short) 25565, IOUtil.readPrimitive(Short.class, dIn)); + assertEquals((byte) 35, IOUtil.readPrimitive(Byte.class, dIn)); + assertEquals((char) 234, IOUtil.readPrimitive(Character.class, dIn)); + assertEquals(234843209243L, IOUtil.readPrimitive(Long.class, dIn)); + assertEquals(3.14f, IOUtil.readPrimitive(Float.class, dIn)); + assertEquals(2.17, IOUtil.readPrimitive(Double.class, dIn)); + assertEquals(true, IOUtil.readPrimitive(Boolean.class, dIn)); + assertEquals(false, IOUtil.readPrimitive(Boolean.class, dIn)); + } +} diff --git a/src/test/java/com/andrewlalis/record_net/RecordMappedSerializerTest.java b/src/test/java/com/andrewlalis/record_net/RecordMappedSerializerTest.java new file mode 100644 index 0000000..5cdcacf --- /dev/null +++ b/src/test/java/com/andrewlalis/record_net/RecordMappedSerializerTest.java @@ -0,0 +1,62 @@ +package com.andrewlalis.record_net; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class RecordMappedSerializerTest { + @Test + public void testRegisterType() { + assertThrows(IllegalArgumentException.class, () -> { + new RecordMappedSerializer().registerType(1, String.class); + }); + assertDoesNotThrow(() -> { + record TmpRecord (int a) {} + new RecordMappedSerializer().registerType(1, TmpRecord.class); + }); + RecordMappedSerializer serializer = new RecordMappedSerializer(); + record TmpRecord1 (int a) {} + assertFalse(serializer.isTypeSupported(TmpRecord1.class)); + serializer.registerType(1, TmpRecord1.class); + assertTrue(serializer.isTypeSupported(TmpRecord1.class)); + } + + @Test + public void testBasicReadAndWrite() throws Exception { + record TmpRecordA (int a, float b, byte[] c) {} + RecordMappedSerializer serializer = new RecordMappedSerializer(); + serializer.registerType(1, TmpRecordA.class); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + TmpRecordA testObj = new TmpRecordA(54, 234.543f, new byte[]{1, 2, 3}); + serializer.writeMessage(testObj, baos); + byte[] data = baos.toByteArray(); + + Object obj = serializer.readMessage(new ByteArrayInputStream(data)); + assertInstanceOf(TmpRecordA.class, obj); + TmpRecordA objA = (TmpRecordA) obj; + assertEquals(testObj.a, objA.a); + assertEquals(testObj.b, objA.b); + assertArrayEquals(testObj.c, objA.c); + } + + @Test + public void testNestedRecords() throws Exception { + record RecordA (int a, int b, float x) {} + record RecordB (RecordA a, boolean flag) {} + RecordMappedSerializer serializer = new RecordMappedSerializer(); + serializer.registerType(1, RecordA.class); + serializer.registerType(2, RecordB.class); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + RecordB testObj = new RecordB(new RecordA(1, 2, 3.5f), false); + serializer.writeMessage(testObj, baos); + byte[] data = baos.toByteArray(); + + Object obj = serializer.readMessage(new ByteArrayInputStream(data)); + assertInstanceOf(RecordB.class, obj); + RecordB b = (RecordB) obj; + assertEquals(testObj, b); + } +} diff --git a/src/test/java/nl/andrewl/record_net/MessageUtilsTest.java b/src/test/java/nl/andrewl/record_net/MessageUtilsTest.java deleted file mode 100644 index e155d9a..0000000 --- a/src/test/java/nl/andrewl/record_net/MessageUtilsTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package nl.andrewl.record_net; - -import nl.andrewl.record_net.msg.ChatMessage; -import org.junit.jupiter.api.Test; - -import java.nio.file.StandardCopyOption; -import java.util.UUID; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class MessageUtilsTest { - @Test - public void testGetByteSize() { - assertEquals(4, MessageUtils.getByteSize((String) null)); - assertEquals(5, MessageUtils.getByteSize("a")); - assertEquals(16, MessageUtils.getByteSize("Hello world!")); - assertEquals(8, MessageUtils.getByteSize("", "")); - assertEquals(10, MessageUtils.getByteSize("a", "b")); - Message msg = new ChatMessage("andrew", 123, "Hello world!"); - int expectedMsgSize = 1 + 4 + 6 + 8 + 4 + 12; - Serializer serializer = new Serializer(); - serializer.registerType(1, ChatMessage.class); - assertEquals(1, MessageUtils.getByteSize(serializer, (Message) null)); - assertEquals(expectedMsgSize, MessageUtils.getByteSize(serializer, msg)); - assertEquals(4 * expectedMsgSize, MessageUtils.getByteSize(serializer, msg, msg, msg, msg)); - assertEquals(16, MessageUtils.getByteSize(serializer, UUID.randomUUID())); - assertEquals(4, MessageUtils.getByteSize(serializer, StandardCopyOption.ATOMIC_MOVE)); - } -} diff --git a/src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java b/src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java deleted file mode 100644 index b24e6e3..0000000 --- a/src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package nl.andrewl.record_net; - -import nl.andrewl.record_net.msg.ChatMessage; -import nl.andrewl.record_net.util.ExtendedDataInputStream; -import nl.andrewl.record_net.util.ExtendedDataOutputStream; -import nl.andrewl.record_net.util.RecordMessageTypeSerializer; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class RecordMessageTypeSerializerTest { - @Test - public void testGenerateForRecord() throws IOException { - Serializer serializer = new Serializer(); - var s1 = RecordMessageTypeSerializer.get(serializer, ChatMessage.class); - ChatMessage msg = new ChatMessage("andrew", 123, "Hello world!"); - int expectedByteSize = 4 + msg.username().length() + 8 + 4 + msg.message().length(); - assertEquals(expectedByteSize, s1.byteSizeFunction().apply(msg)); - - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - ExtendedDataOutputStream eOut = new ExtendedDataOutputStream(serializer, bOut); - s1.writer().write(msg, eOut); - byte[] data = bOut.toByteArray(); - assertEquals(expectedByteSize, data.length); - ChatMessage readMsg = s1.reader().read(new ExtendedDataInputStream(serializer, data)); - assertEquals(msg, readMsg); - - // Only record classes can be generated. - class NonRecordMessage implements Message {} - assertThrows(IllegalArgumentException.class, () -> RecordMessageTypeSerializer.get(serializer, NonRecordMessage.class)); - } -} diff --git a/src/test/java/nl/andrewl/record_net/SerializerTest.java b/src/test/java/nl/andrewl/record_net/SerializerTest.java deleted file mode 100644 index d6fcc16..0000000 --- a/src/test/java/nl/andrewl/record_net/SerializerTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package nl.andrewl.record_net; - -import nl.andrewl.record_net.msg.ChatMessage; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class SerializerTest { - @Test - public void testReadAndWriteMessage() throws IOException { - Serializer s = new Serializer(); - s.registerType(1, ChatMessage.class); - - ChatMessage msg = new ChatMessage("andrew", 123, "Hello world!"); - - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - s.writeMessage(msg, bOut); - byte[] data = bOut.toByteArray(); - assertEquals(MessageUtils.getByteSize(s, msg), data.length); - assertEquals(data[0], 1); - - ChatMessage readMsg = (ChatMessage) s.readMessage(new ByteArrayInputStream(data)); - assertEquals(msg, readMsg); - } -} diff --git a/src/test/java/nl/andrewl/record_net/msg/ChatMessage.java b/src/test/java/nl/andrewl/record_net/msg/ChatMessage.java deleted file mode 100644 index 00f4123..0000000 --- a/src/test/java/nl/andrewl/record_net/msg/ChatMessage.java +++ /dev/null @@ -1,9 +0,0 @@ -package nl.andrewl.record_net.msg; - -import nl.andrewl.record_net.Message; - -public record ChatMessage( - String username, - long timestamp, - String message -) implements Message {} diff --git a/src/test/java/nl/andrewl/record_net/util/ExtendedDataOutputStreamTest.java b/src/test/java/nl/andrewl/record_net/util/ExtendedDataOutputStreamTest.java deleted file mode 100644 index fbb9968..0000000 --- a/src/test/java/nl/andrewl/record_net/util/ExtendedDataOutputStreamTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package nl.andrewl.record_net.util; - -import nl.andrewl.record_net.Serializer; -import org.junit.jupiter.api.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.IOException; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class ExtendedDataOutputStreamTest { - - @Test - public void testWriteString() throws IOException { - Serializer serializer = new Serializer(); - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - ExtendedDataOutputStream eOut = new ExtendedDataOutputStream(serializer, bOut); - eOut.writeString("Hello world!"); - byte[] data = bOut.toByteArray(); - assertEquals(4 + "Hello world!".length(), data.length); - DataInputStream dIn = new DataInputStream(new ByteArrayInputStream(data)); - assertEquals(12, dIn.readInt()); - String s = new String(dIn.readNBytes(12)); - assertEquals("Hello world!", s); - - bOut.reset(); - eOut.writeString(null); - data = bOut.toByteArray(); - assertEquals(4, data.length); - dIn = new DataInputStream(new ByteArrayInputStream(data)); - assertEquals(-1, dIn.readInt()); - } -}