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 @@

command

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