From c25232c3ecaff675e2eadced6a40ccc86111c663 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sat, 16 Apr 2022 13:58:53 +0200 Subject: [PATCH] More refactoring to make the system more extensible. --- .../record_net/MessageTypeSerializer.java | 157 +++--------------- .../record_net/MessageTypeSerializerImpl.java | 18 ++ .../nl/andrewl/record_net/MessageUtils.java | 2 +- .../nl/andrewl/record_net/Serializer.java | 16 +- .../util/ExtendedDataInputStream.java | 4 +- .../util/RecordMessageTypeSerializer.java | 129 ++++++++++++++ ...a => RecordMessageTypeSerializerTest.java} | 7 +- 7 files changed, 191 insertions(+), 142 deletions(-) create mode 100644 src/main/java/nl/andrewl/record_net/MessageTypeSerializerImpl.java create mode 100644 src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java rename src/test/java/nl/andrewl/record_net/{MessageTypeSerializerTest.java => RecordMessageTypeSerializerTest.java} (80%) diff --git a/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java b/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java index 406f7f3..16bb5a2 100644 --- a/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java +++ b/src/main/java/nl/andrewl/record_net/MessageTypeSerializer.java @@ -1,142 +1,35 @@ package nl.andrewl.record_net; -import nl.andrewl.record_net.util.Pair; - -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; /** - * Record containing the components needed to read and write a given message. - *

- * Also contains methods for automatically generating message type - * implementations for standard record-based messages. - *

- * @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. + * A type serializer provides the basic components needed to read and write + * instances of the given message type. + * @param The message type. */ -public record MessageTypeSerializer( - Class messageClass, - Function byteSizeFunction, - MessageReader reader, - MessageWriter writer -) { - /** - * An internal cache for storing generated type serializers. - */ - private static final Map, Serializer>, MessageTypeSerializer> generatedMessageTypes = new HashMap<>(); +public interface MessageTypeSerializer { + /** + * Gets the class of the message type that this serializer handles. + * @return The message class. + */ + Class messageClass(); - /** - * 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()) - ); - } + /** + * Gets a function that computes the size, in bytes, of messages of this + * serializer's type. + * @return A byte size function. + */ + Function byteSizeFunction(); - /** - * 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 MessageTypeSerializer 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 MessageTypeSerializer<>( - messageTypeClass, - generateByteSizeFunction(serializer, components), - generateReader(constructor), - generateWriter(components) - ); - } + /** + * Gets a component that can read messages from an input stream. + * @return The message reader. + */ + MessageReader reader(); - /** - * 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); - } - } - }; - } + /** + * 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 new file mode 100644 index 0000000..4d3a2a6 --- /dev/null +++ b/src/main/java/nl/andrewl/record_net/MessageTypeSerializerImpl.java @@ -0,0 +1,18 @@ +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 index 642b742..ac20360 100644 --- a/src/main/java/nl/andrewl/record_net/MessageUtils.java +++ b/src/main/java/nl/andrewl/record_net/MessageUtils.java @@ -40,7 +40,7 @@ public class MessageUtils { if (msg == null) { return 1; } else { - MessageTypeSerializer typeSerializer = (MessageTypeSerializer) serializer.getTypeSerializer(msg.getClass()); + MessageTypeSerializerImpl typeSerializer = (MessageTypeSerializerImpl) serializer.getTypeSerializer(msg.getClass()); return 1 + typeSerializer.byteSizeFunction().apply(msg); } } diff --git a/src/main/java/nl/andrewl/record_net/Serializer.java b/src/main/java/nl/andrewl/record_net/Serializer.java index b25622f..0a576f8 100644 --- a/src/main/java/nl/andrewl/record_net/Serializer.java +++ b/src/main/java/nl/andrewl/record_net/Serializer.java @@ -2,6 +2,7 @@ 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; @@ -37,21 +38,28 @@ public class Serializer { * their ids. * @param messageTypes A map containing message types mapped to their ids. */ - public Serializer(Map> messageTypes) { + public Serializer(Map> messageTypes) { messageTypes.forEach(this::registerType); } /** - * Helper method which registers a message type to be supported by the - * serializer, by adding it to the normal and inverse mappings. + * 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, MessageTypeSerializer.generateForRecord(this, 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); diff --git a/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java b/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java index d40c723..d51fe06 100644 --- a/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java +++ b/src/main/java/nl/andrewl/record_net/util/ExtendedDataInputStream.java @@ -85,10 +85,10 @@ public class ExtendedDataInputStream extends DataInputStream { int length = this.readInt(); return this.readNBytes(length); } else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) { - var messageType = MessageTypeSerializer.get(serializer, (Class) type.getComponentType()); + var messageType = RecordMessageTypeSerializer.get(serializer, (Class) type.getComponentType()); return this.readArray(messageType); } else if (Message.class.isAssignableFrom(type)) { - var messageType = MessageTypeSerializer.get(serializer, (Class) 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/RecordMessageTypeSerializer.java b/src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java new file mode 100644 index 0000000..45a5e16 --- /dev/null +++ b/src/main/java/nl/andrewl/record_net/util/RecordMessageTypeSerializer.java @@ -0,0 +1,129 @@ +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/test/java/nl/andrewl/record_net/MessageTypeSerializerTest.java b/src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java similarity index 80% rename from src/test/java/nl/andrewl/record_net/MessageTypeSerializerTest.java rename to src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java index cd1c006..b24e6e3 100644 --- a/src/test/java/nl/andrewl/record_net/MessageTypeSerializerTest.java +++ b/src/test/java/nl/andrewl/record_net/RecordMessageTypeSerializerTest.java @@ -3,6 +3,7 @@ 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; @@ -11,11 +12,11 @@ import java.io.IOException; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -public class MessageTypeSerializerTest { +public class RecordMessageTypeSerializerTest { @Test public void testGenerateForRecord() throws IOException { Serializer serializer = new Serializer(); - var s1 = MessageTypeSerializer.get(serializer, ChatMessage.class); + 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)); @@ -30,6 +31,6 @@ public class MessageTypeSerializerTest { // Only record classes can be generated. class NonRecordMessage implements Message {} - assertThrows(IllegalArgumentException.class, () -> MessageTypeSerializer.get(serializer, NonRecordMessage.class)); + assertThrows(IllegalArgumentException.class, () -> RecordMessageTypeSerializer.get(serializer, NonRecordMessage.class)); } }