Refactored completely for version 2.0.0
This commit is contained in:
parent
275316a531
commit
3fc0efca5f
|
@ -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();
|
||||
|
|
27
pom.xml
27
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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>nl.andrewl</groupId>
|
||||
<groupId>com.andrewlalis</groupId>
|
||||
<artifactId>record-net</artifactId>
|
||||
<version>1.3.4</version>
|
||||
<version>2.0.0</version>
|
||||
<name>Record-Net</name>
|
||||
<description>A simple library for reading and writing records deterministically and efficiently.</description>
|
||||
<url>https://github.com/andrewlalis/record-net</url>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
|
@ -19,7 +22,7 @@
|
|||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.8.2</version>
|
||||
<version>5.9.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
|
@ -27,7 +30,7 @@
|
|||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<version>5.8.2</version>
|
||||
<version>5.9.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
@ -40,4 +43,16 @@
|
|||
<url>https://maven.pkg.github.com/andrewlalis/record-net</url>
|
||||
</repository>
|
||||
</distributionManagement>
|
||||
|
||||
<scm>
|
||||
<url>https://github.com/andrewlalis/record-net</url>
|
||||
</scm>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT License</name>
|
||||
<url>https://github.com/andrewlalis/record-net/blob/main/LICENSE</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
</project>
|
|
@ -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 extends Enum<?>> 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.andrewlalis.record_net;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
|
||||
record RecordInfo<T>(RecordComponent[] components, Constructor<T> constructor) {
|
||||
public static <T> RecordInfo<T> forType(Class<T> 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<T> ctor = type.getDeclaredConstructor(paramTypes);
|
||||
return new RecordInfo<>(c, ctor);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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<Integer, Class<?>> messageTypes = new HashMap<>();
|
||||
private final Map<Class<?>, Integer> messageTypeIds = new HashMap<>();
|
||||
private final Map<Class<?>, 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module com.andrewlalis.record_net {
|
||||
exports com.andrewlalis.record_net;
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package nl.andrewl.record_net;
|
||||
|
||||
/**
|
||||
* Represents any message which can be sent over the network.
|
||||
* <p>
|
||||
* All messages consist of a single byte type identifier, followed by a
|
||||
* payload whose structure depends on the message.
|
||||
* </p>
|
||||
*/
|
||||
public interface Message {
|
||||
/**
|
||||
* Convenience method to get the serializer for this message's type, using
|
||||
* the static auto-generated set of serializers.
|
||||
* @param <T> The message type.
|
||||
* @return The serializer to use to read and write messages of this type.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
default <T extends Message> MessageTypeSerializer<T> getTypeSerializer(Serializer serializer) {
|
||||
return (MessageTypeSerializer<T>) serializer.getTypeSerializer(this.getClass());
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
package nl.andrewl.record_net;
|
||||
|
||||
import nl.andrewl.record_net.util.ExtendedDataInputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MessageReader<T extends Message>{
|
||||
/**
|
||||
* Reads all of this message's properties from the given input stream.
|
||||
* <p>
|
||||
* The single byte type identifier has already been read.
|
||||
* </p>
|
||||
* @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;
|
||||
}
|
|
@ -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 <T> The message type.
|
||||
*/
|
||||
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 a function that computes the size, in bytes, of messages of this
|
||||
* serializer's type.
|
||||
* @return A byte size function.
|
||||
*/
|
||||
Function<T, Integer> byteSizeFunction();
|
||||
|
||||
/**
|
||||
* Gets a component that can read messages from an input stream.
|
||||
* @return The message reader.
|
||||
*/
|
||||
MessageReader<T> reader();
|
||||
|
||||
/**
|
||||
* Gets a component that can write messages to an output stream.
|
||||
* @return The message writer.
|
||||
*/
|
||||
MessageWriter<T> writer();
|
||||
}
|
|
@ -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 <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> {}
|
|
@ -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 <T extends Message> int getByteSize(Serializer serializer, T msg) {
|
||||
if (msg == null) {
|
||||
return 1;
|
||||
} else {
|
||||
MessageTypeSerializerImpl<T> typeSerializer = (MessageTypeSerializerImpl<T>) serializer.getTypeSerializer(msg.getClass());
|
||||
return 1 + typeSerializer.byteSizeFunction().apply(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T extends Message> 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;
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package nl.andrewl.record_net;
|
||||
|
||||
import nl.andrewl.record_net.util.ExtendedDataOutputStream;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface MessageWriter<T extends Message> {
|
||||
/**
|
||||
* 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;
|
||||
}
|
|
@ -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<Byte, MessageTypeSerializer<?>> messageTypes = new HashMap<>();
|
||||
|
||||
private final Map<Class<?>, 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<MessageTypeSerializer<?>, 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<Integer, Class<? extends Message>> 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 <T extends Message> void registerType(int id, Class<T> 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);
|
||||
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 <T> The type of message.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Message> MessageTypeSerializer<T> getTypeSerializer(Class<T> messageType) {
|
||||
return (MessageTypeSerializer<T>) 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 <T> The message type.
|
||||
* @throws IOException If an error occurs while writing, or if the message
|
||||
* to write is not supported by this serializer.
|
||||
*/
|
||||
public <T extends Message> void writeMessage(T msg, OutputStream o) throws IOException {
|
||||
writeMessage(msg, new ExtendedDataOutputStream(this, o));
|
||||
}
|
||||
|
||||
public <T extends Message> 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 <T> The message type.
|
||||
* @throws IOException If an error occurs while writing, or if the message
|
||||
* to write is not supported by this serializer.
|
||||
*/
|
||||
public <T extends Message> 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();
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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 extends Enum<?>> T readEnum(Class<T> 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 extends Message> T[] readArray(MessageTypeSerializer<T> 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<? extends Enum<?>>) 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<? extends Message>) type.getComponentType());
|
||||
return readArray(messageType);
|
||||
} else if (Message.class.isAssignableFrom(type)) {
|
||||
var messageType = RecordMessageTypeSerializer.get(serializer, (Class<? extends Message>) type);
|
||||
return messageType.reader().read(this);
|
||||
} else {
|
||||
throw new IOException("Unsupported object type: " + type.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <T> The type of items in the array.
|
||||
* @throws IOException If an error occurs while writing.
|
||||
*/
|
||||
public <T extends Message> 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 <T> The type of the message.
|
||||
* @throws IOException If an error occurs while writing.
|
||||
*/
|
||||
public <T extends Message> 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package nl.andrewl.record_net.util;
|
||||
|
||||
/**
|
||||
* Simple generic pair of two objects.
|
||||
* @param <A> The first object.
|
||||
* @param <B> The second object.
|
||||
*/
|
||||
public record Pair<A, B>(A first, B second) {}
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package nl.andrewl.record_net.util;
|
||||
|
||||
/**
|
||||
* Simple generic triple of objects.
|
||||
* @param <A> The first object.
|
||||
* @param <B> The second object.
|
||||
* @param <C> The third object.
|
||||
*/
|
||||
public record Triple<A, B, C> (A first, B second, C third) {}
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* Contains some useful one-off utility classes.
|
||||
*/
|
||||
package nl.andrewl.record_net.util;
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue