Added channels and more commands.
This commit is contained in:
parent
4faba0d2eb
commit
cc5c90fd54
|
@ -24,5 +24,4 @@
|
||||||
<version>3.1.1</version>
|
<version>3.1.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
|
@ -1,4 +1,5 @@
|
||||||
module concord_client {
|
module concord_client {
|
||||||
requires concord_core;
|
requires concord_core;
|
||||||
requires com.googlecode.lanterna;
|
requires com.googlecode.lanterna;
|
||||||
|
requires static lombok;
|
||||||
}
|
}
|
|
@ -7,11 +7,15 @@ import com.googlecode.lanterna.screen.Screen;
|
||||||
import com.googlecode.lanterna.screen.TerminalScreen;
|
import com.googlecode.lanterna.screen.TerminalScreen;
|
||||||
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
|
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
|
||||||
import com.googlecode.lanterna.terminal.Terminal;
|
import com.googlecode.lanterna.terminal.Terminal;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import nl.andrewl.concord_client.gui.MainWindow;
|
import nl.andrewl.concord_client.gui.MainWindow;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
import nl.andrewl.concord_core.msg.MessageUtils;
|
||||||
import nl.andrewl.concord_core.msg.Serializer;
|
import nl.andrewl.concord_core.msg.Serializer;
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
import nl.andrewl.concord_core.msg.types.Identification;
|
import nl.andrewl.concord_core.msg.types.Identification;
|
||||||
|
import nl.andrewl.concord_core.msg.types.ServerMetaData;
|
||||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
|
@ -21,14 +25,20 @@ import java.net.Socket;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class ConcordClient implements Runnable {
|
public class ConcordClient implements Runnable {
|
||||||
|
|
||||||
private final Socket socket;
|
private final Socket socket;
|
||||||
private final DataInputStream in;
|
private final DataInputStream in;
|
||||||
private final DataOutputStream out;
|
private final DataOutputStream out;
|
||||||
private final long id;
|
private final UUID id;
|
||||||
private final String nickname;
|
private final String nickname;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private UUID currentChannelId;
|
||||||
|
@Getter
|
||||||
|
private ServerMetaData serverMetaData;
|
||||||
private final Set<ClientMessageListener> messageListeners;
|
private final Set<ClientMessageListener> messageListeners;
|
||||||
private volatile boolean running;
|
private volatile boolean running;
|
||||||
|
|
||||||
|
@ -41,6 +51,8 @@ public class ConcordClient implements Runnable {
|
||||||
Message reply = Serializer.readMessage(this.in);
|
Message reply = Serializer.readMessage(this.in);
|
||||||
if (reply instanceof ServerWelcome welcome) {
|
if (reply instanceof ServerWelcome welcome) {
|
||||||
this.id = welcome.getClientId();
|
this.id = welcome.getClientId();
|
||||||
|
this.currentChannelId = welcome.getCurrentChannelId();
|
||||||
|
this.serverMetaData = welcome.getMetaData();
|
||||||
} else {
|
} else {
|
||||||
throw new IOException("Unexpected response from the server after sending identification message.");
|
throw new IOException("Unexpected response from the server after sending identification message.");
|
||||||
}
|
}
|
||||||
|
@ -55,6 +67,10 @@ public class ConcordClient implements Runnable {
|
||||||
this.messageListeners.remove(listener);
|
this.messageListeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendMessage(Message message) throws IOException {
|
||||||
|
Serializer.writeMessage(message, this.out);
|
||||||
|
}
|
||||||
|
|
||||||
public void sendChat(String message) throws IOException {
|
public void sendChat(String message) throws IOException {
|
||||||
Serializer.writeMessage(new Chat(this.id, this.nickname, System.currentTimeMillis(), message), this.out);
|
Serializer.writeMessage(new Chat(this.id, this.nickname, System.currentTimeMillis(), message), this.out);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
package nl.andrewl.concord_client.gui;
|
||||||
|
|
||||||
|
import com.googlecode.lanterna.TerminalSize;
|
||||||
|
import com.googlecode.lanterna.gui2.*;
|
||||||
|
import com.googlecode.lanterna.input.KeyStroke;
|
||||||
|
import com.googlecode.lanterna.input.KeyType;
|
||||||
|
import lombok.Getter;
|
||||||
|
import nl.andrewl.concord_client.ConcordClient;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
public class ChannelChatBox extends Panel {
|
||||||
|
private final ConcordClient client;
|
||||||
|
private Border chatBorder;
|
||||||
|
@Getter
|
||||||
|
private final ChatList chatList;
|
||||||
|
private final TextBox inputTextBox;
|
||||||
|
public ChannelChatBox(ConcordClient client, Window window) {
|
||||||
|
super(new BorderLayout());
|
||||||
|
this.client = client;
|
||||||
|
this.chatList = new ChatList();
|
||||||
|
this.inputTextBox = new TextBox("", TextBox.Style.MULTI_LINE);
|
||||||
|
this.inputTextBox.setCaretWarp(true);
|
||||||
|
this.inputTextBox.setPreferredSize(new TerminalSize(0, 3));
|
||||||
|
|
||||||
|
window.addWindowListener(new WindowListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onInput(Window basePane, KeyStroke keyStroke, AtomicBoolean deliverEvent) {
|
||||||
|
if (keyStroke.getKeyType() == KeyType.Enter && inputTextBox.isFocused() && !keyStroke.isShiftDown()) {
|
||||||
|
String text = inputTextBox.getText();
|
||||||
|
if (text != null && !text.isBlank()) {
|
||||||
|
try {
|
||||||
|
System.out.println("Sending: " + text.trim());
|
||||||
|
client.sendChat(text.trim());
|
||||||
|
inputTextBox.setText("");
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deliverEvent.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.refreshBorder();
|
||||||
|
this.addComponent(this.inputTextBox, BorderLayout.Location.BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void refreshBorder() {
|
||||||
|
String name = client.getServerMetaData().getChannels().stream()
|
||||||
|
.filter(channelData -> channelData.getId().equals(client.getCurrentChannelId()))
|
||||||
|
.findAny().orElseThrow().getName();
|
||||||
|
if (this.chatBorder != null) this.removeComponent(this.chatBorder);
|
||||||
|
this.chatBorder = Borders.doubleLine("#" + name);
|
||||||
|
this.chatBorder.setComponent(this.chatList);
|
||||||
|
this.addComponent(this.chatBorder, BorderLayout.Location.CENTER);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,38 @@
|
||||||
package nl.andrewl.concord_client.gui;
|
package nl.andrewl.concord_client.gui;
|
||||||
|
|
||||||
import com.googlecode.lanterna.gui2.AbstractListBox;
|
import com.googlecode.lanterna.gui2.Button;
|
||||||
|
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;
|
||||||
|
|
||||||
public class ChannelList extends AbstractListBox<String, ChannelList> {
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ChannelList extends Panel {
|
||||||
|
private final ConcordClient client;
|
||||||
|
|
||||||
|
public ChannelList(ConcordClient client) {
|
||||||
|
super(new LinearLayout(Direction.VERTICAL));
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setChannels() {
|
||||||
|
this.removeAllComponents();
|
||||||
|
for (var channel : this.client.getServerMetaData().getChannels()) {
|
||||||
|
String name = channel.getName();
|
||||||
|
if (client.getCurrentChannelId().equals(channel.getId())) {
|
||||||
|
name = "*" + name;
|
||||||
|
}
|
||||||
|
Button b = new Button(name, () -> {
|
||||||
|
System.out.println("Sending request to go to channel " + channel.getName());
|
||||||
|
try {
|
||||||
|
client.sendMessage(new MoveToChannel(channel.getId()));
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.addComponent(b, LinearLayout.createLayoutData(LinearLayout.Alignment.End));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
package nl.andrewl.concord_client.gui;
|
package nl.andrewl.concord_client.gui;
|
||||||
|
|
||||||
import com.googlecode.lanterna.TerminalSize;
|
|
||||||
import com.googlecode.lanterna.gui2.*;
|
import com.googlecode.lanterna.gui2.*;
|
||||||
import com.googlecode.lanterna.input.KeyStroke;
|
import lombok.Getter;
|
||||||
import com.googlecode.lanterna.input.KeyType;
|
|
||||||
import nl.andrewl.concord_client.ClientMessageListener;
|
import nl.andrewl.concord_client.ClientMessageListener;
|
||||||
import nl.andrewl.concord_client.ConcordClient;
|
import nl.andrewl.concord_client.ConcordClient;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main panel in which a user interacts with the application during normal
|
* The main panel in which a user interacts with the application during normal
|
||||||
|
@ -20,8 +18,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
* threads and users in the server.
|
* threads and users in the server.
|
||||||
*/
|
*/
|
||||||
public class ChatPanel extends Panel implements ClientMessageListener {
|
public class ChatPanel extends Panel implements ClientMessageListener {
|
||||||
private final ChatList chatList;
|
@Getter
|
||||||
private final TextBox inputTextBox;
|
private final ChannelChatBox channelChatBox;
|
||||||
private final ChannelList channelList;
|
private final ChannelList channelList;
|
||||||
private final UserList userList;
|
private final UserList userList;
|
||||||
|
|
||||||
|
@ -30,41 +28,13 @@ public class ChatPanel extends Panel implements ClientMessageListener {
|
||||||
public ChatPanel(ConcordClient client, Window window) {
|
public ChatPanel(ConcordClient client, Window window) {
|
||||||
super(new BorderLayout());
|
super(new BorderLayout());
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.chatList = new ChatList();
|
this.channelChatBox = new ChannelChatBox(client, window);
|
||||||
this.inputTextBox = new TextBox("", TextBox.Style.MULTI_LINE);
|
this.channelList = new ChannelList(client);
|
||||||
this.inputTextBox.setCaretWarp(true);
|
this.channelList.setChannels();
|
||||||
this.inputTextBox.setPreferredSize(new TerminalSize(0, 3));
|
|
||||||
|
|
||||||
this.channelList = new ChannelList();
|
|
||||||
this.channelList.addItem("general");
|
|
||||||
this.channelList.addItem("memes");
|
|
||||||
this.channelList.addItem("testing");
|
|
||||||
this.userList = new UserList();
|
this.userList = new UserList();
|
||||||
this.userList.addItem("andrew");
|
this.userList.addItem("andrew");
|
||||||
this.userList.addItem("tester");
|
this.userList.addItem("tester");
|
||||||
|
|
||||||
window.addWindowListener(new WindowListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onInput(Window basePane, KeyStroke keyStroke, AtomicBoolean deliverEvent) {
|
|
||||||
if (keyStroke.getKeyType() == KeyType.Enter) {
|
|
||||||
if (keyStroke.isShiftDown()) {
|
|
||||||
System.out.println("Adding newline");
|
|
||||||
} else {
|
|
||||||
String text = inputTextBox.getText();
|
|
||||||
if (text != null && !text.isBlank()) {
|
|
||||||
try {
|
|
||||||
System.out.println("Sending: " + text.trim());
|
|
||||||
client.sendChat(text.trim());
|
|
||||||
inputTextBox.setText("");
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
deliverEvent.set(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Border b;
|
Border b;
|
||||||
b = Borders.doubleLine("Channels");
|
b = Borders.doubleLine("Channels");
|
||||||
b.setComponent(this.channelList);
|
b.setComponent(this.channelList);
|
||||||
|
@ -74,18 +44,18 @@ public class ChatPanel extends Panel implements ClientMessageListener {
|
||||||
b.setComponent(this.userList);
|
b.setComponent(this.userList);
|
||||||
this.addComponent(b, BorderLayout.Location.RIGHT);
|
this.addComponent(b, BorderLayout.Location.RIGHT);
|
||||||
|
|
||||||
b = Borders.doubleLine("#general");
|
this.addComponent(this.channelChatBox, BorderLayout.Location.CENTER);
|
||||||
b.setComponent(this.chatList);
|
|
||||||
this.addComponent(b, BorderLayout.Location.CENTER);
|
|
||||||
|
|
||||||
this.addComponent(this.inputTextBox, BorderLayout.Location.BOTTOM);
|
|
||||||
this.inputTextBox.takeFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void messageReceived(ConcordClient client, Message message) throws IOException {
|
public void messageReceived(ConcordClient client, Message message) {
|
||||||
if (message instanceof Chat chat) {
|
if (message instanceof Chat chat) {
|
||||||
this.chatList.addItem(chat);
|
this.channelChatBox.getChatList().addItem(chat);
|
||||||
|
} else if (message instanceof MoveToChannel moveToChannel) {
|
||||||
|
client.setCurrentChannelId(moveToChannel.getChannelId());
|
||||||
|
this.channelList.setChannels();
|
||||||
|
this.channelChatBox.getChatList().clearItems();
|
||||||
|
this.channelChatBox.refreshBorder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,6 @@ public class MainWindow extends BasicWindow {
|
||||||
client.addListener(chatPanel);
|
client.addListener(chatPanel);
|
||||||
new Thread(client).start();
|
new Thread(client).start();
|
||||||
this.setComponent(chatPanel);
|
this.setComponent(chatPanel);
|
||||||
Borders.joinLinesWithFrame(this.getTextGUI().getScreen().newTextGraphics());
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents any message which can be sent over the network.
|
* Represents any message which can be sent over the network.
|
||||||
|
@ -35,60 +36,4 @@ public interface Message {
|
||||||
* @throws IOException If an error occurs while reading.
|
* @throws IOException If an error occurs while reading.
|
||||||
*/
|
*/
|
||||||
void read(DataInputStream i) throws IOException;
|
void read(DataInputStream i) throws IOException;
|
||||||
|
|
||||||
// Utility methods.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the number of bytes that the given string will occupy when it is
|
|
||||||
* serialized.
|
|
||||||
* @param s The string.
|
|
||||||
* @return The number of bytes used to serialize the string.
|
|
||||||
*/
|
|
||||||
default int getByteSize(String s) {
|
|
||||||
return Integer.BYTES + s.getBytes(StandardCharsets.UTF_8).length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
default 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
default String readString(DataInputStream i) throws IOException {
|
|
||||||
int length = i.readInt();
|
|
||||||
if (length == -1) return null;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
default void writeEnum(Enum<?> value, DataOutputStream o) throws IOException {
|
|
||||||
o.writeInt(value.ordinal());
|
|
||||||
}
|
|
||||||
|
|
||||||
default <T extends Enum<?>> T readEnum(Class<T> e, DataInputStream i) throws IOException {
|
|
||||||
int ordinal = i.readInt();
|
|
||||||
return e.getEnumConstants()[ordinal];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class which provides method for serializing and deserializing complex
|
||||||
|
* data types.
|
||||||
|
*/
|
||||||
|
public class MessageUtils {
|
||||||
|
public static final int UUID_BYTES = 2 * Long.BYTES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of bytes that the given string will occupy when it is
|
||||||
|
* serialized.
|
||||||
|
* @param s The string.
|
||||||
|
* @return The number of bytes used to serialize the string.
|
||||||
|
*/
|
||||||
|
public static int getByteSize(String s) {
|
||||||
|
return Integer.BYTES + s.getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeEnum(Enum<?> value, DataOutputStream o) throws IOException {
|
||||||
|
o.writeInt(value.ordinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends Enum<?>> T readEnum(Class<T> e, DataInputStream i) throws IOException {
|
||||||
|
int ordinal = i.readInt();
|
||||||
|
return e.getEnumConstants()[ordinal];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeUUID(UUID value, DataOutputStream o) throws IOException {
|
||||||
|
o.writeLong(value.getMostSignificantBits());
|
||||||
|
o.writeLong(value.getLeastSignificantBits());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static UUID readUUID(DataInputStream i) throws IOException {
|
||||||
|
long a = i.readLong();
|
||||||
|
long b = i.readLong();
|
||||||
|
return new UUID(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getByteSize(List<? extends Message> items) {
|
||||||
|
int count = Integer.BYTES;
|
||||||
|
for (var item : items) {
|
||||||
|
count += item.getByteCount();
|
||||||
|
}
|
||||||
|
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 <T extends Message> List<T> readList(Class<T> type, DataInputStream i) throws IOException, ReflectiveOperationException {
|
||||||
|
int size = i.readInt();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
package nl.andrewl.concord_core.msg;
|
package nl.andrewl.concord_core.msg;
|
||||||
|
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.*;
|
||||||
import nl.andrewl.concord_core.msg.types.Identification;
|
|
||||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -18,6 +16,9 @@ public class Serializer {
|
||||||
registerType(0, Identification.class);
|
registerType(0, Identification.class);
|
||||||
registerType(1, ServerWelcome.class);
|
registerType(1, ServerWelcome.class);
|
||||||
registerType(2, Chat.class);
|
registerType(2, Chat.class);
|
||||||
|
registerType(3, MoveToChannel.class);
|
||||||
|
registerType(4, ChatHistoryRequest.class);
|
||||||
|
registerType(5, ChatHistoryResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void registerType(int id, Class<? extends Message> clazz) {
|
private static void registerType(int id, Class<? extends Message> clazz) {
|
||||||
|
|
|
@ -7,6 +7,9 @@ import nl.andrewl.concord_core.msg.Message;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This message contains information about a chat message that a user sent.
|
* This message contains information about a chat message that a user sent.
|
||||||
|
@ -14,12 +17,12 @@ import java.io.IOException;
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class Chat implements Message {
|
public class Chat implements Message {
|
||||||
private long senderId;
|
private UUID senderId;
|
||||||
private String senderNickname;
|
private String senderNickname;
|
||||||
private long timestamp;
|
private long timestamp;
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
public Chat(long senderId, String senderNickname, long timestamp, String message) {
|
public Chat(UUID senderId, String senderNickname, long timestamp, String message) {
|
||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
this.senderNickname = senderNickname;
|
this.senderNickname = senderNickname;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
|
@ -27,17 +30,17 @@ public class Chat implements Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Chat(String message) {
|
public Chat(String message) {
|
||||||
this(-1, null, System.currentTimeMillis(), message);
|
this(null, null, System.currentTimeMillis(), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return 2 * Long.BYTES + getByteSize(this.message) + getByteSize(this.senderNickname);
|
return UUID_BYTES + Long.BYTES + getByteSize(this.senderNickname) + getByteSize(this.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
o.writeLong(this.senderId);
|
writeUUID(this.senderId, o);
|
||||||
writeString(this.senderNickname, o);
|
writeString(this.senderNickname, o);
|
||||||
o.writeLong(this.timestamp);
|
o.writeLong(this.timestamp);
|
||||||
writeString(this.message, o);
|
writeString(this.message, o);
|
||||||
|
@ -45,7 +48,7 @@ public class Chat implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.senderId = i.readLong();
|
this.senderId = readUUID(i);
|
||||||
this.senderNickname = readString(i);
|
this.senderNickname = readString(i);
|
||||||
this.timestamp = i.readLong();
|
this.timestamp = i.readLong();
|
||||||
this.message = readString(i);
|
this.message = readString(i);
|
||||||
|
@ -53,6 +56,6 @@ public class Chat implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return String.format("%s(%d): %s", this.senderNickname, this.senderId, this.message);
|
return String.format("%s: %s", this.senderNickname, this.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,9 @@ import nl.andrewl.concord_core.msg.Message;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static nl.andrewl.concord_core.msg.MessageUtils.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message which clients can send to the server to request some messages from
|
* A message which clients can send to the server to request some messages from
|
||||||
|
@ -46,25 +49,25 @@ import java.io.IOException;
|
||||||
public class ChatHistoryRequest implements Message {
|
public class ChatHistoryRequest implements Message {
|
||||||
public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE}
|
public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE}
|
||||||
|
|
||||||
private long sourceId;
|
private UUID sourceId;
|
||||||
private Source sourceType;
|
private Source sourceType;
|
||||||
private String query;
|
private String query;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return Long.BYTES + Integer.BYTES + getByteSize(this.query);
|
return UUID_BYTES + Integer.BYTES + getByteSize(this.query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
o.writeLong(sourceId);
|
writeUUID(this.sourceId, o);
|
||||||
writeEnum(this.sourceType, o);
|
writeEnum(this.sourceType, o);
|
||||||
writeString(this.query, o);
|
writeString(this.query, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.sourceId = i.readLong();
|
this.sourceId = readUUID(i);
|
||||||
this.sourceType = readEnum(Source.class, i);
|
this.sourceType = readEnum(Source.class, i);
|
||||||
this.query = readString(i);
|
this.query = readString(i);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,19 +3,22 @@ package nl.andrewl.concord_core.msg.types;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
import nl.andrewl.concord_core.msg.MessageUtils;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The response that a server sends to a {@link ChatHistoryRequest}.
|
* 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
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
public class ChatHistoryResponse implements Message {
|
public class ChatHistoryResponse implements Message {
|
||||||
private long sourceId;
|
private UUID sourceId;
|
||||||
private ChatHistoryRequest.Source sourceType;
|
private ChatHistoryRequest.Source sourceType;
|
||||||
List<Chat> messages;
|
List<Chat> messages;
|
||||||
|
|
||||||
|
@ -30,8 +33,8 @@ public class ChatHistoryResponse implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
o.writeLong(this.sourceId);
|
MessageUtils.writeUUID(this.sourceId, o);
|
||||||
writeEnum(this.sourceType, o);
|
MessageUtils.writeEnum(this.sourceType, o);
|
||||||
o.writeInt(messages.size());
|
o.writeInt(messages.size());
|
||||||
for (var message : this.messages) {
|
for (var message : this.messages) {
|
||||||
message.write(o);
|
message.write(o);
|
||||||
|
@ -40,8 +43,8 @@ public class ChatHistoryResponse implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.sourceId = i.readInt();
|
this.sourceId = MessageUtils.readUUID(i);
|
||||||
this.sourceType = readEnum(ChatHistoryRequest.Source.class, i);
|
this.sourceType = MessageUtils.readEnum(ChatHistoryRequest.Source.class, i);
|
||||||
int messageCount = i.readInt();
|
int messageCount = i.readInt();
|
||||||
Chat[] messages = new Chat[messageCount];
|
Chat[] messages = new Chat[messageCount];
|
||||||
for (int k = 0; k < messageCount; k++) {
|
for (int k = 0; k < messageCount; k++) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package nl.andrewl.concord_core.msg.types;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
import nl.andrewl.concord_core.msg.MessageUtils;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
|
@ -23,16 +24,16 @@ public class Identification implements Message {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return getByteSize(this.nickname);
|
return MessageUtils.getByteSize(this.nickname);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
writeString(this.nickname, o);
|
MessageUtils.writeString(this.nickname, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.nickname = readString(i);
|
this.nickname = MessageUtils.readString(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class MoveToChannel implements Message {
|
||||||
|
private UUID channelId;
|
||||||
|
|
||||||
|
public MoveToChannel(UUID channelId) {
|
||||||
|
this.channelId = channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getByteCount() {
|
||||||
|
return UUID_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutputStream o) throws IOException {
|
||||||
|
writeUUID(this.channelId, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void read(DataInputStream i) throws IOException {
|
||||||
|
this.channelId = readUUID(i);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
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.*;
|
||||||
|
|
||||||
|
@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);
|
||||||
|
try {
|
||||||
|
this.channels = readList(ChannelData.class, i);
|
||||||
|
} catch (ReflectiveOperationException e) {
|
||||||
|
throw new IOException("Reflection exception", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,32 +1,46 @@
|
||||||
package nl.andrewl.concord_core.msg.types;
|
package nl.andrewl.concord_core.msg.types;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
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
|
* 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.
|
* the client's identification and registers the client in the server.
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
public class ServerWelcome implements Message {
|
public class ServerWelcome implements Message {
|
||||||
private long clientId;
|
private UUID clientId;
|
||||||
|
private UUID currentChannelId;
|
||||||
|
private ServerMetaData metaData;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getByteCount() {
|
public int getByteCount() {
|
||||||
return Long.BYTES;
|
return 2 * UUID_BYTES + this.metaData.getByteCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(DataOutputStream o) throws IOException {
|
public void write(DataOutputStream o) throws IOException {
|
||||||
o.writeLong(this.clientId);
|
writeUUID(this.clientId, o);
|
||||||
|
writeUUID(this.currentChannelId, o);
|
||||||
|
this.metaData.write(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void read(DataInputStream i) throws IOException {
|
public void read(DataInputStream i) throws IOException {
|
||||||
this.clientId = i.readLong();
|
this.clientId = readUUID(i);
|
||||||
|
this.currentChannelId = readUUID(i);
|
||||||
|
this.metaData = new ServerMetaData();
|
||||||
|
this.metaData.read(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,23 +25,4 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.8.1</version>
|
|
||||||
<configuration>
|
|
||||||
<annotationProcessorPaths>
|
|
||||||
<path>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>1.18.20</version>
|
|
||||||
</path>
|
|
||||||
</annotationProcessorPaths>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
</project>
|
</project>
|
|
@ -0,0 +1,72 @@
|
||||||
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
import nl.andrewl.concord_core.msg.Serializer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a single communication area in which messages are sent by clients
|
||||||
|
* and received by all connected clients.
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
public class Channel {
|
||||||
|
private final ConcordServer server;
|
||||||
|
private UUID id;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private final Set<ClientThread> connectedClients;
|
||||||
|
|
||||||
|
public Channel(ConcordServer server, UUID id, String name) {
|
||||||
|
this.server = server;
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.connectedClients = ConcurrentHashMap.newKeySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addClient(ClientThread clientThread) {
|
||||||
|
this.connectedClients.add(clientThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeClient(ClientThread clientThread) {
|
||||||
|
this.connectedClients.remove(clientThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a message to all clients that are currently connected to this
|
||||||
|
* channel.
|
||||||
|
* @param msg The message to send.
|
||||||
|
* @throws IOException If an error occurs.
|
||||||
|
*/
|
||||||
|
public void sendMessage(Message msg) throws IOException {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(msg.getByteCount() + 1);
|
||||||
|
Serializer.writeMessage(msg, baos);
|
||||||
|
byte[] data = baos.toByteArray();
|
||||||
|
for (var client : this.connectedClients) {
|
||||||
|
client.sendToClient(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof Channel channel)) return false;
|
||||||
|
return name.equals(channel.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public class ChannelManager {
|
||||||
|
private final ConcordServer server;
|
||||||
|
private final Map<String, Channel> channelNameMap;
|
||||||
|
private final Map<UUID, Channel> channelIdMap;
|
||||||
|
|
||||||
|
public ChannelManager(ConcordServer server) {
|
||||||
|
this.server = server;
|
||||||
|
this.channelNameMap = new ConcurrentHashMap<>();
|
||||||
|
this.channelIdMap = new ConcurrentHashMap<>();
|
||||||
|
Channel general = new Channel(server, server.getIdProvider().newId(), "general");
|
||||||
|
Channel memes = new Channel(server, server.getIdProvider().newId(), "memes");
|
||||||
|
this.addChannel(general);
|
||||||
|
this.addChannel(memes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Channel> getChannels() {
|
||||||
|
return Set.copyOf(this.channelIdMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addChannel(Channel channel) {
|
||||||
|
this.channelNameMap.put(channel.getName(), channel);
|
||||||
|
this.channelIdMap.put(channel.getId(), channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeChannel(Channel channel) {
|
||||||
|
this.channelNameMap.remove(channel.getName());
|
||||||
|
this.channelIdMap.remove(channel.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Channel> getChannelByName(String name) {
|
||||||
|
return Optional.ofNullable(this.channelNameMap.get(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Channel> getChannelById(UUID id) {
|
||||||
|
return Optional.ofNullable(this.channelIdMap.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveToChannel(ClientThread client, Channel channel) {
|
||||||
|
if (client.getCurrentChannel() != null) {
|
||||||
|
client.getCurrentChannel().removeClient(client);
|
||||||
|
}
|
||||||
|
channel.addClient(client);
|
||||||
|
client.setCurrentChannel(channel);
|
||||||
|
client.sendToClient(new MoveToChannel(channel.getId()));
|
||||||
|
System.out.println("Moved client " + client.getClientNickname() + " to channel " + channel.getName());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
public class ChatThread {
|
||||||
|
}
|
|
@ -1,11 +1,10 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
import lombok.extern.java.Log;
|
import lombok.extern.java.Log;
|
||||||
import nl.andrewl.concord_core.msg.Message;
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
import nl.andrewl.concord_core.msg.Serializer;
|
import nl.andrewl.concord_core.msg.Serializer;
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
|
||||||
import nl.andrewl.concord_core.msg.types.Identification;
|
import nl.andrewl.concord_core.msg.types.Identification;
|
||||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
||||||
|
|
||||||
|
@ -13,6 +12,7 @@ import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This thread is responsible for handling the connection to a single client of
|
* This thread is responsible for handling the connection to a single client of
|
||||||
|
@ -26,10 +26,16 @@ public class ClientThread extends Thread {
|
||||||
|
|
||||||
private final ConcordServer server;
|
private final ConcordServer server;
|
||||||
|
|
||||||
private Long clientId = null;
|
private UUID clientId = null;
|
||||||
@Getter
|
@Getter
|
||||||
private String clientNickname = null;
|
private String clientNickname = null;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
private Channel currentChannel;
|
||||||
|
|
||||||
|
private volatile boolean running;
|
||||||
|
|
||||||
public ClientThread(Socket socket, ConcordServer server) throws IOException {
|
public ClientThread(Socket socket, ConcordServer server) throws IOException {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
@ -54,42 +60,60 @@ public class ClientThread extends Thread {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void shutdown() {
|
||||||
public void run() {
|
try {
|
||||||
if (!identifyClient()) {
|
this.socket.close();
|
||||||
log.warning("Could not identify the client; aborting connection.");
|
} catch (IOException e) {
|
||||||
return;
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
this.running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
@Override
|
||||||
|
public void run() {
|
||||||
|
this.running = true;
|
||||||
|
if (!identifyClient()) {
|
||||||
|
log.warning("Could not identify the client; aborting connection.");
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this.running) {
|
||||||
try {
|
try {
|
||||||
var msg = Serializer.readMessage(this.in);
|
var msg = Serializer.readMessage(this.in);
|
||||||
if (msg instanceof Chat chat) {
|
this.server.getEventManager().handle(msg, this);
|
||||||
this.server.handleChat(chat);
|
|
||||||
} else if (msg instanceof ChatHistoryRequest historyRequest) {
|
|
||||||
this.server.handleHistoryRequest(historyRequest, this);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.info("Client disconnected: " + e.getMessage());
|
log.info("Client disconnected: " + e.getMessage());
|
||||||
|
this.running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.clientId != null) {
|
if (this.clientId != null) {
|
||||||
this.server.deregisterClient(this.clientId);
|
this.server.deregisterClient(this.clientId);
|
||||||
}
|
}
|
||||||
break;
|
try {
|
||||||
|
if (!this.socket.isClosed()) {
|
||||||
|
this.socket.close();
|
||||||
}
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial method that attempts to obtain identification information from a
|
||||||
|
* newly-connected client. It is the intent that we should close the socket
|
||||||
|
* if the client is not able to identify itself.
|
||||||
|
* @return True if we were able to obtain identification from the client, or
|
||||||
|
* false otherwise.
|
||||||
|
*/
|
||||||
private boolean identifyClient() {
|
private boolean identifyClient() {
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
while (attempts < 5) {
|
while (attempts < 5) {
|
||||||
try {
|
try {
|
||||||
var msg = Serializer.readMessage(this.in);
|
var msg = Serializer.readMessage(this.in);
|
||||||
if (msg instanceof Identification id) {
|
if (msg instanceof Identification id) {
|
||||||
this.clientId = this.server.registerClient(this);
|
|
||||||
this.clientNickname = id.getNickname();
|
this.clientNickname = id.getNickname();
|
||||||
var reply = new ServerWelcome();
|
this.clientId = this.server.registerClient(id, this);
|
||||||
reply.setClientId(this.clientId);
|
|
||||||
Serializer.writeMessage(reply, this.out);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
|
@ -1,85 +1,100 @@
|
||||||
package nl.andrewl.concord_server;
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.java.Log;
|
import lombok.extern.java.Log;
|
||||||
import nl.andrewl.concord_core.msg.Serializer;
|
import nl.andrewl.concord_core.msg.types.Identification;
|
||||||
import nl.andrewl.concord_core.msg.types.Chat;
|
import nl.andrewl.concord_core.msg.types.ServerMetaData;
|
||||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
||||||
import org.dizitart.no2.Document;
|
|
||||||
import org.dizitart.no2.Nitrite;
|
import org.dizitart.no2.Nitrite;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.DataOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.security.SecureRandom;
|
import java.util.Comparator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Random;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Log
|
@Log
|
||||||
public class ConcordServer implements Runnable {
|
public class ConcordServer implements Runnable {
|
||||||
private final Map<Long, ClientThread> clients = new ConcurrentHashMap<>(32);
|
private final Map<UUID, ClientThread> clients;
|
||||||
private final int port;
|
private final int port;
|
||||||
private final Random random;
|
@Getter
|
||||||
|
private final IdProvider idProvider;
|
||||||
|
@Getter
|
||||||
private final Nitrite db;
|
private final Nitrite db;
|
||||||
|
private volatile boolean running;
|
||||||
|
@Getter
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
@Getter
|
||||||
|
private final EventManager eventManager;
|
||||||
|
@Getter
|
||||||
|
private final ChannelManager channelManager;
|
||||||
|
|
||||||
public ConcordServer(int port) {
|
public ConcordServer(int port) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.random = new SecureRandom();
|
this.idProvider = new UUIDProvider();
|
||||||
this.db = Nitrite.builder()
|
this.db = Nitrite.builder()
|
||||||
.filePath("concord-server.db")
|
.filePath("concord-server.db")
|
||||||
.openOrCreate();
|
.openOrCreate();
|
||||||
|
this.clients = new ConcurrentHashMap<>(32);
|
||||||
|
|
||||||
|
this.executorService = Executors.newCachedThreadPool();
|
||||||
|
this.eventManager = new EventManager(this);
|
||||||
|
this.channelManager = new ChannelManager(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public long registerClient(ClientThread clientThread) {
|
/**
|
||||||
long id = this.random.nextLong();
|
* Registers a new client as connected to the server. This is done once the
|
||||||
log.info("Registering new client " + clientThread.getClientNickname() + " with id " + id);
|
* client thread has received the correct identification information from
|
||||||
|
* the client. The server will register the client in its global set of
|
||||||
|
* connected clients, and it will immediately move the client to the default
|
||||||
|
* channel.
|
||||||
|
* @param identification The client's identification data.
|
||||||
|
* @param clientThread The client manager thread.
|
||||||
|
* @return The id of the client.
|
||||||
|
*/
|
||||||
|
public UUID registerClient(Identification identification, ClientThread clientThread) {
|
||||||
|
var id = this.idProvider.newId();
|
||||||
|
log.info("Registering new client " + identification.getNickname() + " with id " + id);
|
||||||
this.clients.put(id, clientThread);
|
this.clients.put(id, clientThread);
|
||||||
|
// Send a welcome reply containing all the initial server info the client needs.
|
||||||
|
ServerMetaData metaData = new ServerMetaData(
|
||||||
|
"Testing Server",
|
||||||
|
this.channelManager.getChannels().stream()
|
||||||
|
.map(channel -> new ServerMetaData.ChannelData(channel.getId(), channel.getName()))
|
||||||
|
.sorted(Comparator.comparing(ServerMetaData.ChannelData::getName))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
var defaultChannel = this.channelManager.getChannelByName("general").orElseThrow();
|
||||||
|
defaultChannel.addClient(clientThread);
|
||||||
|
clientThread.setCurrentChannel(defaultChannel);
|
||||||
|
clientThread.sendToClient(new ServerWelcome(id, defaultChannel.getId(), metaData));
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deregisterClient(long clientId) {
|
public void deregisterClient(UUID clientId) {
|
||||||
this.clients.remove(clientId);
|
var client = this.clients.remove(clientId);
|
||||||
|
if (client != null) {
|
||||||
|
client.getCurrentChannel().removeClient(client);
|
||||||
|
client.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void handleChat(Chat chat) {
|
|
||||||
var collection = db.getCollection("channel-TEST");
|
|
||||||
long messageId = this.random.nextLong();
|
|
||||||
Document doc = Document.createDocument(Long.toHexString(messageId), "message")
|
|
||||||
.put("senderId", Long.toHexString(chat.getSenderId()))
|
|
||||||
.put("senderNickname", chat.getSenderNickname())
|
|
||||||
.put("timestamp", chat.getTimestamp())
|
|
||||||
.put("message", chat.getMessage());
|
|
||||||
collection.insert(doc);
|
|
||||||
db.commit();
|
|
||||||
System.out.println(chat.getSenderNickname() + ": " + chat.getMessage());
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(chat.getByteCount());
|
|
||||||
try {
|
|
||||||
Serializer.writeMessage(chat, new DataOutputStream(baos));
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byte[] data = baos.toByteArray();
|
|
||||||
for (var client : clients.values()) {
|
|
||||||
client.sendToClient(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleHistoryRequest(ChatHistoryRequest request, ClientThread clientThread) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
this.running = true;
|
||||||
ServerSocket serverSocket;
|
ServerSocket serverSocket;
|
||||||
try {
|
try {
|
||||||
serverSocket = new ServerSocket(this.port);
|
serverSocket = new ServerSocket(this.port);
|
||||||
log.info("Opened server on port " + this.port);
|
log.info("Opened server on port " + this.port);
|
||||||
while (true) {
|
while (this.running) {
|
||||||
Socket socket = serverSocket.accept();
|
Socket socket = serverSocket.accept();
|
||||||
log.info("Accepted new socket connection.");
|
log.info("Accepted new socket connection from " + socket.getInetAddress().getHostAddress());
|
||||||
ClientThread clientThread = new ClientThread(socket, this);
|
ClientThread clientThread = new ClientThread(socket, this);
|
||||||
clientThread.start();
|
clientThread.start();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
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.MoveToChannel;
|
||||||
|
import nl.andrewl.concord_server.event.ChannelMoveHandler;
|
||||||
|
import nl.andrewl.concord_server.event.ChatHandler;
|
||||||
|
import nl.andrewl.concord_server.event.MessageHandler;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Log
|
||||||
|
public class EventManager {
|
||||||
|
private final Map<Class<? extends Message>, MessageHandler<?>> messageHandlers;
|
||||||
|
private final ConcordServer server;
|
||||||
|
|
||||||
|
public EventManager(ConcordServer server) {
|
||||||
|
this.server = server;
|
||||||
|
this.messageHandlers = new HashMap<>();
|
||||||
|
this.messageHandlers.put(Chat.class, new ChatHandler());
|
||||||
|
this.messageHandlers.put(MoveToChannel.class, new ChannelMoveHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T extends Message> void handle(T message, ClientThread client) {
|
||||||
|
MessageHandler<T> handler = (MessageHandler<T>) this.messageHandlers.get(message.getClass());
|
||||||
|
if (handler != null) {
|
||||||
|
try {
|
||||||
|
handler.handle(message, client, this.server);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warning("Exception occurred while handling message: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface IdProvider {
|
||||||
|
UUID newId();
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package nl.andrewl.concord_server;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class UUIDProvider implements IdProvider {
|
||||||
|
@Override
|
||||||
|
public UUID newId() {
|
||||||
|
return UUID.randomUUID();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||||
|
import nl.andrewl.concord_server.ClientThread;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles client requests to move to another channel.
|
||||||
|
*/
|
||||||
|
public class ChannelMoveHandler implements MessageHandler<MoveToChannel> {
|
||||||
|
@Override
|
||||||
|
public void handle(MoveToChannel msg, ClientThread client, ConcordServer server) {
|
||||||
|
var optionalChannel = server.getChannelManager().getChannelById(msg.getChannelId());
|
||||||
|
optionalChannel.ifPresent(channel -> server.getChannelManager().moveToChannel(client, channel));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
|
import nl.andrewl.concord_server.ClientThread;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
import org.dizitart.no2.Document;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ChatHandler implements MessageHandler<Chat> {
|
||||||
|
@Override
|
||||||
|
public void handle(Chat msg, ClientThread client, ConcordServer server) throws IOException {
|
||||||
|
server.getExecutorService().submit(() -> {
|
||||||
|
var collection = server.getDb().getCollection("channel-" + client.getCurrentChannel().getId());
|
||||||
|
var messageId = server.getIdProvider().newId();
|
||||||
|
Document doc = new Document(Map.of(
|
||||||
|
"_id", messageId,
|
||||||
|
"senderId", msg.getSenderId(),
|
||||||
|
"senderNickname", msg.getSenderNickname(),
|
||||||
|
"timestamp", msg.getTimestamp(),
|
||||||
|
"message", msg.getMessage()
|
||||||
|
));
|
||||||
|
collection.insert(doc);
|
||||||
|
});
|
||||||
|
System.out.printf("#%s | %s: %s\n", client.getCurrentChannel(), client.getClientNickname(), msg.getMessage());
|
||||||
|
client.getCurrentChannel().sendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.types.Chat;
|
||||||
|
import nl.andrewl.concord_server.ClientThread;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
|
public interface EventListener {
|
||||||
|
default void chatMessageReceived(ConcordServer server, Chat chat, ClientThread client) {}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package nl.andrewl.concord_server.event;
|
||||||
|
|
||||||
|
import nl.andrewl.concord_core.msg.Message;
|
||||||
|
import nl.andrewl.concord_server.ClientThread;
|
||||||
|
import nl.andrewl.concord_server.ConcordServer;
|
||||||
|
|
||||||
|
public interface MessageHandler<T extends Message> {
|
||||||
|
void handle(T msg, ClientThread client, ConcordServer server) throws Exception;
|
||||||
|
}
|
Loading…
Reference in New Issue