diff --git a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
index 377afc2..75720bb 100644
--- a/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
+++ b/client/src/main/java/nl/andrewl/concord_client/ConcordClient.java
@@ -20,6 +20,12 @@ import nl.andrewl.concord_core.msg.Encryption;
import nl.andrewl.concord_core.msg.Message;
import nl.andrewl.concord_core.msg.Serializer;
import nl.andrewl.concord_core.msg.types.*;
+import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
+import nl.andrewl.concord_core.msg.types.client_setup.Identification;
+import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
import java.io.IOException;
import java.io.InputStream;
@@ -84,8 +90,8 @@ public class ConcordClient implements Runnable {
this.serializer.writeMessage(new Identification(nickname, token), this.out);
Message reply = this.serializer.readMessage(this.in);
if (reply instanceof ServerWelcome welcome) {
- var model = new ClientModel(welcome.getClientId(), nickname, welcome.getCurrentChannelId(), welcome.getCurrentChannelName(), welcome.getMetaData());
- this.saveSessionToken(welcome.getSessionToken(), tokensFile);
+ var model = new ClientModel(welcome.clientId(), nickname, welcome.currentChannelId(), welcome.currentChannelName(), welcome.metaData());
+ this.saveSessionToken(welcome.sessionToken(), tokensFile);
// Start fetching initial data for the channel we were initially put into.
this.sendMessage(new ChatHistoryRequest(model.getCurrentChannelId(), ""));
return model;
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java b/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java
index dd78493..6b8c1db 100644
--- a/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java
+++ b/client/src/main/java/nl/andrewl/concord_client/event/ChatHistoryListener.java
@@ -1,7 +1,7 @@
package nl.andrewl.concord_client.event;
import nl.andrewl.concord_client.model.ChatHistory;
-import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
public interface ChatHistoryListener {
default void chatAdded(Chat chat) {}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java
index f955c93..d5c15bd 100644
--- a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java
+++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChannelMovedHandler.java
@@ -2,10 +2,8 @@ package nl.andrewl.concord_client.event.handlers;
import nl.andrewl.concord_client.ConcordClient;
import nl.andrewl.concord_client.event.MessageHandler;
-import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
-import nl.andrewl.concord_core.msg.types.MoveToChannel;
-
-import java.util.Map;
+import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
/**
* When the client receives a {@link MoveToChannel} message, it means that the
@@ -16,7 +14,7 @@ import java.util.Map;
public class ChannelMovedHandler implements MessageHandler {
@Override
public void handle(MoveToChannel msg, ConcordClient client) throws Exception {
- client.getModel().setCurrentChannel(msg.getId(), msg.getChannelName());
- client.sendMessage(new ChatHistoryRequest(msg.getId()));
+ client.getModel().setCurrentChannel(msg.id(), msg.channelName());
+ client.sendMessage(new ChatHistoryRequest(msg.id()));
}
}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.java
index e914801..103394e 100644
--- a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.java
+++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ChatHistoryResponseHandler.java
@@ -2,11 +2,14 @@ package nl.andrewl.concord_client.event.handlers;
import nl.andrewl.concord_client.ConcordClient;
import nl.andrewl.concord_client.event.MessageHandler;
-import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
+
+import java.util.Arrays;
+import java.util.List;
public class ChatHistoryResponseHandler implements MessageHandler {
@Override
public void handle(ChatHistoryResponse msg, ConcordClient client) {
- client.getModel().getChatHistory().setChats(msg.getMessages());
+ client.getModel().getChatHistory().setChats(Arrays.asList(msg.messages()));
}
}
diff --git a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerUsersHandler.java b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerUsersHandler.java
index 3fd949b..a7c6a38 100644
--- a/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerUsersHandler.java
+++ b/client/src/main/java/nl/andrewl/concord_client/event/handlers/ServerUsersHandler.java
@@ -4,9 +4,11 @@ import nl.andrewl.concord_client.ConcordClient;
import nl.andrewl.concord_client.event.MessageHandler;
import nl.andrewl.concord_core.msg.types.ServerUsers;
+import java.util.Arrays;
+
public class ServerUsersHandler implements MessageHandler {
@Override
public void handle(ServerUsers msg, ConcordClient client) {
- client.getModel().setKnownUsers(msg.getUsers());
+ client.getModel().setKnownUsers(Arrays.asList(msg.users()));
}
}
diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java
index df8c495..0bc5e5c 100644
--- a/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java
+++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChannelList.java
@@ -5,7 +5,7 @@ import com.googlecode.lanterna.gui2.Direction;
import com.googlecode.lanterna.gui2.LinearLayout;
import com.googlecode.lanterna.gui2.Panel;
import nl.andrewl.concord_client.ConcordClient;
-import nl.andrewl.concord_core.msg.types.MoveToChannel;
+import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
import java.io.IOException;
@@ -22,15 +22,15 @@ public class ChannelList extends Panel {
public void setChannels() {
this.removeAllComponents();
- for (var channel : this.client.getModel().getServerMetaData().getChannels()) {
- String name = channel.getName();
- if (client.getModel().getCurrentChannelId().equals(channel.getId())) {
+ for (var channel : this.client.getModel().getServerMetaData().channels()) {
+ String name = channel.name();
+ if (client.getModel().getCurrentChannelId().equals(channel.id())) {
name = "*" + name;
}
Button b = new Button(name, () -> {
- if (!client.getModel().getCurrentChannelId().equals(channel.getId())) {
+ if (!client.getModel().getCurrentChannelId().equals(channel.id())) {
try {
- client.sendMessage(new MoveToChannel(channel.getId()));
+ client.sendMessage(new MoveToChannel(channel.id()));
} catch (IOException e) {
e.printStackTrace();
}
diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java
index 8543f05..07cc977 100644
--- a/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java
+++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChatList.java
@@ -3,7 +3,7 @@ package nl.andrewl.concord_client.gui;
import com.googlecode.lanterna.gui2.AbstractListBox;
import nl.andrewl.concord_client.event.ChatHistoryListener;
import nl.andrewl.concord_client.model.ChatHistory;
-import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
/**
* This chat list shows a section of chat messages that have been sent in a
diff --git a/client/src/main/java/nl/andrewl/concord_client/gui/ChatRenderer.java b/client/src/main/java/nl/andrewl/concord_client/gui/ChatRenderer.java
index d3678e5..d245dcb 100644
--- a/client/src/main/java/nl/andrewl/concord_client/gui/ChatRenderer.java
+++ b/client/src/main/java/nl/andrewl/concord_client/gui/ChatRenderer.java
@@ -4,7 +4,7 @@ import com.googlecode.lanterna.TerminalTextUtils;
import com.googlecode.lanterna.graphics.ThemeDefinition;
import com.googlecode.lanterna.gui2.AbstractListBox;
import com.googlecode.lanterna.gui2.TextGUIGraphics;
-import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
import java.time.Instant;
import java.time.ZoneId;
@@ -20,10 +20,10 @@ public class ChatRenderer extends AbstractListBox.ListItemRenderer usersResponse) {
this.removeAllComponents();
for (var user : usersResponse) {
- Button b = new Button(user.getName(), () -> {
- if (!client.getModel().getId().equals(user.getId())) {
- System.out.println("Opening DM channel with user " + user.getName() + ", id: " + user.getId());
+ Button b = new Button(user.name(), () -> {
+ if (!client.getModel().getId().equals(user.id())) {
+ System.out.println("Opening DM channel with user " + user.name() + ", id: " + user.id());
try {
- client.sendMessage(new MoveToChannel(user.getId()));
+ client.sendMessage(new MoveToChannel(user.id()));
} catch (IOException e) {
e.printStackTrace();
}
diff --git a/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java b/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java
index 2014f43..28c57f3 100644
--- a/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java
+++ b/client/src/main/java/nl/andrewl/concord_client/model/ChatHistory.java
@@ -2,7 +2,7 @@ package nl.andrewl.concord_client.model;
import lombok.Getter;
import nl.andrewl.concord_client.event.ChatHistoryListener;
-import nl.andrewl.concord_core.msg.types.Chat;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
diff --git a/core/src/main/java/module-info.java b/core/src/main/java/module-info.java
index ee98e11..cbbf0fa 100644
--- a/core/src/main/java/module-info.java
+++ b/core/src/main/java/module-info.java
@@ -3,5 +3,9 @@ module concord_core {
exports nl.andrewl.concord_core.util to concord_server, concord_client;
exports nl.andrewl.concord_core.msg to concord_server, concord_client;
+
exports nl.andrewl.concord_core.msg.types to concord_server, concord_client;
+ exports nl.andrewl.concord_core.msg.types.client_setup to concord_client, concord_server;
+ exports nl.andrewl.concord_core.msg.types.chat to concord_client, concord_server;
+ exports nl.andrewl.concord_core.msg.types.channel to concord_client, concord_server;
}
\ No newline at end of file
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java b/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java
index 2590c60..ddb4315 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/Encryption.java
@@ -1,6 +1,6 @@
package nl.andrewl.concord_core.msg;
-import nl.andrewl.concord_core.msg.types.KeyData;
+import nl.andrewl.concord_core.msg.types.client_setup.KeyData;
import nl.andrewl.concord_core.util.Pair;
import javax.crypto.Cipher;
@@ -64,20 +64,20 @@ public class Encryption {
// Receive and decode client's unencrypted key data.
KeyData clientKeyData = (KeyData) serializer.readMessage(in);
PublicKey clientPublicKey = KeyFactory.getInstance("EC")
- .generatePublic(new X509EncodedKeySpec(clientKeyData.getPublicKey()));
+ .generatePublic(new X509EncodedKeySpec(clientKeyData.publicKey()));
// Compute secret key from client's public key and our private key.
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
ka.init(keyPair.getPrivate());
ka.doPhase(clientPublicKey, true);
- byte[] secretKey = computeSecretKey(ka.generateSecret(), publicKey, clientKeyData.getPublicKey());
+ byte[] secretKey = computeSecretKey(ka.generateSecret(), publicKey, clientKeyData.publicKey());
// Initialize cipher streams.
Cipher writeCipher = Cipher.getInstance("AES/CFB8/NoPadding");
Cipher readCipher = Cipher.getInstance("AES/CFB8/NoPadding");
Key cipherKey = new SecretKeySpec(secretKey, "AES");
writeCipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(iv));
- readCipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(clientKeyData.getIv()));
+ readCipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(clientKeyData.iv()));
return new Pair<>(
new CipherInputStream(in, readCipher),
new CipherOutputStream(out, writeCipher)
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Message.java b/core/src/main/java/nl/andrewl/concord_core/msg/Message.java
index f307ef0..3be10cb 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/Message.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/Message.java
@@ -1,11 +1,5 @@
package nl.andrewl.concord_core.msg;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.UUID;
-
/**
* Represents any message which can be sent over the network.
*
@@ -14,26 +8,12 @@ import java.util.UUID;
*
*/
public interface Message {
- /**
- * @return The exact number of bytes that this message will use when written
- * to a stream.
- */
- int getByteCount();
+ @SuppressWarnings("unchecked")
+ default MessageType getType() {
+ return MessageType.get((Class) this.getClass());
+ }
- /**
- * Writes this message to the given output stream.
- * @param o The output stream to write to.
- * @throws IOException If an error occurs while writing.
- */
- void write(DataOutputStream o) throws IOException;
-
- /**
- * Reads all of this message's properties from the given input stream.
- *
- * The single byte type identifier has already been read.
- *
- * @param i The input stream to read from.
- * @throws IOException If an error occurs while reading.
- */
- void read(DataInputStream i) throws IOException;
+ default int byteSize() {
+ return getType().byteSizeFunction().apply(this);
+ }
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/MessageReader.java b/core/src/main/java/nl/andrewl/concord_core/msg/MessageReader.java
new file mode 100644
index 0000000..7c2a8ae
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/MessageReader.java
@@ -0,0 +1,19 @@
+package nl.andrewl.concord_core.msg;
+
+import nl.andrewl.concord_core.util.ExtendedDataInputStream;
+
+import java.io.IOException;
+
+@FunctionalInterface
+public interface MessageReader{
+ /**
+ * Reads all of this message's properties from the given input stream.
+ *
+ * The single byte type identifier has already been read.
+ *
+ * @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;
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java b/core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java
new file mode 100644
index 0000000..c70bc14
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java
@@ -0,0 +1,130 @@
+package nl.andrewl.concord_core.msg;
+
+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.
+ *
+ * Also contains methods for automatically generating message type
+ * implementations for standard record-based messages.
+ *
+ * @param 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 MessageType(
+ Class messageClass,
+ Function byteSizeFunction,
+ MessageReader reader,
+ MessageWriter writer
+) {
+ private static final Map, MessageType>> generatedMessageTypes = new HashMap<>();
+
+ /**
+ * Gets the {@link MessageType} 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 The type of the message.
+ * @return The message type.
+ */
+ @SuppressWarnings("unchecked")
+ public static MessageType get(Class messageClass) {
+ return (MessageType) generatedMessageTypes.computeIfAbsent(messageClass, c -> generateForRecord((Class) c));
+ }
+
+ /**
+ * Generates a message type instance for a given class, using reflection to
+ * introspect the fields of the message.
+ *
+ * Note that this only works for record-based messages.
+ *
+ * @param messageTypeClass The class of the message type.
+ * @param The type of the message.
+ * @return A message type instance.
+ */
+ public static MessageType generateForRecord(Class messageTypeClass) {
+ RecordComponent[] components = messageTypeClass.getRecordComponents();
+ Constructor constructor;
+ try {
+ constructor = messageTypeClass.getDeclaredConstructor(Arrays.stream(components)
+ .map(RecordComponent::getType).toArray(Class>[]::new));
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(e);
+ }
+ return new MessageType<>(
+ 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 The message type.
+ * @return A function that computes the byte size of a message of the given
+ * type.
+ */
+ private static Function 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 The message type.
+ * @return A message reader for the given type.
+ */
+ private static MessageReader generateReader(Constructor 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 The type of message.
+ * @return The message writer for the given type.
+ */
+ private static MessageWriter 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);
+ }
+ }
+ };
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java b/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java
index 9617f04..b813e14 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/MessageUtils.java
@@ -1,11 +1,6 @@
package nl.andrewl.concord_core.msg;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
import java.util.UUID;
/**
@@ -14,6 +9,7 @@ import java.util.UUID;
*/
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
@@ -26,118 +22,54 @@ public class MessageUtils {
}
/**
- * Writes a string to the given output stream using a length-prefixed format
- * where an integer length precedes the string's bytes, which are encoded in
- * UTF-8.
- * @param s The string to write.
- * @param o The output stream to write to.
- * @throws IOException If the stream could not be written to.
+ * 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 void writeString(String s, DataOutputStream o) throws IOException {
- if (s == null) {
- o.writeInt(-1);
- } else {
- o.writeInt(s.length());
- o.write(s.getBytes(StandardCharsets.UTF_8));
+ public static int getByteSize(String... strings) {
+ int size = 0;
+ for (var s : strings) {
+ size += getByteSize(s);
}
+ return size;
}
- /**
- * Reads a string from the given input stream, using a length-prefixed
- * format, where an integer length precedes the string's bytes, which are
- * encoded in UTF-8.
- * @param i The input stream to read from.
- * @return The string which was read.
- * @throws IOException If the stream could not be read, or if the string is
- * malformed.
- */
- public static String readString(DataInputStream i) throws IOException {
- int length = i.readInt();
- if (length == -1) return null;
- if (length == 0) return "";
- byte[] data = new byte[length];
- int read = i.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);
- }
-
- /**
- * Writes an enum value to the given stream as the integer ordinal value of
- * the enum value, or -1 if the value is null.
- * @param value The value to write.
- * @param o The output stream.
- * @throws IOException If an error occurs while writing.
- */
- public static void writeEnum(Enum> value, DataOutputStream o) throws IOException {
- if (value == null) {
- o.writeInt(-1);
- } else {
- o.writeInt(value.ordinal());
- }
- }
-
- /**
- * Reads an enum value from the given stream, assuming that the value is
- * represented by an integer ordinal value.
- * @param e The type of enum that is to be read.
- * @param i The input stream to read from.
- * @param The enum type.
- * @return The enum value, or null if -1 was read.
- * @throws IOException If an error occurs while reading.
- */
- public static > T readEnum(Class e, DataInputStream i) throws IOException {
- int ordinal = i.readInt();
- if (ordinal == -1) return null;
- return e.getEnumConstants()[ordinal];
- }
-
- public static void writeUUID(UUID value, DataOutputStream o) throws IOException {
- if (value == null) {
- o.writeLong(-1);
- o.writeLong(-1);
- } else {
- o.writeLong(value.getMostSignificantBits());
- o.writeLong(value.getLeastSignificantBits());
- }
- }
-
- public static UUID readUUID(DataInputStream i) throws IOException {
- long a = i.readLong();
- long b = i.readLong();
- if (a == -1 && b == -1) {
- return null;
- }
- return new UUID(a, b);
- }
-
- public static int getByteSize(List extends Message> items) {
+ public static int getByteSize(T[] items) {
int count = Integer.BYTES;
for (var item : items) {
- count += item.getByteCount();
+ count += item.byteSize();
}
return count;
}
- public static void writeList(List extends Message> items, DataOutputStream o) throws IOException {
- o.writeInt(items.size());
- for (var i : items) {
- i.write(o);
+ 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 ((Message) o).byteSize();
+ } else {
+ throw new IllegalArgumentException("Unsupported object type: " + o.getClass().getSimpleName());
}
}
- public static List readList(Class type, DataInputStream i) throws IOException {
- int size = i.readInt();
- try {
- var constructor = type.getConstructor();
- List items = new ArrayList<>(size);
- for (int k = 0; k < size; k++) {
- var item = constructor.newInstance();
- item.read(i);
- items.add(item);
- }
- return items;
- } catch (ReflectiveOperationException e) {
- throw new IOException(e);
+ public static int getByteSize(Object... objects) {
+ int size = 0;
+ for (var o : objects) {
+ size += getByteSize(o);
}
+ return size;
}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/MessageWriter.java b/core/src/main/java/nl/andrewl/concord_core/msg/MessageWriter.java
new file mode 100644
index 0000000..e4253b0
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/MessageWriter.java
@@ -0,0 +1,16 @@
+package nl.andrewl.concord_core.msg;
+
+import nl.andrewl.concord_core.util.ChainedDataOutputStream;
+
+import java.io.IOException;
+
+@FunctionalInterface
+public interface MessageWriter {
+ /**
+ * 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, ChainedDataOutputStream out) throws IOException;
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java
index 263a089..f7b8945 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/Serializer.java
@@ -1,9 +1,24 @@
package nl.andrewl.concord_core.msg;
import nl.andrewl.concord_core.msg.types.Error;
-import nl.andrewl.concord_core.msg.types.*;
+import nl.andrewl.concord_core.msg.types.ServerMetaData;
+import nl.andrewl.concord_core.msg.types.ServerUsers;
+import nl.andrewl.concord_core.msg.types.channel.CreateThread;
+import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
+import nl.andrewl.concord_core.msg.types.client_setup.Identification;
+import nl.andrewl.concord_core.msg.types.client_setup.KeyData;
+import nl.andrewl.concord_core.msg.types.client_setup.Registration;
+import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
+import nl.andrewl.concord_core.util.ChainedDataOutputStream;
+import nl.andrewl.concord_core.util.ExtendedDataInputStream;
-import java.io.*;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
@@ -17,13 +32,13 @@ 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> messageTypes = new HashMap<>();
+ private final Map> 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, Byte> inverseMessageTypes = new HashMap<>();
+ private final Map, Byte> inverseMessageTypes = new HashMap<>();
/**
* Constructs a new serializer instance, with a standard set of supported
@@ -36,7 +51,7 @@ public class Serializer {
registerType(3, MoveToChannel.class);
registerType(4, ChatHistoryRequest.class);
registerType(5, ChatHistoryResponse.class);
- // Type id 6 removed due to deprecation.
+ registerType(6, Registration.class);
registerType(7, ServerUsers.class);
registerType(8, ServerMetaData.class);
registerType(9, Error.class);
@@ -49,12 +64,12 @@ public class Serializer {
* 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 class of message which is registered with the
- * given byte identifier.
+ * @param messageClass The type of message associated with the given id.
*/
- private synchronized void registerType(int id, Class extends Message> messageClass) {
- messageTypes.put((byte) id, messageClass);
- inverseMessageTypes.put(messageClass, (byte) id);
+ private synchronized void registerType(int id, Class messageClass) {
+ MessageType type = MessageType.get(messageClass);
+ messageTypes.put((byte) id, type);
+ inverseMessageTypes.put(type, (byte) id);
}
/**
@@ -67,19 +82,16 @@ public class Serializer {
* constructed for the incoming data.
*/
public Message readMessage(InputStream i) throws IOException {
- DataInputStream d = new DataInputStream(i);
- byte type = d.readByte();
- var clazz = messageTypes.get(type);
- if (clazz == null) {
- throw new IOException("Unsupported message type: " + type);
+ 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 {
- var constructor = clazz.getConstructor();
- var message = constructor.newInstance();
- message.read(d);
- return message;
+ return type.reader().read(d);
} catch (Throwable e) {
- throw new IOException("Could not instantiate new message object of type " + clazz.getSimpleName(), e);
+ throw new IOException("Could not instantiate new message object of type " + type.getClass().getSimpleName(), e);
}
}
@@ -90,14 +102,14 @@ public class Serializer {
* @throws IOException If an error occurs while writing, or if the message
* to write is not supported by this serializer.
*/
- public void writeMessage(Message msg, OutputStream o) throws IOException {
+ public void writeMessage(Message msg, OutputStream o) throws IOException {
DataOutputStream d = new DataOutputStream(o);
- Byte type = inverseMessageTypes.get(msg.getClass());
- if (type == null) {
+ Byte typeId = inverseMessageTypes.get(msg.getType());
+ if (typeId == null) {
throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
}
- d.writeByte(type);
- msg.write(d);
+ d.writeByte(typeId);
+ msg.getType().writer().write(msg, new ChainedDataOutputStream(d));
d.flush();
}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java
deleted file mode 100644
index 2292d97..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.Objects;
-import java.util.UUID;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
-/**
- * This message contains information about a chat message that a user sent.
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class Chat implements Message {
- private static final long ID_NONE = 0;
-
- private UUID id;
- private UUID senderId;
- private String senderNickname;
- private long timestamp;
- private String message;
-
- public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
- this.id = null;
- this.senderId = senderId;
- this.senderNickname = senderNickname;
- this.timestamp = timestamp;
- this.message = message;
- }
-
- public Chat(String message) {
- this(null, null, System.currentTimeMillis(), message);
- }
-
- @Override
- public int getByteCount() {
- return 2 * UUID_BYTES + Long.BYTES + getByteSize(this.senderNickname) + getByteSize(this.message);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.id, o);
- writeUUID(this.senderId, o);
- writeString(this.senderNickname, o);
- o.writeLong(this.timestamp);
- writeString(this.message, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.id = readUUID(i);
- this.senderId = readUUID(i);
- this.senderNickname = readString(i);
- this.timestamp = i.readLong();
- this.message = readString(i);
- }
-
- @Override
- public String toString() {
- return String.format("%s: %s", this.senderNickname, this.message);
- }
-
- @Override
- public boolean equals(Object o) {
- if (o.getClass().equals(this.getClass())) {
- Chat other = (Chat) o;
- if (Objects.equals(this.getId(), other.getId())) return true;
- return this.getSenderId().equals(other.getSenderId()) &&
- this.getTimestamp() == other.getTimestamp() &&
- this.getSenderNickname().equals(other.getSenderNickname()) &&
- this.message.length() == other.message.length();
- }
- return false;
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java
deleted file mode 100644
index 6a93801..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.List;
-import java.util.UUID;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
-/**
- * The response that a server sends to a {@link ChatHistoryRequest}. The list of
- * messages is ordered by timestamp, with the newest messages appearing first.
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class ChatHistoryResponse implements Message {
- private UUID channelId;
- List messages;
-
- @Override
- public int getByteCount() {
- return UUID_BYTES + getByteSize(messages);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.channelId, o);
- writeList(this.messages, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.channelId = readUUID(i);
- this.messages = readList(Chat.class, i);
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/CreateThread.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/CreateThread.java
deleted file mode 100644
index e9b47e7..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/CreateThread.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.UUID;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
-/**
- * This message is sent by clients when they indicate that they would like to
- * create a new thread in their current channel.
- *
- * Conversely, this message is also sent by the server when a thread has
- * been created by someone, and all clients need to be notified so that they
- * can properly display to the user that a message has been turned into a
- * thread.
- *
- */
-@Data
-@NoArgsConstructor
-public class CreateThread implements Message {
- /**
- * The id of the message from which the thread will be created. This will
- * serve as the entry point of the thread, and the unique identifier for the
- * thread.
- */
- private UUID messageId;
-
- /**
- * The title for the thread. This may be null, in which case the thread does
- * not have any title.
- */
- private String title;
-
- @Override
- public int getByteCount() {
- return UUID_BYTES + getByteSize(title);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.messageId, o);
- writeString(this.title, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.messageId = readUUID(i);
- this.title = readString(i);
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java
index 07de9e0..6331bb4 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/Error.java
@@ -1,24 +1,15 @@
package nl.andrewl.concord_core.msg.types;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
/**
* Error message which can be sent between either the server or client to
* indicate an unsavory situation.
*/
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class Error implements Message {
+public record Error (
+ Level level,
+ String message
+) implements Message {
/**
* The error level gives an indication as to the severity of the error.
* Warnings indicate that a user has attempted to do something which they
@@ -27,9 +18,6 @@ public class Error implements Message {
*/
public enum Level {WARNING, ERROR}
- private Level level;
- private String message;
-
public static Error warning(String message) {
return new Error(Level.WARNING, message);
}
@@ -37,21 +25,4 @@ public class Error implements Message {
public static Error error(String message) {
return new Error(Level.ERROR, message);
}
-
- @Override
- public int getByteCount() {
- return Integer.BYTES + getByteSize(this.message);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeEnum(this.level, o);
- writeString(this.message, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.level = readEnum(Level.class, i);
- this.message = readString(i);
- }
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java
deleted file mode 100644
index e36ab54..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
-/**
- * This message is sent from the client to a server, to provide identification
- * information about the client to the server when the connection is started.
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class Identification implements Message {
- /**
- * The nickname that a client wants to be identified by when in the server.
- * If a valid session token is provided, this can be left as null, and the
- * user will be given the same nickname they had in their previous session.
- */
- private String nickname;
-
- /**
- * A session token that's used to uniquely identify this client as the same
- * as one who has previously connected to the server. If this is null, the
- * client is indicating that they have not connected to this server before.
- */
- private String sessionToken;
-
- public Identification(String nickname) {
- this.nickname = nickname;
- }
-
- @Override
- public int getByteCount() {
- return getByteSize(this.nickname) + getByteSize(sessionToken);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeString(this.nickname, o);
- writeString(this.sessionToken, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.nickname = readString(i);
- this.sessionToken = readString(i);
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/KeyData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/KeyData.java
deleted file mode 100644
index f5bfb80..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/KeyData.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-
-/**
- * This message is sent as the first message from both the server and the client
- * to establish an end-to-end encryption via a key exchange.
- */
-@Getter
-@NoArgsConstructor
-public class KeyData implements Message {
- private byte[] iv;
- private byte[] salt;
- private byte[] publicKey;
-
- public KeyData(byte[] iv, byte[] salt, byte[] publicKey) {
- this.iv = iv;
- this.salt = salt;
- this.publicKey = publicKey;
- }
-
- @Override
- public int getByteCount() {
- return Integer.BYTES * 3 + iv.length + salt.length + publicKey.length;
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- o.writeInt(iv.length);
- o.write(iv);
- o.writeInt(salt.length);
- o.write(salt);
- o.writeInt(publicKey.length);
- o.write(publicKey);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- int ivLength = i.readInt();
- this.iv = i.readNBytes(ivLength);
- int saltLength = i.readInt();
- this.salt = i.readNBytes(saltLength);
- int publicKeyLength = i.readInt();
- this.publicKey = i.readNBytes(publicKeyLength);
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java
deleted file mode 100644
index 390eced..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.UUID;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
-/**
- * A message that's sent to a client when they've been moved to another channel.
- * This indicates to the client that they should perform the necessary requests
- * to update their view to indicate that they're now in a different channel.
- *
- * Conversely, a client can send this request to the server to indicate that
- * they would like to switch to the specified channel.
- *
- *
- * Clients can also send this message and provide the id of another client
- * to request that they enter a private message channel with the referenced
- * client.
- *
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-public class MoveToChannel implements Message {
- /**
- * The id of the channel that the client is requesting or being moved to, or
- * the id of another client that the user wishes to begin private messaging
- * with.
- */
- private UUID id;
-
- /**
- * The name of the channel that the client is moved to. This is null in
- * cases where the client is requesting to move to a channel, and is only
- * provided by the server when it moves a client.
- */
- private String channelName;
-
- public MoveToChannel(UUID channelId) {
- this.id = channelId;
- }
-
- @Override
- public int getByteCount() {
- return UUID_BYTES + getByteSize(this.channelName);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.id, o);
- writeString(this.channelName, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.id = readUUID(i);
- this.channelName = readString(i);
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java
index 82182cc..6d39bab 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerMetaData.java
@@ -1,73 +1,18 @@
package nl.andrewl.concord_core.msg.types;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.List;
import java.util.UUID;
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
/**
* Metadata is sent by the server to clients to inform them of the structure of
* the server. This includes basic information about the server's own properties
* as well as information about all top-level channels.
*/
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class ServerMetaData implements Message {
- private String name;
- private List channels;
-
- @Override
- public int getByteCount() {
- return getByteSize(this.name) + getByteSize(this.channels);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeString(this.name, o);
- writeList(this.channels, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.name = readString(i);
- this.channels = readList(ChannelData.class, i);
- }
-
+public record ServerMetaData (String name, ChannelData[] channels) implements Message {
/**
* Metadata about a top-level channel in the server which is visible and
* joinable for a user.
*/
- @Data
- @NoArgsConstructor
- @AllArgsConstructor
- public static class ChannelData implements Message {
- private UUID id;
- private String name;
-
- @Override
- public int getByteCount() {
- return UUID_BYTES + getByteSize(this.name);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.id, o);
- writeString(this.name, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.id = readUUID(i);
- this.name = readString(i);
- }
- }
+ public static record ChannelData (UUID id, String name) implements Message {}
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerUsers.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerUsers.java
index ece8ac7..4cd4fca 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerUsers.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerUsers.java
@@ -1,40 +1,10 @@
package nl.andrewl.concord_core.msg.types;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.List;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
/**
* This message is sent from the server to the client whenever a change happens
* which requires the server to notify clients about a change of the list of
* global users.
*/
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class ServerUsers implements Message {
- private List users;
-
- @Override
- public int getByteCount() {
- return getByteSize(this.users);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeList(this.users, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.users = readList(UserData.class, i);
- }
-}
+public record ServerUsers (UserData[] users) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java
deleted file mode 100644
index 293e05d..0000000
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package nl.andrewl.concord_core.msg.types;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import nl.andrewl.concord_core.msg.Message;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.util.UUID;
-
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
-/**
- * This message is sent from the server to the client after the server accepts
- * the client's identification and registers the client in the server.
- */
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class ServerWelcome implements Message {
- /**
- * The unique id of this client.
- */
- private UUID clientId;
-
- /**
- * The token which this client can use to reconnect to the server later and
- * still be recognized as the same user.
- */
- private String sessionToken;
-
- /**
- * The id of the channel that the user has been placed in.
- */
- private UUID currentChannelId;
-
- /**
- * The name of the channel that the user has been placed in.
- */
- private String currentChannelName;
-
- /**
- * Information about the server's structure.
- */
- private ServerMetaData metaData;
-
- @Override
- public int getByteCount() {
- return 2 * UUID_BYTES + getByteSize(this.sessionToken) + getByteSize(this.currentChannelName) + this.metaData.getByteCount();
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.clientId, o);
- writeString(this.sessionToken, o);
- writeUUID(this.currentChannelId, o);
- writeString(this.currentChannelName, o);
- this.metaData.write(o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.clientId = readUUID(i);
- this.sessionToken = readString(i);
- this.currentChannelId = readUUID(i);
- this.metaData = new ServerMetaData();
- this.currentChannelName = readString(i);
- this.metaData.read(i);
- }
-}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java
index f050a2b..7ede3c8 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/UserData.java
@@ -1,43 +1,11 @@
package nl.andrewl.concord_core.msg.types;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
import java.util.UUID;
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-import static nl.andrewl.concord_core.msg.MessageUtils.readString;
-
/**
* Standard set of user data that is used mainly as a component of other more
* complex messages.
*/
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class UserData implements Message {
- private UUID id;
- private String name;
-
- @Override
- public int getByteCount() {
- return UUID_BYTES + getByteSize(this.name);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.id, o);
- writeString(this.name, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.id = readUUID(i);
- this.name = readString(i);
- }
-}
+public record UserData (UUID id, String name) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/CreateThread.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/CreateThread.java
new file mode 100644
index 0000000..2c8c0ef
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/CreateThread.java
@@ -0,0 +1,20 @@
+package nl.andrewl.concord_core.msg.types.channel;
+
+import nl.andrewl.concord_core.msg.Message;
+
+import java.util.UUID;
+
+/**
+ * This message is sent by clients when they indicate that they would like to
+ * create a new thread in their current channel.
+ *
+ * Conversely, this message is also sent by the server when a thread has
+ * been created by someone, and all clients need to be notified so that they
+ * can properly display to the user that a message has been turned into a
+ * thread.
+ *
+ *
+ * @param messageId The id of the message that a thread will be/is attached to.
+ * @param title The title of the thread. This may be null.
+ */
+public record CreateThread (UUID messageId, String title) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/MoveToChannel.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/MoveToChannel.java
new file mode 100644
index 0000000..dc963c0
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/channel/MoveToChannel.java
@@ -0,0 +1,32 @@
+package nl.andrewl.concord_core.msg.types.channel;
+
+import nl.andrewl.concord_core.msg.Message;
+
+import java.util.UUID;
+
+/**
+ * A message that's sent to a client when they've been moved to another channel.
+ * This indicates to the client that they should perform the necessary requests
+ * to update their view to indicate that they're now in a different channel.
+ *
+ * Conversely, a client can send this request to the server to indicate that
+ * they would like to switch to the specified channel.
+ *
+ *
+ * Clients can also send this message and provide the id of another client
+ * to request that they enter a private message channel with the referenced
+ * client.
+ *
+ * @param id The id of the channel that the client is requesting or being moved
+ * to, or the id of another client that the user wishes to begin
+ * private messaging with.
+ * @param channelName The name of the channel that the client is moved to. This
+ * is null in cases where the client is requesting to move to
+ * a channel, and is only provided by the server when it
+ * moves a client.
+ */
+public record MoveToChannel (UUID id, String channelName) implements Message {
+ public MoveToChannel(UUID id) {
+ this(id, null);
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java
new file mode 100644
index 0000000..f094ef7
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java
@@ -0,0 +1,43 @@
+package nl.andrewl.concord_core.msg.types.chat;
+
+import nl.andrewl.concord_core.msg.Message;
+
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * This message contains information about a chat message that a user sent.
+ */
+public record Chat (
+ UUID id, UUID senderId, String senderNickname, long timestamp, String message
+) implements Message {
+ public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
+ this(null, senderId, senderNickname, timestamp, message);
+ }
+
+ public Chat(String message) {
+ this(null, null, System.currentTimeMillis(), message);
+ }
+
+ public Chat(UUID newId, Chat original) {
+ this(newId, original.senderId, original.senderNickname, original.timestamp, original.message);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s: %s", this.senderNickname, this.message);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o.getClass().equals(this.getClass())) {
+ Chat other = (Chat) o;
+ if (Objects.equals(this.id, other.id)) return true;
+ return this.senderId.equals(other.senderId) &&
+ this.timestamp == other.timestamp &&
+ this.senderNickname.equals(other.senderNickname) &&
+ this.message.length() == other.message.length();
+ }
+ return false;
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java
similarity index 77%
rename from core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java
rename to core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java
index f45f61a..725b01b 100644
--- a/core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryRequest.java
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryRequest.java
@@ -1,20 +1,12 @@
-package nl.andrewl.concord_core.msg.types;
+package nl.andrewl.concord_core.msg.types.chat;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
import nl.andrewl.concord_core.msg.Message;
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
-import static nl.andrewl.concord_core.msg.MessageUtils.*;
-
/**
* A message which clients can send to the server to request some messages from
* the server's history of all sent messages from a particular source. Every
@@ -51,20 +43,15 @@ import static nl.andrewl.concord_core.msg.MessageUtils.*;
* the list of messages is always sorted by the timestamp.
*
*/
-@Data
-@NoArgsConstructor
-@AllArgsConstructor
-public class ChatHistoryRequest implements Message {
- private UUID channelId;
- private String query;
-
+public record ChatHistoryRequest (UUID channelId, String query) implements Message {
public ChatHistoryRequest(UUID channelId) {
this(channelId, "");
}
public ChatHistoryRequest(UUID channelId, Map params) {
- this.channelId = channelId;
- this.query = params.entrySet().stream()
+ this(
+ channelId,
+ params.entrySet().stream()
.map(entry -> {
if (entry.getKey().contains(";") || entry.getKey().contains("=")) {
throw new IllegalArgumentException("Parameter key \"" + entry.getKey() + "\" contains invalid characters.");
@@ -74,7 +61,8 @@ public class ChatHistoryRequest implements Message {
}
return entry.getKey() + "=" + entry.getValue();
})
- .collect(Collectors.joining(";"));
+ .collect(Collectors.joining(";"))
+ );
}
/**
@@ -92,21 +80,4 @@ public class ChatHistoryRequest implements Message {
}
return params;
}
-
- @Override
- public int getByteCount() {
- return UUID_BYTES + getByteSize(this.query);
- }
-
- @Override
- public void write(DataOutputStream o) throws IOException {
- writeUUID(this.channelId, o);
- writeString(this.query, o);
- }
-
- @Override
- public void read(DataInputStream i) throws IOException {
- this.channelId = readUUID(i);
- this.query = readString(i);
- }
}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java
new file mode 100644
index 0000000..4743bfa
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java
@@ -0,0 +1,11 @@
+package nl.andrewl.concord_core.msg.types.chat;
+
+import nl.andrewl.concord_core.msg.Message;
+
+import java.util.UUID;
+
+/**
+ * The response that a server sends to a {@link ChatHistoryRequest}. The list of
+ * messages is ordered by timestamp, with the newest messages appearing first.
+ */
+public record ChatHistoryResponse (UUID channelId, Chat[] messages) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java
new file mode 100644
index 0000000..ed2d237
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java
@@ -0,0 +1,11 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+
+/**
+ * This message is sent from the client to a server, to provide identification
+ * information about the client to the server when the connection is started.
+ *
+ * @param nickname
+ */
+public record Identification(String nickname, String sessionToken) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java
new file mode 100644
index 0000000..f46d18f
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java
@@ -0,0 +1,9 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+
+/**
+ * This message is sent as the first message from both the server and the client
+ * to establish an end-to-end encryption via a key exchange.
+ */
+public record KeyData (byte[] iv, byte[] salt, byte[] publicKey) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java
new file mode 100644
index 0000000..4421dc5
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java
@@ -0,0 +1,9 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+
+/**
+ * The data that new users should send to a server in order to register in that
+ * server.
+ */
+public record Registration (String username, String password) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ServerWelcome.java b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ServerWelcome.java
new file mode 100644
index 0000000..2ef985e
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ServerWelcome.java
@@ -0,0 +1,25 @@
+package nl.andrewl.concord_core.msg.types.client_setup;
+
+import nl.andrewl.concord_core.msg.Message;
+import nl.andrewl.concord_core.msg.types.ServerMetaData;
+
+import java.util.UUID;
+
+/**
+ * This message is sent from the server to the client after the server accepts
+ * the client's identification and registers the client in the server.
+ *
+ * @param clientId The unique id of this client.
+ * @param sessionToken The token which this client can use to reconnect to the
+ * server later and still be recognized as the same user.
+ * @param currentChannelId The id of the channel that the user is placed in.
+ * @param currentChannelName The name of the channel that the user is placed in.
+ * @param metaData Information about the server's structure.
+ */
+public record ServerWelcome (
+ UUID clientId,
+ String sessionToken,
+ UUID currentChannelId,
+ String currentChannelName,
+ ServerMetaData metaData
+) implements Message {}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java b/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java
new file mode 100644
index 0000000..48e0687
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java
@@ -0,0 +1,108 @@
+package nl.andrewl.concord_core.util;
+
+import nl.andrewl.concord_core.msg.Message;
+
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+/**
+ * A more complex output stream which redefines certain methods for convenience
+ * with method chaining.
+ */
+public class ChainedDataOutputStream {
+ private final DataOutputStream out;
+
+ public ChainedDataOutputStream(DataOutputStream out) {
+ this.out = out;
+ }
+
+ public ChainedDataOutputStream writeInt(int x) throws IOException {
+ out.writeInt(x);
+ return this;
+ }
+
+ public ChainedDataOutputStream writeString(String s) throws IOException {
+ if (s == null) {
+ out.writeInt(-1);
+ } else {
+ out.writeInt(s.length());
+ out.write(s.getBytes(StandardCharsets.UTF_8));
+ }
+ return this;
+ }
+
+ public ChainedDataOutputStream writeStrings(String... strings) throws IOException {
+ for (var s : strings) {
+ writeString(s);
+ }
+ return this;
+ }
+
+ public ChainedDataOutputStream writeEnum(Enum> value) throws IOException {
+ if (value == null) {
+ out.writeInt(-1);
+ } else {
+ out.writeInt(value.ordinal());
+ }
+ return this;
+ }
+
+ public ChainedDataOutputStream writeUUID(UUID uuid) throws IOException {
+ if (uuid == null) {
+ out.writeLong(-1);
+ out.writeLong(-1);
+ } else {
+ out.writeLong(uuid.getMostSignificantBits());
+ out.writeLong(uuid.getLeastSignificantBits());
+ }
+ return this;
+ }
+
+ public ChainedDataOutputStream writeArray(T[] array) throws IOException {
+ this.out.writeInt(array.length);
+ for (var item : array) {
+ item.getType().writer().write(item, this);
+ }
+ return this;
+ }
+
+ public ChainedDataOutputStream writeMessage(Message msg) throws IOException {
+ msg.getType().writer().write(msg, this);
+ return 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.
+ * @return The chained output stream.
+ * @throws IOException If an error occurs.
+ */
+ public ChainedDataOutputStream writeObject(Object o, Class> type) throws IOException {
+ if (type.equals(Integer.class) || type.equals(int.class)) {
+ this.writeInt((Integer) o);
+ } else if (type.equals(Long.class) || type.equals(long.class)) {
+ this.out.writeLong((Long) o);
+ } else if (type.equals(String.class)) {
+ this.writeString((String) o);
+ } else if (type.equals(UUID.class)) {
+ this.writeUUID((UUID) o);
+ } else if (type.isEnum()) {
+ this.writeEnum((Enum>) o);
+ } else if (type.equals(byte[].class)) {
+ byte[] b = (byte[]) o;
+ this.writeInt(b.length);
+ this.out.write(b);
+ } else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) {
+ this.writeArray((Message[]) o);
+ } else if (Message.class.isAssignableFrom(type)) {
+ this.writeMessage((Message) o);
+ } else {
+ throw new IOException("Unsupported object type: " + o.getClass().getSimpleName());
+ }
+ return this;
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java b/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java
new file mode 100644
index 0000000..aada77f
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java
@@ -0,0 +1,88 @@
+package nl.andrewl.concord_core.util;
+
+import nl.andrewl.concord_core.msg.Message;
+import nl.andrewl.concord_core.msg.MessageType;
+
+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 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 readEnum(Class 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[] readArray(MessageType 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 = MessageType.get((Class extends Message>) type.getComponentType());
+ return this.readArray(messageType);
+ } else if (Message.class.isAssignableFrom(type)) {
+ var messageType = MessageType.get((Class extends Message>) type);
+ return messageType.reader().read(this);
+ } else {
+ throw new IOException("Unsupported object type: " + type.getSimpleName());
+ }
+ }
+}
diff --git a/core/src/main/java/nl/andrewl/concord_core/util/Triple.java b/core/src/main/java/nl/andrewl/concord_core/util/Triple.java
new file mode 100644
index 0000000..28bed57
--- /dev/null
+++ b/core/src/main/java/nl/andrewl/concord_core/util/Triple.java
@@ -0,0 +1,3 @@
+package nl.andrewl.concord_core.util;
+
+public record Triple (A first, B second, C third) {}
diff --git a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
index d599f3c..9bc4e90 100644
--- a/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
+++ b/server/src/main/java/nl/andrewl/concord_server/ConcordServer.java
@@ -22,7 +22,6 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
/**
* The main server implementation, which handles accepting new clients.
@@ -128,8 +127,8 @@ public class ConcordServer implements Runnable {
this.config.getName(),
this.channelManager.getChannels().stream()
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
- .sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
- .collect(Collectors.toList())
+ .sorted(Comparator.comparing(ServerMetaData.ChannelData::name))
+ .toList().toArray(new ServerMetaData.ChannelData[0])
);
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java b/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java
index 777030f..d46ffab 100644
--- a/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java
+++ b/server/src/main/java/nl/andrewl/concord_server/channel/Channel.java
@@ -76,7 +76,7 @@ public class Channel implements Comparable {
* @throws IOException If an error occurs.
*/
public void sendMessage(Message msg) throws IOException {
- ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1);
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.byteSize() + 1);
this.server.getSerializer().writeMessage(msg, baos);
byte[] data = baos.toByteArray();
for (var client : this.connectedClients) {
@@ -93,7 +93,7 @@ public class Channel implements Comparable {
for (var clientThread : this.getConnectedClients()) {
users.add(clientThread.toData());
}
- users.sort(Comparator.comparing(UserData::getName));
+ users.sort(Comparator.comparing(UserData::name));
return users;
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/channel/ChannelManager.java b/server/src/main/java/nl/andrewl/concord_server/channel/ChannelManager.java
index 0aef8dc..ffdb50e 100644
--- a/server/src/main/java/nl/andrewl/concord_server/channel/ChannelManager.java
+++ b/server/src/main/java/nl/andrewl/concord_server/channel/ChannelManager.java
@@ -1,6 +1,6 @@
package nl.andrewl.concord_server.channel;
-import nl.andrewl.concord_core.msg.types.MoveToChannel;
+import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.client.ClientThread;
import nl.andrewl.concord_server.util.CollectionUtils;
diff --git a/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java b/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java
index ca01b48..e9e668e 100644
--- a/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java
+++ b/server/src/main/java/nl/andrewl/concord_server/cli/command/ListClientsCommand.java
@@ -15,7 +15,7 @@ public class ListClientsCommand implements ServerCliCommand {
} else {
StringBuilder sb = new StringBuilder("Online Users:\n");
for (var userData : users) {
- sb.append("\t").append(userData.getName()).append(" (").append(userData.getId()).append(")\n");
+ sb.append("\t").append(userData.name()).append(" (").append(userData.id()).append(")\n");
}
System.out.print(sb);
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
index 6907f59..7d4c6a7 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientManager.java
@@ -2,7 +2,10 @@ package nl.andrewl.concord_server.client;
import nl.andrewl.concord_core.msg.Message;
import nl.andrewl.concord_core.msg.types.Error;
-import nl.andrewl.concord_core.msg.types.*;
+import nl.andrewl.concord_core.msg.types.ServerUsers;
+import nl.andrewl.concord_core.msg.types.UserData;
+import nl.andrewl.concord_core.msg.types.client_setup.Identification;
+import nl.andrewl.concord_core.msg.types.client_setup.ServerWelcome;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.util.CollectionUtils;
import nl.andrewl.concord_server.util.StringUtils;
@@ -54,7 +57,7 @@ public class ClientManager {
public void handleLogIn(Identification identification, ClientThread clientThread) {
ClientConnectionData data;
try {
- data = identification.getSessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification);
+ data = identification.sessionToken() == null ? getNewClientData(identification) : getClientDataFromDb(identification);
} catch (InvalidIdentificationException e) {
clientThread.sendToClient(Error.warning(e.getMessage()));
return;
@@ -75,7 +78,7 @@ public class ClientManager {
data.newClient ? " for the first time" : "",
defaultChannel
);
- this.broadcast(new ServerUsers(this.getClients()));
+ this.broadcast(new ServerUsers(this.getClients().toArray(new UserData[0])));
}
/**
@@ -89,7 +92,7 @@ public class ClientManager {
client.getCurrentChannel().removeClient(client);
client.shutdown();
System.out.println("Client " + client + " has disconnected.");
- this.broadcast(new ServerUsers(this.getClients()));
+ this.broadcast(new ServerUsers(this.getClients().toArray(new UserData[0])));
}
}
@@ -99,7 +102,7 @@ public class ClientManager {
* @param message The message to send.
*/
public void broadcast(Message message) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream(message.getByteCount());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(message.byteSize());
try {
this.server.getSerializer().writeMessage(message, baos);
byte[] data = baos.toByteArray();
@@ -129,11 +132,11 @@ public class ClientManager {
private static record ClientConnectionData(UUID id, String nickname, String sessionToken, boolean newClient) {}
private ClientConnectionData getClientDataFromDb(Identification identification) throws InvalidIdentificationException {
- var cursor = this.userCollection.find(Filters.eq("sessionToken", identification.getSessionToken()));
+ var cursor = this.userCollection.find(Filters.eq("sessionToken", identification.sessionToken()));
Document doc = cursor.firstOrDefault();
if (doc != null) {
UUID id = doc.get("id", UUID.class);
- String nickname = identification.getNickname();
+ String nickname = identification.nickname();
if (nickname != null) {
doc.put("nickname", nickname);
} else {
@@ -150,7 +153,7 @@ public class ClientManager {
private ClientConnectionData getNewClientData(Identification identification) throws InvalidIdentificationException {
UUID id = this.server.getIdProvider().newId();
- String nickname = identification.getNickname();
+ String nickname = identification.nickname();
if (nickname == null) {
throw new InvalidIdentificationException("Missing nickname.");
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java b/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java
index 29589d9..2e580c9 100644
--- a/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java
+++ b/server/src/main/java/nl/andrewl/concord_server/client/ClientThread.java
@@ -4,7 +4,7 @@ import lombok.Getter;
import lombok.Setter;
import nl.andrewl.concord_core.msg.Encryption;
import nl.andrewl.concord_core.msg.Message;
-import nl.andrewl.concord_core.msg.types.Identification;
+import nl.andrewl.concord_core.msg.types.client_setup.Identification;
import nl.andrewl.concord_core.msg.types.UserData;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.channel.Channel;
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java
index cdab006..9a589a2 100644
--- a/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java
+++ b/server/src/main/java/nl/andrewl/concord_server/event/ChannelMoveHandler.java
@@ -1,11 +1,10 @@
package nl.andrewl.concord_server.event;
import nl.andrewl.concord_core.msg.types.Error;
-import nl.andrewl.concord_core.msg.types.MoveToChannel;
+import nl.andrewl.concord_core.msg.types.channel.MoveToChannel;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.client.ClientThread;
-import java.util.List;
import java.util.Set;
/**
@@ -17,11 +16,11 @@ import java.util.Set;
public class ChannelMoveHandler implements MessageHandler {
@Override
public void handle(MoveToChannel msg, ClientThread client, ConcordServer server) {
- var optionalChannel = server.getChannelManager().getChannelById(msg.getId());
+ var optionalChannel = server.getChannelManager().getChannelById(msg.id());
if (optionalChannel.isPresent()) {
server.getChannelManager().moveToChannel(client, optionalChannel.get());
} else {
- var optionalClient = server.getClientManager().getClientById(msg.getId());
+ var optionalClient = server.getClientManager().getClientById(msg.id());
if (optionalClient.isPresent()) {
var privateChannel = server.getChannelManager().getPrivateChannel(Set.of(
client.getClientId(),
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java
index e2c751e..09402fc 100644
--- a/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java
+++ b/server/src/main/java/nl/andrewl/concord_server/event/ChatHandler.java
@@ -1,7 +1,7 @@
package nl.andrewl.concord_server.event;
-import nl.andrewl.concord_core.msg.types.Chat;
import nl.andrewl.concord_core.msg.types.Error;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.client.ClientThread;
import org.dizitart.no2.Document;
@@ -17,7 +17,7 @@ import java.util.Map;
public class ChatHandler implements MessageHandler {
@Override
public void handle(Chat msg, ClientThread client, ConcordServer server) throws IOException {
- if (msg.getMessage().length() > server.getConfig().getMaxMessageLength()) {
+ if (msg.message().length() > server.getConfig().getMaxMessageLength()) {
client.getCurrentChannel().sendMessage(Error.warning("Message is too long."));
return;
}
@@ -27,17 +27,17 @@ public class ChatHandler implements MessageHandler {
malicious UUID, so we overwrite it with a server-generated id which we
know is safe.
*/
- msg.setId(server.getIdProvider().newId());
+ msg = new Chat(server.getIdProvider().newId(), msg);
var collection = client.getCurrentChannel().getMessageCollection();
Document doc = new Document(Map.of(
- "id", msg.getId(),
- "senderId", msg.getSenderId(),
- "senderNickname", msg.getSenderNickname(),
- "timestamp", msg.getTimestamp(),
- "message", msg.getMessage()
+ "id", msg.id(),
+ "senderId", msg.senderId(),
+ "senderNickname", msg.senderNickname(),
+ "timestamp", msg.timestamp(),
+ "message", msg.message()
));
collection.insert(doc);
- System.out.printf("#%s | %s: %s\n", client.getCurrentChannel(), client.getClientNickname(), msg.getMessage());
+ System.out.printf("#%s | %s: %s\n", client.getCurrentChannel(), client.getClientNickname(), msg.message());
client.getCurrentChannel().sendMessage(msg);
}
}
diff --git a/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java b/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java
index e8b0049..17dd811 100644
--- a/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java
+++ b/server/src/main/java/nl/andrewl/concord_server/event/ChatHistoryRequestHandler.java
@@ -1,9 +1,9 @@
package nl.andrewl.concord_server.event;
-import nl.andrewl.concord_core.msg.types.Chat;
-import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
-import nl.andrewl.concord_core.msg.types.ChatHistoryResponse;
import nl.andrewl.concord_core.msg.types.Error;
+import nl.andrewl.concord_core.msg.types.chat.Chat;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryRequest;
+import nl.andrewl.concord_core.msg.types.chat.ChatHistoryResponse;
import nl.andrewl.concord_server.ConcordServer;
import nl.andrewl.concord_server.channel.Channel;
import nl.andrewl.concord_server.client.ClientThread;
@@ -19,10 +19,10 @@ public class ChatHistoryRequestHandler implements MessageHandler