Added initial prototype.
This commit is contained in:
		
							parent
							
								
									ae50a15c00
								
							
						
					
					
						commit
						468758c7ab
					
				| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
/.idea
 | 
			
		||||
/target
 | 
			
		||||
							
								
								
									
										11
									
								
								README.md
								
								
								
								
							
							
						
						
									
										11
									
								
								README.md
								
								
								
								
							| 
						 | 
				
			
			@ -1,2 +1,11 @@
 | 
			
		|||
# Concord
 | 
			
		||||
Console-based real-time messaging platform.
 | 
			
		||||
Console-based real-time messaging platform, inspired by Discord.
 | 
			
		||||
 | 
			
		||||
This platform is organized by many independent servers, each of which supports the following:
 | 
			
		||||
- Multiple message channels. By default, there's one `general` channel.
 | 
			
		||||
- Broadcasting itself on certain discovery servers for users to find. The server decides where it wants to be discovered, if at all.
 | 
			
		||||
- Starting threads as spin-offs of a source message (with infinite recursion, i.e. threads within threads).
 | 
			
		||||
- Private message between users in a server. **No support for private messaging users outside the context of a server.**
 | 
			
		||||
- Banning users from the server.
 | 
			
		||||
 | 
			
		||||
Each server uses a single [Nitrite](https://www.dizitart.org/nitrite-database/#what-is-nitrite) database to hold messages and other information.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/target
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <parent>
 | 
			
		||||
        <artifactId>concord</artifactId>
 | 
			
		||||
        <groupId>nl.andrewl</groupId>
 | 
			
		||||
        <version>1.0-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <artifactId>concord-client</artifactId>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>nl.andrewl</groupId>
 | 
			
		||||
            <artifactId>concord-core</artifactId>
 | 
			
		||||
            <version>${project.parent.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>com.googlecode.lanterna</groupId>
 | 
			
		||||
            <artifactId>lanterna</artifactId>
 | 
			
		||||
            <version>3.1.1</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
    </dependencies>
 | 
			
		||||
 | 
			
		||||
</project>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
module concord_client {
 | 
			
		||||
	requires concord_core;
 | 
			
		||||
	requires com.googlecode.lanterna;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,9 @@
 | 
			
		|||
package nl.andrewl.concord_client;
 | 
			
		||||
 | 
			
		||||
import nl.andrewl.concord_core.msg.Message;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
public interface ClientMessageListener {
 | 
			
		||||
	void messageReceived(ConcordClient client, Message message) throws IOException;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,109 @@
 | 
			
		|||
package nl.andrewl.concord_client;
 | 
			
		||||
 | 
			
		||||
import com.googlecode.lanterna.gui2.MultiWindowTextGUI;
 | 
			
		||||
import com.googlecode.lanterna.gui2.Window;
 | 
			
		||||
import com.googlecode.lanterna.gui2.WindowBasedTextGUI;
 | 
			
		||||
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 nl.andrewl.concord_client.gui.MainWindow;
 | 
			
		||||
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.Identification;
 | 
			
		||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
 | 
			
		||||
 | 
			
		||||
import java.io.DataInputStream;
 | 
			
		||||
import java.io.DataOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.Socket;
 | 
			
		||||
import java.util.HashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
public class ConcordClient implements Runnable {
 | 
			
		||||
 | 
			
		||||
	private final Socket socket;
 | 
			
		||||
	private final DataInputStream in;
 | 
			
		||||
	private final DataOutputStream out;
 | 
			
		||||
	private final long id;
 | 
			
		||||
	private final String nickname;
 | 
			
		||||
	private final Set<ClientMessageListener> messageListeners;
 | 
			
		||||
	private volatile boolean running;
 | 
			
		||||
 | 
			
		||||
	public ConcordClient(String host, int port, String nickname) throws IOException {
 | 
			
		||||
		this.socket = new Socket(host, port);
 | 
			
		||||
		this.in = new DataInputStream(this.socket.getInputStream());
 | 
			
		||||
		this.out = new DataOutputStream(this.socket.getOutputStream());
 | 
			
		||||
		this.nickname = nickname;
 | 
			
		||||
		Serializer.writeMessage(new Identification(nickname), this.out);
 | 
			
		||||
		Message reply = Serializer.readMessage(this.in);
 | 
			
		||||
		if (reply instanceof ServerWelcome welcome) {
 | 
			
		||||
			this.id = welcome.getClientId();
 | 
			
		||||
		} else {
 | 
			
		||||
			throw new IOException("Unexpected response from the server after sending identification message.");
 | 
			
		||||
		}
 | 
			
		||||
		this.messageListeners = new HashSet<>();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void addListener(ClientMessageListener listener) {
 | 
			
		||||
		this.messageListeners.add(listener);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void removeListener(ClientMessageListener listener) {
 | 
			
		||||
		this.messageListeners.remove(listener);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void sendChat(String message) throws IOException {
 | 
			
		||||
		Serializer.writeMessage(new Chat(this.id, this.nickname, System.currentTimeMillis(), message), this.out);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void shutdown() {
 | 
			
		||||
		this.running = false;
 | 
			
		||||
		if (!this.socket.isClosed()) {
 | 
			
		||||
			try {
 | 
			
		||||
				this.socket.close();
 | 
			
		||||
			} catch (IOException e) {
 | 
			
		||||
				e.printStackTrace();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
		this.running = true;
 | 
			
		||||
		while (this.running) {
 | 
			
		||||
			try {
 | 
			
		||||
				Message msg = Serializer.readMessage(this.in);
 | 
			
		||||
				for (var listener : this.messageListeners) {
 | 
			
		||||
					listener.messageReceived(this, msg);
 | 
			
		||||
				}
 | 
			
		||||
			} catch (IOException e) {
 | 
			
		||||
				e.printStackTrace();
 | 
			
		||||
				this.running = false;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			this.socket.close();
 | 
			
		||||
		} catch (IOException e) {
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	public static void main(String[] args) throws IOException {
 | 
			
		||||
		Terminal term = new DefaultTerminalFactory().createTerminal();
 | 
			
		||||
		Screen screen = new TerminalScreen(term);
 | 
			
		||||
		WindowBasedTextGUI gui = new MultiWindowTextGUI(screen);
 | 
			
		||||
		screen.startScreen();
 | 
			
		||||
 | 
			
		||||
		Window window = new MainWindow();
 | 
			
		||||
		window.setHints(List.of(Window.Hint.FULL_SCREEN));
 | 
			
		||||
		gui.addWindow(window);
 | 
			
		||||
 | 
			
		||||
		window.waitUntilClosed();
 | 
			
		||||
		screen.stopScreen();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
package nl.andrewl.concord_client.gui;
 | 
			
		||||
 | 
			
		||||
import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
			
		||||
 | 
			
		||||
public class ChannelList extends AbstractListBox<String, ChannelList> {
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,31 @@
 | 
			
		|||
package nl.andrewl.concord_client.gui;
 | 
			
		||||
 | 
			
		||||
import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
			
		||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
			
		||||
 | 
			
		||||
public class ChatList extends AbstractListBox<Chat, ChatList> {
 | 
			
		||||
	/**
 | 
			
		||||
	 * Adds one more item to the list box, at the end.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @param item Item to add to the list box
 | 
			
		||||
	 * @return Itself
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public synchronized ChatList addItem(Chat item) {
 | 
			
		||||
		super.addItem(item);
 | 
			
		||||
		this.setSelectedIndex(this.getItemCount() - 1);
 | 
			
		||||
		return this;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Method that constructs the {@code ListItemRenderer} that this list box should use to draw the elements of the
 | 
			
		||||
	 * list box. This can be overridden to supply a custom renderer. Note that this is not the renderer used for the
 | 
			
		||||
	 * entire list box but for each item, called one by one.
 | 
			
		||||
	 *
 | 
			
		||||
	 * @return {@code ListItemRenderer} to use when drawing the items in the list
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	protected ListItemRenderer<Chat, ChatList> createDefaultListItemRenderer() {
 | 
			
		||||
		return new ChatRenderer();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,91 @@
 | 
			
		|||
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 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 java.io.IOException;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The main panel in which a user interacts with the application during normal
 | 
			
		||||
 * operation. In here, the user is shown a list of the most recent chats in
 | 
			
		||||
 * their current channel or thread, a text-box for sending a message, and some
 | 
			
		||||
 * meta information in the sidebars which provides the user with a list of all
 | 
			
		||||
 * threads and users in the server.
 | 
			
		||||
 */
 | 
			
		||||
public class ChatPanel extends Panel implements ClientMessageListener {
 | 
			
		||||
	private final ChatList chatList;
 | 
			
		||||
	private final TextBox inputTextBox;
 | 
			
		||||
	private final ChannelList channelList;
 | 
			
		||||
	private final UserList userList;
 | 
			
		||||
 | 
			
		||||
	private final ConcordClient client;
 | 
			
		||||
 | 
			
		||||
	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.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);
 | 
			
		||||
		this.addComponent(b, BorderLayout.Location.LEFT);
 | 
			
		||||
 | 
			
		||||
		b = Borders.doubleLine("Users");
 | 
			
		||||
		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();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void messageReceived(ConcordClient client, Message message) throws IOException {
 | 
			
		||||
		if (message instanceof Chat chat) {
 | 
			
		||||
			this.chatList.addItem(chat);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
package nl.andrewl.concord_client.gui;
 | 
			
		||||
 | 
			
		||||
import com.googlecode.lanterna.TerminalTextUtils;
 | 
			
		||||
import com.googlecode.lanterna.graphics.ThemeDefinition;
 | 
			
		||||
import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
			
		||||
import com.googlecode.lanterna.gui2.TextGUIGraphics;
 | 
			
		||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
			
		||||
 | 
			
		||||
import java.time.Instant;
 | 
			
		||||
import java.time.ZoneId;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
 | 
			
		||||
public class ChatRenderer extends AbstractListBox.ListItemRenderer<Chat, ChatList> {
 | 
			
		||||
	@Override
 | 
			
		||||
	public void drawItem(TextGUIGraphics graphics, ChatList listBox, int index, Chat chat, boolean selected, boolean focused) {
 | 
			
		||||
		ThemeDefinition themeDefinition = listBox.getTheme().getDefinition(AbstractListBox.class);
 | 
			
		||||
		if(selected && focused) {
 | 
			
		||||
			graphics.applyThemeStyle(themeDefinition.getSelected());
 | 
			
		||||
		}
 | 
			
		||||
		else {
 | 
			
		||||
			graphics.applyThemeStyle(themeDefinition.getNormal());
 | 
			
		||||
		}
 | 
			
		||||
		graphics.putString(0, 0, chat.getSenderNickname());
 | 
			
		||||
		Instant timestamp = Instant.ofEpochMilli(chat.getTimestamp());
 | 
			
		||||
		String timeStr = timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("HH:mm:ss"));
 | 
			
		||||
		String label = chat.getSenderNickname() + " @ " + timeStr + " : " + chat.getMessage();
 | 
			
		||||
		label = TerminalTextUtils.fitString(label, graphics.getSize().getColumns());
 | 
			
		||||
		while(TerminalTextUtils.getColumnWidth(label) < graphics.getSize().getColumns()) {
 | 
			
		||||
			label += " ";
 | 
			
		||||
		}
 | 
			
		||||
		graphics.putString(0, 0, label);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
package nl.andrewl.concord_client.gui;
 | 
			
		||||
 | 
			
		||||
import com.googlecode.lanterna.gui2.*;
 | 
			
		||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
 | 
			
		||||
import com.googlecode.lanterna.input.KeyStroke;
 | 
			
		||||
import nl.andrewl.concord_client.ConcordClient;
 | 
			
		||||
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean;
 | 
			
		||||
 | 
			
		||||
public class MainWindow extends BasicWindow {
 | 
			
		||||
	public MainWindow() {
 | 
			
		||||
		super("Concord");
 | 
			
		||||
		Panel panel = new Panel(new LinearLayout(Direction.VERTICAL));
 | 
			
		||||
 | 
			
		||||
		Button button = new Button("Connect to Server");
 | 
			
		||||
		button.addListener(b -> connectToServer());
 | 
			
		||||
		panel.addComponent(button);
 | 
			
		||||
 | 
			
		||||
		this.setComponent(panel);
 | 
			
		||||
		this.addWindowListener(new WindowListenerAdapter() {
 | 
			
		||||
			@Override
 | 
			
		||||
			public void onInput(Window basePane, KeyStroke keyStroke, AtomicBoolean hasBeenHandled) {
 | 
			
		||||
				if (keyStroke.getCharacter() != null && keyStroke.getCharacter() == 'c' && keyStroke.isCtrlDown()) {
 | 
			
		||||
					System.exit(0);
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void connectToServer() {
 | 
			
		||||
		System.out.println("Connecting to server!");
 | 
			
		||||
		var addressDialog = new TextInputDialogBuilder()
 | 
			
		||||
				.setTitle("Server Address")
 | 
			
		||||
				.setDescription("Enter the address of the server to connect to. For example, \"localhost:1234\".")
 | 
			
		||||
				.setInitialContent("localhost:8123")
 | 
			
		||||
				.build();
 | 
			
		||||
		String address = addressDialog.showDialog(this.getTextGUI());
 | 
			
		||||
		if (address == null) return;
 | 
			
		||||
		String[] parts = address.split(":");
 | 
			
		||||
		if (parts.length != 2) return;
 | 
			
		||||
		String host = parts[0];
 | 
			
		||||
		int port = Integer.parseInt(parts[1]);
 | 
			
		||||
 | 
			
		||||
		var nameDialog = new TextInputDialogBuilder()
 | 
			
		||||
				.setTitle("Nickname")
 | 
			
		||||
				.setDescription("Enter a nickname to use while in the server.")
 | 
			
		||||
				.build();
 | 
			
		||||
		String nickname = nameDialog.showDialog(this.getTextGUI());
 | 
			
		||||
		if (nickname == null) return;
 | 
			
		||||
 | 
			
		||||
		try {
 | 
			
		||||
			var client = new ConcordClient(host, port, nickname);
 | 
			
		||||
			var chatPanel = new ChatPanel(client, this);
 | 
			
		||||
			client.addListener(chatPanel);
 | 
			
		||||
			new Thread(client).start();
 | 
			
		||||
			this.setComponent(chatPanel);
 | 
			
		||||
			Borders.joinLinesWithFrame(this.getTextGUI().getScreen().newTextGraphics());
 | 
			
		||||
		} catch (IOException e) {
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
package nl.andrewl.concord_client.gui;
 | 
			
		||||
 | 
			
		||||
import com.googlecode.lanterna.gui2.AbstractListBox;
 | 
			
		||||
 | 
			
		||||
public class UserList extends AbstractListBox<String, UserList> {
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
/target
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <parent>
 | 
			
		||||
        <artifactId>concord</artifactId>
 | 
			
		||||
        <groupId>nl.andrewl</groupId>
 | 
			
		||||
        <version>1.0-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <artifactId>concord-core</artifactId>
 | 
			
		||||
</project>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
module concord_core {
 | 
			
		||||
	requires static lombok;
 | 
			
		||||
 | 
			
		||||
	exports nl.andrewl.concord_core.msg to concord_server, concord_client;
 | 
			
		||||
	exports nl.andrewl.concord_core.msg.types to concord_server, concord_client;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,85 @@
 | 
			
		|||
package nl.andrewl.concord_core.msg;
 | 
			
		||||
 | 
			
		||||
import java.io.DataInputStream;
 | 
			
		||||
import java.io.DataOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents any message which can be sent over the network.
 | 
			
		||||
 * <p>
 | 
			
		||||
 *     All messages consist of a single byte type identifier, followed by a
 | 
			
		||||
 *     payload whose structure depends on the message.
 | 
			
		||||
 * </p>
 | 
			
		||||
 */
 | 
			
		||||
public interface Message {
 | 
			
		||||
	/**
 | 
			
		||||
	 * @return The exact number of bytes that this message will use when written
 | 
			
		||||
	 * to a stream.
 | 
			
		||||
	 */
 | 
			
		||||
	int getByteCount();
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Writes this message to the given output stream.
 | 
			
		||||
	 * @param o The output stream to write to.
 | 
			
		||||
	 * @throws IOException If an error occurs while writing.
 | 
			
		||||
	 */
 | 
			
		||||
	void write(DataOutputStream o) throws IOException;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Reads all of this message's properties from the given input stream.
 | 
			
		||||
	 * <p>
 | 
			
		||||
	 *     The single byte type identifier has already been read.
 | 
			
		||||
	 * </p>
 | 
			
		||||
	 * @param i The input stream to read from.
 | 
			
		||||
	 * @throws IOException If an error occurs while reading.
 | 
			
		||||
	 */
 | 
			
		||||
	void read(DataInputStream i) throws IOException;
 | 
			
		||||
 | 
			
		||||
	// 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);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,55 @@
 | 
			
		|||
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 java.io.*;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This class is responsible for reading and writing messages from streams.
 | 
			
		||||
 */
 | 
			
		||||
public class Serializer {
 | 
			
		||||
	private static final Map<Byte, Class<? extends Message>> messageTypes = new HashMap<>();
 | 
			
		||||
	private static final Map<Class<? extends Message>, Byte> inverseMessageTypes = new HashMap<>();
 | 
			
		||||
	static {
 | 
			
		||||
		registerType(0, Identification.class);
 | 
			
		||||
		registerType(1, ServerWelcome.class);
 | 
			
		||||
		registerType(2, Chat.class);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private static void registerType(int id, Class<? extends Message> clazz) {
 | 
			
		||||
		messageTypes.put((byte) id, clazz);
 | 
			
		||||
		inverseMessageTypes.put(clazz, (byte) id);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static Message readMessage(InputStream i) throws IOException {
 | 
			
		||||
		DataInputStream d = new DataInputStream(i);
 | 
			
		||||
		byte type = d.readByte();
 | 
			
		||||
		var clazz = messageTypes.get(type);
 | 
			
		||||
		if (clazz == null) {
 | 
			
		||||
			throw new IOException("Unsupported message type: " + type);
 | 
			
		||||
		}
 | 
			
		||||
		try {
 | 
			
		||||
			var constructor = clazz.getConstructor();
 | 
			
		||||
			var message = constructor.newInstance();
 | 
			
		||||
			message.read(d);
 | 
			
		||||
			return message;
 | 
			
		||||
		} catch (Throwable e) {
 | 
			
		||||
			throw new IOException("Could not instantiate new message object of type " + clazz.getSimpleName(), e);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static void writeMessage(Message msg, OutputStream o) throws IOException {
 | 
			
		||||
		DataOutputStream d = new DataOutputStream(o);
 | 
			
		||||
		Byte type = inverseMessageTypes.get(msg.getClass());
 | 
			
		||||
		if (type == null) {
 | 
			
		||||
			throw new IOException("Unsupported message type: " + msg.getClass().getSimpleName());
 | 
			
		||||
		}
 | 
			
		||||
		d.writeByte(type);
 | 
			
		||||
		msg.write(d);
 | 
			
		||||
		d.flush();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,58 @@
 | 
			
		|||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This message contains information about a chat message that a user sent.
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
public class Chat implements Message {
 | 
			
		||||
	private long senderId;
 | 
			
		||||
	private String senderNickname;
 | 
			
		||||
	private long timestamp;
 | 
			
		||||
	private String message;
 | 
			
		||||
 | 
			
		||||
	public Chat(long senderId, String senderNickname, long timestamp, String message) {
 | 
			
		||||
		this.senderId = senderId;
 | 
			
		||||
		this.senderNickname = senderNickname;
 | 
			
		||||
		this.timestamp = timestamp;
 | 
			
		||||
		this.message = message;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public Chat(String message) {
 | 
			
		||||
		this(-1, null, System.currentTimeMillis(), message);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int getByteCount() {
 | 
			
		||||
		return 2 * Long.BYTES + getByteSize(this.message) + getByteSize(this.senderNickname);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void write(DataOutputStream o) throws IOException {
 | 
			
		||||
		o.writeLong(this.senderId);
 | 
			
		||||
		writeString(this.senderNickname, o);
 | 
			
		||||
		o.writeLong(this.timestamp);
 | 
			
		||||
		writeString(this.message, o);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void read(DataInputStream i) throws IOException {
 | 
			
		||||
		this.senderId = i.readLong();
 | 
			
		||||
		this.senderNickname = readString(i);
 | 
			
		||||
		this.timestamp = i.readLong();
 | 
			
		||||
		this.message = readString(i);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public String toString() {
 | 
			
		||||
		return String.format("%s(%d): %s", this.senderNickname, this.senderId, this.message);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,38 @@
 | 
			
		|||
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;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This message is sent from the client to a server, to provide identification
 | 
			
		||||
 * information about the client to the server when the connection is started.
 | 
			
		||||
 */
 | 
			
		||||
@Data
 | 
			
		||||
@NoArgsConstructor
 | 
			
		||||
public class Identification implements Message {
 | 
			
		||||
	private String nickname;
 | 
			
		||||
 | 
			
		||||
	public Identification(String nickname) {
 | 
			
		||||
		this.nickname = nickname;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int getByteCount() {
 | 
			
		||||
		return getByteSize(this.nickname);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void write(DataOutputStream o) throws IOException {
 | 
			
		||||
		writeString(this.nickname, o);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void read(DataInputStream i) throws IOException {
 | 
			
		||||
		this.nickname = readString(i);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,32 @@
 | 
			
		|||
package nl.andrewl.concord_core.msg.types;
 | 
			
		||||
 | 
			
		||||
import lombok.Data;
 | 
			
		||||
import nl.andrewl.concord_core.msg.Message;
 | 
			
		||||
 | 
			
		||||
import java.io.DataInputStream;
 | 
			
		||||
import java.io.DataOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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
 | 
			
		||||
public class ServerWelcome implements Message {
 | 
			
		||||
	private long clientId;
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int getByteCount() {
 | 
			
		||||
		return Long.BYTES;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void write(DataOutputStream o) throws IOException {
 | 
			
		||||
		o.writeLong(this.clientId);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void read(DataInputStream i) throws IOException {
 | 
			
		||||
		this.clientId = i.readLong();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
/**
 | 
			
		||||
 * This package contains all the components needed by both the server and the
 | 
			
		||||
 * client.
 | 
			
		||||
 */
 | 
			
		||||
package nl.andrewl.concord_core;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,51 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <groupId>nl.andrewl</groupId>
 | 
			
		||||
    <artifactId>concord</artifactId>
 | 
			
		||||
    <packaging>pom</packaging>
 | 
			
		||||
    <version>1.0-SNAPSHOT</version>
 | 
			
		||||
    <modules>
 | 
			
		||||
        <module>server</module>
 | 
			
		||||
        <module>core</module>
 | 
			
		||||
        <module>client</module>
 | 
			
		||||
    </modules>
 | 
			
		||||
 | 
			
		||||
    <properties>
 | 
			
		||||
        <maven.compiler.source>16</maven.compiler.source>
 | 
			
		||||
        <maven.compiler.target>16</maven.compiler.target>
 | 
			
		||||
        <java.version>16</java.version>
 | 
			
		||||
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 | 
			
		||||
    </properties>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.projectlombok</groupId>
 | 
			
		||||
            <artifactId>lombok</artifactId>
 | 
			
		||||
            <version>1.18.20</version>
 | 
			
		||||
            <scope>provided</scope>
 | 
			
		||||
        </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 @@
 | 
			
		|||
/target
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
 | 
			
		||||
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 | 
			
		||||
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 | 
			
		||||
    <parent>
 | 
			
		||||
        <artifactId>concord</artifactId>
 | 
			
		||||
        <groupId>nl.andrewl</groupId>
 | 
			
		||||
        <version>1.0-SNAPSHOT</version>
 | 
			
		||||
    </parent>
 | 
			
		||||
    <modelVersion>4.0.0</modelVersion>
 | 
			
		||||
 | 
			
		||||
    <artifactId>concord-server</artifactId>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>nl.andrewl</groupId>
 | 
			
		||||
            <artifactId>concord-core</artifactId>
 | 
			
		||||
            <version>${project.parent.version}</version>
 | 
			
		||||
        </dependency>
 | 
			
		||||
 | 
			
		||||
        <dependency>
 | 
			
		||||
            <groupId>org.dizitart</groupId>
 | 
			
		||||
            <artifactId>nitrite</artifactId>
 | 
			
		||||
            <version>3.4.3</version>
 | 
			
		||||
        </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,9 @@
 | 
			
		|||
module concord_server {
 | 
			
		||||
	requires nitrite;
 | 
			
		||||
	requires static lombok;
 | 
			
		||||
 | 
			
		||||
	requires java.base;
 | 
			
		||||
	requires java.logging;
 | 
			
		||||
 | 
			
		||||
	requires concord_core;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,99 @@
 | 
			
		|||
package nl.andrewl.concord_server;
 | 
			
		||||
 | 
			
		||||
import lombok.Getter;
 | 
			
		||||
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.Identification;
 | 
			
		||||
import nl.andrewl.concord_core.msg.types.ServerWelcome;
 | 
			
		||||
 | 
			
		||||
import java.io.DataInputStream;
 | 
			
		||||
import java.io.DataOutputStream;
 | 
			
		||||
import java.io.IOException;
 | 
			
		||||
import java.net.Socket;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This thread is responsible for handling the connection to a single client of
 | 
			
		||||
 * a server.
 | 
			
		||||
 */
 | 
			
		||||
@Log
 | 
			
		||||
public class ClientThread extends Thread {
 | 
			
		||||
	private final Socket socket;
 | 
			
		||||
	private final DataInputStream in;
 | 
			
		||||
	private final DataOutputStream out;
 | 
			
		||||
 | 
			
		||||
	private final ConcordServer server;
 | 
			
		||||
 | 
			
		||||
	private Long clientId = null;
 | 
			
		||||
	@Getter
 | 
			
		||||
	private String clientNickname = null;
 | 
			
		||||
 | 
			
		||||
	public ClientThread(Socket socket, ConcordServer server) throws IOException {
 | 
			
		||||
		this.socket = socket;
 | 
			
		||||
		this.server = server;
 | 
			
		||||
		this.in = new DataInputStream(socket.getInputStream());
 | 
			
		||||
		this.out = new DataOutputStream(socket.getOutputStream());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void sendToClient(Message message) {
 | 
			
		||||
		try {
 | 
			
		||||
			Serializer.writeMessage(message, this.out);
 | 
			
		||||
		} catch (IOException e) {
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void sendToClient(byte[] bytes) {
 | 
			
		||||
		try {
 | 
			
		||||
			this.out.write(bytes);
 | 
			
		||||
			this.out.flush();
 | 
			
		||||
		} catch (IOException e) {
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
		if (!identifyClient()) {
 | 
			
		||||
			log.warning("Could not identify the client; aborting connection.");
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		while (true) {
 | 
			
		||||
			try {
 | 
			
		||||
				var msg = Serializer.readMessage(this.in);
 | 
			
		||||
				if (msg instanceof Chat chat) {
 | 
			
		||||
					this.server.handleChat(chat);
 | 
			
		||||
				}
 | 
			
		||||
			} catch (IOException e) {
 | 
			
		||||
				log.info("Client disconnected: " + e.getMessage());
 | 
			
		||||
				if (this.clientId != null) {
 | 
			
		||||
					this.server.deregisterClient(this.clientId);
 | 
			
		||||
				}
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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);
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
			} catch (IOException e) {
 | 
			
		||||
				e.printStackTrace();
 | 
			
		||||
			}
 | 
			
		||||
			attempts++;
 | 
			
		||||
		}
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,75 @@
 | 
			
		|||
package nl.andrewl.concord_server;
 | 
			
		||||
 | 
			
		||||
import lombok.extern.java.Log;
 | 
			
		||||
import nl.andrewl.concord_core.msg.Serializer;
 | 
			
		||||
import nl.andrewl.concord_core.msg.types.Chat;
 | 
			
		||||
 | 
			
		||||
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.Map;
 | 
			
		||||
import java.util.Random;
 | 
			
		||||
import java.util.concurrent.ConcurrentHashMap;
 | 
			
		||||
 | 
			
		||||
@Log
 | 
			
		||||
public class ConcordServer implements Runnable {
 | 
			
		||||
	private final Map<Long, ClientThread> clients = new ConcurrentHashMap<>(32);
 | 
			
		||||
	private final int port;
 | 
			
		||||
	private final Random random;
 | 
			
		||||
 | 
			
		||||
	public ConcordServer(int port) {
 | 
			
		||||
		this.port = port;
 | 
			
		||||
		this.random = new SecureRandom();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public long registerClient(ClientThread clientThread) {
 | 
			
		||||
		long id = this.random.nextLong();
 | 
			
		||||
		log.info("Registering new client " + clientThread.getClientNickname() + " with id " + id);
 | 
			
		||||
		this.clients.put(id, clientThread);
 | 
			
		||||
		return id;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void deregisterClient(long clientId) {
 | 
			
		||||
		this.clients.remove(clientId);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public void handleChat(Chat chat) {
 | 
			
		||||
		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);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void run() {
 | 
			
		||||
		ServerSocket serverSocket;
 | 
			
		||||
		try {
 | 
			
		||||
			serverSocket = new ServerSocket(this.port);
 | 
			
		||||
			log.info("Opened server on port " + this.port);
 | 
			
		||||
			while (true) {
 | 
			
		||||
				Socket socket = serverSocket.accept();
 | 
			
		||||
				log.info("Accepted new socket connection.");
 | 
			
		||||
				ClientThread clientThread = new ClientThread(socket, this);
 | 
			
		||||
				clientThread.start();
 | 
			
		||||
			}
 | 
			
		||||
		} catch (IOException e) {
 | 
			
		||||
			e.printStackTrace();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public static void main(String[] args) {
 | 
			
		||||
		var server = new ConcordServer(8123);
 | 
			
		||||
		server.run();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,12 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<configuration>
 | 
			
		||||
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
 | 
			
		||||
        <encoder>
 | 
			
		||||
            <pattern>%d{dd.MM.yyyy HH:mm:ss} %boldCyan(%-34.-34thread) %red(%10.10X{jda.shard}) %boldGreen(%-15.-15logger{0}) %highlight(%-6level) %msg%n</pattern>
 | 
			
		||||
        </encoder>
 | 
			
		||||
    </appender>
 | 
			
		||||
 | 
			
		||||
    <root level="info">
 | 
			
		||||
        <appender-ref ref="STDOUT" />
 | 
			
		||||
    </root>
 | 
			
		||||
</configuration>
 | 
			
		||||
		Loading…
	
		Reference in New Issue