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