From 631ded2afb51e5549d6fc891195db43f342fd626 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Fri, 24 Sep 2021 19:37:57 +0200 Subject: [PATCH] Refactored core message structure to use records. --- .../andrewl/concord_client/ConcordClient.java | 10 +- .../event/ChatHistoryListener.java | 2 +- .../event/handlers/ChannelMovedHandler.java | 10 +- .../handlers/ChatHistoryResponseHandler.java | 7 +- .../event/handlers/ServerUsersHandler.java | 4 +- .../concord_client/gui/ChannelList.java | 12 +- .../andrewl/concord_client/gui/ChatList.java | 2 +- .../concord_client/gui/ChatRenderer.java | 8 +- .../andrewl/concord_client/gui/UserList.java | 10 +- .../concord_client/model/ChatHistory.java | 2 +- core/src/main/java/module-info.java | 4 + .../andrewl/concord_core/msg/Encryption.java | 8 +- .../nl/andrewl/concord_core/msg/Message.java | 34 +---- .../concord_core/msg/MessageReader.java | 19 +++ .../andrewl/concord_core/msg/MessageType.java | 130 ++++++++++++++++ .../concord_core/msg/MessageUtils.java | 140 +++++------------- .../concord_core/msg/MessageWriter.java | 16 ++ .../andrewl/concord_core/msg/Serializer.java | 62 ++++---- .../andrewl/concord_core/msg/types/Chat.java | 83 ----------- .../msg/types/ChatHistoryResponse.java | 43 ------ .../concord_core/msg/types/CreateThread.java | 56 ------- .../andrewl/concord_core/msg/types/Error.java | 37 +---- .../msg/types/Identification.java | 56 ------- .../concord_core/msg/types/KeyData.java | 52 ------- .../concord_core/msg/types/MoveToChannel.java | 67 --------- .../msg/types/ServerMetaData.java | 59 +------- .../concord_core/msg/types/ServerUsers.java | 32 +--- .../concord_core/msg/types/ServerWelcome.java | 72 --------- .../concord_core/msg/types/UserData.java | 34 +---- .../msg/types/channel/CreateThread.java | 20 +++ .../msg/types/channel/MoveToChannel.java | 32 ++++ .../concord_core/msg/types/chat/Chat.java | 43 ++++++ .../types/{ => chat}/ChatHistoryRequest.java | 43 +----- .../msg/types/chat/ChatHistoryResponse.java | 11 ++ .../types/client_setup/Identification.java | 11 ++ .../msg/types/client_setup/KeyData.java | 9 ++ .../msg/types/client_setup/Registration.java | 9 ++ .../msg/types/client_setup/ServerWelcome.java | 25 ++++ .../util/ChainedDataOutputStream.java | 108 ++++++++++++++ .../util/ExtendedDataInputStream.java | 88 +++++++++++ .../nl/andrewl/concord_core/util/Triple.java | 3 + .../andrewl/concord_server/ConcordServer.java | 5 +- .../concord_server/channel/Channel.java | 4 +- .../channel/ChannelManager.java | 2 +- .../cli/command/ListClientsCommand.java | 2 +- .../concord_server/client/ClientManager.java | 19 ++- .../concord_server/client/ClientThread.java | 2 +- .../event/ChannelMoveHandler.java | 7 +- .../concord_server/event/ChatHandler.java | 18 +-- .../event/ChatHistoryRequestHandler.java | 14 +- .../concord_server/event/EventManager.java | 6 +- 51 files changed, 705 insertions(+), 847 deletions(-) create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/MessageReader.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/MessageType.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/MessageWriter.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/Chat.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/ChatHistoryResponse.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/CreateThread.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/Identification.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/KeyData.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/MoveToChannel.java delete mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/ServerWelcome.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/channel/CreateThread.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/channel/MoveToChannel.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/chat/Chat.java rename core/src/main/java/nl/andrewl/concord_core/msg/types/{ => chat}/ChatHistoryRequest.java (77%) create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/chat/ChatHistoryResponse.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Identification.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/KeyData.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/Registration.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/msg/types/client_setup/ServerWelcome.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/util/ChainedDataOutputStream.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/util/ExtendedDataInputStream.java create mode 100644 core/src/main/java/nl/andrewl/concord_core/util/Triple.java 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 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 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 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>) 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) type.getComponentType()); + return this.readArray(messageType); + } else if (Message.class.isAssignableFrom(type)) { + var messageType = MessageType.get((Class) 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