Added channels and more commands.
This commit is contained in:
parent
4faba0d2eb
commit
cc5c90fd54
|
@ -24,5 +24,4 @@
|
|||
<version>3.1.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
|
@ -1,4 +1,5 @@
|
|||
module concord_client {
|
||||
requires concord_core;
|
||||
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.terminal.DefaultTerminalFactory;
|
||||
import com.googlecode.lanterna.terminal.Terminal;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import nl.andrewl.concord_client.gui.MainWindow;
|
||||
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.types.Chat;
|
||||
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 java.io.DataInputStream;
|
||||
|
@ -21,14 +25,20 @@ import java.net.Socket;
|
|||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class ConcordClient implements Runnable {
|
||||
|
||||
private final Socket socket;
|
||||
private final DataInputStream in;
|
||||
private final DataOutputStream out;
|
||||
private final long id;
|
||||
private final UUID id;
|
||||
private final String nickname;
|
||||
@Getter
|
||||
@Setter
|
||||
private UUID currentChannelId;
|
||||
@Getter
|
||||
private ServerMetaData serverMetaData;
|
||||
private final Set<ClientMessageListener> messageListeners;
|
||||
private volatile boolean running;
|
||||
|
||||
|
@ -41,6 +51,8 @@ public class ConcordClient implements Runnable {
|
|||
Message reply = Serializer.readMessage(this.in);
|
||||
if (reply instanceof ServerWelcome welcome) {
|
||||
this.id = welcome.getClientId();
|
||||
this.currentChannelId = welcome.getCurrentChannelId();
|
||||
this.serverMetaData = welcome.getMetaData();
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
|
||||
public void sendMessage(Message message) throws IOException {
|
||||
Serializer.writeMessage(message, this.out);
|
||||
}
|
||||
|
||||
public void sendChat(String message) throws IOException {
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
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.ClientMessageListener;
|
||||
import nl.andrewl.concord_client.ConcordClient;
|
||||
import nl.andrewl.concord_core.msg.Message;
|
||||
import nl.andrewl.concord_core.msg.types.Chat;
|
||||
import nl.andrewl.concord_core.msg.types.MoveToChannel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public class ChatPanel extends Panel implements ClientMessageListener {
|
||||
private final ChatList chatList;
|
||||
private final TextBox inputTextBox;
|
||||
@Getter
|
||||
private final ChannelChatBox channelChatBox;
|
||||
private final ChannelList channelList;
|
||||
private final UserList userList;
|
||||
|
||||
|
@ -30,41 +28,13 @@ public class ChatPanel extends Panel implements ClientMessageListener {
|
|||
public ChatPanel(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));
|
||||
|
||||
this.channelList = new ChannelList();
|
||||
this.channelList.addItem("general");
|
||||
this.channelList.addItem("memes");
|
||||
this.channelList.addItem("testing");
|
||||
this.channelChatBox = new ChannelChatBox(client, window);
|
||||
this.channelList = new ChannelList(client);
|
||||
this.channelList.setChannels();
|
||||
this.userList = new UserList();
|
||||
this.userList.addItem("andrew");
|
||||
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;
|
||||
b = Borders.doubleLine("Channels");
|
||||
b.setComponent(this.channelList);
|
||||
|
@ -74,18 +44,18 @@ public class ChatPanel extends Panel implements ClientMessageListener {
|
|||
b.setComponent(this.userList);
|
||||
this.addComponent(b, BorderLayout.Location.RIGHT);
|
||||
|
||||
b = Borders.doubleLine("#general");
|
||||
b.setComponent(this.chatList);
|
||||
this.addComponent(b, BorderLayout.Location.CENTER);
|
||||
|
||||
this.addComponent(this.inputTextBox, BorderLayout.Location.BOTTOM);
|
||||
this.inputTextBox.takeFocus();
|
||||
this.addComponent(this.channelChatBox, BorderLayout.Location.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(ConcordClient client, Message message) throws IOException {
|
||||
public void messageReceived(ConcordClient client, Message message) {
|
||||
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);
|
||||
new Thread(client).start();
|
||||
this.setComponent(chatPanel);
|
||||
Borders.joinLinesWithFrame(this.getTextGUI().getScreen().newTextGraphics());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ 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.
|
||||
|
@ -35,60 +36,4 @@ public interface Message {
|
|||
* @throws IOException If an error occurs while reading.
|
||||
*/
|
||||
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;
|
||||
|
||||
import nl.andrewl.concord_core.msg.types.Chat;
|
||||
import nl.andrewl.concord_core.msg.types.Identification;
|
||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
|
||||
import nl.andrewl.concord_core.msg.types.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.HashMap;
|
||||
|
@ -18,6 +16,9 @@ public class Serializer {
|
|||
registerType(0, Identification.class);
|
||||
registerType(1, ServerWelcome.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) {
|
||||
|
|
|
@ -7,6 +7,9 @@ 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 contains information about a chat message that a user sent.
|
||||
|
@ -14,12 +17,12 @@ import java.io.IOException;
|
|||
@Data
|
||||
@NoArgsConstructor
|
||||
public class Chat implements Message {
|
||||
private long senderId;
|
||||
private UUID senderId;
|
||||
private String senderNickname;
|
||||
private long timestamp;
|
||||
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.senderNickname = senderNickname;
|
||||
this.timestamp = timestamp;
|
||||
|
@ -27,17 +30,17 @@ public class Chat implements Message {
|
|||
}
|
||||
|
||||
public Chat(String message) {
|
||||
this(-1, null, System.currentTimeMillis(), message);
|
||||
this(null, null, System.currentTimeMillis(), message);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public void write(DataOutputStream o) throws IOException {
|
||||
o.writeLong(this.senderId);
|
||||
writeUUID(this.senderId, o);
|
||||
writeString(this.senderNickname, o);
|
||||
o.writeLong(this.timestamp);
|
||||
writeString(this.message, o);
|
||||
|
@ -45,7 +48,7 @@ public class Chat implements Message {
|
|||
|
||||
@Override
|
||||
public void read(DataInputStream i) throws IOException {
|
||||
this.senderId = i.readLong();
|
||||
this.senderId = readUUID(i);
|
||||
this.senderNickname = readString(i);
|
||||
this.timestamp = i.readLong();
|
||||
this.message = readString(i);
|
||||
|
@ -53,6 +56,6 @@ public class Chat implements Message {
|
|||
|
||||
@Override
|
||||
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.DataOutputStream;
|
||||
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
|
||||
|
@ -46,25 +49,25 @@ import java.io.IOException;
|
|||
public class ChatHistoryRequest implements Message {
|
||||
public enum Source {CHANNEL, THREAD, DIRECT_MESSAGE}
|
||||
|
||||
private long sourceId;
|
||||
private UUID sourceId;
|
||||
private Source sourceType;
|
||||
private String query;
|
||||
|
||||
@Override
|
||||
public int getByteCount() {
|
||||
return Long.BYTES + Integer.BYTES + getByteSize(this.query);
|
||||
return UUID_BYTES + Integer.BYTES + getByteSize(this.query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(DataOutputStream o) throws IOException {
|
||||
o.writeLong(sourceId);
|
||||
writeUUID(this.sourceId, o);
|
||||
writeEnum(this.sourceType, o);
|
||||
writeString(this.query, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(DataInputStream i) throws IOException {
|
||||
this.sourceId = i.readLong();
|
||||
this.sourceId = readUUID(i);
|
||||
this.sourceType = readEnum(Source.class, i);
|
||||
this.query = readString(i);
|
||||
}
|
||||
|
|
|
@ -3,19 +3,22 @@ package nl.andrewl.concord_core.msg.types;
|
|||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.concord_core.msg.Message;
|
||||
import nl.andrewl.concord_core.msg.MessageUtils;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
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
|
||||
@NoArgsConstructor
|
||||
public class ChatHistoryResponse implements Message {
|
||||
private long sourceId;
|
||||
private UUID sourceId;
|
||||
private ChatHistoryRequest.Source sourceType;
|
||||
List<Chat> messages;
|
||||
|
||||
|
@ -30,8 +33,8 @@ public class ChatHistoryResponse implements Message {
|
|||
|
||||
@Override
|
||||
public void write(DataOutputStream o) throws IOException {
|
||||
o.writeLong(this.sourceId);
|
||||
writeEnum(this.sourceType, o);
|
||||
MessageUtils.writeUUID(this.sourceId, o);
|
||||
MessageUtils.writeEnum(this.sourceType, o);
|
||||
o.writeInt(messages.size());
|
||||
for (var message : this.messages) {
|
||||
message.write(o);
|
||||
|
@ -40,8 +43,8 @@ public class ChatHistoryResponse implements Message {
|
|||
|
||||
@Override
|
||||
public void read(DataInputStream i) throws IOException {
|
||||
this.sourceId = i.readInt();
|
||||
this.sourceType = readEnum(ChatHistoryRequest.Source.class, i);
|
||||
this.sourceId = MessageUtils.readUUID(i);
|
||||
this.sourceType = MessageUtils.readEnum(ChatHistoryRequest.Source.class, i);
|
||||
int messageCount = i.readInt();
|
||||
Chat[] messages = new Chat[messageCount];
|
||||
for (int k = 0; k < messageCount; k++) {
|
||||
|
|
|
@ -3,6 +3,7 @@ package nl.andrewl.concord_core.msg.types;
|
|||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import nl.andrewl.concord_core.msg.Message;
|
||||
import nl.andrewl.concord_core.msg.MessageUtils;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
|
@ -23,16 +24,16 @@ public class Identification implements Message {
|
|||
|
||||
@Override
|
||||
public int getByteCount() {
|
||||
return getByteSize(this.nickname);
|
||||
return MessageUtils.getByteSize(this.nickname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(DataOutputStream o) throws IOException {
|
||||
writeString(this.nickname, o);
|
||||
MessageUtils.writeString(this.nickname, o);
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
|
||||
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 {
|
||||
private long clientId;
|
||||
private UUID clientId;
|
||||
private UUID currentChannelId;
|
||||
private ServerMetaData metaData;
|
||||
|
||||
@Override
|
||||
public int getByteCount() {
|
||||
return Long.BYTES;
|
||||
return 2 * UUID_BYTES + this.metaData.getByteCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(DataOutputStream o) throws IOException {
|
||||
o.writeLong(this.clientId);
|
||||
writeUUID(this.clientId, o);
|
||||
writeUUID(this.currentChannelId, o);
|
||||
this.metaData.write(o);
|
||||
}
|
||||
|
||||
@Override
|
||||
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>
|
||||
|
||||
</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>
|
|
@ -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;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.java.Log;
|
||||
import nl.andrewl.concord_core.msg.Message;
|
||||
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.ServerWelcome;
|
||||
|
||||
|
@ -13,6 +12,7 @@ import java.io.DataInputStream;
|
|||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 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 Long clientId = null;
|
||||
private UUID clientId = null;
|
||||
@Getter
|
||||
private String clientNickname = null;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private Channel currentChannel;
|
||||
|
||||
private volatile boolean running;
|
||||
|
||||
public ClientThread(Socket socket, ConcordServer server) throws IOException {
|
||||
this.socket = socket;
|
||||
this.server = server;
|
||||
|
@ -54,42 +60,60 @@ public class ClientThread extends Thread {
|
|||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.running = true;
|
||||
if (!identifyClient()) {
|
||||
log.warning("Could not identify the client; aborting connection.");
|
||||
return;
|
||||
this.running = false;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
while (this.running) {
|
||||
try {
|
||||
var msg = Serializer.readMessage(this.in);
|
||||
if (msg instanceof Chat chat) {
|
||||
this.server.handleChat(chat);
|
||||
} else if (msg instanceof ChatHistoryRequest historyRequest) {
|
||||
this.server.handleHistoryRequest(historyRequest, this);
|
||||
}
|
||||
this.server.getEventManager().handle(msg, this);
|
||||
} catch (IOException e) {
|
||||
log.info("Client disconnected: " + e.getMessage());
|
||||
if (this.clientId != null) {
|
||||
this.server.deregisterClient(this.clientId);
|
||||
}
|
||||
break;
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clientId != null) {
|
||||
this.server.deregisterClient(this.clientId);
|
||||
}
|
||||
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() {
|
||||
int attempts = 0;
|
||||
while (attempts < 5) {
|
||||
try {
|
||||
var msg = Serializer.readMessage(this.in);
|
||||
if (msg instanceof Identification id) {
|
||||
this.clientId = this.server.registerClient(this);
|
||||
this.clientNickname = id.getNickname();
|
||||
var reply = new ServerWelcome();
|
||||
reply.setClientId(this.clientId);
|
||||
Serializer.writeMessage(reply, this.out);
|
||||
this.clientId = this.server.registerClient(id, this);
|
||||
return true;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
|
|
@ -1,85 +1,100 @@
|
|||
package nl.andrewl.concord_server;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.java.Log;
|
||||
import nl.andrewl.concord_core.msg.Serializer;
|
||||
import nl.andrewl.concord_core.msg.types.Chat;
|
||||
import nl.andrewl.concord_core.msg.types.ChatHistoryRequest;
|
||||
import org.dizitart.no2.Document;
|
||||
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 org.dizitart.no2.Nitrite;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Log
|
||||
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 Random random;
|
||||
@Getter
|
||||
private final IdProvider idProvider;
|
||||
@Getter
|
||||
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) {
|
||||
this.port = port;
|
||||
this.random = new SecureRandom();
|
||||
this.idProvider = new UUIDProvider();
|
||||
this.db = Nitrite.builder()
|
||||
.filePath("concord-server.db")
|
||||
.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();
|
||||
log.info("Registering new client " + clientThread.getClientNickname() + " with id " + id);
|
||||
/**
|
||||
* Registers a new client as connected to the server. This is done once the
|
||||
* 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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
public void deregisterClient(long clientId) {
|
||||
this.clients.remove(clientId);
|
||||
}
|
||||
|
||||
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;
|
||||
public void deregisterClient(UUID clientId) {
|
||||
var client = this.clients.remove(clientId);
|
||||
if (client != null) {
|
||||
client.getCurrentChannel().removeClient(client);
|
||||
client.shutdown();
|
||||
}
|
||||
byte[] data = baos.toByteArray();
|
||||
for (var client : clients.values()) {
|
||||
client.sendToClient(data);
|
||||
}
|
||||
}
|
||||
|
||||
public void handleHistoryRequest(ChatHistoryRequest request, ClientThread clientThread) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.running = true;
|
||||
ServerSocket serverSocket;
|
||||
try {
|
||||
serverSocket = new ServerSocket(this.port);
|
||||
log.info("Opened server on port " + this.port);
|
||||
while (true) {
|
||||
while (this.running) {
|
||||
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.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