Added first version stuff.
This commit is contained in:
		
							parent
							
								
									1bcbc8be54
								
							
						
					
					
						commit
						33715fef02
					
				| 
						 | 
					@ -0,0 +1,3 @@
 | 
				
			||||||
 | 
					.idea/
 | 
				
			||||||
 | 
					target/
 | 
				
			||||||
 | 
					*.iml
 | 
				
			||||||
							
								
								
									
										40
									
								
								README.md
								
								
								
								
							
							
						
						
									
										40
									
								
								README.md
								
								
								
								
							| 
						 | 
					@ -1,2 +1,40 @@
 | 
				
			||||||
# record-net
 | 
					# record-net
 | 
				
			||||||
Simple, performant message library for Java.
 | 
					Simple, performant message library for Java, using records.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					record-net gives you the advantages of reflection, without the runtime costs. By registering message types before starting your work, record-net is able to generate custom serializers and deserializers for all registered message types, which translates to read and write speeds that are nearly equivalent to directly writing bytes to a stream.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 java.io.ByteArrayOutputStream;
 | 
				
			||||||
 | 
					import java.io.IOException;
 | 
				
			||||||
 | 
					import java.net.Socket;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Example {
 | 
				
			||||||
 | 
					    record ChatMessage(
 | 
				
			||||||
 | 
					            long timestamp,
 | 
				
			||||||
 | 
					            String username,
 | 
				
			||||||
 | 
					            String msg
 | 
				
			||||||
 | 
					    ) implements Message {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static void main(String[] args) throws IOException {
 | 
				
			||||||
 | 
					        var ser = new Serializer();
 | 
				
			||||||
 | 
					        ser.registerType(1, ChatMessage.class);
 | 
				
			||||||
 | 
					        var socket = new Socket("127.0.0.1", 8081);
 | 
				
			||||||
 | 
					        var bOut = new ByteArrayOutputStream();
 | 
				
			||||||
 | 
					        var msg = new ChatMessage(
 | 
				
			||||||
 | 
					                System.currentTimeMillis(),
 | 
				
			||||||
 | 
					                "andrew",
 | 
				
			||||||
 | 
					                "Hello world!"
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        ser.writeMessage(msg, socket.getOutputStream());
 | 
				
			||||||
 | 
					        ChatMessage response = (ChatMessage) ser.readMessage(socket.getInputStream());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Get record-net
 | 
				
			||||||
 | 
					This project is published as a package on GitHub. You can view available packages [here](https://github.com/andrewlalis/record-net/packages).
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,43 @@
 | 
				
			||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
				
			||||||
 | 
					         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
				
			||||||
 | 
					         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>
 | 
				
			||||||
 | 
					    <artifactId>record-net</artifactId>
 | 
				
			||||||
 | 
					    <version>1.0.0</version>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <properties>
 | 
				
			||||||
 | 
					        <maven.compiler.source>17</maven.compiler.source>
 | 
				
			||||||
 | 
					        <maven.compiler.target>17</maven.compiler.target>
 | 
				
			||||||
 | 
					        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
				
			||||||
 | 
					    </properties>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <dependencies>
 | 
				
			||||||
 | 
					        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.junit.jupiter</groupId>
 | 
				
			||||||
 | 
					            <artifactId>junit-jupiter-api</artifactId>
 | 
				
			||||||
 | 
					            <version>5.8.2</version>
 | 
				
			||||||
 | 
					            <scope>test</scope>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>org.junit.jupiter</groupId>
 | 
				
			||||||
 | 
					            <artifactId>junit-jupiter-engine</artifactId>
 | 
				
			||||||
 | 
					            <version>5.8.2</version>
 | 
				
			||||||
 | 
					            <scope>test</scope>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					    </dependencies>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- Setup for deploying to GitHub packages with mvn deploy. -->
 | 
				
			||||||
 | 
					    <distributionManagement>
 | 
				
			||||||
 | 
					        <repository>
 | 
				
			||||||
 | 
					            <id>github</id>
 | 
				
			||||||
 | 
					            <name>Github record-net Apache Maven Packages</name>
 | 
				
			||||||
 | 
					            <url>https://maven.pkg.github.com/andrewlalis/record-net</url>
 | 
				
			||||||
 | 
					        </repository>
 | 
				
			||||||
 | 
					    </distributionManagement>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					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() {
 | 
				
			||||||
 | 
							return MessageTypeSerializer.get((Class<T>) this.getClass());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Convenience method to determine the size of this message in bytes.
 | 
				
			||||||
 | 
						 * @return The size of this message, in bytes.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						default int byteSize() {
 | 
				
			||||||
 | 
							return getTypeSerializer().byteSizeFunction().apply(this);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,131 @@
 | 
				
			||||||
 | 
					package 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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Record containing the components needed to read and write a given message.
 | 
				
			||||||
 | 
					 * <p>
 | 
				
			||||||
 | 
					 *     Also contains methods for automatically generating message type
 | 
				
			||||||
 | 
					 *     implementations for standard record-based messages.
 | 
				
			||||||
 | 
					 * </p>
 | 
				
			||||||
 | 
					 * @param <T> The type of message.
 | 
				
			||||||
 | 
					 * @param messageClass The class of the message.
 | 
				
			||||||
 | 
					 * @param byteSizeFunction A function that computes the byte size of the message.
 | 
				
			||||||
 | 
					 * @param reader A reader that can read messages from an input stream.
 | 
				
			||||||
 | 
					 * @param writer A writer that write messages from an input stream.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public record MessageTypeSerializer<T extends Message>(
 | 
				
			||||||
 | 
							Class<T> messageClass,
 | 
				
			||||||
 | 
							Function<T, Integer> byteSizeFunction,
 | 
				
			||||||
 | 
							MessageReader<T> reader,
 | 
				
			||||||
 | 
							MessageWriter<T> writer
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						private static final Map<Class<?>, MessageTypeSerializer<?>> generatedMessageTypes = new HashMap<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Gets the {@link MessageTypeSerializer} instance for a given message class, and
 | 
				
			||||||
 | 
						 * generates a new implementation if none exists yet.
 | 
				
			||||||
 | 
						 * @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(Class<T> messageClass) {
 | 
				
			||||||
 | 
							return (MessageTypeSerializer<T>) generatedMessageTypes.computeIfAbsent(messageClass, c -> generateForRecord((Class<T>) c));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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 messageTypeClass The class of the message type.
 | 
				
			||||||
 | 
						 * @param <T> The type of the message.
 | 
				
			||||||
 | 
						 * @return A message type instance.
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						public static <T extends Message> MessageTypeSerializer<T> generateForRecord(Class<T> messageTypeClass) {
 | 
				
			||||||
 | 
							RecordComponent[] components = messageTypeClass.getRecordComponents();
 | 
				
			||||||
 | 
							if (components == null) throw new IllegalArgumentException("Cannot generate a MessageTypeSerializer for non-record class " + messageTypeClass.getSimpleName());
 | 
				
			||||||
 | 
							Constructor<T> constructor;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								constructor = messageTypeClass.getDeclaredConstructor(Arrays.stream(components)
 | 
				
			||||||
 | 
										.map(RecordComponent::getType).toArray(Class<?>[]::new));
 | 
				
			||||||
 | 
							} catch (NoSuchMethodException e) {
 | 
				
			||||||
 | 
								throw new IllegalArgumentException(e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return new MessageTypeSerializer<>(
 | 
				
			||||||
 | 
									messageTypeClass,
 | 
				
			||||||
 | 
									generateByteSizeFunction(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 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(RecordComponent[] components) {
 | 
				
			||||||
 | 
							return msg -> {
 | 
				
			||||||
 | 
								int size = 0;
 | 
				
			||||||
 | 
								for (var component : components) {
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										size += MessageUtils.getByteSize(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);
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,79 @@
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static int getByteSize(Message msg) {
 | 
				
			||||||
 | 
							return 1 + (msg == null ? 0 : msg.byteSize());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static <T extends Message> int getByteSize(T[] items) {
 | 
				
			||||||
 | 
							int count = Integer.BYTES;
 | 
				
			||||||
 | 
							for (var item : items) {
 | 
				
			||||||
 | 
								count += getByteSize(item);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return count;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static int getByteSize(Object o) {
 | 
				
			||||||
 | 
							if (o instanceof Integer) {
 | 
				
			||||||
 | 
								return Integer.BYTES;
 | 
				
			||||||
 | 
							} else if (o instanceof Long) {
 | 
				
			||||||
 | 
								return Long.BYTES;
 | 
				
			||||||
 | 
							} 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((Message[]) o);
 | 
				
			||||||
 | 
							} else if (o instanceof Message) {
 | 
				
			||||||
 | 
								return getByteSize((Message) o);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new IllegalArgumentException("Unsupported object type: " + o.getClass().getSimpleName());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public static int getByteSize(Object... objects) {
 | 
				
			||||||
 | 
							int size = 0;
 | 
				
			||||||
 | 
							for (var o : objects) {
 | 
				
			||||||
 | 
								size += getByteSize(o);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return size;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,16 @@
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,124 @@
 | 
				
			||||||
 | 
					package nl.andrewl.record_net;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.util.ExtendedDataInputStream;
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.util.ExtendedDataOutputStream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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<>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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<Byte, Class<? extends Message>> messageTypes) {
 | 
				
			||||||
 | 
							messageTypes.forEach(this::registerType);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Helper method which registers a message type to be supported by the
 | 
				
			||||||
 | 
						 * serializer, by adding it to the normal and inverse mappings.
 | 
				
			||||||
 | 
						 * @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) {
 | 
				
			||||||
 | 
							if (id < 0 || id > 127) throw new IllegalArgumentException("Invalid id.");
 | 
				
			||||||
 | 
							MessageTypeSerializer<T> type = MessageTypeSerializer.get(messageClass);
 | 
				
			||||||
 | 
							messageTypes.put((byte)id, type);
 | 
				
			||||||
 | 
							inverseMessageTypes.put(type, (byte)id);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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 {
 | 
				
			||||||
 | 
							ExtendedDataInputStream d = new ExtendedDataInputStream(i);
 | 
				
			||||||
 | 
							byte typeId = d.readByte();
 | 
				
			||||||
 | 
							var type = messageTypes.get(typeId);
 | 
				
			||||||
 | 
							if (type == null) {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported message type: " + typeId);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								return type.reader().read(d);
 | 
				
			||||||
 | 
							} 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 {
 | 
				
			||||||
 | 
							DataOutputStream d = new DataOutputStream(o);
 | 
				
			||||||
 | 
							Byte typeId = inverseMessageTypes.get(msg.getTypeSerializer());
 | 
				
			||||||
 | 
							if (typeId == null) {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							d.writeByte(typeId);
 | 
				
			||||||
 | 
							msg.getTypeSerializer().writer().write(msg, new ExtendedDataOutputStream(d));
 | 
				
			||||||
 | 
							d.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 {
 | 
				
			||||||
 | 
							ByteArrayOutputStream out = new ByteArrayOutputStream(1 + msg.byteSize());
 | 
				
			||||||
 | 
							writeMessage(msg, out);
 | 
				
			||||||
 | 
							return out.toByteArray();
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 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;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,93 @@
 | 
				
			||||||
 | 
					package nl.andrewl.record_net.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.Message;
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.MessageTypeSerializer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
 | 
						public ExtendedDataInputStream(InputStream in) {
 | 
				
			||||||
 | 
							super(in);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						public ExtendedDataInputStream(byte[] data) {
 | 
				
			||||||
 | 
							this(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);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						@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 this.readInt();
 | 
				
			||||||
 | 
							} else if (type.equals(Long.class) || type.equals(long.class)) {
 | 
				
			||||||
 | 
								return this.readLong();
 | 
				
			||||||
 | 
							} else if (type.equals(String.class)) {
 | 
				
			||||||
 | 
								return this.readString();
 | 
				
			||||||
 | 
							} else if (type.equals(UUID.class)) {
 | 
				
			||||||
 | 
								return this.readUUID();
 | 
				
			||||||
 | 
							} else if (type.isEnum()) {
 | 
				
			||||||
 | 
								return this.readEnum((Class<? extends Enum<?>>) type);
 | 
				
			||||||
 | 
							} else if (type.isAssignableFrom(byte[].class)) {
 | 
				
			||||||
 | 
								int length = this.readInt();
 | 
				
			||||||
 | 
								return this.readNBytes(length);
 | 
				
			||||||
 | 
							} else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) {
 | 
				
			||||||
 | 
								var messageType = MessageTypeSerializer.get((Class<? extends Message>) type.getComponentType());
 | 
				
			||||||
 | 
								return this.readArray(messageType);
 | 
				
			||||||
 | 
							} else if (Message.class.isAssignableFrom(type)) {
 | 
				
			||||||
 | 
								var messageType = MessageTypeSerializer.get((Class<? extends Message>) type);
 | 
				
			||||||
 | 
								return messageType.reader().read(this);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								throw new IOException("Unsupported object type: " + type.getSimpleName());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,161 @@
 | 
				
			||||||
 | 
					package nl.andrewl.record_net.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
 | 
						public ExtendedDataOutputStream(OutputStream out) {
 | 
				
			||||||
 | 
							super(out);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 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().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((Integer) o);
 | 
				
			||||||
 | 
							} else if (type.equals(Long.class) || type.equals(long.class)) {
 | 
				
			||||||
 | 
								writeLong((Long) 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.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());
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					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) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					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) {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,4 @@
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Contains some useful one-off utility classes.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package nl.andrewl.record_net.util;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					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 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 MessageTypeSerializerTest {
 | 
				
			||||||
 | 
					    @Test
 | 
				
			||||||
 | 
					    public void testGenerateForRecord() throws IOException {
 | 
				
			||||||
 | 
					        var s1 = MessageTypeSerializer.get(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));
 | 
				
			||||||
 | 
					        assertEquals(expectedByteSize, msg.byteSize());
 | 
				
			||||||
 | 
					        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
 | 
				
			||||||
 | 
					        ExtendedDataOutputStream eOut = new ExtendedDataOutputStream(bOut);
 | 
				
			||||||
 | 
					        s1.writer().write(msg, eOut);
 | 
				
			||||||
 | 
					        byte[] data = bOut.toByteArray();
 | 
				
			||||||
 | 
					        assertEquals(expectedByteSize, data.length);
 | 
				
			||||||
 | 
					        ChatMessage readMsg = s1.reader().read(new ExtendedDataInputStream(data));
 | 
				
			||||||
 | 
					        assertEquals(msg, readMsg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Only record classes can be generated.
 | 
				
			||||||
 | 
					        class NonRecordMessage implements Message {}
 | 
				
			||||||
 | 
					        assertThrows(IllegalArgumentException.class, () -> MessageTypeSerializer.get(NonRecordMessage.class));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					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;
 | 
				
			||||||
 | 
					        assertEquals(1, MessageUtils.getByteSize((Message) null));
 | 
				
			||||||
 | 
					        assertEquals(expectedMsgSize, MessageUtils.getByteSize(msg));
 | 
				
			||||||
 | 
					        assertEquals(4 * expectedMsgSize, MessageUtils.getByteSize(msg, msg, msg, msg));
 | 
				
			||||||
 | 
					        assertEquals(16, MessageUtils.getByteSize(UUID.randomUUID()));
 | 
				
			||||||
 | 
					        assertEquals(4, MessageUtils.getByteSize(StandardCopyOption.ATOMIC_MOVE));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,29 @@
 | 
				
			||||||
 | 
					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(1 + msg.byteSize(), data.length);
 | 
				
			||||||
 | 
					        assertEquals(data[0], 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ChatMessage readMsg = (ChatMessage) s.readMessage(new ByteArrayInputStream(data));
 | 
				
			||||||
 | 
					        assertEquals(msg, readMsg);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					package nl.andrewl.record_net.msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nl.andrewl.record_net.Message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					public record ChatMessage(
 | 
				
			||||||
 | 
					        String username,
 | 
				
			||||||
 | 
					        long timestamp,
 | 
				
			||||||
 | 
					        String message
 | 
				
			||||||
 | 
					) implements Message {}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					package nl.andrewl.record_net.util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 {
 | 
				
			||||||
 | 
					        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
 | 
				
			||||||
 | 
					        ExtendedDataOutputStream eOut = new ExtendedDataOutputStream(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