Added first version stuff.

This commit is contained in:
Andrew Lalis 2022-04-16 13:03:21 +02:00
parent 1bcbc8be54
commit 33715fef02
20 changed files with 895 additions and 1 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.idea/
target/
*.iml

View File

@ -1,2 +1,40 @@
# 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).

43
pom.xml Normal file
View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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);
}
}
};
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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());
}
}
}

View File

@ -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());
}
}
}

View File

@ -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) {}

View File

@ -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) {}

View File

@ -0,0 +1,4 @@
/**
* Contains some useful one-off utility classes.
*/
package nl.andrewl.record_net.util;

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -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());
}
}