Merge pull request #5 from andrewlalis/messageRefactor
Refactored core message structure to use records.
This commit is contained in:
commit
c35fbbec9e
|
@ -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;
|
||||
|
|
|
@ -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) {}
|
||||
|
|
|
@ -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<MoveToChannel> {
|
||||
@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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ChatHistoryResponse> {
|
||||
@Override
|
||||
public void handle(ChatHistoryResponse msg, ConcordClient client) {
|
||||
client.getModel().getChatHistory().setChats(msg.getMessages());
|
||||
client.getModel().getChatHistory().setChats(Arrays.asList(msg.messages()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ServerUsers> {
|
||||
@Override
|
||||
public void handle(ServerUsers msg, ConcordClient client) {
|
||||
client.getModel().setKnownUsers(msg.getUsers());
|
||||
client.getModel().setKnownUsers(Arrays.asList(msg.users()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Chat, ChatLis
|
|||
else {
|
||||
graphics.applyThemeStyle(themeDefinition.getNormal());
|
||||
}
|
||||
graphics.putString(0, 0, chat.getSenderNickname());
|
||||
Instant timestamp = Instant.ofEpochMilli(chat.getTimestamp());
|
||||
graphics.putString(0, 0, chat.senderNickname());
|
||||
Instant timestamp = Instant.ofEpochMilli(chat.timestamp());
|
||||
String timeStr = timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm"));
|
||||
String label = chat.getSenderNickname() + "@" + timeStr + " : " + chat.getMessage();
|
||||
String label = chat.senderNickname() + "@" + timeStr + " : " + chat.message();
|
||||
label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
|
||||
while(TerminalTextUtils.getColumnWidth(label) < graphics.getSize().getColumns()) {
|
||||
label += " ";
|
||||
|
|
|
@ -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 nl.andrewl.concord_core.msg.types.UserData;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -22,11 +22,11 @@ public class UserList extends Panel {
|
|||
public void updateUsers(List<UserData> 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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
|
@ -14,26 +8,12 @@ import java.util.UUID;
|
|||
* </p>
|
||||
*/
|
||||
public interface Message {
|
||||
/**
|
||||
* @return The exact number of bytes that this message will use when written
|
||||
* to a stream.
|
||||
*/
|
||||
int getByteCount();
|
||||
@SuppressWarnings("unchecked")
|
||||
default <T extends Message> MessageType<T> getType() {
|
||||
return MessageType.get((Class<T>) 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.
|
||||
* <p>
|
||||
* The single byte type identifier has already been read.
|
||||
* </p>
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T extends Message>{
|
||||
/**
|
||||
* Reads all of this message's properties from the given input stream.
|
||||
* <p>
|
||||
* The single byte type identifier has already been read.
|
||||
* </p>
|
||||
* @param in The input stream to read from.
|
||||
* @return The message that was read.
|
||||
* @throws IOException If an error occurs while reading.
|
||||
*/
|
||||
T read(ExtendedDataInputStream in) throws IOException;
|
||||
}
|
|
@ -0,0 +1,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.
|
||||
* <p>
|
||||
* Also contains methods for automatically generating message type
|
||||
* implementations for standard record-based messages.
|
||||
* </p>
|
||||
* @param <T> The type of message.
|
||||
* @param messageClass The class of the message.
|
||||
* @param byteSizeFunction A function that computes the byte size of the message.
|
||||
* @param reader A reader that can read messages from an input stream.
|
||||
* @param writer A writer that write messages from an input stream.
|
||||
*/
|
||||
public record MessageType<T extends Message>(
|
||||
Class<T> messageClass,
|
||||
Function<T, Integer> byteSizeFunction,
|
||||
MessageReader<T> reader,
|
||||
MessageWriter<T> writer
|
||||
) {
|
||||
private static final Map<Class<?>, 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 <T> The type of the message.
|
||||
* @return The message type.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T extends Message> MessageType<T> get(Class<T> messageClass) {
|
||||
return (MessageType<T>) generatedMessageTypes.computeIfAbsent(messageClass, c -> generateForRecord((Class<T>) c));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a message type instance for a given class, using reflection to
|
||||
* introspect the fields of the message.
|
||||
* <p>
|
||||
* Note that this only works for record-based messages.
|
||||
* </p>
|
||||
* @param messageTypeClass The class of the message type.
|
||||
* @param <T> The type of the message.
|
||||
* @return A message type instance.
|
||||
*/
|
||||
public static <T extends Message> MessageType<T> generateForRecord(Class<T> messageTypeClass) {
|
||||
RecordComponent[] components = messageTypeClass.getRecordComponents();
|
||||
Constructor<T> constructor;
|
||||
try {
|
||||
constructor = messageTypeClass.getDeclaredConstructor(Arrays.stream(components)
|
||||
.map(RecordComponent::getType).toArray(Class<?>[]::new));
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
return new 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 <T> The message type.
|
||||
* @return A function that computes the byte size of a message of the given
|
||||
* type.
|
||||
*/
|
||||
private static <T extends Message> Function<T, Integer> generateByteSizeFunction(RecordComponent[] components) {
|
||||
return msg -> {
|
||||
int size = 0;
|
||||
for (var component : components) {
|
||||
try {
|
||||
size += MessageUtils.getByteSize(component.getAccessor().invoke(msg));
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
return size;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a message reader for the given message constructor method. It
|
||||
* will try to read objects from the input stream according to the
|
||||
* parameters of the canonical constructor of a message record.
|
||||
* @param constructor The canonical constructor of the message record.
|
||||
* @param <T> The message type.
|
||||
* @return A message reader for the given type.
|
||||
*/
|
||||
private static <T extends Message> MessageReader<T> generateReader(Constructor<T> constructor) {
|
||||
return in -> {
|
||||
Object[] values = new Object[constructor.getParameterCount()];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
values[i] = in.readObject(constructor.getParameterTypes()[i]);
|
||||
}
|
||||
try {
|
||||
return constructor.newInstance(values);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a message writer for the given message record components.
|
||||
* @param components The record components to write.
|
||||
* @param <T> The type of message.
|
||||
* @return The message writer for the given type.
|
||||
*/
|
||||
private static <T extends Message> MessageWriter<T> generateWriter(RecordComponent[] components) {
|
||||
return (msg, out) -> {
|
||||
for (var component: components) {
|
||||
try {
|
||||
out.writeObject(component.getAccessor().invoke(msg), component.getType());
|
||||
} catch (ReflectiveOperationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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 <T> The enum type.
|
||||
* @return The enum value, or null if -1 was read.
|
||||
* @throws IOException If an error occurs while reading.
|
||||
*/
|
||||
public static <T extends Enum<?>> T readEnum(Class<T> 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 <T extends Message> 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 <T extends Message> List<T> readList(Class<T> type, DataInputStream i) throws IOException {
|
||||
int size = i.readInt();
|
||||
try {
|
||||
var constructor = type.getConstructor();
|
||||
List<T> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<T extends Message> {
|
||||
/**
|
||||
* Writes this message to the given output stream.
|
||||
* @param msg The message to write.
|
||||
* @param out The output stream to write to.
|
||||
* @throws IOException If an error occurs while writing.
|
||||
*/
|
||||
void write(T msg, ChainedDataOutputStream out) throws IOException;
|
||||
}
|
|
@ -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<Byte, Class<? extends Message>> messageTypes = new HashMap<>();
|
||||
private final Map<Byte, MessageType<?>> 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<Class<? extends Message>, Byte> inverseMessageTypes = new HashMap<>();
|
||||
private final Map<MessageType<?>, 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 <T extends Message> void registerType(int id, Class<T> messageClass) {
|
||||
MessageType<T> 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 <T extends Message> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<Chat> 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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p>
|
||||
* Conversely, a client can send this request to the server to indicate that
|
||||
* they would like to switch to the specified channel.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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<ChannelData> 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 {}
|
||||
}
|
||||
|
|
|
@ -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<UserData> 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 {}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {}
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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 {}
|
|
@ -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.
|
||||
* <p>
|
||||
* Conversely, a client can send this request to the server to indicate that
|
||||
* they would like to switch to the specified channel.
|
||||
* </p>
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* @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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* </p>
|
||||
*/
|
||||
@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<String, String> 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);
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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 {}
|
|
@ -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 <T extends Message> ChainedDataOutputStream writeArray(T[] array) throws IOException {
|
||||
this.out.writeInt(array.length);
|
||||
for (var item : array) {
|
||||
item.getType().writer().write(item, this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T extends Message> 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;
|
||||
}
|
||||
}
|
|
@ -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 extends Enum<?>> T readEnum(Class<T> e) throws IOException {
|
||||
int ordinal = super.readInt();
|
||||
if (ordinal == -1) return null;
|
||||
return e.getEnumConstants()[ordinal];
|
||||
}
|
||||
|
||||
public UUID readUUID() throws IOException {
|
||||
long a = super.readLong();
|
||||
long b = super.readLong();
|
||||
if (a == -1 && b == -1) {
|
||||
return null;
|
||||
}
|
||||
return new UUID(a, b);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Message> T[] readArray(MessageType<T> type) throws IOException {
|
||||
int length = super.readInt();
|
||||
T[] array = (T[]) Array.newInstance(type.messageClass(), length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
array[i] = type.reader().read(this);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an object from the stream that is of a certain expected type.
|
||||
* @param type The type of object to read.
|
||||
* @return The object that was read.
|
||||
* @throws IOException If an error occurs while reading.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Object readObject(Class<?> type) throws IOException {
|
||||
if (type.equals(Integer.class) || type.equals(int.class)) {
|
||||
return this.readInt();
|
||||
} else if (type.equals(Long.class) || type.equals(long.class)) {
|
||||
return this.readLong();
|
||||
} else if (type.equals(String.class)) {
|
||||
return this.readString();
|
||||
} else if (type.equals(UUID.class)) {
|
||||
return this.readUUID();
|
||||
} else if (type.isEnum()) {
|
||||
return this.readEnum((Class<? extends Enum<?>>) type);
|
||||
} else if (type.isAssignableFrom(byte[].class)) {
|
||||
int length = this.readInt();
|
||||
return this.readNBytes(length);
|
||||
} else if (type.isArray() && Message.class.isAssignableFrom(type.getComponentType())) {
|
||||
var messageType = 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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package nl.andrewl.concord_core.util;
|
||||
|
||||
public record Triple<A, B, C> (A first, B second, C third) {}
|
|
@ -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])
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ public class Channel implements Comparable<Channel> {
|
|||
* @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<Channel> {
|
|||
for (var clientThread : this.getConnectedClients()) {
|
||||
users.add(clientThread.toData());
|
||||
}
|
||||
users.sort(Comparator.comparing(UserData::getName));
|
||||
users.sort(Comparator.comparing(UserData::name));
|
||||
return users;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<MoveToChannel> {
|
||||
@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(),
|
||||
|
|
|
@ -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<Chat> {
|
||||
@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<Chat> {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ChatHistoryRequ
|
|||
@Override
|
||||
public void handle(ChatHistoryRequest msg, ClientThread client, ConcordServer server) {
|
||||
// First try and find a public channel with the given id.
|
||||
var channel = server.getChannelManager().getChannelById(msg.getChannelId()).orElse(null);
|
||||
var channel = server.getChannelManager().getChannelById(msg.channelId()).orElse(null);
|
||||
if (channel == null) {
|
||||
// Couldn't find a public channel, so look for a private channel this client is involved in.
|
||||
channel = server.getChannelManager().getPrivateChannel(client.getClientId(), msg.getChannelId()).orElse(null);
|
||||
channel = server.getChannelManager().getPrivateChannel(client.getClientId(), msg.channelId()).orElse(null);
|
||||
}
|
||||
// If we couldn't find a public or private channel, give up.
|
||||
if (channel == null) {
|
||||
|
@ -55,7 +55,7 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
|
|||
for (var doc : cursor) {
|
||||
chats.add(this.read(doc));
|
||||
}
|
||||
client.sendToClient(new ChatHistoryResponse(channel.getId(), chats));
|
||||
client.sendToClient(new ChatHistoryResponse(channel.getId(), chats.toArray(new Chat[0])));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,7 +89,7 @@ public class ChatHistoryRequestHandler implements MessageHandler<ChatHistoryRequ
|
|||
chats.add(this.read(doc));
|
||||
}
|
||||
Collections.reverse(chats);
|
||||
return new ChatHistoryResponse(channel.getId(), chats);
|
||||
return new ChatHistoryResponse(channel.getId(), chats.toArray(new Chat[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,9 +2,9 @@ package nl.andrewl.concord_server.event;
|
|||
|
||||
import lombok.extern.java.Log;
|
||||
import nl.andrewl.concord_core.msg.Message;
|
||||
import nl.andrewl.concord_core.msg.types.Chat;
|
||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
||||
import nl.andrewl.concord_core.msg.types.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.channel.MoveToChannel;
|
||||
import nl.andrewl.concord_server.ConcordServer;
|
||||
import nl.andrewl.concord_server.client.ClientThread;
|
||||
|
||||
|
|
Loading…
Reference in New Issue