diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml
new file mode 100644
index 0000000..aaa0c55
--- /dev/null
+++ b/.idea/checkstyle-idea.xml
@@ -0,0 +1,18 @@
+
+
+
+ 10.21.1
+ JavaOnly
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 0000000..b9d18bf
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
index ee3ff27..e3258e2 100644
--- a/.idea/encodings.xml
+++ b/.idea/encodings.xml
@@ -2,8 +2,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..3d3b9b3
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.MD b/CHANGELOG.MD
index 441eb19..411b146 100644
--- a/CHANGELOG.MD
+++ b/CHANGELOG.MD
@@ -4,15 +4,34 @@
### Enhancements
-- Use GraalVM instead of Launch4j for creating Windows native images. Launch4j made necessary for the user to have JRE
+-- Commit 2 --
+
+- Add UTF-8 support natively on terminal
+- Code structure enhancements
+- Use exception as cancellation mechanism
+- Add more user-friendly warnings
+- Add Google Code Style
+- Add JavaDocs
+- Faster resource closing on Server and Client classes
+
+-- [Commit 1](https://github.com/FelipeKobra/JMessenger/commit/1260371d40a75ea7e6108d8e7c3683d85e746ee1) --
+
+- Use GraalVM instead of Launch4j for creating Windows native images. Launch4j made necessary for
+ the user to have JRE
- Added JLine in bundles for better compatibility with the native image build
- Change Deprecated JLine Jansi to JNI Version
### Bug Fixes
+-- [Commit 1](https://github.com/FelipeKobra/JMessenger/commit/1260371d40a75ea7e6108d8e7c3683d85e746ee1) --
+
- Fixed showing terminal buffered messages a lot of times on closing the server
- No more Exceptions when exiting of the application via `CTRL + C`
+-- Commit 2 --
+
+- Fix wrong maven folder structure
+
## [0.0.2](https://github.com/FelipeKobra/JMessenger/tree/v0.0.2) - January 12, 2025
### Enhancements
diff --git a/README.md b/README.md
index 0548a0c..3bdcf98 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
JMessenger
-**JMessenger** is a terminal-based chat application that allows you to communicate seamlessly with others over
+**JMessenger** is a terminal-based chatUtils application that allows you to communicate seamlessly with others over
the internet. It supports both client and server functionalities, making it easy to set up and use.
## Table of Contents
@@ -9,7 +9,6 @@ the internet. It supports both client and server functionalities, making it easy
- [Notes](#notes)
- [Installation](#installation)
- [Usage](#usage)
- - [UTF-8 Support](#utf-8-support)
- [Windows Executable](#windows-executable)
- [JAR](#jar)
- [Building](#building)
@@ -35,24 +34,6 @@ the internet. It supports both client and server functionalities, making it easy
## Usage
-### UTF-8 Support
-
-For being able to see emojis and different symbols on your terminal, you need to check it's encoding. For enabling
-the UTF-8 Charset on the current terminal instance check some tips
-
-- Powershell
-
-```powershell
-$OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding
-```
-
-- Bash
-
-```bash
-export LANG=en_US.UTF-8
-export LC_ALL=en_US.UTF-8
-```
-
### Windows Executable
Download the executable (`.exe`) from the [Releases](https://github.com/FelipeKobra/JavaTerminalChat/releases) section.
diff --git a/TODO.md b/TODO.md
index 8f8e0f1..d4f4de6 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,7 +13,7 @@
- [ ] Notify clients when someone connects/disconnects
- [ ] Better warning when server not found on client instead of throwing an exception
-- [ ] Make user terminal automatically support UTF8 Characters
+- [X] Make user terminal automatically support UTF8 Characters
- [x] Disable Debug on native-image
## Administration
@@ -26,4 +26,4 @@
- [ ] User ban
- [ ] User unban
- [ ] View banned members
-- [ ] Notify who is the admin on chat
\ No newline at end of file
+- [ ] Notify who is the admin on chatUtils
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 6e28e9b..328f2d8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,238 +1,278 @@
- 4.0.0
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ JMessenger
-
- org.gladiator
- JMessenger
- 0.0.2
-
- Robust and efficient chat designed for terminal-based communication
- 2025
-
- org.gladiator
-
+
+
+
+
+
+ maven-checkstyle-plugin
+
+ google_checks.xml
+ false
+ true
+ true
+ false
+
+
+
+
+ check
+
+ validate
+ validate
+
+
+ org.apache.maven.plugins
+ 3.6.0
+
+
+
+
+ maven-shade-plugin
+
+
+
+ *:*
+
+ module-info.class
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+ META-INF/LICENSE.txt
+ META-INF/MANIFEST.MF
+ META-INF/versions/9/module-info.class
+
+
+
+
+
+
+
+
+ Server
+ ${artifacts.output.directory}
+
+
+ app.server.ServerMain
+
+
+
+
+ shade
+
+ shade-server-main
+ package
+
+
+
+
+ Client
+ ${artifacts.output.directory}
+
+
+ app.client.ClientMain
+
+
+
+
+ shade
+
+ shade-client-main
+ package
+
+
+ org.apache.maven.plugins
+ 3.6.0
+
+
+
+
+ native-maven-plugin
+
+
+ ${native.build.args}
+
+ false
+ false
+ target/artifacts
+ true
+ true
+ false
+
+
+
+
+
+ Server
+ app.server.ServerMain
+
+
+ compile-no-fork
+
+ build-native-server
+ package
+
+
+
+
+ Client
+ app.client.ClientMain
+
+
+ compile-no-fork
+
+ build-native-client
+ package
+
+
+ false
+ org.graalvm.buildtools
+ ${native.maven.plugin.version}
+
+
+
+
+ exec-maven-plugin
+
+
+
+
+ -NoProfile
+ -Command
+
+ "src\main\resources\scripts\artifacts-hashing.ps1"
+
+
+ powershell
+
+
+ exec
+
+ package
+
+
+ org.codehaus.mojo
+ 3.5.0
+
+
+ src/main/java/org/gladiator
+
+
+
+
+
+ commons-lang3
+ org.apache.commons
+ 3.17.0
+
+
+
+
+ porter
+ com.sshtools
+ 1.0.2
+
+
+
+
+ jline-terminal
+ org.jline
+ ${jline.bundle.version}
+
+
+
+ jline-terminal-jni
+ org.jline
+ ${jline.bundle.version}
+
+
+
+ jline-native
+ org.jline
+ ${jline.bundle.version}
+
+
+
+ jline-console
+ org.jline
+ ${jline.bundle.version}
+
+
+
+ jline-style
+ org.jline
+ ${jline.bundle.version}
+
+
+
+ jline-reader
+ org.jline
+ ${jline.bundle.version}
+
+
+
+
+ slf4j-api
+ org.slf4j
+ 2.0.16
+
+
+
+ logback-core
+ ch.qos.logback
+ 1.5.16
+
+
+
+ logback-classic
+ ch.qos.logback
+ 1.5.16
+
+
+ Robust and efficient chatUtils designed for terminal-based communication
+
-
-
- 21
- 21
- UTF-8
- ${project.build.directory}/artifacts
-
- 3.28.0
- 0.10.4
-
+
+ org.gladiator
+ 2025
+ 4.0.0
+
+ org.gladiator
+
-
- src/main/org/gladiator
-
-
-
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.6.0
-
-
-
- *:*
-
- module-info.class
- META-INF/*.SF
- META-INF/*.DSA
- META-INF/*.RSA
- META-INF/LICENSE.txt
- META-INF/MANIFEST.MF
- META-INF/versions/9/module-info.class
-
-
-
-
-
-
-
- shade-server-main
- package
-
- shade
-
-
- Server
- ${artifacts.output.directory}
-
-
- app.server.ServerMain
-
-
-
-
-
-
- shade-client-main
- package
-
- shade
-
-
- Client
- ${artifacts.output.directory}
-
-
- app.client.ClientMain
-
-
-
-
-
-
-
-
-
- org.graalvm.buildtools
- native-maven-plugin
- ${native.maven.plugin.version}
- false
-
- target/artifacts
- true
- false
- false
- false
- true
-
- -J-Dfile.encoding=UTF-8
-
-
-
-
- build-native-server
-
- compile-no-fork
-
- package
-
- Server
- app.server.ServerMain
-
-
-
- build-native-client
-
- compile-no-fork
-
- package
-
- Client
- app.client.ClientMain
-
-
-
-
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.5.0
-
-
- package
-
- exec
-
-
- powershell
-
- -NoProfile
- -Command
-
- "src\main\resources\scripts\artifacts-hashing.ps1"
-
-
-
-
-
-
-
-
-
-
-
-
- org.apache.commons
- commons-lang3
- 3.17.0
-
-
-
-
- com.sshtools
- porter
- 1.0.2
-
-
-
-
- org.jline
- jline-terminal
- ${jline.bundle.version}
-
-
-
- org.jline
- jline-terminal-jni
- ${jline.bundle.version}
-
-
-
- org.jline
- jline-native
- ${jline.bundle.version}
-
-
-
- org.jline
- jline-console
- ${jline.bundle.version}
-
-
-
- org.jline
- jline-style
- ${jline.bundle.version}
-
-
-
- org.jline
- jline-reader
- ${jline.bundle.version}
-
-
-
-
- org.slf4j
- slf4j-api
- 2.0.16
-
-
-
- ch.qos.logback
- logback-core
- 1.5.16
-
-
-
- ch.qos.logback
- logback-classic
- 1.5.16
-
-
+
+
+
+ dev
+
+ -Ob --no-fallback
+
+
+
+ prod
+
+ -O3
+
+
+
+
+
+ ${project.build.directory}/artifacts
+ 3.28.0
+ 21
+ --no-fallback
+ 0.10.4
+ UTF-8
+
+
+ 0.0.3
\ No newline at end of file
diff --git a/src/main/java/org/gladiator/app/client/Client.java b/src/main/java/org/gladiator/app/client/Client.java
new file mode 100644
index 0000000..93a1d1b
--- /dev/null
+++ b/src/main/java/org/gladiator/app/client/Client.java
@@ -0,0 +1,268 @@
+package app.client;
+
+import app.client.config.ClientConfig;
+import app.client.config.ClientConfigProvider;
+import app.exception.EndApplicationException;
+import app.util.ChatUtils;
+import app.util.connection.ConnectionMessageUtils;
+import app.util.io.SocketIo;
+import app.util.io.SocketIoAsyncFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UncheckedIOException;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import javax.net.SocketFactory;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.UserInterruptException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Client class represents a client that connects to a server, exchanges messages, and handles
+ * user interactions.
+ */
+public final class Client implements AutoCloseable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Client.class);
+
+ private final ClientConfig config;
+ private final Socket socket;
+ private final ExecutorService executor;
+ private final ChatUtils chatUtils;
+
+ private Client(final ClientConfig config, final Socket socket, final ExecutorService executor,
+ final ChatUtils chatUtils) {
+ this.config = config;
+ this.socket = socket;
+ this.executor = executor;
+ this.chatUtils = chatUtils;
+ }
+
+ /**
+ * Creates a new Client instance by initializing the necessary components and establishing a
+ * connection to the server.
+ *
+ * @return a new Client instance
+ * @throws EndApplicationException if an error occurs during client creation
+ */
+ public static Client createClient()
+ throws EndApplicationException {
+
+ Client client = null;
+ try {
+ final ChatUtils chatUtils = ChatUtils.create(">");
+ final ClientConfig clientConfig = new ClientConfigProvider(
+ chatUtils).createClientConfig();
+ final Socket socket = createSocket(chatUtils, clientConfig.serverAddress(),
+ clientConfig.port());
+ final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
+
+ client = new Client(clientConfig, socket, executor, chatUtils);
+ } catch (final UserInterruptException | EndOfFileException e) {
+ handleException(ChatUtils.USER_INTERRUPT_MESSAGE, e);
+ }
+
+ return client;
+ }
+
+ /**
+ * Creates a socket connection to the specified server address and port.
+ *
+ * @param chatUtils the ChatUtils instance for user interaction
+ * @param serverAddress the address of the server to connect to
+ * @param port the port number to connect to
+ * @return a Socket connected to the server
+ * @throws EndApplicationException if an error occurs during socket creation
+ */
+ private static Socket createSocket(final ChatUtils chatUtils,
+ final String serverAddress, final int port)
+ throws EndApplicationException {
+
+ Socket clientSocket = null;
+
+ try {
+ clientSocket = SocketFactory.getDefault()
+ .createSocket(serverAddress, port);
+ } catch (final UnknownHostException e) {
+ handleException(chatUtils, "Server Address not found",
+ "Server " + serverAddress + " not Found", e);
+ } catch (final ConnectException e) {
+ handleException(chatUtils, "Connection time out with server",
+ "Timed out when trying to connect to server: " + serverAddress, e);
+ } catch (final IOException e) {
+ handleException("Error connecting to server", e);
+ }
+ return clientSocket;
+ }
+
+ /**
+ * Handles exceptions by logging the message, displaying a user message, and throwing an
+ * EndApplicationException.
+ *
+ * @param chatUtils the ChatUtils instance for user interaction
+ * @param logMessage the message to log
+ * @param userMessage the message to display to the user
+ * @param exception the exception to handle
+ * @throws EndApplicationException the wrapped exception
+ */
+ private static void handleException(final ChatUtils chatUtils, final String logMessage,
+ final String userMessage,
+ final Exception exception)
+ throws EndApplicationException {
+ LOGGER.debug(logMessage, exception);
+ chatUtils.displayOnScreen(userMessage);
+ throw new EndApplicationException(exception);
+ }
+
+ /**
+ * Handles exceptions by logging the message and throwing an EndApplicationException.
+ *
+ * @param logMessage the message to log
+ * @param exception the exception to handle
+ * @throws EndApplicationException the wrapped exception
+ */
+ private static void handleException(final String logMessage, final Exception exception)
+ throws EndApplicationException {
+ LOGGER.debug(logMessage, exception);
+ throw new EndApplicationException(exception);
+ }
+
+ /**
+ * Runs the client by establishing a connection, exchanging names with the server, and handling
+ * message sending and receiving.
+ *
+ * @throws EndApplicationException if an error occurs during client execution
+ */
+ public void run() throws EndApplicationException {
+ final SocketIo socketIo = SocketIoAsyncFactory.create(socket, executor);
+
+ final BufferedReader reader = socketIo.getReader();
+ final PrintWriter writer = socketIo.getWriter();
+
+ final String serverName = exchangeNames(writer, reader);
+
+ chatUtils.prettyPrint("Connection Established with " + serverName);
+
+ chatUtils.displayOnScreen("Type `quit` to exit");
+
+ final CompletableFuture receiveMessagesFuture = CompletableFuture.runAsync(
+ () -> receiveMessages(reader), executor);
+
+ final CompletableFuture sendMessagesFuture = CompletableFuture.runAsync(
+ () -> sendMessages(writer), executor);
+
+ CompletableFuture.allOf(receiveMessagesFuture, sendMessagesFuture).join();
+ reconnectPrompt();
+ }
+
+ /**
+ * Receives messages from the server and displays them to the user.
+ *
+ * @param reader the BufferedReader to read messages from the server
+ */
+ private void receiveMessages(final BufferedReader reader) {
+ try {
+ reader.lines()
+ .map(ConnectionMessageUtils::fromRawString)
+ .forEach(msg ->
+ chatUtils.showNewMessage(msg.toString()));
+ } catch (final UncheckedIOException e) {
+ LOGGER.debug("The connection with the server has ended");
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ /**
+ * Sends messages from the user to the server.
+ *
+ * @param writer the PrintWriter to send messages to the server
+ */
+ private void sendMessages(final PrintWriter writer) {
+
+ try {
+ String line = chatUtils.getUserInput();
+
+ while (null != line) {
+ if ("quit".equals(line)) {
+ break;
+ }
+
+ final String msg = ConnectionMessageUtils.toRawString(config.name(), line);
+ writer.println(msg);
+ line = chatUtils.getUserInput();
+ }
+ } catch (final EndOfFileException | UserInterruptException e) {
+ LOGGER.debug(ChatUtils.USER_INTERRUPT_MESSAGE, e);
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ /**
+ * Exchanges names with the server by sending the client's name and receiving the server's name.
+ *
+ * @param writer the PrintWriter to send the client's name to the server
+ * @param reader the BufferedReader to receive the server's name
+ * @return the name of the server
+ */
+ private String exchangeNames(final PrintWriter writer, final BufferedReader reader) {
+
+ final CompletableFuture sendNameFuture = CompletableFuture.runAsync(() -> {
+ writer.println(config.name());
+ final String logMessage = "Sent name (" + config.name() + ") to server.";
+ LOGGER.debug(logMessage);
+ }, executor);
+
+ final CompletableFuture receiveNameFuture = CompletableFuture.supplyAsync(() -> {
+ String serverName = "";
+ try {
+ serverName = reader.readLine();
+ final String logMessage = "Received name from server: " + serverName + ".";
+ LOGGER.debug(logMessage);
+ } catch (final IOException e) {
+ LOGGER.error("Error receiving server name: {}", e, e);
+ }
+
+ return serverName;
+ }, executor);
+
+ CompletableFuture.allOf(sendNameFuture, receiveNameFuture).join();
+
+ return receiveNameFuture.join();
+ }
+
+ /**
+ * Prompts the user to reconnect to another server.
+ *
+ * @throws EndApplicationException if the user chooses not to reconnect or an error occurs
+ */
+ private void reconnectPrompt() throws EndApplicationException {
+ try {
+ final String choice = chatUtils.getUserInput("Connect to another server? y/N: ");
+ if (!"Y".equalsIgnoreCase(choice)) {
+ throw new EndApplicationException("User chose to not reconnnnect");
+ }
+ } catch (final UserInterruptException | EndOfFileException e) {
+ throw new EndApplicationException(e);
+ }
+ }
+
+ @Override
+ public void close() {
+ LOGGER.debug("Closing connection");
+ try {
+ chatUtils.close();
+ executor.shutdownNow();
+ socket.close();
+ } catch (final IOException e) {
+ LOGGER.error("Error closing the socket connection: {}", e, e);
+ }
+ }
+}
diff --git a/src/main/java/org/gladiator/app/client/ClientMain.java b/src/main/java/org/gladiator/app/client/ClientMain.java
new file mode 100644
index 0000000..8e49b7a
--- /dev/null
+++ b/src/main/java/org/gladiator/app/client/ClientMain.java
@@ -0,0 +1,36 @@
+package app.client;
+
+import app.exception.EndApplicationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The main entry point for the client application. This class is responsible for starting and
+ * running the client.
+ */
+public final class ClientMain {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClientMain.class);
+
+ private ClientMain() {
+ }
+
+ /**
+ * The main method that starts the client application.
+ *
+ * @param args command-line arguments
+ */
+ public static void main(final String... args) {
+
+ while (true) {
+ try (final Client client = Client.createClient()) {
+ client.run();
+ } catch (final EndApplicationException e) {
+ LOGGER.debug("Client Ended", e);
+ break;
+ }
+ }
+
+ }
+}
+
diff --git a/src/main/java/org/gladiator/app/client/config/ClientConfig.java b/src/main/java/org/gladiator/app/client/config/ClientConfig.java
new file mode 100644
index 0000000..a48a470
--- /dev/null
+++ b/src/main/java/org/gladiator/app/client/config/ClientConfig.java
@@ -0,0 +1,13 @@
+package app.client.config;
+
+/**
+ * Configuration class for the client. This class holds the client's name, server address, and port
+ * number.
+ *
+ * @param name the name of the client
+ * @param serverAddress the address of the server to connect to
+ * @param port the port number to connect to
+ */
+public record ClientConfig(String name, String serverAddress, int port) {
+
+}
diff --git a/src/main/java/org/gladiator/app/client/config/ClientConfigProvider.java b/src/main/java/org/gladiator/app/client/config/ClientConfigProvider.java
new file mode 100644
index 0000000..349768c
--- /dev/null
+++ b/src/main/java/org/gladiator/app/client/config/ClientConfigProvider.java
@@ -0,0 +1,89 @@
+package app.client.config;
+
+import app.util.ChatUtils;
+import environment.Port;
+
+
+/**
+ * Provides configuration for the client by interacting with the user to receive input.
+ */
+public final class ClientConfigProvider {
+
+ private final ChatUtils chatUtils;
+
+ /**
+ * Constructs a new ClientConfigProvider with the specified ChatUtils instance.
+ *
+ * @param chatUtils the ChatUtils instance to use for user interaction
+ */
+ public ClientConfigProvider(final ChatUtils chatUtils) {
+ this.chatUtils = chatUtils;
+ }
+
+ /**
+ * Creates a new ClientConfig by interacting with the user to receive the necessary input.
+ *
+ * @return a new ClientConfig instance with the user's input
+ */
+ public ClientConfig createClientConfig() {
+
+ final String clientName = receiveName();
+ final String serverAddress = receiveAddress();
+ final int serverPort = receivePort();
+
+ return new ClientConfig(clientName, serverAddress, serverPort);
+ }
+
+ private String receiveName() {
+ String name = "";
+ while (name.isBlank()) {
+ name = chatUtils.getUserInput("Choose your name: ");
+ }
+ return name;
+ }
+
+ private String receiveAddress() {
+ return chatUtils.askUserOption("Server IP", "localhost");
+ }
+
+ private int receivePort() {
+ return readPort(chatUtils);
+ }
+
+ private int readPort(final ChatUtils chatUtils) {
+ int serverPort = 0;
+ boolean isPortValid = false;
+
+ while (!isPortValid) {
+ final String serverPortString = chatUtils.askUserOption("server port",
+ String.valueOf(Port.PORT_DEFAULT));
+ if (serverPortString.isBlank()) {
+ serverPort = Port.PORT_DEFAULT;
+ } else {
+ try {
+ serverPort = Integer.parseInt(serverPortString);
+ } catch (final NumberFormatException e) {
+ chatUtils.displayOnScreen("Insert a valid numerical port");
+ }
+ }
+
+ isPortValid = checkServerPortRange(serverPort);
+ }
+
+ return serverPort;
+ }
+
+ private boolean checkServerPortRange(final int serverPort) {
+ if (Port.PORT_MIN < serverPort && Port.PORT_MAX > serverPort) {
+ return true;
+ }
+
+ chatUtils.displayOnScreen(
+ "Insert a number within the valid port range (" + Port.PORT_MIN + " - " + Port.PORT_MAX
+ + ")");
+
+ return false;
+
+ }
+}
+
diff --git a/src/main/java/org/gladiator/app/exception/EndApplicationException.java b/src/main/java/org/gladiator/app/exception/EndApplicationException.java
new file mode 100644
index 0000000..9bc26c3
--- /dev/null
+++ b/src/main/java/org/gladiator/app/exception/EndApplicationException.java
@@ -0,0 +1,25 @@
+package app.exception;
+
+/**
+ * Exception thrown to indicate that the application should end.
+ */
+public class EndApplicationException extends Exception {
+
+ /**
+ * Constructs a new EndApplicationException with the specified cause.
+ *
+ * @param cause the cause of the exception
+ */
+ public EndApplicationException(final Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Constructs a new EndApplicationException with the specified message.
+ *
+ * @param message the detail message
+ */
+ public EndApplicationException(final String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/org/gladiator/app/server/Server.java b/src/main/java/org/gladiator/app/server/Server.java
new file mode 100644
index 0000000..4f2a5d7
--- /dev/null
+++ b/src/main/java/org/gladiator/app/server/Server.java
@@ -0,0 +1,298 @@
+package app.server;
+
+import app.exception.EndApplicationException;
+import app.server.config.ServerConfig;
+import app.server.config.ServerConfigFactory;
+import app.util.ChatUtils;
+import app.util.PortMapper;
+import app.util.connection.Connection;
+import app.util.connection.ConnectionMessage;
+import app.util.connection.ConnectionMessageUtils;
+import app.util.io.SocketIo;
+import app.util.io.SocketIoAsyncFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UncheckedIOException;
+import java.net.BindException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
+import javax.net.ServerSocketFactory;
+import org.jline.reader.EndOfFileException;
+import org.jline.reader.UserInterruptException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents a server that manages connections with clients.
+ */
+public final class Server implements AutoCloseable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Server.class.getName());
+ private final List clientConnections = new CopyOnWriteArrayList<>();
+ private final AtomicBoolean isClosingManually = new AtomicBoolean(false);
+
+ private final ServerConfig serverConfig;
+ private final ServerSocket serverSocket;
+ private final ChatUtils chatUtils;
+ private final PortMapper portMapper;
+ private final ExecutorService executor;
+
+
+ private Server(final ServerConfig serverConfig, final ServerSocket serverSocket,
+ final ChatUtils chatUtils, final PortMapper portMapper, final ExecutorService executor) {
+ this.serverConfig = serverConfig;
+ this.serverSocket = serverSocket;
+ this.chatUtils = chatUtils;
+ this.portMapper = portMapper;
+ this.executor = executor;
+ }
+
+ /**
+ * Creates a new server with the default configuration.
+ *
+ * @return A new server instance.
+ * @throws EndApplicationException If an error occurs during server creation.
+ */
+ public static Server createServer()
+ throws EndApplicationException {
+
+ final Server server;
+ final ChatUtils chatUtils = ChatUtils.create(">");
+ try {
+ final ServerConfig serverConfig = new ServerConfigFactory(chatUtils).create();
+ final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
+ final ServerSocket serverSocket = ServerSocketFactory.getDefault()
+ .createServerSocket(serverConfig.port());
+
+ final PortMapper portMapper = PortMapper.createDefault();
+ portMapper.openPort(serverConfig.port());
+
+ server = new Server(serverConfig, serverSocket, chatUtils, portMapper, executor);
+ } catch (final BindException e) {
+ LOGGER.debug("Port already in use", e);
+ chatUtils.displayOnScreen(
+ "Address already in use, check if you have another server opened in the same port");
+ throw new EndApplicationException(e);
+ } catch (final IOException e) {
+ LOGGER.error("Error creating Server Socket", e);
+ throw new EndApplicationException(e);
+ } catch (final UserInterruptException e) {
+ LOGGER.debug(ChatUtils.USER_INTERRUPT_MESSAGE);
+ throw new EndApplicationException(e);
+ }
+
+ Objects.requireNonNull(server);
+ return server;
+ }
+
+ /**
+ * Starts the server and begins listening for connections.
+ */
+ public void runServer() {
+ LOGGER.info("Server Started...");
+ final CompletableFuture listenToConnectionsFuture = CompletableFuture.runAsync(
+ this::listenToConnections, executor);
+
+ final CompletableFuture broadcastToConnectionsFuture = CompletableFuture.runAsync(
+ this::broadcastToConnections, executor);
+
+ CompletableFuture.allOf(listenToConnectionsFuture, broadcastToConnectionsFuture).join();
+ }
+
+ /**
+ * Listens for incoming connections from clients.
+ */
+ private void listenToConnections() {
+ LOGGER.info("Listening to connections...");
+
+ while (!serverSocket.isClosed() && serverSocket.isBound()) {
+ final Socket clientSocket;
+ try {
+ clientSocket = serverSocket.accept();
+
+ final SocketIo socketIo = SocketIoAsyncFactory.create(clientSocket, executor);
+ final PrintWriter writer = socketIo.getWriter();
+ final BufferedReader reader = socketIo.getReader();
+
+ final String clientName = exchangeNames(writer, reader);
+
+ chatUtils.showNewMessage("User " + clientName + " Connected");
+
+ final Connection clientConnection = new Connection(clientName, clientSocket, reader,
+ writer);
+ clientConnections.add(clientConnection);
+
+ receiveMessages(reader, clientConnection);
+ } catch (final IOException e) {
+ LOGGER.debug(
+ "Connection listening ended normally or error during Socket Server accept method: {}",
+ e.getMessage());
+ }
+ }
+
+ executor.shutdownNow();
+ }
+
+ /**
+ * Broadcasts messages to all connected clients.
+ */
+ private void broadcastToConnections() {
+ LOGGER.info("Broadcasting to connections...");
+ LOGGER.info("Type `quit` to exit");
+
+ try {
+ String line = chatUtils.getUserInput();
+
+ while (null != line) {
+ if ("quit".equals(line)) {
+ break;
+ }
+
+ final String msg = ConnectionMessageUtils.toRawString(serverConfig.name(), line);
+ broadcastMessageToConnections(msg);
+ line = chatUtils.getUserInput();
+ }
+ } catch (final EndOfFileException | UserInterruptException e) {
+ LOGGER.debug(ChatUtils.USER_INTERRUPT_MESSAGE, e);
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ /**
+ * Broadcasts a message to all connected clients.
+ *
+ * @param msg The message to be broadcasted.
+ */
+ private void broadcastMessageToConnections(final String msg) {
+ final List> futures = new ArrayList<>();
+ for (final Connection connection : clientConnections) {
+ final CompletableFuture future = CompletableFuture.runAsync(
+ () -> connection.getOutput().println(msg), executor);
+ futures.add(future);
+ }
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+ }
+
+ /**
+ * Receives messages from a client.
+ *
+ * @param reader The reader for the client's messages.
+ * @param clientConnection The connection to the client.
+ */
+ private void receiveMessages(final BufferedReader reader, final Connection clientConnection) {
+ final String clientName = clientConnection.getName();
+
+ executor.execute(() -> {
+ try {
+ processMessages(reader, clientConnection);
+ } catch (final UncheckedIOException e) {
+ LOGGER.debug("Connection with {} ended abruptly", clientName);
+ } finally {
+ closeConnection(clientConnection, clientName);
+ }
+ });
+ }
+
+ /**
+ * Processes messages received from a client.
+ *
+ * @param reader The reader for the client's messages.
+ * @param connection The connection to the client.
+ */
+ private void processMessages(final BufferedReader reader, final Connection connection) {
+ reader.lines().map(ConnectionMessageUtils::fromRawString).forEach(msg -> {
+ chatUtils.showNewMessage(msg.toString());
+ sendToOtherConnections(msg, connection);
+ });
+ }
+
+ /**
+ * Sends a message to all connected clients, except the client that sent the message.
+ *
+ * @param msg The message to be sent.
+ * @param connection The connection to the client that sent the message.
+ */
+ private void sendToOtherConnections(final ConnectionMessage msg, final Connection connection) {
+ clientConnections.stream().filter(Predicate.not(connection::equals)).forEach(
+ otherConnection -> CompletableFuture.runAsync(
+ () -> otherConnection.getOutput().println(msg.toRawString()), executor));
+ }
+
+ /**
+ * Closes a connection to a client.
+ *
+ * @param connection The connection to be closed.
+ * @param clientName The name of the client.
+ */
+ private void closeConnection(final Connection connection, final String clientName) {
+ if (!isClosingManually.get()) {
+ LOGGER.debug("User Disconnected: {}", clientName);
+ chatUtils.showNewMessage("User " + clientName + " Disconnected");
+ connection.removeConnection(clientConnections);
+ }
+ }
+
+ /**
+ * Exchanges names between the server and a client.
+ *
+ * @param writer The writer for the client's messages.
+ * @param reader The reader for the client's messages.
+ * @return The name of the client.
+ */
+ private String exchangeNames(final PrintWriter writer, final BufferedReader reader) {
+
+ final CompletableFuture sendServerNameFuture = CompletableFuture.runAsync(() -> {
+ writer.println(serverConfig.name());
+ final String logMessage = "Sent name " + serverConfig.name() + " to client.";
+ LOGGER.debug(logMessage);
+ }, executor);
+
+ final CompletableFuture receiveClientNameFuture = CompletableFuture.supplyAsync(() -> {
+ String clientName = "";
+ try {
+ clientName = reader.readLine();
+ final String logMessage = "Received name from client: " + clientName + ".";
+ LOGGER.debug(logMessage);
+ } catch (final IOException e) {
+ LOGGER.error("Error receiving client name: {}", e, e);
+ }
+ return clientName;
+ }, executor);
+
+ CompletableFuture.allOf(sendServerNameFuture, receiveClientNameFuture).join();
+
+ return receiveClientNameFuture.join();
+ }
+
+ @Override
+ public void close() {
+ LOGGER.info("Closing all connections...");
+
+ isClosingManually.set(true);
+
+ try {
+ for (final Connection connection : clientConnections) {
+ connection.close();
+ }
+ serverSocket.close();
+ } catch (final IOException e) {
+ LOGGER.error("Error during closing server socket", e);
+ } finally {
+ chatUtils.close();
+ executor.shutdown();
+ portMapper.closeAll(serverConfig.port());
+ }
+ }
+
+}
diff --git a/src/main/java/org/gladiator/app/server/ServerMain.java b/src/main/java/org/gladiator/app/server/ServerMain.java
new file mode 100644
index 0000000..1c13d8b
--- /dev/null
+++ b/src/main/java/org/gladiator/app/server/ServerMain.java
@@ -0,0 +1,30 @@
+package app.server;
+
+import app.exception.EndApplicationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Main class for starting the server application.
+ */
+public final class ServerMain {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ServerMain.class);
+
+ private ServerMain() {
+ }
+
+ /**
+ * The main method to start the server.
+ *
+ * @param args Command line arguments.
+ */
+ public static void main(final String... args) {
+ try (
+ final Server server = Server.createServer()) {
+ server.runServer();
+ } catch (final EndApplicationException e) {
+ LOGGER.debug("Server Ended", e);
+ }
+ }
+}
diff --git a/src/main/java/org/gladiator/app/server/config/ServerConfig.java b/src/main/java/org/gladiator/app/server/config/ServerConfig.java
new file mode 100644
index 0000000..36d8751
--- /dev/null
+++ b/src/main/java/org/gladiator/app/server/config/ServerConfig.java
@@ -0,0 +1,50 @@
+package app.server.config;
+
+import static environment.Port.PORT_DEFAULT;
+import static environment.Port.PORT_MAX;
+import static environment.Port.PORT_MIN;
+
+import java.util.Objects;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Represents the configuration for a server, including its name and port.
+ */
+public record ServerConfig(String name, int port) {
+
+ /**
+ * Constructs a new ServerConfig with the specified name and port.
+ *
+ * @param name the name of the server
+ * @param port the port number of the server
+ */
+ public ServerConfig {
+ validateArgs(name, port);
+ }
+
+ /**
+ * Constructs a new ServerConfig with default name and port.
+ */
+ public ServerConfig() {
+ this(getDefaultName(), getDefaultPort());
+ }
+
+
+ public static String getDefaultName() {
+ return "Server";
+ }
+
+
+ private static int getDefaultPort() {
+ return PORT_DEFAULT;
+ }
+
+
+ private void validateArgs(final String name, final int port) {
+ Objects.requireNonNull(name);
+ Validate.notBlank(name);
+ Validate.inclusiveBetween(PORT_MIN, PORT_MAX, port);
+ }
+
+
+}
diff --git a/src/main/java/org/gladiator/app/server/config/ServerConfigFactory.java b/src/main/java/org/gladiator/app/server/config/ServerConfigFactory.java
new file mode 100644
index 0000000..d33bbd8
--- /dev/null
+++ b/src/main/java/org/gladiator/app/server/config/ServerConfigFactory.java
@@ -0,0 +1,77 @@
+package app.server.config;
+
+import app.util.ChatUtils;
+import environment.Port;
+import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * Factory class for creating {@link ServerConfig} instances.
+ */
+public class ServerConfigFactory {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ServerConfigFactory.class);
+
+ private final ChatUtils chatUtils;
+
+ /**
+ * Constructs a new {@link ServerConfigFactory} with the specified {@link ChatUtils}.
+ *
+ * @param chatUtils the {@link ChatUtils} instance for user interaction
+ */
+ public ServerConfigFactory(final ChatUtils chatUtils) {
+ this.chatUtils = chatUtils;
+ }
+
+
+ /**
+ * Creates a new {@link ServerConfig} instance based on user input.
+ *
+ * @return a {@link ServerConfig} instance
+ */
+ public ServerConfig create() {
+ String isCustom;
+
+ isCustom = chatUtils.getUserInput("Want to change the default settings? y/N: ");
+ isCustom = isCustom.toUpperCase();
+
+ return "Y".equals(isCustom) ? createCustom() : createDefault();
+ }
+
+ private ServerConfig createCustom() {
+ String serverName = "";
+ int serverPort;
+
+ try {
+ chatUtils.displayOnScreen("Leave the field blank for the default setting");
+
+ serverName = getCustomConfig("Server Name", ServerConfig.getDefaultName());
+ serverPort = Integer.parseInt(
+ getCustomConfig("Server Port", String.valueOf(Port.PORT_DEFAULT)));
+ Validate.inclusiveBetween(Port.PORT_MIN, Port.PORT_MAX, serverPort);
+
+ } catch (final NumberFormatException e) {
+ LOGGER.error("Number not recognized, using default port");
+ serverPort = Port.PORT_DEFAULT;
+ } catch (final IllegalArgumentException e) {
+ LOGGER.error("Invalid port number provided. Using default port.");
+ serverPort = Port.PORT_DEFAULT;
+ }
+
+ return new ServerConfig(serverName, serverPort);
+ }
+
+ private ServerConfig createDefault() {
+ return new ServerConfig();
+ }
+
+ private String getCustomConfig(final String configName, final String defaultConfig) {
+ String config = chatUtils.askUserOption(configName, defaultConfig);
+ if (config.isBlank()) {
+ config = defaultConfig;
+ }
+ return config;
+ }
+}
diff --git a/src/main/java/org/gladiator/app/util/ChatUtils.java b/src/main/java/org/gladiator/app/util/ChatUtils.java
new file mode 100644
index 0000000..51c5ddf
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/ChatUtils.java
@@ -0,0 +1,143 @@
+package app.util;
+
+import java.io.IOException;
+import java.util.Objects;
+import org.jline.reader.LineReader;
+import org.jline.reader.LineReaderBuilder;
+import org.jline.reader.UserInterruptException;
+import org.jline.terminal.Terminal;
+import org.jline.terminal.TerminalBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for handling chat-related operations.
+ */
+public final class ChatUtils implements AutoCloseable {
+
+ public static final String USER_INTERRUPT_MESSAGE = "User stopped the console reading,"
+ + " probably by pressing CTRL + C";
+ private static final Logger LOGGER = LoggerFactory.getLogger(ChatUtils.class);
+ private final String userPrompt;
+ private final Terminal terminal;
+ private final LineReader lineReader;
+
+ private ChatUtils(final String userPrompt, final Terminal terminal, final LineReader lineReader) {
+ this.userPrompt = userPrompt;
+ this.terminal = terminal;
+ this.lineReader = lineReader;
+ }
+
+ /**
+ * Creates a new {@link ChatUtils} instance with the specified user prompt.
+ *
+ * @param userPrompt The prompt to display to the user.
+ * @return A new {@link ChatUtils} instance.
+ */
+ public static ChatUtils create(final String userPrompt) {
+ Terminal terminal = null;
+ LineReader lineReader = null;
+ try {
+ terminal = TerminalBuilder.terminal();
+ lineReader = LineReaderBuilder.builder().terminal(terminal)
+ .variable(LineReader.DISABLE_HISTORY, true).build();
+ } catch (final IOException e) {
+ LOGGER.error("Error creating client terminal: {}", e.getMessage(), e);
+ }
+
+ Objects.requireNonNull(terminal);
+ terminal.enterRawMode();
+ terminal.echo(true);
+
+ return new ChatUtils(userPrompt, terminal, lineReader);
+ }
+
+ /**
+ * Displays a new message on the screen.
+ *
+ * @param msg The message to display.
+ */
+ public void showNewMessage(final String msg) {
+ cleanLine();
+ displayOnScreen(msg);
+ showBufferedUserPrompt();
+ }
+
+
+ /**
+ * Reads user input from the console using the default prompt.
+ *
+ * @return The user input.
+ */
+ public String getUserInput() throws UserInterruptException {
+ return lineReader.readLine(userPrompt + " ");
+ }
+
+ /**
+ * Reads user input from the console using the specified prompt.
+ *
+ * @param prompt The prompt to display to the user.
+ * @return The user input.
+ */
+ public String getUserInput(final String prompt) {
+ return lineReader.readLine(prompt);
+ }
+
+ /**
+ * Displays a message on the screen.
+ *
+ * @param msg The message to display.
+ */
+ public void displayOnScreen(final String msg) {
+ terminal.writer().println(msg);
+ }
+
+ /**
+ * Asks the user for an option with a default value.
+ *
+ * @param optionName The name of the option.
+ * @param defaultOption The default value for the option.
+ * @return The user input.
+ */
+ public String askUserOption(final String optionName, final String defaultOption) {
+ return getUserInput(
+ "Type the " + optionName + " (" + defaultOption + "):");
+ }
+
+ /**
+ * Prints a string with a decorative border.
+ *
+ * @param str The string to print.
+ */
+ public void prettyPrint(final String str) {
+ final String division = "=".repeat(str.length());
+
+ System.out.println(System.lineSeparator() + division);
+ displayOnScreen(str);
+ displayOnScreen(division + System.lineSeparator());
+ }
+
+ /**
+ * Displays the buffered user prompt on the screen.
+ */
+ private void showBufferedUserPrompt() {
+ print(userPrompt + " " + lineReader.getBuffer().toString());
+ }
+
+ private void cleanLine() {
+ print("\r\033[K");
+ }
+
+ private void print(final String str) {
+ terminal.writer().print(str);
+ }
+
+ @Override
+ public void close() {
+ try {
+ terminal.close();
+ } catch (final IOException e) {
+ LOGGER.error("Error closing terminal: {}", e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/main/java/org/gladiator/app/util/PortMapper.java b/src/main/java/org/gladiator/app/util/PortMapper.java
new file mode 100644
index 0000000..d604da5
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/PortMapper.java
@@ -0,0 +1,117 @@
+package app.util;
+
+import com.sshtools.porter.UPnP.Discovery;
+import com.sshtools.porter.UPnP.DiscoveryBuilder;
+import com.sshtools.porter.UPnP.Gateway;
+import com.sshtools.porter.UPnP.Protocol;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class for managing port mappings using UPnP.
+ */
+public final class PortMapper implements AutoCloseable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PortMapper.class);
+ private static final Protocol protocol = Protocol.TCP;
+ private static final String DISCOVERY_PROCESS_INTERRUPTED = "UPnP discovery process interrupted";
+
+ private final ExecutorService executor;
+ private final Discovery discovery;
+ private Gateway gateway;
+
+ private PortMapper(final ExecutorService executor, final Discovery discovery) {
+ this.executor = executor;
+ this.discovery = discovery;
+ }
+
+ /**
+ * Creates a default {@link PortMapper} instance with a virtual thread executor and a discovery
+ * instance.
+ *
+ * @return A new {@link PortMapper} instance.
+ */
+ public static PortMapper createDefault() {
+ final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
+
+ final Discovery discovery = new DiscoveryBuilder().withSoTimeout(600)
+ .withoutShutdownHooks()
+ .onGateway(gw -> LOGGER.debug("Gateway found {}", gw.ip()))
+ .build();
+
+ final PortMapper portMapper = new PortMapper(executor, discovery);
+
+ executor.execute(() -> {
+ try {
+ portMapper.setGateway(discovery.gateway().orElse(null));
+ } catch (final IllegalStateException e) {
+ LOGGER.debug(DISCOVERY_PROCESS_INTERRUPTED, e);
+ }
+ });
+
+ return portMapper;
+ }
+
+ /**
+ * Opens a port on the router for the specified port number using the configured gateway.
+ *
+ * @param port The port number to open.
+ */
+ public void openPort(final int port) {
+ executor.execute(() -> {
+ try {
+ if (null == gateway) {
+
+ discovery.awaitCompletion(5, TimeUnit.SECONDS);
+ }
+ if (null != gateway) {
+ final boolean mapped = gateway.map(port, protocol);
+ if (mapped) {
+ LOGGER.debug("Port {} mapped with success", port);
+ }
+ }
+ } catch (final IllegalStateException e) {
+ LOGGER.debug(DISCOVERY_PROCESS_INTERRUPTED, e);
+ }
+ });
+ }
+
+
+ /**
+ * Closes the port on the router specified and the resources of PortMapper.
+ *
+ * @param port The port to be close on the router
+ */
+ public void closeAll(final int port) {
+ closePort(port);
+ close();
+ }
+
+ /**
+ * Closes the port on the router for the specified port number using the configured gateway.
+ *
+ * @param port The port number to close.
+ */
+ private void closePort(final int port) {
+ if (null != gateway) {
+ gateway.unmap(port, protocol);
+ LOGGER.debug("Port {} unmapped with success", port);
+ }
+ }
+
+ private void setGateway(final Gateway gateway) {
+ this.gateway = gateway;
+ }
+
+ /**
+ * Closes the {@link PortMapper}, shutting down the executor and closing the discovery instance.
+ */
+ @Override
+ public void close() {
+ discovery.close();
+ executor.shutdownNow();
+ }
+}
diff --git a/src/main/java/org/gladiator/app/util/connection/Connection.java b/src/main/java/org/gladiator/app/util/connection/Connection.java
new file mode 100644
index 0000000..4e07309
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/connection/Connection.java
@@ -0,0 +1,88 @@
+package app.util.connection;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.util.List;
+import java.util.Objects;
+import org.apache.commons.lang3.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Represents a connection to a client. This class handles the input and output streams for the
+ * client connection.
+ *
+ * @see app.server.Server
+ */
+public final class Connection implements AutoCloseable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(Connection.class);
+
+ private final String name;
+ private final Socket socket;
+ private final BufferedReader input;
+ private final PrintWriter output;
+
+
+ /**
+ * Constructs a new Connection.
+ *
+ * @param name The name of the client.
+ * @param socket The socket for the connection.
+ * @param input The input stream for the connection.
+ * @param output The output stream for the connection.
+ * @throws NullPointerException if any of the parameters are null.
+ * @throws IllegalArgumentException if the name is blank.
+ */
+ public Connection(final String name, final Socket socket, final BufferedReader input,
+ final PrintWriter output) {
+ Validate.notBlank(name);
+ Objects.requireNonNull(socket);
+ Objects.requireNonNull(input);
+ Objects.requireNonNull(output);
+ this.name = name;
+ this.socket = socket;
+ this.input = input;
+ this.output = output;
+ }
+
+ /**
+ * Removes this connection from the list of connections and closes it.
+ *
+ * @param connections The list of connections.
+ * @see #close()
+ */
+ public void removeConnection(final List connections) {
+ connections.remove(this);
+ this.close();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public PrintWriter getOutput() {
+ return output;
+ }
+
+ /**
+ * Closes the connection, including the input and output streams and the socket.
+ *
+ * @see java.io.Closeable#close()
+ */
+ @Override
+ public void close() {
+ try {
+ output.close();
+ input.close();
+ socket.close();
+ } catch (final IOException e) {
+ LOGGER.error("Error closing the connection: {}", e, e);
+ }
+ }
+}
+
+
+
diff --git a/src/main/java/org/gladiator/app/util/connection/ConnectionMessage.java b/src/main/java/org/gladiator/app/util/connection/ConnectionMessage.java
new file mode 100644
index 0000000..0ecc056
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/connection/ConnectionMessage.java
@@ -0,0 +1,44 @@
+package app.util.connection;
+
+import java.util.Objects;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Represents a message exchanged between connections. This class is immutable and uses the record
+ * feature of Java.
+ */
+public record ConnectionMessage(String senderName, String content) {
+
+ /**
+ * Constructs a new ConnectionMessage.
+ *
+ * @param senderName The name of the sender.
+ * @param content The content of the message.
+ * @throws NullPointerException if any of the parameters are null.
+ * @throws IllegalArgumentException if the senderName is blank.
+ */
+ public ConnectionMessage {
+ Validate.notBlank(senderName);
+ Objects.requireNonNull(content);
+ }
+
+ /**
+ * Converts the message to a raw string format. This format is necessary during the transmission
+ * of the message, so it is easier to split the sender name to its content
+ *
+ * @return The raw string representation of the message.
+ */
+ public String toRawString() {
+ return senderName + "," + content;
+ }
+
+ /**
+ * Returns a string representation of the message.
+ *
+ * @return The string representation of the message.
+ */
+ @Override
+ public String toString() {
+ return senderName + ": " + content;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/gladiator/app/util/connection/ConnectionMessageUtils.java b/src/main/java/org/gladiator/app/util/connection/ConnectionMessageUtils.java
new file mode 100644
index 0000000..8e751e7
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/connection/ConnectionMessageUtils.java
@@ -0,0 +1,40 @@
+package app.util.connection;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+
+/**
+ * Utility class for handling connection messages.
+ */
+public final class ConnectionMessageUtils {
+
+ private ConnectionMessageUtils() {
+ }
+
+ /**
+ * Converts a raw string to a {@link ConnectionMessage}.
+ *
+ * @param message The raw string message.
+ * @return The {@link ConnectionMessage} object.
+ * @throws IllegalArgumentException if the message is blank or does not match the expected
+ * pattern.
+ */
+ public static ConnectionMessage fromRawString(final String message) {
+ Validate.notBlank(message);
+ Validate.matchesPattern(message, "([\\w\\s]*),(.*)");
+ final String[] split = StringUtils.split(message, ",", 2);
+ return new ConnectionMessage(split[0], split[1]);
+ }
+
+ /**
+ * Converts the sender name and content to a raw string format.
+ *
+ * @param senderName The name of the sender.
+ * @param content The content of the message.
+ * @return The raw string representation of the message.
+ */
+ public static String toRawString(final String senderName, final String content) {
+ return senderName + "," + content;
+ }
+
+}
diff --git a/src/main/java/org/gladiator/app/util/io/SocketIo.java b/src/main/java/org/gladiator/app/util/io/SocketIo.java
new file mode 100644
index 0000000..ab18f08
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/io/SocketIo.java
@@ -0,0 +1,22 @@
+package app.util.io;
+
+import java.io.BufferedReader;
+import java.io.PrintWriter;
+
+/**
+ * A model to represent the reader and writer of the socket.
+ *
+ * @param reader The socket {@link java.io.InputStream} reader
+ * @param writer The socket {@link java.io.OutputStream} writer
+ */
+public record SocketIo(BufferedReader reader, PrintWriter writer) {
+
+ public BufferedReader getReader() {
+ return reader;
+ }
+
+ public PrintWriter getWriter() {
+ return writer;
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/gladiator/app/util/io/SocketIoAsyncFactory.java b/src/main/java/org/gladiator/app/util/io/SocketIoAsyncFactory.java
new file mode 100644
index 0000000..63937ed
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/io/SocketIoAsyncFactory.java
@@ -0,0 +1,101 @@
+package app.util.io;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Factory class for creating asynchronous SocketIo instances.
+ */
+public final class SocketIoAsyncFactory {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(
+ SocketIoAsyncFactory.class.getName());
+
+ private SocketIoAsyncFactory() {
+ }
+
+ /**
+ * Creates a new {@link SocketIo} instance using the provided client socket and executor.
+ *
+ * @param clientSocket The client socket.
+ * @param executor The executor to use for asynchronous operations.
+ * @return A new {@link SocketIo} instance.
+ */
+ public static SocketIo create(final Socket clientSocket, final Executor executor) {
+ final SocketIoStreams socketIoStreams = createSocketIoStreams(clientSocket, executor);
+
+ final BufferedReader reader = new BufferedReader(
+ new InputStreamReader(socketIoStreams.socketInputStream(), StandardCharsets.UTF_8));
+ final PrintWriter writer = new PrintWriter(socketIoStreams.socketOutputStream(), true,
+ StandardCharsets.UTF_8);
+ return new SocketIo(reader, writer);
+ }
+
+ /**
+ * Creates a new {@link SocketIoStreams} instance using the provided client socket and executor.
+ *
+ * @param clientSocket The client socket.
+ * @param executor The executor to use for asynchronous operations.
+ * @return A new {@link SocketIoStreams} instance.
+ */
+ private static SocketIoStreams createSocketIoStreams(final Socket clientSocket,
+ final Executor executor) {
+ final CompletableFuture clientInputStreamFuture = CompletableFuture.supplyAsync(
+ () -> createInputStream(clientSocket), executor);
+
+ final CompletableFuture clientOutputStreamFuture = CompletableFuture.supplyAsync(
+ () -> createOutputStream(clientSocket), executor);
+
+ CompletableFuture.allOf(clientInputStreamFuture, clientOutputStreamFuture).join();
+
+ final InputStream clientInputStream = clientInputStreamFuture.join();
+ final OutputStream clientOutputStream = clientOutputStreamFuture.join();
+
+ return new SocketIoStreams(clientInputStream, clientOutputStream);
+ }
+
+ /**
+ * Creates an input stream from the provided client socket.
+ *
+ * @param clientSocket The client socket.
+ * @return The input stream.
+ */
+ private static InputStream createInputStream(final Socket clientSocket) {
+ InputStream clientInputStream = null;
+ try {
+ clientInputStream = clientSocket.getInputStream();
+ } catch (final IOException e) {
+ LOGGER.debug("Input Stream closed: {}", String.valueOf(e));
+ }
+ return clientInputStream;
+ }
+
+ /**
+ * Creates an output stream from the provided client socket.
+ *
+ * @param clientSocket The client socket.
+ * @return The output stream.
+ */
+ private static OutputStream createOutputStream(final Socket clientSocket) {
+ OutputStream clientOutputStream = null;
+ try {
+ clientOutputStream = clientSocket.getOutputStream();
+ } catch (final IOException e) {
+ LOGGER.debug("Output Stream closed: {}", String.valueOf(e));
+ }
+ return clientOutputStream;
+ }
+
+}
+
+
diff --git a/src/main/java/org/gladiator/app/util/io/SocketIoStreams.java b/src/main/java/org/gladiator/app/util/io/SocketIoStreams.java
new file mode 100644
index 0000000..4bd4380
--- /dev/null
+++ b/src/main/java/org/gladiator/app/util/io/SocketIoStreams.java
@@ -0,0 +1,11 @@
+package app.util.io;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A record that holds the input and output streams for a socket.
+ */
+public record SocketIoStreams(InputStream socketInputStream, OutputStream socketOutputStream) {
+
+}
diff --git a/src/main/java/org/gladiator/environment/Port.java b/src/main/java/org/gladiator/environment/Port.java
new file mode 100644
index 0000000..6af90ac
--- /dev/null
+++ b/src/main/java/org/gladiator/environment/Port.java
@@ -0,0 +1,23 @@
+package environment;
+
+/**
+ * Utility class for port-related constants.
+ */
+public final class Port {
+
+ /**
+ * The default port number.
+ */
+ public static final int PORT_DEFAULT = 50_000;
+ /**
+ * The minimum port number.
+ */
+ public static final int PORT_MIN = 0;
+ /**
+ * The maximum port number.
+ */
+ public static final int PORT_MAX = 65_535;
+
+ private Port() {
+ }
+}
diff --git a/src/main/org/gladiator/app/client/Client.java b/src/main/org/gladiator/app/client/Client.java
deleted file mode 100644
index 6707224..0000000
--- a/src/main/org/gladiator/app/client/Client.java
+++ /dev/null
@@ -1,163 +0,0 @@
-package app.client;
-
-import app.util.AsyncSocketIO;
-import app.util.Chat;
-import app.util.SingletonTerminal;
-import app.util.connection.ConnectionMessage;
-import org.jline.reader.EndOfFileException;
-import org.jline.reader.UserInterruptException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.SocketFactory;
-import java.io.*;
-import java.net.ConnectException;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public final class Client implements AutoCloseable {
- private static final Logger LOGGER = LoggerFactory.getLogger(Client.class.getName());
-
- private final ClientConfig config;
- private final Socket socket;
- private final ExecutorService executor;
- private final Chat chat;
- private final AtomicBoolean isRunning;
-
- private Client(ClientConfig config, Socket socket, ExecutorService executor, Chat chat, AtomicBoolean isRunning) {
- this.config = config;
- this.socket = socket;
- this.executor = executor;
- this.chat = chat;
- this.isRunning = isRunning;
- }
-
- public static Client createClient(AtomicBoolean isRunning) {
- Client client = null;
-
- try {
- ClientConfig clientConfig = ClientConfig.createClientConfig();
- Socket socket = SocketFactory.getDefault().createSocket(clientConfig.getServerAddress(), clientConfig.getPort());
- ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
- Chat chat = new Chat(">");
-
- client = new Client(clientConfig, socket, executor, chat, isRunning);
- } catch (UnknownHostException | ConnectException e) {
- LOGGER.error("Server not found", e);
- } catch (IOException e) {
- LOGGER.error("Error creating Socket", e);
- }
-
- return client;
- }
-
- public static Logger getLogger() {
- return LOGGER;
- }
-
- public void run() {
- final AsyncSocketIO socketIO = AsyncSocketIO.getAsyncSocketIO(socket, executor);
-
- final var reader = new BufferedReader(new InputStreamReader(socketIO.getInputStream(), StandardCharsets.UTF_8));
- final var writer = new PrintWriter(socketIO.getOutputStream(), true, StandardCharsets.UTF_8);
-
- String serverName = exchangeNames(writer, reader);
-
- chat.prettyPrint("Connection Established with " + serverName);
-
- System.out.println("Type `quit` to exit");
- CompletableFuture.allOf(receiveMessages(reader), sendMessages(writer)).join();
- reconnectPrompt();
- }
-
- private CompletableFuture receiveMessages(BufferedReader reader) {
- return CompletableFuture.runAsync(() -> {
- try {
- reader.lines()
- .map(ConnectionMessage::fromRawString)
- .forEach(msg -> {
- chat.showConnectionMessage(msg);
- chat.showUserPrompt();
- System.out.print(SingletonTerminal.TERMINAL.getLineReader().getBuffer());
- });
- } catch (UncheckedIOException e) {
- LOGGER.debug("The connection with the server has ended");
- } finally {
- executor.shutdownNow();
- }
- }, executor);
- }
-
- private CompletableFuture sendMessages(PrintWriter writer) {
- return CompletableFuture.runAsync(() -> {
- String line;
- try {
- while ((line = SingletonTerminal.TERMINAL.getLineReader().readLine(chat.userPrompt() + " ")) != null) {
- if (line.equals("quit")) {
- break;
- }
- String msg = ConnectionMessage.toRawString(config.getName(), line);
- writer.println(msg);
- }
- } catch (EndOfFileException | UserInterruptException e) {
- LOGGER.debug("User stopped the console reading, probably by pressing CTRL + C", e);
- } finally {
- executor.shutdownNow();
- }
- }, executor);
- }
-
- private String exchangeNames(PrintWriter writer, BufferedReader reader) {
-
- final var sendNameFuture = CompletableFuture.runAsync(() -> {
- writer.println(config.getName());
- String logMessage = "Sent name (" + config.getName() + ") to server.";
- Client.getLogger().debug(logMessage);
- }, executor);
-
- final var receiveNameFuture = CompletableFuture.supplyAsync(() -> {
- String serverName = "";
- try {
- serverName = reader.readLine();
- String logMessage = "Received name from server: " + serverName + ".";
- Client.getLogger().debug(logMessage);
- } catch (IOException e) {
- Client.getLogger().error("Error receiving server name: {}", e, e);
- }
-
- return serverName;
- }, executor);
-
- CompletableFuture.allOf(sendNameFuture, receiveNameFuture).join();
-
- return receiveNameFuture.join();
- }
-
- private void reconnectPrompt() {
- try {
- String choice = SingletonTerminal.TERMINAL.getLineReader().readLine("Connect to another server? y/N: ");
- if (!choice.equalsIgnoreCase("Y")) {
- throw new UserInterruptException("Reconnection choice not made");
- }
- } catch (UserInterruptException | EndOfFileException e) {
- isRunning.set(false);
- chat.prettyPrint("Chat Ended");
- }
- }
-
- @Override
- public void close() {
- LOGGER.debug("Closing connection");
- try {
- executor.shutdown();
- socket.close();
- } catch (IOException e) {
- LOGGER.error("Error closing the socket connection: {}", e, e);
- }
- }
-}
diff --git a/src/main/org/gladiator/app/client/ClientConfig.java b/src/main/org/gladiator/app/client/ClientConfig.java
deleted file mode 100644
index 3e2aac5..0000000
--- a/src/main/org/gladiator/app/client/ClientConfig.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package app.client;
-
-import app.util.SingletonTerminal;
-import environment.Port;
-import org.apache.commons.lang3.Validate;
-
-public class ClientConfig {
- private final String name;
- private final String serverAddress;
- private final int port;
-
- private ClientConfig(String name, String serverAddress, int port) {
- this.name = name;
- this.serverAddress = serverAddress;
- this.port = port;
- }
-
- public static ClientConfig createClientConfig() {
- final String clientName = receiveName();
- final String serverAddress = receiveAddress();
- final int serverPort = receivePort();
-
- return new ClientConfig(clientName, serverAddress, serverPort);
- }
-
- private static String receiveName() {
- String name = "";
- while (name.isBlank()) {
- name = SingletonTerminal.TERMINAL.getLineReader().readLine("Choose your name: ");
- }
- return name;
- }
-
- private static String receiveAddress() {
- return SingletonTerminal.TERMINAL.getLineReader().readLine("Type the Server IP (default = localhost): ");
- }
-
- private static int receivePort() {
- int serverPort;
- try {
- String serverPortString = SingletonTerminal.TERMINAL.getLineReader().readLine("Type the server port (default = " + Port.PORT_DEFAULT + "): ");
- if (serverPortString.isBlank()) {
- serverPort = Port.PORT_DEFAULT;
- } else {
- serverPort = Integer.parseInt(serverPortString);
- }
-
- Validate.inclusiveBetween(Port.PORT_MIN, Port.PORT_MAX, serverPort);
- } catch (IllegalArgumentException e) {
- Client.getLogger().error("Invalid Port, using default " + Port.PORT_DEFAULT);
- serverPort = Port.PORT_DEFAULT;
- }
-
- return serverPort;
- }
-
- public String getName() {
- return name;
- }
-
- public String getServerAddress() {
- return serverAddress;
- }
-
- public int getPort() {
- return port;
- }
-}
diff --git a/src/main/org/gladiator/app/client/ClientMain.java b/src/main/org/gladiator/app/client/ClientMain.java
deleted file mode 100644
index 817dbf7..0000000
--- a/src/main/org/gladiator/app/client/ClientMain.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package app.client;
-
-import app.server.Server;
-import app.util.SingletonTerminal;
-import org.jline.reader.UserInterruptException;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public final class ClientMain {
- public static void main(final String... args) {
- AtomicBoolean isRunning = new AtomicBoolean(true);
- while (isRunning.get()) {
- try (Client client = Client.createClient(isRunning)) {
- client.run();
- } catch (UserInterruptException e) {
- isRunning.set(false);
- Server.getLogger().debug("User didn't finished typing during a line read, probably by pressing CTRL+C", e);
- }
- }
- SingletonTerminal.TERMINAL.close();
- }
-}
diff --git a/src/main/org/gladiator/app/server/Server.java b/src/main/org/gladiator/app/server/Server.java
deleted file mode 100644
index e09fff8..0000000
--- a/src/main/org/gladiator/app/server/Server.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package app.server;
-
-import app.server.config.ServerConfig;
-import app.server.config.ServerConfigFactory;
-import app.util.AsyncSocketIO;
-import app.util.Chat;
-import app.util.PortMapper;
-import app.util.SingletonTerminal;
-import app.util.connection.Connection;
-import app.util.connection.ConnectionMessage;
-import org.jline.reader.EndOfFileException;
-import org.jline.reader.UserInterruptException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.ServerSocketFactory;
-import java.io.*;
-import java.net.BindException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Predicate;
-
-public final class Server implements AutoCloseable {
- private static final Logger LOGGER = LoggerFactory.getLogger(Server.class.getName());
- private final List clientConnections = new CopyOnWriteArrayList<>();
- private final AtomicBoolean isClosingManually = new AtomicBoolean(false);
-
- private final ServerConfig serverConfig;
- private final ServerSocket serverSocket;
- private final ExecutorService executor;
- private final Chat chat;
- private final PortMapper portMapper;
-
- private Server(ServerConfig serverConfig, ServerSocket serverSocket,
- ExecutorService executor, Chat chat, PortMapper portMapper) {
- this.serverConfig = serverConfig;
- this.serverSocket = serverSocket;
- this.executor = executor;
- this.chat = chat;
- this.portMapper = portMapper;
- }
-
- public static Logger getLogger() {
- return LOGGER;
- }
-
- public static Server createServer() throws UserInterruptException {
- Server server = null;
- try {
- ServerConfig serverConfig = new ServerConfigFactory().create();
- ServerSocket serverSocket = ServerSocketFactory.getDefault().createServerSocket(serverConfig.port());
- ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
- Chat chat = new Chat(">");
-
- PortMapper portMapper = PortMapper.createDefault();
- portMapper.openPort(serverConfig.port());
-
- server = new Server(serverConfig, serverSocket, executor, chat, portMapper);
- } catch (BindException e) {
- LOGGER.error("Address already in use, check if you have another server opened in the same port");
- } catch (IOException e) {
- LOGGER.error("Error creating Server Socket", e);
- }
-
- Objects.requireNonNull(server);
- return server;
- }
-
- public void runServer() {
- LOGGER.info("Server Started...");
- CompletableFuture.anyOf(listenToConnections(), broadcastToConnections()).join();
- }
-
- private CompletableFuture listenToConnections() {
- LOGGER.info("Listening to connections...");
-
- return CompletableFuture.runAsync(() -> {
- while (!serverSocket.isClosed() && serverSocket.isBound()) {
- final Socket clientSocket;
- try {
- clientSocket = serverSocket.accept();
-
- final AsyncSocketIO asyncSocketIO = AsyncSocketIO.getAsyncSocketIO(clientSocket, executor);
-
- final var reader = new BufferedReader(new InputStreamReader(asyncSocketIO.getInputStream(), StandardCharsets.UTF_8));
- final var writer = new PrintWriter(asyncSocketIO.getOutputStream(), true, StandardCharsets.UTF_8);
-
- String clientName = exchangeNames(writer, reader);
-
- chat.cleanLine();
- System.out.println("User " + clientName + " connected");
- chat.showBufferedMessage(SingletonTerminal.TERMINAL.getLineReader().getBuffer().toString());
-
- Connection clientConnection = new Connection(clientName, clientSocket, reader, writer);
- clientConnections.add(clientConnection);
-
- receiveMessages(reader, clientConnection);
- } catch (IOException e) {
- LOGGER.debug("Connection listening ended normally or error during Socket Server accept method: {}", e.getMessage());
- }
- }
- }, executor);
- }
-
- private CompletableFuture broadcastToConnections() {
- LOGGER.info("Broadcasting to connections...");
-
- System.out.println("Type `quit` to exit");
- return CompletableFuture.runAsync(() -> {
- String line;
- try {
- while ((line = SingletonTerminal.TERMINAL.getLineReader().readLine(chat.userPrompt() + " ")) != null) {
- if (line.equals("quit")) {
- throw new UserInterruptException("User typed `quit`");
- }
-
- String msg = ConnectionMessage.toRawString(serverConfig.name(), line);
-
- List> futures = new ArrayList<>();
-
- for (Connection connection : clientConnections) {
- CompletableFuture future = CompletableFuture.runAsync(()
- -> connection.output().println(msg), executor);
- futures.add(future);
- }
-
- CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
- }
- } catch (EndOfFileException | UserInterruptException e) {
- LOGGER.debug("User stopped the console reading, probably by pressing CTRL + C: {}", String.valueOf(e));
- }
- }, executor);
- }
-
- private void receiveMessages(BufferedReader reader, Connection clientConnection) {
- executor.execute(() -> {
- try {
- reader.lines()
- .map(ConnectionMessage::fromRawString)
- .forEach(msg -> {
- chat.showConnectionMessage(msg);
- chat.showBufferedMessage(SingletonTerminal.TERMINAL.getLineReader().getBuffer().toString());
- clientConnections
- .stream()
- .filter(Predicate.not(clientConnection::equals))
- .forEach(connection -> CompletableFuture.runAsync(() ->
- connection.output().println(msg.toRawString())));
- });
-
- } catch (UncheckedIOException ignored) {
- LOGGER.debug("Connection with {} ended abruptly", clientConnection.name());
- } finally {
- if (!isClosingManually.get()) {
- LOGGER.info("User Disconnected: {}", clientConnection.name());
- chat.showBufferedMessage(SingletonTerminal.TERMINAL.getLineReader().getBuffer().toString());
- clientConnection.close();
- clientConnections.remove(clientConnection);
- }
- }
- });
- }
-
- private String exchangeNames(PrintWriter writer, BufferedReader reader) {
-
- final var sendServerNameFuture =
- CompletableFuture
- .runAsync(() -> {
- writer.println(serverConfig.name());
- String logMessage = "Sent name " +
- serverConfig.name() +
- " to client.";
- Server.getLogger().debug(logMessage);
- }, executor);
-
- final var receiveClientNameFuture = CompletableFuture
- .supplyAsync(() -> {
- String clientName = "";
- try {
- clientName = reader.readLine();
- String logMessage = "Received name from client: " + clientName + ".";
- Server.getLogger().debug(logMessage);
- } catch (IOException e) {
- Server.getLogger().error("Error receiving client name: {}", e, e);
- }
- return clientName;
- }, executor);
-
- CompletableFuture.allOf(sendServerNameFuture, receiveClientNameFuture).join();
-
- return receiveClientNameFuture.join();
- }
-
- @Override
- public void close() throws IOException {
- LOGGER.info("Closing all connections...");
-
- isClosingManually.set(true);
- final var futures = clientConnections.stream().map(connection ->
- CompletableFuture.runAsync(connection::close)).toArray(CompletableFuture[]::new);
- CompletableFuture.allOf(futures).join();
-
- SingletonTerminal.TERMINAL.close();
- executor.shutdown();
- serverSocket.close();
- portMapper.closePort(serverConfig.port());
- portMapper.close();
- }
-
-}
diff --git a/src/main/org/gladiator/app/server/ServerMain.java b/src/main/org/gladiator/app/server/ServerMain.java
deleted file mode 100644
index bfd1040..0000000
--- a/src/main/org/gladiator/app/server/ServerMain.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package app.server;
-
-import org.jline.reader.UserInterruptException;
-
-import java.io.IOException;
-
-public final class ServerMain {
- public static void main(final String... args) {
- try (Server server = Server.createServer()) {
- server.runServer();
- } catch (IOException e) {
- Server.getLogger().error("Error closing Server Socket", e);
- } catch (UserInterruptException e) {
- Server.getLogger().debug("User didn't finished typing during a line read, probably by pressing CTRL+C", e);
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/org/gladiator/app/server/config/ServerConfig.java b/src/main/org/gladiator/app/server/config/ServerConfig.java
deleted file mode 100644
index a21e1a1..0000000
--- a/src/main/org/gladiator/app/server/config/ServerConfig.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package app.server.config;
-
-import org.apache.commons.lang3.Validate;
-
-import java.util.Objects;
-
-import static environment.Port.PORT_MAX;
-import static environment.Port.PORT_MIN;
-
-// God class
-public record ServerConfig(String name, int port) {
-
- public ServerConfig {
- validateArgs(name, port);
- }
-
- public ServerConfig() {
- this(getDefaultName(), getDefaultPort());
- }
-
- public static String getDefaultName() {
- return "Server";
- }
-
- public static int getDefaultPort() {
- return 50_000;
- }
-
- private void validateArgs(String name, int port) {
- Objects.requireNonNull(name);
- Validate.notBlank(name);
- Validate.inclusiveBetween(PORT_MIN, PORT_MAX, port);
- }
-
-}
diff --git a/src/main/org/gladiator/app/server/config/ServerConfigFactory.java b/src/main/org/gladiator/app/server/config/ServerConfigFactory.java
deleted file mode 100644
index e1908bd..0000000
--- a/src/main/org/gladiator/app/server/config/ServerConfigFactory.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package app.server.config;
-
-import app.server.Server;
-import app.util.SingletonTerminal;
-import environment.Port;
-import org.apache.commons.lang3.Validate;
-import org.jline.reader.LineReader;
-
-
-public class ServerConfigFactory {
-
- public ServerConfig create() {
- ServerConfig serverConfig;
- String isCustom;
-
- LineReader reader = SingletonTerminal.TERMINAL.getLineReader();
-
- isCustom = reader.readLine("Want to change the default settings? y/N: ");
- isCustom = isCustom.toUpperCase();
- serverConfig = isCustom.equals("Y") ? createCustom() : createDefault();
-
- return serverConfig;
- }
-
- private ServerConfig createCustom() {
- String serverName = "";
- int serverPort;
-
- try {
- LineReader lineReader = SingletonTerminal.TERMINAL.getLineReader();
- System.out.println("Leave the field blank for the default setting");
-
- serverName = getCustomConfig(lineReader, "Server Name", ServerConfig.getDefaultName());
- serverPort = Integer.parseInt(getCustomConfig(lineReader, "Server Port", String.valueOf(ServerConfig.getDefaultPort())));
- Validate.inclusiveBetween(Port.PORT_MIN, Port.PORT_MAX, serverPort);
-
- } catch (IllegalArgumentException e) {
- Server.getLogger().error("Invalid port number provided. Using default port.");
- serverPort = ServerConfig.getDefaultPort();
- }
-
- return new ServerConfig(serverName, serverPort);
- }
-
- private ServerConfig createDefault() {
- return new ServerConfig();
- }
-
- private String getCustomConfig(LineReader reader, String configName, String defaultConfig) {
- String config = reader.readLine(configName + " (default = " + defaultConfig + "): ");
- if (config.isBlank()) {
- config = defaultConfig;
- }
- return config;
- }
-}
diff --git a/src/main/org/gladiator/app/util/AsyncSocketIO.java b/src/main/org/gladiator/app/util/AsyncSocketIO.java
deleted file mode 100644
index dd0342c..0000000
--- a/src/main/org/gladiator/app/util/AsyncSocketIO.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package app.util;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.Socket;
-import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
-
-public final class AsyncSocketIO {
- private static final Logger LOGGER = LoggerFactory.getLogger(AsyncSocketIO.class.getName());
- private final InputStream inputStream;
- private final OutputStream outputStream;
-
- private AsyncSocketIO(InputStream inputStream, OutputStream outputStream) {
- this.inputStream = Objects.requireNonNull(inputStream);
- this.outputStream = Objects.requireNonNull(outputStream);
- }
-
- public static AsyncSocketIO getAsyncSocketIO(Socket clientSocket, Executor executor) {
- final var clientInputStreamFuture = CompletableFuture.supplyAsync(() ->
- createInputStream(clientSocket), executor);
-
- final var clientOutputStreamFuture = CompletableFuture.supplyAsync(() ->
- createOutputStream(clientSocket), executor);
-
- CompletableFuture.allOf(clientInputStreamFuture, clientOutputStreamFuture).join();
-
- AsyncSocketIO asyncSocketIO = null;
- try {
- asyncSocketIO = new AsyncSocketIO(
- clientInputStreamFuture.get(),
- clientOutputStreamFuture.get());
- } catch (InterruptedException e) {
- LOGGER.error("The current thread was interrupted while waiting: {}", e, e);
- Thread.currentThread().interrupt();
- } catch (ExecutionException e) {
- LOGGER.debug("this future completed exceptionally: {}", String.valueOf(e));
- }
-
- return asyncSocketIO;
- }
-
- private static InputStream createInputStream(Socket clientSocket) {
- InputStream clientInputStream = null;
- try {
- clientInputStream = clientSocket.getInputStream();
- } catch (IOException e) {
- LOGGER.debug("Input Stream closed: {}", String.valueOf(e));
- }
- return clientInputStream;
- }
-
- private static OutputStream createOutputStream(Socket clientSocket) {
- OutputStream clientOutputStream = null;
- try {
- clientOutputStream = clientSocket.getOutputStream();
- } catch (IOException e) {
- LOGGER.debug("Output Stream closed: {}", String.valueOf(e));
- }
- return clientOutputStream;
- }
-
- public InputStream getInputStream() {
- return inputStream;
- }
-
- public OutputStream getOutputStream() {
- return outputStream;
- }
-}
\ No newline at end of file
diff --git a/src/main/org/gladiator/app/util/Chat.java b/src/main/org/gladiator/app/util/Chat.java
deleted file mode 100644
index aca6de1..0000000
--- a/src/main/org/gladiator/app/util/Chat.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package app.util;
-
-import app.util.connection.ConnectionMessage;
-
-public class Chat {
- private final String userPrompt;
-
- public Chat(String userPrompt) {
- this.userPrompt = userPrompt;
- }
-
- public void prettyPrint(String str) {
- String division = "=".repeat(str.length());
- System.out.println("\n" + division);
- System.out.println(str);
- System.out.println(division + "\n");
- }
-
- public void cleanLine() {
- System.out.print("\r\033[K");
- }
-
- public void showUserPrompt() {
- System.out.print(userPrompt + " ");
- }
-
- public void showBufferedMessage(String bufferedMessage) {
- showUserPrompt();
- System.out.print(bufferedMessage);
- }
-
- public void showConnectionMessage(final ConnectionMessage message) {
- cleanLine();
- System.out.println(message);
- }
-
- public String userPrompt() {
- return userPrompt;
- }
-
-}
diff --git a/src/main/org/gladiator/app/util/PortMapper.java b/src/main/org/gladiator/app/util/PortMapper.java
deleted file mode 100644
index bbfccef..0000000
--- a/src/main/org/gladiator/app/util/PortMapper.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package app.util;
-
-import com.sshtools.porter.UPnP;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-
-public class PortMapper implements AutoCloseable {
- private final static Logger LOGGER = LoggerFactory.getLogger(PortMapper.class);
- private final static UPnP.Protocol protocol = UPnP.Protocol.TCP;
-
- private final ExecutorService executor;
- private final UPnP.Discovery discovery;
- private UPnP.Gateway gateway;
-
- public PortMapper(ExecutorService executor, UPnP.Discovery discovery) {
- this.executor = executor;
- this.discovery = discovery;
- }
-
- public static PortMapper createDefault() {
- ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
-
- UPnP.Discovery discovery = new UPnP.DiscoveryBuilder().withSoTimeout(600)
- .withoutShutdownHooks()
- .onGateway(gw -> LOGGER.debug("Gateway found {}", gw.ip()))
- .build();
-
- PortMapper portMapper = new PortMapper(executor, discovery);
-
- executor.execute(() -> portMapper.setGateway(discovery.gateway().orElse(null)));
-
- return portMapper;
- }
-
- public void openPort(int port) {
- executor.execute(() -> {
- if (gateway == null) {
- discovery.awaitCompletion(5, TimeUnit.SECONDS);
- }
- if (gateway != null) {
- boolean mapped = gateway.map(port, protocol);
- if (mapped) {
- LOGGER.debug("Port {} mapped with success", port);
- }
- }
- });
- }
-
- public void closePort(int port) {
- if (gateway != null) {
- gateway.unmap(port, protocol);
- LOGGER.debug("Port {} unmapped with success", port);
- }
- }
-
- public void setGateway(UPnP.Gateway gateway) {
- this.gateway = gateway;
- }
-
- @Override
- public void close() {
- executor.shutdown();
- discovery.close();
- }
-}
diff --git a/src/main/org/gladiator/app/util/SingletonTerminal.java b/src/main/org/gladiator/app/util/SingletonTerminal.java
deleted file mode 100644
index 8d7bbfd..0000000
--- a/src/main/org/gladiator/app/util/SingletonTerminal.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package app.util;
-
-import org.jline.reader.LineReader;
-import org.jline.reader.LineReaderBuilder;
-import org.jline.terminal.Terminal;
-import org.jline.terminal.TerminalBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Objects;
-
-public class SingletonTerminal implements AutoCloseable {
- public static final SingletonTerminal TERMINAL = new SingletonTerminal();
- public static final Logger LOGGER = LoggerFactory.getLogger(SingletonTerminal.class);
-
- private final Terminal terminal;
- private final LineReader lineReader;
-
- private SingletonTerminal() {
- Terminal tempTerminal = null;
- LineReader tempLineReader = null;
- try {
- tempTerminal = TerminalBuilder.terminal();
- tempLineReader = LineReaderBuilder.builder().terminal(tempTerminal).variable(LineReader.DISABLE_HISTORY, true).build();
- } catch (IOException e) {
- LOGGER.error("Error creating client terminal: {}", e.getMessage(), e);
- }
-
- Objects.requireNonNull(tempTerminal);
- tempTerminal.enterRawMode();
- tempTerminal.echo(true);
- this.terminal = tempTerminal;
- this.lineReader = tempLineReader;
- }
-
- public Terminal getTerminal() {
- return terminal;
- }
-
- public LineReader getLineReader() {
- return lineReader;
- }
-
- public void close() {
- try {
- terminal.close();
- } catch (IOException e) {
- LOGGER.error("Error closing terminal: {}", e.getMessage(), e);
- }
- }
-}
diff --git a/src/main/org/gladiator/app/util/connection/Connection.java b/src/main/org/gladiator/app/util/connection/Connection.java
deleted file mode 100644
index f0c2fb3..0000000
--- a/src/main/org/gladiator/app/util/connection/Connection.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package app.util.connection;
-
-import app.server.Server;
-import org.apache.commons.lang3.Validate;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.net.Socket;
-import java.util.Objects;
-
-public record Connection(String name, Socket socket, BufferedReader input,
- PrintWriter output) implements AutoCloseable {
-
- public Connection {
- Validate.notBlank(name);
- Objects.requireNonNull(socket);
- Objects.requireNonNull(input);
- Objects.requireNonNull(output);
- }
-
- public void close() {
- try {
- output.close();
- input.close();
- socket.close();
- } catch (IOException e) {
- Server.getLogger().error("Error closing the connection: {}", e, e);
- }
-
- }
-
-}
diff --git a/src/main/org/gladiator/app/util/connection/ConnectionMessage.java b/src/main/org/gladiator/app/util/connection/ConnectionMessage.java
deleted file mode 100644
index 32162be..0000000
--- a/src/main/org/gladiator/app/util/connection/ConnectionMessage.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package app.util.connection;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.Validate;
-
-import java.util.Objects;
-
-public record ConnectionMessage(String senderName, String content) {
- public ConnectionMessage {
- Validate.notBlank(senderName);
- Objects.requireNonNull(content);
- }
-
- public static ConnectionMessage fromRawString(String message) {
- Validate.notBlank(message);
- Validate.matchesPattern(message, "([\\w\\s]*),(.*)");
- final var split = StringUtils.split(message, ",", 2);
- return new ConnectionMessage(split[0], split[1]);
- }
-
- public static String toRawString(String senderName, String content) {
- return senderName + "," + content;
- }
-
- @Override
- public String toString() {
- return senderName + ": " + content;
- }
-
- public String toRawString() {
- return senderName + "," + content;
- }
-}
diff --git a/src/main/org/gladiator/environment/Port.java b/src/main/org/gladiator/environment/Port.java
deleted file mode 100644
index 0306f8a..0000000
--- a/src/main/org/gladiator/environment/Port.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package environment;
-
-public interface Port {
- int PORT_DEFAULT = 50_000;
- int PORT_MIN = 0;
- int PORT_MAX = 65_535;
-}
diff --git a/src/main/resources/META-INF/native-image/reachability-metadata.json b/src/main/resources/META-INF/native-image/reachability-metadata.json
deleted file mode 100644
index 52eaf38..0000000
--- a/src/main/resources/META-INF/native-image/reachability-metadata.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "resources": [
- {
- "glob": "**/logback.xml"
- }
- ]
-}
\ No newline at end of file