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 extends Message>) type.getComponentType());
+ var messageType = RecordMessageTypeSerializer.get(serializer, (Class extends Message>) type.getComponentType());
return this.readArray(messageType);
} else if (Message.class.isAssignableFrom(type)) {
- var messageType = MessageTypeSerializer.get(serializer, (Class extends Message>) type);
+ var messageType = RecordMessageTypeSerializer.get(serializer, (Class extends Message>) 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));
}
}