Refactored completely for version 2.0.0

This commit is contained in:
Andrew Lalis 2023-09-21 18:35:19 -04:00
parent 275316a531
commit 3fc0efca5f
30 changed files with 505 additions and 983 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
module com.andrewlalis.record_net {
exports com.andrewlalis.record_net;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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