More refactoring to make the system more extensible.
This commit is contained in:
parent
1d45822c67
commit
c25232c3ec
|
@ -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.
|
||||
* <p>
|
||||
* Also contains methods for automatically generating message type
|
||||
* implementations for standard record-based messages.
|
||||
* </p>
|
||||
* @param <T> 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 <T> The message type.
|
||||
*/
|
||||
public record MessageTypeSerializer<T extends Message>(
|
||||
Class<T> messageClass,
|
||||
Function<T, Integer> byteSizeFunction,
|
||||
MessageReader<T> reader,
|
||||
MessageWriter<T> writer
|
||||
) {
|
||||
/**
|
||||
* An internal cache for storing generated type serializers.
|
||||
*/
|
||||
private static final Map<Pair<Class<?>, Serializer>, MessageTypeSerializer<?>> generatedMessageTypes = new HashMap<>();
|
||||
public interface MessageTypeSerializer<T extends Message> {
|
||||
/**
|
||||
* Gets the class of the message type that this serializer handles.
|
||||
* @return The message class.
|
||||
*/
|
||||
Class<T> 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 <T> The type of the message.
|
||||
* @return The message type.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends Message> MessageTypeSerializer<T> get(Serializer serializer, Class<T> messageClass) {
|
||||
return (MessageTypeSerializer<T>) generatedMessageTypes.computeIfAbsent(
|
||||
new Pair<>(messageClass, serializer),
|
||||
p -> generateForRecord(serializer, (Class<T>) p.first())
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Gets a function that computes the size, in bytes, of messages of this
|
||||
* serializer's type.
|
||||
* @return A byte size function.
|
||||
*/
|
||||
Function<T, Integer> byteSizeFunction();
|
||||
|
||||
/**
|
||||
* Generates a message type instance for a given class, using reflection to
|
||||
* introspect the fields of the message.
|
||||
* <p>
|
||||
* Note that this only works for record-based messages.
|
||||
* </p>
|
||||
* @param serializer The serializer context to get a type serializer for.
|
||||
* @param messageTypeClass The class of the message type.
|
||||
* @param <T> The type of the message.
|
||||
* @return A message type instance.
|
||||
*/
|
||||
public static <T extends Message> MessageTypeSerializer<T> generateForRecord(Serializer serializer, Class<T> messageTypeClass) {
|
||||
RecordComponent[] components = messageTypeClass.getRecordComponents();
|
||||
if (components == null) throw new IllegalArgumentException("Cannot generate a MessageTypeSerializer for non-record class " + messageTypeClass.getSimpleName());
|
||||
Constructor<T> 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<T> 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 <T> The message type.
|
||||
* @return A function that computes the byte size of a message of the given
|
||||
* type.
|
||||
*/
|
||||
private static <T extends Message> Function<T, Integer> 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 <T> The message type.
|
||||
* @return A message reader for the given type.
|
||||
*/
|
||||
private static <T extends Message> MessageReader<T> generateReader(Constructor<T> 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 <T> The type of message.
|
||||
* @return The message writer for the given type.
|
||||
*/
|
||||
private static <T extends Message> MessageWriter<T> 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<T> writer();
|
||||
}
|
||||
|
|
|
@ -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 <T> 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<T extends Message>(
|
||||
Class<T> messageClass,
|
||||
Function<T, Integer> byteSizeFunction,
|
||||
MessageReader<T> reader,
|
||||
MessageWriter<T> writer
|
||||
) implements MessageTypeSerializer<T> {}
|
|
@ -40,7 +40,7 @@ public class MessageUtils {
|
|||
if (msg == null) {
|
||||
return 1;
|
||||
} else {
|
||||
MessageTypeSerializer<T> typeSerializer = (MessageTypeSerializer<T>) serializer.getTypeSerializer(msg.getClass());
|
||||
MessageTypeSerializerImpl<T> typeSerializer = (MessageTypeSerializerImpl<T>) serializer.getTypeSerializer(msg.getClass());
|
||||
return 1 + typeSerializer.byteSizeFunction().apply(msg);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Byte, Class<? extends Message>> messageTypes) {
|
||||
public Serializer(Map<Integer, Class<? extends Message>> 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 <T extends Message> void registerType(int id, Class<T> 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 <T> The message type.
|
||||
*/
|
||||
public synchronized <T extends Message> void registerTypeSerializer(int id, MessageTypeSerializer<T> typeSerializer) {
|
||||
if (id < 0 || id > 127) throw new IllegalArgumentException("Invalid id.");
|
||||
messageTypes.put((byte) id, typeSerializer);
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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<Pair<Class<?>, 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 <T> The type of the message.
|
||||
* @return The message type.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends Message> MessageTypeSerializer<T> get(Serializer serializer, Class<T> messageClass) {
|
||||
return (MessageTypeSerializer<T>) generatedMessageTypes.computeIfAbsent(
|
||||
new Pair<>(messageClass, serializer),
|
||||
p -> generateForRecord(serializer, (Class<T>) p.first())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a message type instance for a given class, using reflection to
|
||||
* introspect the fields of the message.
|
||||
* <p>
|
||||
* Note that this only works for record-based messages.
|
||||
* </p>
|
||||
* @param serializer The serializer context to get a type serializer for.
|
||||
* @param messageTypeClass The class of the message type.
|
||||
* @param <T> The type of the message.
|
||||
* @return A message type instance.
|
||||
*/
|
||||
public static <T extends Message> MessageTypeSerializerImpl<T> generateForRecord(Serializer serializer, Class<T> messageTypeClass) {
|
||||
RecordComponent[] components = messageTypeClass.getRecordComponents();
|
||||
if (components == null) throw new IllegalArgumentException("Cannot generate a MessageTypeSerializer for non-record class " + messageTypeClass.getSimpleName());
|
||||
Constructor<T> 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 <T> The message type.
|
||||
* @return A function that computes the byte size of a message of the given
|
||||
* type.
|
||||
*/
|
||||
private static <T extends Message> Function<T, Integer> 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 <T> The message type.
|
||||
* @return A message reader for the given type.
|
||||
*/
|
||||
private static <T extends Message> MessageReader<T> generateReader(Constructor<T> 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 <T> The type of message.
|
||||
* @return The message writer for the given type.
|
||||
*/
|
||||
private static <T extends Message> MessageWriter<T> 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue