From 911d423f189e8827da58fc1735a95913276235ba Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:06:59 -0400 Subject: [PATCH 01/56] All source code --- M4/Part1/Client.java | 175 +++++++++++++++++++++++++ M4/Part1/Server.java | 56 ++++++++ M4/Part2/Client.java | 200 ++++++++++++++++++++++++++++ M4/Part2/Server.java | 70 ++++++++++ M4/Part3/Client.java | 245 +++++++++++++++++++++++++++++++++++ M4/Part3/Constants.java | 5 + M4/Part3/Server.java | 149 +++++++++++++++++++++ M4/Part3/ServerThread.java | 225 ++++++++++++++++++++++++++++++++ M4/Part3/TextFX.java | 72 ++++++++++ M4/Part3HW/Client.java | 245 +++++++++++++++++++++++++++++++++++ M4/Part3HW/Constants.java | 5 + M4/Part3HW/Server.java | 149 +++++++++++++++++++++ M4/Part3HW/ServerThread.java | 225 ++++++++++++++++++++++++++++++++ M4/Part3HW/TextFX.java | 72 ++++++++++ 14 files changed, 1893 insertions(+) create mode 100644 M4/Part1/Client.java create mode 100644 M4/Part1/Server.java create mode 100644 M4/Part2/Client.java create mode 100644 M4/Part2/Server.java create mode 100644 M4/Part3/Client.java create mode 100644 M4/Part3/Constants.java create mode 100644 M4/Part3/Server.java create mode 100644 M4/Part3/ServerThread.java create mode 100644 M4/Part3/TextFX.java create mode 100644 M4/Part3HW/Client.java create mode 100644 M4/Part3HW/Constants.java create mode 100644 M4/Part3HW/Server.java create mode 100644 M4/Part3HW/ServerThread.java create mode 100644 M4/Part3HW/TextFX.java diff --git a/M4/Part1/Client.java b/M4/Part1/Client.java new file mode 100644 index 0000000..ce12481 --- /dev/null +++ b/M4/Part1/Client.java @@ -0,0 +1,175 @@ +package M4.Part1; + +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Demoing single direction of Client sending data to a Server + */ +public class Client { + + private Socket server = null; + private PrintWriter out = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); // 192.168.0.2:3000 + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); // localhost:3000 + private boolean isRunning = false; + + public Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine + // if the server had a problem and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + + } + + /** + * Takes an ip address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new PrintWriter(server.getOutputStream(), true); + // channel to list to server + System.out.println("Client connected"); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an ip address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return + */ + private boolean isConnection(String text) { + // https://www.w3schools.com/java/java_regex.asp + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if a text was a command or triggered a command + */ + private boolean processClientCommand(String text) { + if (isConnection(text)) { + // replaces multiple spaces with single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + return true; + } else if ("/quit".equalsIgnoreCase(text)) { + isRunning = false; + return true; + } + return false; + } + + public void start() throws IOException { + + System.out.println("Client starting"); + try (Scanner si = new Scanner(System.in);) { + String line = ""; + isRunning = true; + while (isRunning) { + try { + System.out.println("Waiting for input"); + line = si.nextLine(); + if (!processClientCommand(line)) { + if (isConnected()) { + out.println(line); + // https://stackoverflow.com/a/8190411 + // you'll notice it triggers on the second request after server socket closes + if (out.checkError()) { + System.out.println("Connection to server may have been lost"); + } + } else { + System.out.println("Not connected to server"); + } + } + } catch (Exception e) { + System.out.println("Connection dropped"); + break; + } + } + System.out.println("Exited loop"); + } catch (Exception e) { + System.out.println("Exception from start()"); + e.printStackTrace(); + } finally { + close(); + } + } + + private void close() { + try { + System.out.println("Closing output stream"); + out.close(); + } catch (NullPointerException ne) { + System.out.println("Server was never opened so this exception is ok"); + } catch (Exception e) { + e.printStackTrace(); + } + try { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } catch (IOException e) { + e.printStackTrace(); + } catch (NullPointerException ne) { + System.out.println("Server was never opened so this exception is ok"); + } + } + + public static void main(String[] args) { + Client client = new Client(); + + try { + // if start is private, it's valid here since this main is part of the class + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/M4/Part1/Server.java b/M4/Part1/Server.java new file mode 100644 index 0000000..3a032cb --- /dev/null +++ b/M4/Part1/Server.java @@ -0,0 +1,56 @@ +package M4.Part1; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; + +public class Server { + private int port = 3000; + + private void start(int port) { + this.port = port; + System.out.println("Listening on port " + this.port); + // server listening + try (ServerSocket serverSocket = new ServerSocket(port); + // client wait + Socket client = serverSocket.accept(); // blocking; + + // read from client + BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));) { + + System.out.println("Client connected, waiting for message"); + String fromClient = ""; + while ((fromClient = in.readLine()) != null) { + if ("/kill server".equalsIgnoreCase(fromClient)) { + // normally you wouldn't have a remote kill command, this is just for example + // sake + System.out.println("Client killed server"); + break; + } else { + System.out.println("From client: " + fromClient); + } + } + } catch (IOException e) { + System.out.println("Exception from start()"); + e.printStackTrace(); + } finally { + System.out.println("closing server socket"); + } + } + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = new Server(); + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } +} \ No newline at end of file diff --git a/M4/Part2/Client.java b/M4/Part2/Client.java new file mode 100644 index 0000000..8f53489 --- /dev/null +++ b/M4/Part2/Client.java @@ -0,0 +1,200 @@ +package M4.Part2; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Demoing bi-directional communication between client and server + */ +public class Client { + + private Socket server = null; + private PrintWriter out = null; + private BufferedReader in = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); + private boolean isRunning = false; + + public Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine + // if the server had a problem and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + + } + + /** + * Takes an ip address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new PrintWriter(server.getOutputStream(), true); + // channel to list to server + in = new BufferedReader(new InputStreamReader(server.getInputStream())); + System.out.println("Client connected"); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an ip address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return + */ + private boolean isConnection(String text) { + // https://www.w3schools.com/java/java_regex.asp + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if a text was a command or triggered a command + */ + private boolean processClientCommand(String text) { + if (isConnection(text)) { + // replaces multiple spaces with single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + return true; + } else if ("/quit".equalsIgnoreCase(text)) { + isRunning = false; + return true; + } + return false; + } + + public void start() throws IOException { + + System.out.println("Client starting"); + try (Scanner si = new Scanner(System.in);) { + String line = ""; + isRunning = true; + while (isRunning) { + try { + System.out.println("Waiting for input"); + line = si.nextLine(); + if (!processClientCommand(line)) { + if (isConnected()) { + out.println(line); + // https://stackoverflow.com/a/8190411 + // you'll notice it triggers on the second request after server socket closes + if (out.checkError()) { + System.out.println("Connection to server may have been lost"); + } + // wait for reply + // Note: now that we're attempting a read + // we'll immediately get notified if the server's connection closes + // Note2: if the server terminates before we send a message, client will exit + // after the out.println() continues + String fromServer = in.readLine(); + + if (fromServer != null) { + System.out.println("Reply from server: " + fromServer); + } else { + System.out.println("Server disconnected"); + break; + } + } else { + System.out.println("Not connected to server"); + } + } + } catch (Exception e) { + System.out.println("Connection dropped"); + break; + } + } + System.out.println("Exited loop"); + } catch (Exception e) { + System.out.println("Exception from start()"); + e.printStackTrace(); + } finally { + close(); + } + } + + private void close() { + try { + System.out.println("Closing output stream"); + out.close(); + } catch (NullPointerException ne) { + System.out.println("Outputstream was never opened so this exception is ok"); + } catch (Exception e) { + e.printStackTrace(); + } + try { + System.out.println("Closing input stream"); + in.close(); + } catch (NullPointerException ne) { + System.out.println("InputStream was never opened so this exception is ok"); + } catch (Exception e) { + e.printStackTrace(); + } + try { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } catch (IOException e) { + e.printStackTrace(); + } catch (NullPointerException ne) { + System.out.println("Server was never opened so this exception is ok"); + } + } + + public static void main(String[] args) { + Client client = new Client(); + + try { + // if start is private, it's valid here since this main is part of the class + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/M4/Part2/Server.java b/M4/Part2/Server.java new file mode 100644 index 0000000..39b3a61 --- /dev/null +++ b/M4/Part2/Server.java @@ -0,0 +1,70 @@ +package M4.Part2; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; + +public class Server { + private int port = 3000; + + private void start(int port) { + this.port = port; + System.out.println("Listening on port " + this.port); + // server listening + try (ServerSocket serverSocket = new ServerSocket(port); + // client wait + Socket client = serverSocket.accept(); // blocking; + // send to client + PrintWriter out = new PrintWriter(client.getOutputStream(), true); + // read from client + BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));) { + + System.out.println("Client connected, waiting for message"); + String fromClient = ""; + while ((fromClient = in.readLine()) != null) { + System.out.println("From client: " + fromClient); + if ("/kill server".equalsIgnoreCase(fromClient)) { + // normally you wouldn't have a remote kill command, this is just for example + // sake + System.out.println("Client killed server"); + break; + } else if (fromClient.startsWith("/reverse")) { + // another example of server-side command + // Note: In the future command format processing will be client-side + // then client will send just the necessary data to the server so the server + // doesn't need to do as much string processing + StringBuilder sb = new StringBuilder(fromClient.replace("/reverse ", "")); + sb.reverse(); + String rev = sb.toString(); + System.out.println("To client: " + rev); + out.println(rev); + } else { + System.out.println("To client: " + fromClient); + out.println(fromClient); + } + } + } catch (IOException e) { + System.out.println("Exception from start()"); + e.printStackTrace(); + } finally { + System.out.println("closing server socket"); + } + } + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = new Server(); + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } +} \ No newline at end of file diff --git a/M4/Part3/Client.java b/M4/Part3/Client.java new file mode 100644 index 0000000..45197b3 --- /dev/null +++ b/M4/Part3/Client.java @@ -0,0 +1,245 @@ +package M4.Part3; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import M4.Part3.TextFX.Color; + +/** + * Demoing bi-directional communication between client and server in a + * multi-client scenario + */ +public class Client { + + private Socket server = null; + private ObjectOutputStream out = null; + private ObjectInputStream in = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); + private volatile boolean isRunning = true; // volatile for thread-safe visibility + + public Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine if the server had a problem + // and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + } + + /** + * Takes an IP address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new ObjectOutputStream(server.getOutputStream()); + // channel to listen to server + in = new ObjectInputStream(server.getInputStream()); + System.out.println("Client connected"); + // Use CompletableFuture to run listenToServer() in a separate thread + CompletableFuture.runAsync(this::listenToServer); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an IP address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return true if the text is a valid connection command + */ + private boolean isConnection(String text) { + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if the text was a command or triggered a command + * @throws IOException + */ + private boolean processClientCommand(String text) throws IOException { + boolean wasCommand = false; + if (isConnection(text)) { + // replaces multiple spaces with a single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + wasCommand = true; + } else if ("/quit".equalsIgnoreCase(text)) { + close(); + wasCommand = true; + } else if ("/disconnect".equalsIgnoreCase(text)) { + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, "disconnect" }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } else if (text.startsWith("/reverse")) { + text = text.replace("/reverse", "").trim(); + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, "reverse", text }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } + return wasCommand; + } + + public void start() throws IOException { + System.out.println("Client starting"); + + // Use CompletableFuture to run listenToInput() in a separate thread + CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); + + // Wait for inputFuture to complete to ensure proper termination + inputFuture.join(); + } + + /** + * Listens for messages from the server + */ + private void listenToServer() { + try { + while (isRunning && isConnected()) { + String fromServer = (String) in.readObject(); // blocking read + if (fromServer != null) { + System.out.println(TextFX.colorize(fromServer, Color.BLUE)); + } else { + System.out.println("Server disconnected"); + break; + } + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (isRunning) { + System.out.println("Connection dropped"); + e.printStackTrace(); + } + } finally { + closeServerConnection(); + } + System.out.println("listenToServer thread stopped"); + } + + /** + * Listens for keyboard input from the user + */ + private void listenToInput() { + try (Scanner si = new Scanner(System.in)) { + System.out.println("Waiting for input"); // moved here to avoid console spam + while (isRunning) { // Run until isRunning is false + String userInput = si.nextLine(); + if (!processClientCommand(userInput)) { + sendToServer(userInput); + } + } + } catch (IOException ioException) { + System.out.println("Error in listentToInput()"); + ioException.printStackTrace(); + } + System.out.println("listenToInput thread stopped"); + } + + private void sendToServer(String message) throws IOException { + if (isConnected()) { + out.writeObject(message); + out.flush(); // good practice to ensure data is written out immediately + } else { + System.out.println( + "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); + } + } + + /** + * Closes the client connection and associated resources + */ + private void close() { + isRunning = false; + closeServerConnection(); + System.out.println("Client terminated"); + // System.exit(0); // Terminate the application + } + + /** + * Closes the server connection and associated resources + */ + private void closeServerConnection() { + try { + if (out != null) { + System.out.println("Closing output stream"); + out.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (in != null) { + System.out.println("Closing input stream"); + in.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (server != null) { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + Client client = new Client(); + try { + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/M4/Part3/Constants.java b/M4/Part3/Constants.java new file mode 100644 index 0000000..8b285de --- /dev/null +++ b/M4/Part3/Constants.java @@ -0,0 +1,5 @@ +package M4.Part3; + +public abstract class Constants { + final public static String COMMAND_TRIGGER = "[cmd]"; +} \ No newline at end of file diff --git a/M4/Part3/Server.java b/M4/Part3/Server.java new file mode 100644 index 0000000..d7c294f --- /dev/null +++ b/M4/Part3/Server.java @@ -0,0 +1,149 @@ +package M4.Part3; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ConcurrentHashMap; + +public class Server { + private int port = 3000; + // connected clients + // Use ConcurrentHashMap for thread-safe client management + // the Long will be a unique client identifier, and ServerThread is the instance + private final ConcurrentHashMap connectedClients = new ConcurrentHashMap<>(); + private boolean isRunning = true; + + private void start(int port) { + this.port = port; + // server listening + System.out.println("Listening on port " + this.port); + // Simplified client connection loop + try (ServerSocket serverSocket = new ServerSocket(port)) { + while (isRunning) { + System.out.println("Waiting for next client"); + Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection + System.out.println("Client connected"); + // wrap socket in a ServerThread, pass a callback to notify the Server when + // they're initialized + ServerThread serverThread = new ServerThread(incomingClient, this, this::onServerThreadInitialized); + // start the thread (typically an external entity manages the lifecycle and we + // don't have the thread start itself) + serverThread.start(); + // Note: We don't yet add the ServerThread reference to our connectedClients map + } + } catch (IOException e) { + System.err.println("Error accepting connection"); + e.printStackTrace(); + } finally { + System.out.println("Closing server socket"); + } + } + + /** + * Callback passed to ServerThread to inform Server they're ready to receive + * data + * + * @param serverThread + */ + private void onServerThreadInitialized(ServerThread serverThread) { + // add to connected clients list (unique id and actual reference) + connectedClients.put(serverThread.getClientId(), serverThread); + relay(null, String.format("*User[%s] connected*", serverThread.getClientId())); + } + + /** + * Takes a ServerThread and removes them from the Server + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param serverThread + */ + private synchronized void disconnect(ServerThread serverThread) { + serverThread.disconnect(); + // remove disconnecting ServerThread from map + ServerThread disconnectingServerThread = connectedClients.remove(serverThread.getClientId()); + if (disconnectingServerThread != null) { + // Improved logging with user ID + relay(null, "User[" + disconnectingServerThread.getClientId() + "] disconnected"); + } + } + + /** + * Relays the message from the sender to all connectedClients + * Internally calls processCommand and evaluates as necessary. + * Note: Clients that fail to receive a message get removed from + * connectedClients. + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + private synchronized void relay(ServerThread sender, String message) { + // we'll temporarily use the thread id as the client identifier to + // show in all client's chat. This isn't good practice since it's subject to + // change as clients connect/disconnect (i.e., a reconnecting client likely + // won't get the same id) + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? "Server" : String.format("User[%s]", sender.getClientId()); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + // end temp identifier + + // loop over clients and send out the message; remove client if message failed + // to be sent + // Note: this uses a lambda expression for each item in the values() collection, + // it's one way we can safely remove items during iteration + + connectedClients.values().removeIf(serverThread -> { + boolean failedToSend = !serverThread.sendToClient(formattedMessage); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected client[%s] from list", serverThread.getClientId())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + // start handle actions + /** + * Expose access to the disconnect action + * + * @param serverThread + */ + protected synchronized void handleDisconnect(ServerThread sender) { + disconnect(sender); + } + + protected synchronized void handleReverseText(ServerThread sender, String text) { + StringBuilder sb = new StringBuilder(text); + sb.reverse(); + String rev = sb.toString(); + relay(sender, rev); + } + + protected synchronized void handleMessage(ServerThread sender, String text) { + relay(sender, text); + } + // end handle actions + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = new Server(); + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } + +} \ No newline at end of file diff --git a/M4/Part3/ServerThread.java b/M4/Part3/ServerThread.java new file mode 100644 index 0000000..3c0d4ee --- /dev/null +++ b/M4/Part3/ServerThread.java @@ -0,0 +1,225 @@ +package M4.Part3; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; + +import M4.Part3.TextFX.Color; + +/** + * A server-side representation of a single client + */ +public class ServerThread extends Thread { + private Socket client; // communication directly to "my" client + private boolean isRunning = false; // control variable to stop this thread + private ObjectOutputStream out; // exposed here for send() + private Server server;// ref to our server so we can call methods on it + // more easily + private long clientId; + private Consumer onInitializationComplete; // callback to inform when this object is ready + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + private void info(String message) { + System.out.println(String.format("Thread[%s]: %s", this.getClientId(), message)); + } + + /** + * Returns the status of this ServerThread + * + * @return + */ + public boolean isRunning() { + return isRunning; + } + + /** + * Wraps the Socket connection and takes a Server reference and a callback + * + * @param myClient + * @param server + * @param onInitializationComplete method to inform listener that this object is + * ready + */ + protected ServerThread(Socket myClient, Server server, Consumer onInitializationComplete) { + Objects.requireNonNull(myClient, "Client socket cannot be null"); + Objects.requireNonNull(server, "Server cannot be null"); + Objects.requireNonNull(onInitializationComplete, "callback cannot be null"); + info("ServerThread created"); + // get communication channels to single client + this.client = myClient; + this.server = server; // In the future we'll have a different way to reference the Server + this.clientId = this.threadId(); // An id associated with the thread instance, used as a temporary identifier + this.onInitializationComplete = onInitializationComplete; + + } + + public long getClientId() { + // Note: We return clientId instead of threadId as we'll change this identifier + // in the future + return this.clientId; + } + + /** + * One of the two ways to get this to exit the listen loop + */ + protected void disconnect() { + if (!isRunning) { + // prevent multiple triggers if this gets called consecutively + return; + } + info("Thread being disconnected by server"); + isRunning = false; + this.interrupt(); // breaks out of blocking read in the run() method + cleanup(); // good practice to ensure data is written out immediately + } + + /** + * Sends the message over the socket + * + * @param message + * @return true if no errors were encountered + */ + protected boolean sendToClient(String message) { + if (!isRunning) { + return false; + } + try { + out.writeObject(message); + out.flush(); + return true; + } catch (IOException e) { + info("Error sending message to client (most likely disconnected)"); + // comment this out to inspect the stack trace + // e.printStackTrace(); + cleanup(); + return false; + } + } + + @Override + public void run() { + info("Thread starting"); + try (ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(client.getInputStream());) { + this.out = out; + isRunning = true; + onInitializationComplete.accept(this); // Notify server that initialization is complete + String fromClient; + /** + * isRunning is a flag to let us manage the loop exit condition + * fromClient (in.readObject()) is a blocking method that waits until data is + * received + * - null would likely mean a disconnect so we use a "set and check" logic to + * alternatively exit the loop + */ + while (isRunning) { + try { + fromClient = (String) in.readObject(); // blocking method + if (fromClient == null) { + throw new IOException("Connection interrupted"); // Specific exception for a clean break + } else { + info(TextFX.colorize("Received from my client: " + fromClient, Color.CYAN)); + processPayload(fromClient); + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (Thread.currentThread().isInterrupted()) { + info("Thread interrupted during read (likely from the disconnect() method)"); + break; + } + info("IO exception while reading from client"); + e.printStackTrace(); + break; + } + } // close while loop + } catch (Exception e) { + // happens when client disconnects + info("General Exception"); + e.printStackTrace(); + info("My Client disconnected"); + } finally { + isRunning = false; + info("Exited thread loop. Cleaning up connection"); + cleanup(); + } + } + + private void processPayload(String incoming) { + if (!processCommand(incoming)) { + // if not command; send message to all clients via Server + server.handleMessage(this, incoming); + } + + } + + /** + * Attempts to see if the message is a command and process its action + * + * @param message + * @param sender + * @return true if it was a command, false otherwise + */ + private boolean processCommand(String message) { + boolean wasCommand = false; // control var to use as the return status + + // using "[cmd]" as a temporary trigger until we update how the data is passed + // over the socket + if (message.startsWith(Constants.COMMAND_TRIGGER)) { + // expected format will be csv for now to keep it simple + String[] commandData = message.split(","); + if (commandData.length >= 2) { + + // index 0 is the trigger word + // index 1 is the command + final String command = commandData[1].trim(); + System.out.println(TextFX.colorize("Checking command: " + command, Color.YELLOW)); + // index N are the data from the command + // Note: not all commands require data, some are simply actions/triggers to + // process like quit + switch (command) { + case "quit": + case "disconnect": + case "logout": + case "logoff": + server.handleDisconnect(this); + wasCommand = true; + break; + case "reverse": + // ignore the first two indexes (trigger, command) + String relevantText = String.join(" ", Arrays.copyOfRange(commandData, 2, commandData.length)); + server.handleReverseText(this, relevantText); + wasCommand = true; + break; + // added more cases/breaks as needed for other commands + default: + break; + } + } + + } + return wasCommand; + } + + private void cleanup() { + info("ServerThread cleanup() start"); + try { + // close server-side end of connection + client.close(); + info("Closed Server-side Socket"); + } catch (IOException e) { + info("Client already closed"); + } + info("ServerThread cleanup() end"); + } +} \ No newline at end of file diff --git a/M4/Part3/TextFX.java b/M4/Part3/TextFX.java new file mode 100644 index 0000000..2870b75 --- /dev/null +++ b/M4/Part3/TextFX.java @@ -0,0 +1,72 @@ +package M4.Part3; + +/** + * Utility to attempt to provide colored text in the terminal. + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ +public abstract class TextFX { + + /** + * TextFX.Color list of available colors + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ + public enum Color { + BLACK("\033[0;30m"), + RED("\033[0;31m"), + GREEN("\033[0;32m"), + YELLOW("\033[0;33m"), + BLUE("\033[0;34m"), + PURPLE("\033[0;35m"), + CYAN("\033[0;36m"), + WHITE("\033[0;37m"); + + private final String code; + + Color(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + } + + public static final String RESET = "\033[0m"; + + /** + * Generates a String with the original message wrapped in the ASCII of the + * color and RESET + * + *

+ * Note: May not work for all terminals + *

+ *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ * + * @param text Input text to colorize + * @param color Enum of Color choice from TextFX.Color + * @return wrapped String + */ + public static String colorize(String text, Color color) { + StringBuilder builder = new StringBuilder(); + builder.append(color.getCode()); + builder.append(text); + builder.append(RESET); + return builder.toString(); + } + + public static void main(String[] args) { + // Example usage: + System.out.println(TextFX.colorize("Hello, world!", Color.RED)); + System.out.println(TextFX.colorize("This is some blue text.", Color.BLUE)); + System.out.println(TextFX.colorize("And this is green!", Color.GREEN)); + } +} \ No newline at end of file diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java new file mode 100644 index 0000000..574788a --- /dev/null +++ b/M4/Part3HW/Client.java @@ -0,0 +1,245 @@ +package M4.Part3HW; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import M4.Part3.TextFX.Color; + +/** + * Demoing bi-directional communication between client and server in a + * multi-client scenario + */ +public class Client { + + private Socket server = null; + private ObjectOutputStream out = null; + private ObjectInputStream in = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); + private volatile boolean isRunning = true; // volatile for thread-safe visibility + + public Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine if the server had a problem + // and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + } + + /** + * Takes an IP address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new ObjectOutputStream(server.getOutputStream()); + // channel to listen to server + in = new ObjectInputStream(server.getInputStream()); + System.out.println("Client connected"); + // Use CompletableFuture to run listenToServer() in a separate thread + CompletableFuture.runAsync(this::listenToServer); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an IP address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return true if the text is a valid connection command + */ + private boolean isConnection(String text) { + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if the text was a command or triggered a command + * @throws IOException + */ + private boolean processClientCommand(String text) throws IOException { + boolean wasCommand = false; + if (isConnection(text)) { + // replaces multiple spaces with a single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + wasCommand = true; + } else if ("/quit".equalsIgnoreCase(text)) { + close(); + wasCommand = true; + } else if ("/disconnect".equalsIgnoreCase(text)) { + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, "disconnect" }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } else if (text.startsWith("/reverse")) { + text = text.replace("/reverse", "").trim(); + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, "reverse", text }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } + return wasCommand; + } + + public void start() throws IOException { + System.out.println("Client starting"); + + // Use CompletableFuture to run listenToInput() in a separate thread + CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); + + // Wait for inputFuture to complete to ensure proper termination + inputFuture.join(); + } + + /** + * Listens for messages from the server + */ + private void listenToServer() { + try { + while (isRunning && isConnected()) { + String fromServer = (String) in.readObject(); // blocking read + if (fromServer != null) { + System.out.println(TextFX.colorize(fromServer, Color.BLUE)); + } else { + System.out.println("Server disconnected"); + break; + } + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (isRunning) { + System.out.println("Connection dropped"); + e.printStackTrace(); + } + } finally { + closeServerConnection(); + } + System.out.println("listenToServer thread stopped"); + } + + /** + * Listens for keyboard input from the user + */ + private void listenToInput() { + try (Scanner si = new Scanner(System.in)) { + System.out.println("Waiting for input"); // moved here to avoid console spam + while (isRunning) { // Run until isRunning is false + String userInput = si.nextLine(); + if (!processClientCommand(userInput)) { + sendToServer(userInput); + } + } + } catch (IOException ioException) { + System.out.println("Error in listentToInput()"); + ioException.printStackTrace(); + } + System.out.println("listenToInput thread stopped"); + } + + private void sendToServer(String message) throws IOException { + if (isConnected()) { + out.writeObject(message); + out.flush(); // good practice to ensure data is written out immediately + } else { + System.out.println( + "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); + } + } + + /** + * Closes the client connection and associated resources + */ + private void close() { + isRunning = false; + closeServerConnection(); + System.out.println("Client terminated"); + // System.exit(0); // Terminate the application + } + + /** + * Closes the server connection and associated resources + */ + private void closeServerConnection() { + try { + if (out != null) { + System.out.println("Closing output stream"); + out.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (in != null) { + System.out.println("Closing input stream"); + in.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (server != null) { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + Client client = new Client(); + try { + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/M4/Part3HW/Constants.java b/M4/Part3HW/Constants.java new file mode 100644 index 0000000..796bbf6 --- /dev/null +++ b/M4/Part3HW/Constants.java @@ -0,0 +1,5 @@ +package M4.Part3HW; + +public abstract class Constants { + final public static String COMMAND_TRIGGER = "[cmd]"; +} \ No newline at end of file diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java new file mode 100644 index 0000000..b66396f --- /dev/null +++ b/M4/Part3HW/Server.java @@ -0,0 +1,149 @@ +package M4.Part3HW; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ConcurrentHashMap; + +public class Server { + private int port = 3000; + // connected clients + // Use ConcurrentHashMap for thread-safe client management + // the Long will be a unique client identifier, and ServerThread is the instance + private final ConcurrentHashMap connectedClients = new ConcurrentHashMap<>(); + private boolean isRunning = true; + + private void start(int port) { + this.port = port; + // server listening + System.out.println("Listening on port " + this.port); + // Simplified client connection loop + try (ServerSocket serverSocket = new ServerSocket(port)) { + while (isRunning) { + System.out.println("Waiting for next client"); + Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection + System.out.println("Client connected"); + // wrap socket in a ServerThread, pass a callback to notify the Server when + // they're initialized + ServerThread serverThread = new ServerThread(incomingClient, this, this::onServerThreadInitialized); + // start the thread (typically an external entity manages the lifecycle and we + // don't have the thread start itself) + serverThread.start(); + // Note: We don't yet add the ServerThread reference to our connectedClients map + } + } catch (IOException e) { + System.err.println("Error accepting connection"); + e.printStackTrace(); + } finally { + System.out.println("Closing server socket"); + } + } + + /** + * Callback passed to ServerThread to inform Server they're ready to receive + * data + * + * @param serverThread + */ + private void onServerThreadInitialized(ServerThread serverThread) { + // add to connected clients list (unique id and actual reference) + connectedClients.put(serverThread.getClientId(), serverThread); + relay(null, String.format("*User[%s] connected*", serverThread.getClientId())); + } + + /** + * Takes a ServerThread and removes them from the Server + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param serverThread + */ + private synchronized void disconnect(ServerThread serverThread) { + serverThread.disconnect(); + // remove disconnecting ServerThread from map + ServerThread disconnectingServerThread = connectedClients.remove(serverThread.getClientId()); + if (disconnectingServerThread != null) { + // Improved logging with user ID + relay(null, "User[" + disconnectingServerThread.getClientId() + "] disconnected"); + } + } + + /** + * Relays the message from the sender to all connectedClients + * Internally calls processCommand and evaluates as necessary. + * Note: Clients that fail to receive a message get removed from + * connectedClients. + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + private synchronized void relay(ServerThread sender, String message) { + // we'll temporarily use the thread id as the client identifier to + // show in all client's chat. This isn't good practice since it's subject to + // change as clients connect/disconnect (i.e., a reconnecting client likely + // won't get the same id) + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? "Server" : String.format("User[%s]", sender.getClientId()); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + // end temp identifier + + // loop over clients and send out the message; remove client if message failed + // to be sent + // Note: this uses a lambda expression for each item in the values() collection, + // it's one way we can safely remove items during iteration + + connectedClients.values().removeIf(serverThread -> { + boolean failedToSend = !serverThread.sendToClient(formattedMessage); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected client[%s] from list", serverThread.getClientId())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + // start handle actions + /** + * Expose access to the disconnect action + * + * @param serverThread + */ + protected synchronized void handleDisconnect(ServerThread sender) { + disconnect(sender); + } + + protected synchronized void handleReverseText(ServerThread sender, String text) { + StringBuilder sb = new StringBuilder(text); + sb.reverse(); + String rev = sb.toString(); + relay(sender, rev); + } + + protected synchronized void handleMessage(ServerThread sender, String text) { + relay(sender, text); + } + // end handle actions + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = new Server(); + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } + +} \ No newline at end of file diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java new file mode 100644 index 0000000..944908e --- /dev/null +++ b/M4/Part3HW/ServerThread.java @@ -0,0 +1,225 @@ +package M4.Part3HW; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; + +import M4.Part3.TextFX.Color; + +/** + * A server-side representation of a single client + */ +public class ServerThread extends Thread { + private Socket client; // communication directly to "my" client + private boolean isRunning = false; // control variable to stop this thread + private ObjectOutputStream out; // exposed here for send() + private Server server;// ref to our server so we can call methods on it + // more easily + private long clientId; + private Consumer onInitializationComplete; // callback to inform when this object is ready + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + private void info(String message) { + System.out.println(String.format("Thread[%s]: %s", this.getClientId(), message)); + } + + /** + * Returns the status of this ServerThread + * + * @return + */ + public boolean isRunning() { + return isRunning; + } + + /** + * Wraps the Socket connection and takes a Server reference and a callback + * + * @param myClient + * @param server + * @param onInitializationComplete method to inform listener that this object is + * ready + */ + protected ServerThread(Socket myClient, Server server, Consumer onInitializationComplete) { + Objects.requireNonNull(myClient, "Client socket cannot be null"); + Objects.requireNonNull(server, "Server cannot be null"); + Objects.requireNonNull(onInitializationComplete, "callback cannot be null"); + info("ServerThread created"); + // get communication channels to single client + this.client = myClient; + this.server = server; // In the future we'll have a different way to reference the Server + this.clientId = this.threadId(); // An id associated with the thread instance, used as a temporary identifier + this.onInitializationComplete = onInitializationComplete; + + } + + public long getClientId() { + // Note: We return clientId instead of threadId as we'll change this identifier + // in the future + return this.clientId; + } + + /** + * One of the two ways to get this to exit the listen loop + */ + protected void disconnect() { + if (!isRunning) { + // prevent multiple triggers if this gets called consecutively + return; + } + info("Thread being disconnected by server"); + isRunning = false; + this.interrupt(); // breaks out of blocking read in the run() method + cleanup(); // good practice to ensure data is written out immediately + } + + /** + * Sends the message over the socket + * + * @param message + * @return true if no errors were encountered + */ + protected boolean sendToClient(String message) { + if (!isRunning) { + return false; + } + try { + out.writeObject(message); + out.flush(); + return true; + } catch (IOException e) { + info("Error sending message to client (most likely disconnected)"); + // comment this out to inspect the stack trace + // e.printStackTrace(); + cleanup(); + return false; + } + } + + @Override + public void run() { + info("Thread starting"); + try (ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(client.getInputStream());) { + this.out = out; + isRunning = true; + onInitializationComplete.accept(this); // Notify server that initialization is complete + String fromClient; + /** + * isRunning is a flag to let us manage the loop exit condition + * fromClient (in.readObject()) is a blocking method that waits until data is + * received + * - null would likely mean a disconnect so we use a "set and check" logic to + * alternatively exit the loop + */ + while (isRunning) { + try { + fromClient = (String) in.readObject(); // blocking method + if (fromClient == null) { + throw new IOException("Connection interrupted"); // Specific exception for a clean break + } else { + info(TextFX.colorize("Received from my client: " + fromClient, Color.CYAN)); + processPayload(fromClient); + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (Thread.currentThread().isInterrupted()) { + info("Thread interrupted during read (likely from the disconnect() method)"); + break; + } + info("IO exception while reading from client"); + e.printStackTrace(); + break; + } + } // close while loop + } catch (Exception e) { + // happens when client disconnects + info("General Exception"); + e.printStackTrace(); + info("My Client disconnected"); + } finally { + isRunning = false; + info("Exited thread loop. Cleaning up connection"); + cleanup(); + } + } + + private void processPayload(String incoming) { + if (!processCommand(incoming)) { + // if not command; send message to all clients via Server + server.handleMessage(this, incoming); + } + + } + + /** + * Attempts to see if the message is a command and process its action + * + * @param message + * @param sender + * @return true if it was a command, false otherwise + */ + private boolean processCommand(String message) { + boolean wasCommand = false; // control var to use as the return status + + // using "[cmd]" as a temporary trigger until we update how the data is passed + // over the socket + if (message.startsWith(Constants.COMMAND_TRIGGER)) { + // expected format will be csv for now to keep it simple + String[] commandData = message.split(","); + if (commandData.length >= 2) { + + // index 0 is the trigger word + // index 1 is the command + final String command = commandData[1].trim(); + System.out.println(TextFX.colorize("Checking command: " + command, Color.YELLOW)); + // index N are the data from the command + // Note: not all commands require data, some are simply actions/triggers to + // process like quit + switch (command) { + case "quit": + case "disconnect": + case "logout": + case "logoff": + server.handleDisconnect(this); + wasCommand = true; + break; + case "reverse": + // ignore the first two indexes (trigger, command) + String relevantText = String.join(" ", Arrays.copyOfRange(commandData, 2, commandData.length)); + server.handleReverseText(this, relevantText); + wasCommand = true; + break; + // added more cases/breaks as needed for other commands + default: + break; + } + } + + } + return wasCommand; + } + + private void cleanup() { + info("ServerThread cleanup() start"); + try { + // close server-side end of connection + client.close(); + info("Closed Server-side Socket"); + } catch (IOException e) { + info("Client already closed"); + } + info("ServerThread cleanup() end"); + } +} \ No newline at end of file diff --git a/M4/Part3HW/TextFX.java b/M4/Part3HW/TextFX.java new file mode 100644 index 0000000..5d85df3 --- /dev/null +++ b/M4/Part3HW/TextFX.java @@ -0,0 +1,72 @@ +package M4.Part3HW; + +/** + * Utility to attempt to provide colored text in the terminal. + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ +public abstract class TextFX { + + /** + * TextFX.Color list of available colors + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ + public enum Color { + BLACK("\033[0;30m"), + RED("\033[0;31m"), + GREEN("\033[0;32m"), + YELLOW("\033[0;33m"), + BLUE("\033[0;34m"), + PURPLE("\033[0;35m"), + CYAN("\033[0;36m"), + WHITE("\033[0;37m"); + + private final String code; + + Color(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + } + + public static final String RESET = "\033[0m"; + + /** + * Generates a String with the original message wrapped in the ASCII of the + * color and RESET + * + *

+ * Note: May not work for all terminals + *

+ *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ * + * @param text Input text to colorize + * @param color Enum of Color choice from TextFX.Color + * @return wrapped String + */ + public static String colorize(String text, Color color) { + StringBuilder builder = new StringBuilder(); + builder.append(color.getCode()); + builder.append(text); + builder.append(RESET); + return builder.toString(); + } + + public static void main(String[] args) { + // Example usage: + System.out.println(TextFX.colorize("Hello, world!", Color.RED)); + System.out.println(TextFX.colorize("This is some blue text.", Color.BLUE)); + System.out.println(TextFX.colorize("And this is green!", Color.GREEN)); + } +} \ No newline at end of file From 7d8ede937df2b2c192639db3c9bf262a81af723c Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:27:52 -0400 Subject: [PATCH 02/56] UCID and steps --- M4/Part3HW/Client.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index 574788a..3658776 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -98,6 +98,10 @@ private boolean isConnection(String text) { * @return true if the text was a command or triggered a command * @throws IOException */ + // 10/21/25 - UCID rk975 + // Steps taken to solve this: 1. add another elif statement + // 2. detect the user input for /flip command 3. create data array + // 4. join the array to a string 5. Send the command to the server private boolean processClientCommand(String text) throws IOException { boolean wasCommand = false; if (isConnection(text)) { From 61c637a31fe427b9e7a2485b153989b46b6d58fa Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:34:39 -0400 Subject: [PATCH 03/56] else if statement --- M4/Part3HW/Client.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index 3658776..a8f1ed8 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -126,6 +126,13 @@ private boolean processClientCommand(String text) throws IOException { sendToServer(String.join(",", commandData)); wasCommand = true; } + + else if ("/flip".equalsIgnoreCase(text.trim())) { + String[] commandData = { Constants.COMMAND_TRIGGER, "flip" }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } + return wasCommand; } From 48501b81541631acf8cc2b8b6f7421e1612e7631 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:39:31 -0400 Subject: [PATCH 04/56] Added ucid and date --- M4/Part3HW/Server.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index b66396f..d2973c2 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -131,7 +131,11 @@ protected synchronized void handleMessage(ServerThread sender, String text) { relay(sender, text); } // end handle actions - + // 10/21/25 UCID - rk975 + // Steps to solve: 1. Instantiated Random for pseudo-random generation. + // 2. Evaluated boolean to determine coin state. + // 3. Constructed formatted output string dynamically. + // 4. Invoked relay() to multicast message. public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 8c332c945999629196632918cfc1a40ebe4d85f3 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:45:47 -0400 Subject: [PATCH 05/56] one other method added --- M4/Part3HW/Server.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index d2973c2..555c615 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -136,6 +136,14 @@ protected synchronized void handleMessage(ServerThread sender, String text) { // 2. Evaluated boolean to determine coin state. // 3. Constructed formatted output string dynamically. // 4. Invoked relay() to multicast message. + + protected synchronized void handleFlipCommand(ServerThread sender) { + Random random = new Random(); + String result = random.nextBoolean() ? "Heads" : "Tails"; + String message = String.format("User[%s] flipped a coin and got %s", sender.getClientId(), result); + relay(null, message); // relay as server message + } + public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 1f91229d5fac6751c9df491eb3bcb4b4b8305165 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:50:42 -0400 Subject: [PATCH 06/56] date and ucid --- M4/Part3HW/ServerThread.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index 944908e..fb0a29e 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -201,6 +201,13 @@ private boolean processCommand(String message) { server.handleReverseText(this, relevantText); wasCommand = true; break; + + // date 10/21/25 - UCID - rk975 + // Steps taken. Matched "flip" command in switch. + // Invoked server’s handleFlipCommand() method. + // Passed current client thread context. + // Set command flag to true. + // added more cases/breaks as needed for other commands default: break; From 974146973b8249d9d8dadd1e0cce9b03c999418c Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 18:56:10 -0400 Subject: [PATCH 07/56] case flip added --- M4/Part3HW/ServerThread.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index fb0a29e..f32e0e6 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -207,7 +207,11 @@ private boolean processCommand(String message) { // Invoked server’s handleFlipCommand() method. // Passed current client thread context. // Set command flag to true. - + case "flip": + server.handleFlipCommand(this); + wasCommand = true; + break; + // added more cases/breaks as needed for other commands default: break; From 615f824f290971d08c4b346e52ccda629ec9e266 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:13:13 -0400 Subject: [PATCH 08/56] Fixed package errors --- M4/Part3HW/Client.java | 2 +- M4/Part3HW/ServerThread.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index a8f1ed8..e2040c3 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -10,7 +10,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import M4.Part3.TextFX.Color; +import M4.Part3HW.TextFX.Color; /** * Demoing bi-directional communication between client and server in a diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index f32e0e6..edc5930 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -8,7 +8,7 @@ import java.util.Objects; import java.util.function.Consumer; -import M4.Part3.TextFX.Color; +import M4.Part3HW.TextFX.Color; /** * A server-side representation of a single client From 4edb2c7a816acadc309c836535bb80481c6b1993 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:15:23 -0400 Subject: [PATCH 09/56] added random --- M4/Part3HW/Server.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index 555c615..dc27a80 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -4,6 +4,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ConcurrentHashMap; +import java.util.Random; public class Server { private int port = 3000; @@ -143,7 +144,7 @@ protected synchronized void handleFlipCommand(ServerThread sender) { String message = String.format("User[%s] flipped a coin and got %s", sender.getClientId(), result); relay(null, message); // relay as server message } - + public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 524a656b1a0d238e22f9fd3a58598eb9176bbc62 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:33:22 -0400 Subject: [PATCH 10/56] date and ucid --- M4/Part3HW/Server.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index dc27a80..5022eb4 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -144,7 +144,13 @@ protected synchronized void handleFlipCommand(ServerThread sender) { String message = String.format("User[%s] flipped a coin and got %s", sender.getClientId(), result); relay(null, message); // relay as server message } - + // 10/21/25 UCID - rk975 + // Added new private message logic for /pm command + // Steps to solve: + // 1. Extract target user id and message text. + // 2. Locate receiver from connectedClients. + // 3. Construct "PM from : ". + // 4. Send only to sender and receiver. public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 94ca482fb1155155e7b3608605d9a44dfe8ebc7b Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:43:23 -0400 Subject: [PATCH 11/56] Handle messages --- M4/Part3HW/Server.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index 5022eb4..de95ca7 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -151,6 +151,23 @@ protected synchronized void handleFlipCommand(ServerThread sender) { // 2. Locate receiver from connectedClients. // 3. Construct "PM from : ". // 4. Send only to sender and receiver. + + protected synchronized void handlePrivateMessage(ServerThread sender, long targetId, String messageText) { + ServerThread receiver = connectedClients.get(targetId); + + if (receiver == null) { + // Notify sender that the target user doesn't exist + sender.sendToClient("Server: User[" + targetId + "] not found or not connected."); + return; + } + + String formatted = String.format("PM from User[%s]: %s", sender.getClientId(), messageText); + + // Send to both sender and receiver only + sender.sendToClient("Server: " + formatted); + receiver.sendToClient("Server: " + formatted); + } + public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 9d40091f23a21e9de877b30bf988cc409c596e8f Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:46:17 -0400 Subject: [PATCH 12/56] add ucid and date --- M4/Part3HW/Client.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index e2040c3..14db0b0 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -133,6 +133,14 @@ else if ("/flip".equalsIgnoreCase(text.trim())) { wasCommand = true; } + // 10/21/25 - UCID rk975 + // Steps taken to solve this: 1. Added support for new /pm command. + // 2. Parse target id and message from user input. + // 3. Format message and send it to the server. + // 4. ServerThread and Server handle the rest. + + + return wasCommand; } From 41ceb2b42199f5734b707e95c867116cbba6bfbf Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:54:32 -0400 Subject: [PATCH 13/56] else if statement added --- M4/Part3HW/Client.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index 14db0b0..97fa201 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -139,7 +139,18 @@ else if ("/flip".equalsIgnoreCase(text.trim())) { // 3. Format message and send it to the server. // 4. ServerThread and Server handle the rest. - + else if (text.startsWith("/pm ")) { + String[] parts = text.trim().split(" ", 3); + if (parts.length >= 3) { + String targetId = parts[1].trim(); + String message = parts[2].trim(); + String[] commandData = { Constants.COMMAND_TRIGGER, "pm", targetId, message }; + sendToServer(String.join(",", commandData)); + } else { + System.out.println("Usage: /pm "); + } + wasCommand = true; + } return wasCommand; } From 98dd649c1f2edcf55158436252a7aa8cffa8748c Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 19:58:42 -0400 Subject: [PATCH 14/56] added steps ucid and date --- M4/Part3HW/ServerThread.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index edc5930..3eaf2de 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -212,6 +212,13 @@ private boolean processCommand(String message) { wasCommand = true; break; + // date 10/21/25 - UCID - rk975 + // Added /pm command handling logic. + // Steps: + // 1. Parse targetId and message from commandData. + // 2. Call server.handlePrivateMessage(sender, targetId, messageText). + + // added more cases/breaks as needed for other commands default: break; From 20cbb1f5783e52c24c758c418a326c3f45dfeb0a Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:12:46 -0400 Subject: [PATCH 15/56] another case statement --- M4/Part3HW/ServerThread.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index 3eaf2de..ed55d67 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -218,7 +218,21 @@ private boolean processCommand(String message) { // 1. Parse targetId and message from commandData. // 2. Call server.handlePrivateMessage(sender, targetId, messageText). - + case "pm": + if (commandData.length >= 4) { + try { + long targetId = Long.parseLong(commandData[2].trim()); + String messageText = String.join(" ", Arrays.copyOfRange(commandData, 3, commandData.length)); + server.handlePrivateMessage(this, targetId, messageText); + } catch (NumberFormatException e) { + sendToClient("Server: Invalid target user ID."); + } + } else { + sendToClient("Server: Usage - /pm "); + } + wasCommand = true; + break; + // added more cases/breaks as needed for other commands default: break; From f67c21d588d185cd293eeb225f31579c2b4a7c45 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:30:08 -0400 Subject: [PATCH 16/56] date, ucid and steps --- M4/Part3HW/Client.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index 97fa201..3305dc0 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -152,6 +152,13 @@ else if (text.startsWith("/pm ")) { wasCommand = true; } + // 10/21/25 - UCID rk975 + // Added support for /shuffle + // Steps: + // 1) Detect "/shuffle" + // 2) Extract message text + // 3) Send as: [cmd],shuffle, + return wasCommand; } From eda60b2e5de7a2125825e00787dc1c424ad4f4df Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:41:41 -0400 Subject: [PATCH 17/56] Shuffle is else statement --- M4/Part3HW/Client.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/M4/Part3HW/Client.java b/M4/Part3HW/Client.java index 3305dc0..da925cc 100644 --- a/M4/Part3HW/Client.java +++ b/M4/Part3HW/Client.java @@ -159,6 +159,17 @@ else if (text.startsWith("/pm ")) { // 2) Extract message text // 3) Send as: [cmd],shuffle, + else if (text.startsWith("/shuffle")) { + String message = text.replaceFirst("(?i)^/shuffle\\s*", "").trim(); + if (message.isEmpty()) { + System.out.println("Usage: /shuffle "); + } else { + String[] commandData = { Constants.COMMAND_TRIGGER, "shuffle", message }; + sendToServer(String.join(",", commandData)); + } + wasCommand = true; + } + return wasCommand; } From d26314afe461a4ac414a290c7a48e2dcf515865f Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:44:13 -0400 Subject: [PATCH 18/56] date and ucid and steps --- M4/Part3HW/ServerThread.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index ed55d67..ca10b9a 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -232,7 +232,11 @@ private boolean processCommand(String message) { } wasCommand = true; break; - + // date 10/21/25 - UCID - rk975 + // Added /shuffle command handling logic. + // Steps: + // 1. Extract text portion after command. + // 2. Call server.handleShuffleMessage(sender, text) // added more cases/breaks as needed for other commands default: break; From 2f71ea1e48ce61d695a8c64edb2e0f924864909d Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:51:19 -0400 Subject: [PATCH 19/56] case statement for shuffle --- M4/Part3HW/ServerThread.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/M4/Part3HW/ServerThread.java b/M4/Part3HW/ServerThread.java index ca10b9a..68a9cce 100644 --- a/M4/Part3HW/ServerThread.java +++ b/M4/Part3HW/ServerThread.java @@ -238,6 +238,17 @@ private boolean processCommand(String message) { // 1. Extract text portion after command. // 2. Call server.handleShuffleMessage(sender, text) // added more cases/breaks as needed for other commands + + case "shuffle": + if (commandData.length >= 3) { + String shuffleText = String.join(" ", Arrays.copyOfRange(commandData, 2, commandData.length)); + server.handleShuffleMessage(this, shuffleText); + } else { + sendToClient("Server: Usage - /shuffle "); + } + wasCommand = true; + break; + default: break; } From 33cd9887939cda123846a4e03eaef94405bbefd4 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:52:50 -0400 Subject: [PATCH 20/56] steps, ucid, and date --- M4/Part3HW/Server.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index de95ca7..e3282c4 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -167,7 +167,16 @@ protected synchronized void handlePrivateMessage(ServerThread sender, long targe sender.sendToClient("Server: " + formatted); receiver.sendToClient("Server: " + formatted); } - + + // 10/21/25 UCID - rk975 + // Added handleShuffleMessage() for /shuffle command + // Steps: + // 1. Convert message string into a List. + // 2. Randomly shuffle characters using Collections.shuffle(). + // 3. Join shuffled chars into a string. + // 4. Broadcast "Shuffled from : " from the Server. + + public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 09f7918b10bf4ed28783ff1570ce62b7032f291e Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Tue, 21 Oct 2025 20:59:34 -0400 Subject: [PATCH 21/56] method for shuffle --- M4/Part3HW/Server.java | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/M4/Part3HW/Server.java b/M4/Part3HW/Server.java index e3282c4..853f285 100644 --- a/M4/Part3HW/Server.java +++ b/M4/Part3HW/Server.java @@ -176,7 +176,28 @@ protected synchronized void handlePrivateMessage(ServerThread sender, long targe // 3. Join shuffled chars into a string. // 4. Broadcast "Shuffled from : " from the Server. + protected synchronized void handleShuffleMessage(ServerThread sender, String text) { + if (text == null || text.trim().isEmpty()) { + sender.sendToClient("Server: Usage - /shuffle "); + return; + } + + // Convert text to a list of characters for shuffling + List chars = new ArrayList<>(); + for (char c : text.toCharArray()) { + chars.add(c); + } + Collections.shuffle(chars); // random shuffle + StringBuilder shuffled = new StringBuilder(); + for (char c : chars) { + shuffled.append(c); + } + + String shuffledMsg = String.format("Shuffled from User[%s]: %s", sender.getClientId(), shuffled.toString()); + relay(null, shuffledMsg); // broadcast to all clients as server message + } + public static void main(String[] args) { System.out.println("Server Starting"); Server server = new Server(); From 2feabed6301901800330405f8d03b2d41232d22d Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 16:19:37 -0500 Subject: [PATCH 22/56] all m5/m4 files --- M5/Part4/Client.java | 275 +++++++++++++++ M5/Part4/Command.java | 30 ++ M5/Part4/Constants.java | 6 + M5/Part4/CustomIT114Exception.java | 11 + M5/Part4/DuplicateRoomException.java | 12 + M5/Part4/Room.java | 223 ++++++++++++ M5/Part4/RoomNotFoundException.java | 13 + M5/Part4/Server.java | 194 +++++++++++ M5/Part4/ServerThread.java | 266 ++++++++++++++ M5/Part4/TextFX.java | 72 ++++ M5/Part5/BaseServerThread.java | 219 ++++++++++++ M5/Part5/Client.java | 495 +++++++++++++++++++++++++++ M5/Part5/Command.java | 33 ++ M5/Part5/ConnectionPayload.java | 28 ++ M5/Part5/Constants.java | 8 + M5/Part5/CustomIT114Exception.java | 12 + M5/Part5/DuplicateRoomException.java | 13 + M5/Part5/Payload.java | 57 +++ M5/Part5/PayloadType.java | 15 + M5/Part5/Room.java | 264 ++++++++++++++ M5/Part5/RoomAction.java | 6 + M5/Part5/RoomNotFoundException.java | 14 + M5/Part5/Server.java | 200 +++++++++++ M5/Part5/ServerThread.java | 167 +++++++++ M5/Part5/TextFX.java | 72 ++++ M5/Part5/User.java | 43 +++ 26 files changed, 2748 insertions(+) create mode 100644 M5/Part4/Client.java create mode 100644 M5/Part4/Command.java create mode 100644 M5/Part4/Constants.java create mode 100644 M5/Part4/CustomIT114Exception.java create mode 100644 M5/Part4/DuplicateRoomException.java create mode 100644 M5/Part4/Room.java create mode 100644 M5/Part4/RoomNotFoundException.java create mode 100644 M5/Part4/Server.java create mode 100644 M5/Part4/ServerThread.java create mode 100644 M5/Part4/TextFX.java create mode 100644 M5/Part5/BaseServerThread.java create mode 100644 M5/Part5/Client.java create mode 100644 M5/Part5/Command.java create mode 100644 M5/Part5/ConnectionPayload.java create mode 100644 M5/Part5/Constants.java create mode 100644 M5/Part5/CustomIT114Exception.java create mode 100644 M5/Part5/DuplicateRoomException.java create mode 100644 M5/Part5/Payload.java create mode 100644 M5/Part5/PayloadType.java create mode 100644 M5/Part5/Room.java create mode 100644 M5/Part5/RoomAction.java create mode 100644 M5/Part5/RoomNotFoundException.java create mode 100644 M5/Part5/Server.java create mode 100644 M5/Part5/ServerThread.java create mode 100644 M5/Part5/TextFX.java create mode 100644 M5/Part5/User.java diff --git a/M5/Part4/Client.java b/M5/Part4/Client.java new file mode 100644 index 0000000..d435623 --- /dev/null +++ b/M5/Part4/Client.java @@ -0,0 +1,275 @@ + +package M5.Part4; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import M5.Part4.TextFX.Color; + +/** + * Demoing bi-directional communication between client and server in a + * multi-client scenario + */ +public enum Client { + INSTANCE; + + private Socket server = null; + private ObjectOutputStream out = null; + private ObjectInputStream in = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); + private volatile boolean isRunning = true; // volatile for thread-safe visibility + + // needs to be private now that the enum logic is handling this + private Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine if the server had a problem + // and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + } + + /** + * Takes an IP address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new ObjectOutputStream(server.getOutputStream()); + // channel to listen to server + in = new ObjectInputStream(server.getInputStream()); + System.out.println("Client connected"); + // Use CompletableFuture to run listenToServer() in a separate thread + CompletableFuture.runAsync(this::listenToServer); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an IP address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return true if the text is a valid connection command + */ + private boolean isConnection(String text) { + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if the text was a command or triggered a command + * @throws IOException + */ + private boolean processClientCommand(String text) throws IOException { + boolean wasCommand = false; + if (isConnection(text)) { + // replaces multiple spaces with a single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + wasCommand = true; + } else if ("/quit".equalsIgnoreCase(text)) { + close(); + wasCommand = true; + } else if ("/disconnect".equalsIgnoreCase(text)) { + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, Command.DISCONNECT.command }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } else if (text.startsWith("/reverse")) { + text = text.replace("/reverse", "").trim(); + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, Command.REVERSE.command, text }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } else if (text.startsWith("/createroom")) { + text = text.replace("/createroom", "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + return true; + } + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, Command.CREATE_ROOM.command, text }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } else if (text.startsWith("/joinroom")) { + text = text.replace("/joinroom", "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + return true; + } + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, Command.JOIN_ROOM.command, text }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } else if (text.startsWith("/leave")) { + // Note: Accounts for /leave and /leaveroom variants (or anything beginning with + // /leave) + // index 0 = trigger, index 1 = command, index N = command data + String[] commandData = { Constants.COMMAND_TRIGGER, Command.LEAVE_ROOM.command }; + sendToServer(String.join(",", commandData)); + wasCommand = true; + } + return wasCommand; + } + + public void start() throws IOException { + System.out.println("Client starting"); + + // Use CompletableFuture to run listenToInput() in a separate thread + CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); + + // Wait for inputFuture to complete to ensure proper termination + inputFuture.join(); + } + + /** + * Listens for messages from the server + */ + private void listenToServer() { + try { + while (isRunning && isConnected()) { + String fromServer = (String) in.readObject(); // blocking read + if (fromServer != null) { + System.out.println(TextFX.colorize(fromServer, Color.BLUE)); + } else { + System.out.println("Server disconnected"); + break; + } + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (isRunning) { + System.out.println("Connection dropped"); + e.printStackTrace(); + } + } finally { + closeServerConnection(); + } + System.out.println("listenToServer thread stopped"); + } + + /** + * Listens for keyboard input from the user + */ + private void listenToInput() { + try (Scanner si = new Scanner(System.in)) { + System.out.println("Waiting for input"); // moved here to avoid console spam + while (isRunning) { // Run until isRunning is false + String userInput = si.nextLine(); + if (!processClientCommand(userInput)) { + sendToServer(userInput); + } + } + } catch (IOException ioException) { + System.out.println("Error in listentToInput()"); + ioException.printStackTrace(); + } + System.out.println("listenToInput thread stopped"); + } + + private void sendToServer(String message) throws IOException { + if (isConnected()) { + out.writeObject(message); + out.flush(); // good practice to ensure data is written out immediately + } else { + System.out.println( + "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); + } + } + + /** + * Closes the client connection and associated resources + */ + private void close() { + isRunning = false; + closeServerConnection(); + System.out.println("Client terminated"); + // System.exit(0); // Terminate the application + } + + /** + * Closes the server connection and associated resources + */ + private void closeServerConnection() { + try { + if (out != null) { + System.out.println("Closing output stream"); + out.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (in != null) { + System.out.println("Closing input stream"); + in.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (server != null) { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + Client client = Client.INSTANCE; + try { + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} diff --git a/M5/Part4/Command.java b/M5/Part4/Command.java new file mode 100644 index 0000000..1cd3969 --- /dev/null +++ b/M5/Part4/Command.java @@ -0,0 +1,30 @@ +package M5.Part4; + +import java.util.HashMap; + +public enum Command { + QUIT("quit"), + DISCONNECT("disconnect"), + LOGOUT("logout"), + LOGOFF("logoff"), + REVERSE("reverse"), + CREATE_ROOM("createroom"), + LEAVE_ROOM("leaveroom"), + JOIN_ROOM("joinroom"); + + private static final HashMap BY_COMMAND = new HashMap<>(); + static { + for (Command e : values()) { + BY_COMMAND.put(e.command, e); + } + } + public final String command; + + private Command(String command) { + this.command = command; + } + + public static Command stringToCommand(String command) { + return BY_COMMAND.get(command); + } +} \ No newline at end of file diff --git a/M5/Part4/Constants.java b/M5/Part4/Constants.java new file mode 100644 index 0000000..d33bbbd --- /dev/null +++ b/M5/Part4/Constants.java @@ -0,0 +1,6 @@ +package M5.Part4; + +public abstract class Constants { + final public static String COMMAND_TRIGGER = "[cmd]"; + final public static String SINGLE_SPACE = " "; +} diff --git a/M5/Part4/CustomIT114Exception.java b/M5/Part4/CustomIT114Exception.java new file mode 100644 index 0000000..ff7e38d --- /dev/null +++ b/M5/Part4/CustomIT114Exception.java @@ -0,0 +1,11 @@ +package M5.Part4; + +public abstract class CustomIT114Exception extends Exception { + public CustomIT114Exception(String message) { + super(message); + } + + public CustomIT114Exception(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/M5/Part4/DuplicateRoomException.java b/M5/Part4/DuplicateRoomException.java new file mode 100644 index 0000000..986eff2 --- /dev/null +++ b/M5/Part4/DuplicateRoomException.java @@ -0,0 +1,12 @@ +package M5.Part4; + +public class DuplicateRoomException extends CustomIT114Exception { + public DuplicateRoomException(String message) { + super(message); + } + + public DuplicateRoomException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/M5/Part4/Room.java b/M5/Part4/Room.java new file mode 100644 index 0000000..f277c11 --- /dev/null +++ b/M5/Part4/Room.java @@ -0,0 +1,223 @@ +package M5.Part4; + +import java.util.concurrent.ConcurrentHashMap; + +import M5.Part4.TextFX.Color; + +public class Room { + private final String name;// unique name of the Room + private volatile boolean isRunning = false; + private final ConcurrentHashMap clientsInRoom = new ConcurrentHashMap(); + + public final static String LOBBY = "lobby"; + + private void info(String message) { + System.out.println(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); + } + + public Room(String name) { + this.name = name; + isRunning = true; + info("Created"); + } + + public String getName() { + return this.name; + } + + protected synchronized void addClient(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + if (clientsInRoom.containsKey(client.getClientId())) { + info("Attempting to add a client that already exists in the room"); + return; + } + clientsInRoom.put(client.getClientId(), client); + client.setCurrentRoom(this); + // notify clients of someone joining + // relay(null, String.format("User[%s] joined the room", client.getClientId())); + joinStatusRelay(client, true); + + } + + protected synchronized void removeClient(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + if (!clientsInRoom.containsKey(client.getClientId())) { + info("Attempting to remove a client that doesn't exist in the room"); + return; + } + ServerThread removedClient = clientsInRoom.get(client.getClientId()); + if (removedClient != null) { + // notify clients of someone joining + joinStatusRelay(removedClient, false); + clientsInRoom.remove(client.getClientId()); + autoCleanup(); + } + } + + private void joinStatusRelay(ServerThread client, boolean didJoin) { + clientsInRoom.values().removeIf(serverThread -> { + String formattedMessage = String.format("Room[%s] %s %s the room", + getName(), + client.getClientId() == serverThread.getClientId() ? "You" + : String.format("User[%s]", client.getClientId()), + didJoin ? "joined" : "left"); + boolean failedToSend = !serverThread.sendToClient(formattedMessage); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected client[%s] from list", serverThread.getClientId())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + /** + * Sends a basic String message from the sender to all connectedClients + * Internally calls processCommand and evaluates as necessary. + * Note: Clients that fail to receive a message get removed from + * connectedClients. + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + protected synchronized void relay(ServerThread sender, String message) { + if (!isRunning) { // block action if Room isn't running + return; + } + + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? String.format("Room[%s]", getName()) + : String.format("User[%s]", sender.getClientId()); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't be changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + + // loop over clients and send out the message; remove client if message failed + // to be sent + // Note: this uses a lambda expression for each item in the values() collection, + // it's one way we can safely remove items during iteration + info(String.format("sending message to %s recipients: %s", clientsInRoom.size(), formattedMessage)); + + clientsInRoom.values().removeIf(serverThread -> { + boolean failedToSend = !serverThread.sendToClient(formattedMessage); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected client[%s] from list", serverThread.getClientId())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + /** + * Takes a ServerThread and removes them from the Server + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param client + */ + private synchronized void disconnect(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + ServerThread disconnectingServerThread = clientsInRoom.remove(client.getClientId()); + if (disconnectingServerThread != null) { + disconnectingServerThread.disconnect(); + relay(null, "User[" + disconnectingServerThread.getClientId() + "] disconnected"); + } + autoCleanup(); + } + + protected synchronized void disconnectAll() { + info("Disconnect All triggered"); + if (!isRunning) { + return; + } + clientsInRoom.values().removeIf(client -> { + disconnect(client); + return true; + }); + info("Disconnect All finished"); + } + + /** + * Attempts to close the room to free up resources if it's empty + */ + private void autoCleanup() { + if (!Room.LOBBY.equalsIgnoreCase(name) && clientsInRoom.isEmpty()) { + close(); + } + } + + public void close() { + // attempt to gracefully close and migrate clients + if (!clientsInRoom.isEmpty()) { + relay(null, "Room is shutting down, migrating to lobby"); + info(String.format("migrating %s clients", clientsInRoom.size())); + clientsInRoom.values().removeIf(client -> { + try { + Server.INSTANCE.joinRoom(Room.LOBBY, client); + } catch (RoomNotFoundException e) { + e.printStackTrace(); + // TODO, fill in, this shouldn't happen though + } + return true; + }); + } + Server.INSTANCE.removeRoom(this); + isRunning = false; + clientsInRoom.clear(); + info(String.format("closed")); + } + + // start handle methods + public void handleCreateRoom(ServerThread sender, String roomName) { + try { + Server.INSTANCE.createRoom(roomName); + Server.INSTANCE.joinRoom(roomName, sender); + } catch (RoomNotFoundException e) { + info("Room wasn't found (this shouldn't happen)"); + e.printStackTrace(); + } catch (DuplicateRoomException e) { + sender.sendToClient(String.format("Room %s already exists", roomName)); + } + } + + public void handleJoinRoom(ServerThread sender, String roomName) { + try { + Server.INSTANCE.joinRoom(roomName, sender); + } catch (RoomNotFoundException e) { + sender.sendToClient(String.format("Room %s doesn't exist", roomName)); + } + } + + /** + * Expose access to the disconnect action + * + * @param serverThread + */ + protected synchronized void handleDisconnect(ServerThread sender) { + disconnect(sender); + } + + protected synchronized void handleReverseText(ServerThread sender, String text) { + StringBuilder sb = new StringBuilder(text); + sb.reverse(); + String rev = sb.toString(); + relay(sender, rev); + } + + protected synchronized void handleMessage(ServerThread sender, String text) { + relay(sender, text); + } + // end handle methods +} diff --git a/M5/Part4/RoomNotFoundException.java b/M5/Part4/RoomNotFoundException.java new file mode 100644 index 0000000..c95434c --- /dev/null +++ b/M5/Part4/RoomNotFoundException.java @@ -0,0 +1,13 @@ +package M5.Part4; + +public class RoomNotFoundException extends CustomIT114Exception { + + public RoomNotFoundException(String message) { + super(message); + } + + public RoomNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/M5/Part4/Server.java b/M5/Part4/Server.java new file mode 100644 index 0000000..76f9eb3 --- /dev/null +++ b/M5/Part4/Server.java @@ -0,0 +1,194 @@ +package M5.Part4; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ConcurrentHashMap; + +import M5.Part4.TextFX.Color; + +public enum Server { + INSTANCE; // Singleton instance + + private int port = 3000; + // connected clients + // Use ConcurrentHashMap for thread-safe client management + // The key is the unique Room name and the Room is the instance + private final ConcurrentHashMap rooms = new ConcurrentHashMap<>(); + private boolean isRunning = true; + + private void info(String message) { + System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); + } + + private Server() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + info("JVM is shutting down. Perform cleanup tasks."); + shutdown(); + })); + } + + /** + * Gracefully disconnect clients + */ + private void shutdown() { + try { + // chose removeIf over forEach to avoid potential + // ConcurrentModificationException + // since empty rooms tell the server to remove themselves + rooms.values().removeIf(room -> { + room.disconnectAll(); + return true; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void start(int port) { + this.port = port; + // server listening + info("Listening on port " + this.port); + // Simplified client connection loop + try (ServerSocket serverSocket = new ServerSocket(port)) { + createRoom(Room.LOBBY);// create the first room (lobby) + while (isRunning) { + info("Waiting for next client"); + Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection + info("Client connected"); + // wrap socket in a ServerThread, pass a callback to notify the Server when + // they're initialized + ServerThread serverThread = new ServerThread(incomingClient, this::onServerThreadInitialized); + // start the thread (typically an external entity manages the lifecycle and we + // don't have the thread start itself) + serverThread.start(); + // Note: We don't yet add the ServerThread reference to our connectedClients map + } + } catch (DuplicateRoomException e) { + System.err.println(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); + } catch (IOException e) { + System.err.println(TextFX.colorize("Error accepting connection", Color.RED)); + e.printStackTrace(); + } finally { + info("Closing server socket"); + } + } + + /** + * Callback passed to ServerThread to inform Server they're ready to receive + * data + * + * @param serverThread + */ + private void onServerThreadInitialized(ServerThread serverThread) { + // add initialized client to the lobby + info(String.format("*User[%s] initialized*", serverThread.getClientId())); + try { + joinRoom(Room.LOBBY, serverThread); + info(String.format("*User[%s] added to Lobby*", serverThread.getClientId())); + } catch (RoomNotFoundException e) { + info(String.format("*Error adding User[%s] to Lobby*", serverThread.getClientId())); + e.printStackTrace(); + } + } + + /** + * Attempts to create a new Room and add it to the tracked rooms collection + * + * @param name Unique name of the room + * @return true if it was created and false if it wasn't + * @throws DuplicateRoomException + */ + protected void createRoom(String name) throws DuplicateRoomException { + final String nameCheck = name.toLowerCase(); + if (rooms.containsKey(nameCheck)) { + throw new DuplicateRoomException(String.format("Room %s already exists", name)); + } + Room room = new Room(name); + rooms.put(nameCheck, room); + info(String.format("Created new Room %s", name)); + } + + /** + * Attempts to move a client (ServerThread) between rooms + * + * @param name the target room to join + * @param client the client moving + * @throws RoomNotFoundException + * + */ + protected void joinRoom(String name, ServerThread client) throws RoomNotFoundException { + final String nameCheck = name.toLowerCase(); + if (!rooms.containsKey(nameCheck)) { + throw new RoomNotFoundException(String.format("Room %s wasn't found", name)); + } + Room currentRoom = client.getCurrentRoom(); + if (currentRoom != null) { + info("Removing client from previous Room " + currentRoom.getName()); + currentRoom.removeClient(client); + } + Room next = rooms.get(nameCheck); + next.addClient(client); + } + + protected void removeRoom(Room room) { + rooms.remove(room.getName().toLowerCase()); + info(String.format("Removed room %s", room.getName())); + } + + /** + * + *

+ * Note: Not a common use-case; just updated for example sake. + *

+ * Relays the message from the sender to all rooms + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + private synchronized void relayToAllRooms(ServerThread sender, String message) { + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? "Server" : String.format("User[%s]", sender.getClientId()); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + // end temp identifier + + // loop over Rooms and send out the message + // Note: this uses a lambda expression for each item in the values() collection + + rooms.values().forEach(room -> { + room.relay(sender, formattedMessage); + }); + } + + /** + * Used to send a message to all Rooms. + * This is just an example and we likely won't be using this + * + * @param sender + * @param message + */ + public synchronized void broadcastMessageToAllRooms(ServerThread sender, String message) { + relayToAllRooms(sender, message); + } + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = Server.INSTANCE; + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } + +} diff --git a/M5/Part4/ServerThread.java b/M5/Part4/ServerThread.java new file mode 100644 index 0000000..1e30541 --- /dev/null +++ b/M5/Part4/ServerThread.java @@ -0,0 +1,266 @@ +package M5.Part4; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.util.Arrays; +import java.util.Objects; +import java.util.function.Consumer; +import M5.Part4.TextFX.Color; + +/** + * A server-side representation of a single client + */ +public class ServerThread extends Thread { + private Socket client; // communication directly to "my" client + private boolean isRunning = false; // control variable to stop this thread + private ObjectOutputStream out; // exposed here for send() + + private long clientId; + private Consumer onInitializationComplete; // callback to inform when this object is ready + private Room currentRoom; + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + private void info(String message) { + System.out.println(String.format("Thread[%s]: %s", this.getClientId(), message)); + } + + /** + * Returns the status of this ServerThread + * + * @return + */ + public boolean isRunning() { + return isRunning; + } + + /** + * Wraps the Socket connection and takes a Server reference and a callback + * + * @param myClient + * @param server + * @param onInitializationComplete method to inform listener that this object is + * ready + */ + protected ServerThread(Socket myClient, Consumer onInitializationComplete) { + Objects.requireNonNull(myClient, "Client socket cannot be null"); + Objects.requireNonNull(onInitializationComplete, "callback cannot be null"); + info("ServerThread created"); + // get communication channels to single client + this.client = myClient; + this.clientId = this.threadId(); // An id associated with the thread instance, used as a temporary identifier + this.onInitializationComplete = onInitializationComplete; + + } + + public long getClientId() { + // Note: We return clientId instead of threadId as we'll change this identifier + // in the future + return this.clientId; + } + + /** + * Returns the current Room associated with this ServerThread + * + * @return + */ + protected Room getCurrentRoom() { + return this.currentRoom; + } + + /** + * Allows the setting of a non-null Room reference to this ServerThread + * + * @param room + */ + protected void setCurrentRoom(Room room) { + if (room == null) { + throw new NullPointerException("Room argument can't be null"); + } + if (room == currentRoom) { + System.out.println( + String.format("ServerThread set to the same room [%s], was this intentional?", room.getName())); + } + currentRoom = room; + } + + /** + * One of the two ways to get this to exit the listen loop + */ + protected void disconnect() { + if (!isRunning) { + // prevent multiple triggers if this gets called consecutively + return; + } + info("Thread being disconnected by server"); + isRunning = false; + this.interrupt(); // breaks out of blocking read in the run() method + cleanup(); // good practice to ensure data is written out immediately + } + + /** + * Sends the message over the socket + * + * @param message + * @return true if no errors were encountered + */ + protected boolean sendToClient(String message) { + if (!isRunning) { + return false; + } + try { + out.writeObject(message); + out.flush(); + return true; + } catch (IOException e) { + info("Error sending message to client (most likely disconnected)"); + // comment this out to inspect the stack trace + // e.printStackTrace(); + cleanup(); + return false; + } + } + + @Override + public void run() { + info("Thread starting"); + try (ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(client.getInputStream());) { + this.out = out; + isRunning = true; + onInitializationComplete.accept(this); // Notify server that initialization is complete + String fromClient; + /** + * isRunning is a flag to let us manage the loop exit condition + * fromClient (in.readObject()) is a blocking method that waits until data is + * received + * - null would likely mean a disconnect so we use a "set and check" logic to + * alternatively exit the loop + */ + while (isRunning) { + try { + fromClient = (String) in.readObject(); // blocking method + if (fromClient == null) { + throw new IOException("Connection interrupted"); // Specific exception for a clean break + } else { + info(TextFX.colorize("Received from my client: " + fromClient, Color.CYAN)); + processPayload(fromClient); + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (Thread.currentThread().isInterrupted()) { + info("Thread interrupted during read (likely from the disconnect() method)"); + break; + } + info("IO exception while reading from client"); + e.printStackTrace(); + break; + } + } // close while loop + } catch (Exception e) { + // happens when client disconnects + info("General Exception"); + e.printStackTrace(); + info("My Client disconnected"); + } finally { + isRunning = false; + info("Exited thread loop. Cleaning up connection"); + cleanup(); + } + } + + private void processPayload(String incoming) { + Objects.requireNonNull(currentRoom, "Can't process a payload when the current room is null"); + if (!processCommand(incoming)) { + // if not command; send message to all clients via Server + currentRoom.handleMessage(this, incoming); + } + } + + /** + * Attempts to see if the message is a command and process its action + * + * @param message + * @param sender + * @return true if it was a command, false otherwise + */ + private boolean processCommand(String message) { + Objects.requireNonNull(currentRoom, "Can't process a command when the current room is null"); + + boolean wasCommand = false; // control var to use as the return status + + // using "[cmd]" as a temporary trigger until we update how the data is passed + // over the socket + if (message.startsWith(Constants.COMMAND_TRIGGER)) { + // expected format will be csv for now to keep it simple + String[] commandData = message.split(","); + if (commandData.length >= 2) { + + // index 0 is the trigger word + // index 1 is the command + + // part 4 added the Command enum + final Command command = Command.stringToCommand(commandData[1].trim()); + + System.out.println(TextFX.colorize("Checking command: " + command, Color.YELLOW)); + // index N are the data from the command + // Note: not all commands require data, some are simply actions/triggers to + // process like quit + switch (command) { + case Command.QUIT: + case Command.DISCONNECT: + case Command.LOGOUT: + case Command.LOGOFF: + currentRoom.handleDisconnect(this); + wasCommand = true; + break; + case Command.REVERSE: + // ignore the first two indexes (trigger, command) + String relevantText = String.join(" ", Arrays.copyOfRange(commandData, 2, commandData.length)); + currentRoom.handleReverseText(this, relevantText); + wasCommand = true; + break; + case Command.CREATE_ROOM: + currentRoom.handleCreateRoom(this, commandData[2]); + wasCommand = true; + break; + case Command.JOIN_ROOM: + currentRoom.handleJoinRoom(this, commandData[2]); + wasCommand = true; + break; + case Command.LEAVE_ROOM: + // leaving simply joins the lobby since a ServerThread must always exist in a + // Room + currentRoom.handleJoinRoom(this, Room.LOBBY); + wasCommand = true; + break; + // added more cases/breaks as needed for other commands + default: + break; + } + } + + } + return wasCommand; + } + + private void cleanup() { + info("ServerThread cleanup() start"); + try { + // close server-side end of connection + client.close(); + info("Closed Server-side Socket"); + } catch (IOException e) { + info("Client already closed"); + } + info("ServerThread cleanup() end"); + } +} diff --git a/M5/Part4/TextFX.java b/M5/Part4/TextFX.java new file mode 100644 index 0000000..4782701 --- /dev/null +++ b/M5/Part4/TextFX.java @@ -0,0 +1,72 @@ +package M5.Part4; + +/** + * Utility to attempt to provide colored text in the terminal. + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ +public abstract class TextFX { + + /** + * TextFX.Color list of available colors + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ + public enum Color { + BLACK("\033[0;30m"), + RED("\033[0;31m"), + GREEN("\033[0;32m"), + YELLOW("\033[0;33m"), + BLUE("\033[0;34m"), + PURPLE("\033[0;35m"), + CYAN("\033[0;36m"), + WHITE("\033[0;37m"); + + private final String code; + + Color(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + } + + public static final String RESET = "\033[0m"; + + /** + * Generates a String with the original message wrapped in the ASCII of the + * color and RESET + * + *

+ * Note: May not work for all terminals + *

+ *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ * + * @param text Input text to colorize + * @param color Enum of Color choice from TextFX.Color + * @return wrapped String + */ + public static String colorize(String text, Color color) { + StringBuilder builder = new StringBuilder(); + builder.append(color.getCode()); + builder.append(text); + builder.append(RESET); + return builder.toString(); + } + + public static void main(String[] args) { + // Example usage: + System.out.println(TextFX.colorize("Hello, world!", Color.RED)); + System.out.println(TextFX.colorize("This is some blue text.", Color.BLUE)); + System.out.println(TextFX.colorize("And this is green!", Color.GREEN)); + } +} diff --git a/M5/Part5/BaseServerThread.java b/M5/Part5/BaseServerThread.java new file mode 100644 index 0000000..2f2c08c --- /dev/null +++ b/M5/Part5/BaseServerThread.java @@ -0,0 +1,219 @@ +package M5.Part5; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; + +/** + * Base class the handles the underlying connection between Client and + * Server-side + */ +public abstract class BaseServerThread extends Thread { + + protected boolean isRunning = false; // control variable to stop this thread + protected ObjectOutputStream out; // exposed here for send() + protected Socket client; // communication directly to "my" client + private User user = new User(); + protected Room currentRoom; + + /** + * Returns the current Room associated with this ServerThread + * + * @return + */ + protected Room getCurrentRoom() { + return this.currentRoom; + } + + /** + * Allows the setting of a non-null Room reference to this ServerThread + * + * @param room + */ + protected void setCurrentRoom(Room room) { + if (room == null) { + throw new NullPointerException("Room argument can't be null"); + } + if (room == currentRoom) { + System.out.println( + String.format("ServerThread set to the same room [%s], was this intentional?", room.getName())); + } + currentRoom = room; + } + + /** + * Returns the status of this ServerThread + * + * @return + */ + public boolean isRunning() { + return isRunning; + } + + public void setClientId(long clientId) { + this.user.setClientId(clientId); + } + + public long getClientId() { + // Note: We return clientId instead of threadId as we'll change this identifier + // in the future + return this.user.getClientId(); + } + + /** + * Sets the client name and triggers onInitialized() + * + * @param clientName + */ + protected void setClientName(String clientName) { + this.user.setClientName(clientName); + onInitialized(); + } + + public String getClientName() { + return this.user.getClientName(); + } + + public String getDisplayName() { + return this.user.getDisplayName(); + } + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + protected abstract void info(String message); + + /** + * Triggered when object is fully initialized + */ + protected abstract void onInitialized(); + + /** + * Receives a Payload and passes data to proper handler + * + * @param payload + */ + protected abstract void processPayload(Payload payload); + + /** + * Sends the payload over the socket + * + * @param payload + * @return true if no errors were encountered + */ + protected boolean sendToClient(Payload payload) { + if (!isRunning) { + return true; + } + try { + info("Sending to client: " + payload); + out.writeObject(payload); + out.flush(); + return true; + } catch (IOException e) { + info("Error sending message to client (most likely disconnected)"); + // comment this out to inspect the stack trace + // e.printStackTrace(); + cleanup(); + return false; + } + } + + /** + * Terminates the server-side of the connection + */ + protected void disconnect() { + if (!isRunning) { + // prevent multiple triggers if this gets called consecutively + return; + } + info("Thread being disconnected by server"); + isRunning = false; + this.interrupt(); // breaks out of blocking read in the run() method + cleanup(); // good practice to ensure data is written out immediately + } + + @Override + public void run() { + info("Thread starting"); + try (ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(client.getInputStream());) { + this.out = out; + isRunning = true; + new java.util.Timer().schedule(new java.util.TimerTask() { + @Override + public void run() { + if (getClientName() == null || getClientName().isBlank()) { + info("Client name not received. Disconnecting"); + disconnect(); + } + } + }, 3000); + Payload fromClient; + /** + * isRunning is a flag to let us manage the loop exit condition + * fromClient (in.readObject()) is a blocking method that waits until data is + * received + * - null would likely mean a disconnect so we use a "set and check" logic to + * alternatively exit the loop + */ + while (isRunning) { + try { + fromClient = (Payload) in.readObject(); // blocking method + if (fromClient != null) { + info("Received from my client: " + fromClient); + processPayload(fromClient); + } else { + throw new IOException("Connection interrupted"); // Specific exception for a clean break + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (Thread.currentThread().isInterrupted()) { + info("Thread interrupted during read (likely from the disconnect() method)"); + break; + } + info("IO exception while reading from client"); + e.printStackTrace(); + break; + } + } // close while loop + } catch (Exception e) { + // happens when client disconnects + info("General Exception"); + e.printStackTrace(); + info("My Client disconnected"); + } finally { + if (currentRoom != null) { + currentRoom.handleDisconnect(this); + } + isRunning = false; + info("Exited thread loop. Cleaning up connection"); + cleanup(); + } + } + + /** + * Cleanup method to close the connection and reset the user object + */ + protected void cleanup() { + info("ServerThread cleanup() start"); + try { + // close server-side end of connection + currentRoom = null; + out.close(); + client.close(); + user.reset(); + info("Closed Server-side Socket"); + } catch (IOException e) { + info("Client already closed"); + } + + info("ServerThread cleanup() end"); + } +} diff --git a/M5/Part5/Client.java b/M5/Part5/Client.java new file mode 100644 index 0000000..bb912b2 --- /dev/null +++ b/M5/Part5/Client.java @@ -0,0 +1,495 @@ + +package M5.Part5; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import M5.Part5.TextFX.Color; + +/** + * Demoing bi-directional communication between client and server in a + * multi-client scenario + */ +public enum Client { + INSTANCE; + + private Socket server = null; + private ObjectOutputStream out = null; + private ObjectInputStream in = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); + private volatile boolean isRunning = true; // volatile for thread-safe visibility + private final ConcurrentHashMap knownClients = new ConcurrentHashMap(); + private User myUser = new User(); + + private void error(String message) { + System.out.println(TextFX.colorize(String.format("%s", message), Color.RED)); + } + + // needs to be private now that the enum logic is handling this + private Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine if the server had a problem + // and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + } + + /** + * Takes an IP address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new ObjectOutputStream(server.getOutputStream()); + // channel to listen to server + in = new ObjectInputStream(server.getInputStream()); + System.out.println("Client connected"); + // Use CompletableFuture to run listenToServer() in a separate thread + CompletableFuture.runAsync(this::listenToServer); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an IP address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return true if the text is a valid connection command + */ + private boolean isConnection(String text) { + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if the text was a command or triggered a command + * @throws IOException + */ + private boolean processClientCommand(String text) throws IOException { + boolean wasCommand = false; + if (text.startsWith(Constants.COMMAND_TRIGGER)) { + text = text.substring(1); // remove the / + // System.out.println("Checking command: " + text); + if (isConnection("/" + text)) { + if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { + System.out.println( + TextFX.colorize("Please set your name via /name before connecting", Color.RED)); + return true; + } + // replaces multiple spaces with a single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + sendClientName(myUser.getClientName());// sync follow-up data (handshake) + wasCommand = true; + } else if (text.startsWith(Command.NAME.command)) { + text = text.replace(Command.NAME.command, "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a name as an argument", Color.RED)); + return true; + } + myUser.setClientName(text);// temporary until we get a response from the server + System.out.println(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), + Color.YELLOW)); + wasCommand = true; + } else if (text.equalsIgnoreCase(Command.LIST_USERS.command)) { + System.out.println(TextFX.colorize("Known clients:", Color.CYAN)); + knownClients.forEach((key, value) -> { + System.out.println(TextFX.colorize(String.format("%s%s", value.getDisplayName(), + key == myUser.getClientId() ? " (you)" : ""), Color.CYAN)); + }); + wasCommand = true; + } else if (Command.QUIT.command.equalsIgnoreCase(text)) { + close(); + wasCommand = true; + } else if (Command.DISCONNECT.command.equalsIgnoreCase(text)) { + sendDisconnect(); + wasCommand = true; + } else if (text.startsWith(Command.REVERSE.command)) { + text = text.replace(Command.REVERSE.command, "").trim(); + sendReverse(text); + wasCommand = true; + } else if (text.startsWith(Command.CREATE_ROOM.command)) { + text = text.replace(Command.CREATE_ROOM.command, "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + return true; + } + sendRoomAction(text, RoomAction.CREATE); + wasCommand = true; + } else if (text.startsWith(Command.JOIN_ROOM.command)) { + text = text.replace(Command.JOIN_ROOM.command, "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + return true; + } + sendRoomAction(text, RoomAction.JOIN); + wasCommand = true; + } else if (text.startsWith(Command.LEAVE_ROOM.command) || text.startsWith("leave")) { + // Note: Accounts for /leave and /leaveroom variants (or anything beginning with + // /leave) + sendRoomAction(text, RoomAction.LEAVE); + wasCommand = true; + } + } + return wasCommand; + } + + // Start Send*() methods + + /** + * Sends a room action to the server + * + * @param roomName + * @param roomAction (join, leave, create) + * @throws IOException + */ + private void sendRoomAction(String roomName, RoomAction roomAction) throws IOException { + Payload payload = new Payload(); + payload.setMessage(roomName); + switch (roomAction) { + case RoomAction.CREATE: + payload.setPayloadType(PayloadType.ROOM_CREATE); + break; + case RoomAction.JOIN: + payload.setPayloadType(PayloadType.ROOM_JOIN); + break; + case RoomAction.LEAVE: + payload.setPayloadType(PayloadType.ROOM_LEAVE); + break; + default: + System.out.println(TextFX.colorize("Invalid room action", Color.RED)); + break; + } + sendToServer(payload); + } + + /** + * Sends a reverse message action to the server + * + * @param message + * @throws IOException + */ + private void sendReverse(String message) throws IOException { + Payload payload = new Payload(); + payload.setMessage(message); + payload.setPayloadType(PayloadType.REVERSE); + sendToServer(payload); + + } + + /** + * Sends a disconnect action to the server + * + * @throws IOException + */ + private void sendDisconnect() throws IOException { + Payload payload = new Payload(); + payload.setPayloadType(PayloadType.DISCONNECT); + sendToServer(payload); + } + + /** + * Sends a message to the server + * + * @param message + * @throws IOException + */ + private void sendMessage(String message) throws IOException { + Payload payload = new Payload(); + payload.setMessage(message); + payload.setPayloadType(PayloadType.MESSAGE); + sendToServer(payload); + } + + /** + * Sends the client's name to the server (what the user desires to be called) + * + * @param name + * @throws IOException + */ + private void sendClientName(String name) throws IOException { + ConnectionPayload payload = new ConnectionPayload(); + payload.setClientName(name); + payload.setPayloadType(PayloadType.CLIENT_CONNECT); + sendToServer(payload); + } + + private void sendToServer(Payload payload) throws IOException { + if (isConnected()) { + out.writeObject(payload); + out.flush(); // good practice to ensure data is written out immediately + } else { + System.out.println( + "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); + } + } + // End Send*() methods + + public void start() throws IOException { + System.out.println("Client starting"); + + // Use CompletableFuture to run listenToInput() in a separate thread + CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); + + // Wait for inputFuture to complete to ensure proper termination + inputFuture.join(); + } + + /** + * Listens for messages from the server + */ + private void listenToServer() { + try { + while (isRunning && isConnected()) { + Payload fromServer = (Payload) in.readObject(); // blocking read + if (fromServer != null) { + processPayload(fromServer); + + } else { + System.out.println("Server disconnected"); + break; + } + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (isRunning) { + System.out.println("Connection dropped"); + e.printStackTrace(); + } + } finally { + closeServerConnection(); + } + System.out.println("listenToServer thread stopped"); + } + + private void processPayload(Payload payload) { + switch (payload.getPayloadType()) { + case CLIENT_CONNECT:// unused + break; + case CLIENT_ID: + processClientData(payload); + break; + case DISCONNECT: + processDisconnect(payload); + break; + case MESSAGE: + processMessage(payload); + break; + case REVERSE: + processReverse(payload); + break; + case ROOM_CREATE: // unused + break; + case ROOM_JOIN: + processRoomAction(payload); + break; + case ROOM_LEAVE: + processRoomAction(payload); + break; + case SYNC_CLIENT: + processRoomAction(payload); + break; + default: + System.out.println(TextFX.colorize("Unhandled payload type", Color.YELLOW)); + break; + + } + } + + // Start process*() methods + private void processClientData(Payload payload) { + if (myUser.getClientId() != Constants.DEFAULT_CLIENT_ID) { + System.out.println(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); + + } + myUser.setClientId(payload.getClientId()); + myUser.setClientName(((ConnectionPayload) payload).getClientName());// confirmation from Server + knownClients.put(myUser.getClientId(), myUser); + System.out.println(TextFX.colorize("Connected", Color.GREEN)); + } + + private void processDisconnect(Payload payload) { + if (payload.getClientId() == myUser.getClientId()) { + knownClients.clear(); + myUser.reset(); + System.out.println(TextFX.colorize("You disconnected", Color.RED)); + } else if (knownClients.containsKey(payload.getClientId())) { + User disconnectedUser = knownClients.remove(payload.getClientId()); + if (disconnectedUser != null) { + System.out.println(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), + Color.RED)); + } + } + + } + + private void processRoomAction(Payload payload) { + if (!(payload instanceof ConnectionPayload)) { + error("Invalid payload subclass for processRoomAction"); + return; + } + ConnectionPayload connectionPayload = (ConnectionPayload) payload; + // use DEFAULT_CLIENT_ID to clear knownClients (mostly for disconnect and room + // transitions) + if (connectionPayload.getClientId() == Constants.DEFAULT_CLIENT_ID) { + knownClients.clear(); + return; + } + switch (connectionPayload.getPayloadType()) { + + case ROOM_LEAVE: + // remove from map + if (knownClients.containsKey(connectionPayload.getClientId())) { + knownClients.remove(connectionPayload.getClientId()); + } + if (connectionPayload.getMessage() != null) { + System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); + } + + break; + case ROOM_JOIN: + if (connectionPayload.getMessage() != null) { + System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); + } + // cascade to manage knownClients + case SYNC_CLIENT: + // add to map + if (!knownClients.containsKey(connectionPayload.getClientId())) { + User user = new User(); + user.setClientId(connectionPayload.getClientId()); + user.setClientName(connectionPayload.getClientName()); + knownClients.put(connectionPayload.getClientId(), user); + } + break; + default: + error("Invalid payload type for processRoomAction"); + break; + } + } + + private void processMessage(Payload payload) { + System.out.println(TextFX.colorize(payload.getMessage(), Color.BLUE)); + } + + private void processReverse(Payload payload) { + System.out.println(TextFX.colorize(payload.getMessage(), Color.PURPLE)); + } + // End process*() methods + + /** + * Listens for keyboard input from the user + */ + private void listenToInput() { + try (Scanner si = new Scanner(System.in)) { + System.out.println("Waiting for input"); // moved here to avoid console spam + while (isRunning) { // Run until isRunning is false + String userInput = si.nextLine(); + if (!processClientCommand(userInput)) { + sendMessage(userInput); + } + } + } catch (IOException ioException) { + System.out.println("Error in listentToInput()"); + ioException.printStackTrace(); + } + System.out.println("listenToInput thread stopped"); + } + + /** + * Closes the client connection and associated resources + */ + private void close() { + isRunning = false; + closeServerConnection(); + System.out.println("Client terminated"); + // System.exit(0); // Terminate the application + } + + /** + * Closes the server connection and associated resources + */ + private void closeServerConnection() { + try { + if (out != null) { + System.out.println("Closing output stream"); + out.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (in != null) { + System.out.println("Closing input stream"); + in.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (server != null) { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + Client client = Client.INSTANCE; + try { + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} diff --git a/M5/Part5/Command.java b/M5/Part5/Command.java new file mode 100644 index 0000000..008712c --- /dev/null +++ b/M5/Part5/Command.java @@ -0,0 +1,33 @@ + +package M5.Part5; + +import java.util.HashMap; + +public enum Command { + QUIT("quit"), + DISCONNECT("disconnect"), + LOGOUT("logout"), + LOGOFF("logoff"), + REVERSE("reverse"), + CREATE_ROOM("createroom"), + LEAVE_ROOM("leaveroom"), + JOIN_ROOM("joinroom"), + NAME("name"), + LIST_USERS("users"); + + private static final HashMap BY_COMMAND = new HashMap<>(); + static { + for (Command e : values()) { + BY_COMMAND.put(e.command, e); + } + } + public final String command; + + private Command(String command) { + this.command = command; + } + + public static Command stringToCommand(String command) { + return BY_COMMAND.get(command); + } +} diff --git a/M5/Part5/ConnectionPayload.java b/M5/Part5/ConnectionPayload.java new file mode 100644 index 0000000..a5a5c9e --- /dev/null +++ b/M5/Part5/ConnectionPayload.java @@ -0,0 +1,28 @@ + +package M5.Part5; + +public class ConnectionPayload extends Payload { + private String clientName; + + /** + * @return the clientName + */ + public String getClientName() { + return clientName; + } + + /** + * @param clientName the clientName to set + */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + @Override + public String toString() { + return super.toString() + + String.format(" ClientName: [%s]", + getClientName()); + } + +} diff --git a/M5/Part5/Constants.java b/M5/Part5/Constants.java new file mode 100644 index 0000000..86aa03a --- /dev/null +++ b/M5/Part5/Constants.java @@ -0,0 +1,8 @@ + +package M5.Part5; + +public abstract class Constants { + final public static String COMMAND_TRIGGER = "/"; + final public static String SINGLE_SPACE = " "; + final public static long DEFAULT_CLIENT_ID = -1; +} diff --git a/M5/Part5/CustomIT114Exception.java b/M5/Part5/CustomIT114Exception.java new file mode 100644 index 0000000..efee261 --- /dev/null +++ b/M5/Part5/CustomIT114Exception.java @@ -0,0 +1,12 @@ + +package M5.Part5; + +public abstract class CustomIT114Exception extends Exception { + public CustomIT114Exception(String message) { + super(message); + } + + public CustomIT114Exception(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/M5/Part5/DuplicateRoomException.java b/M5/Part5/DuplicateRoomException.java new file mode 100644 index 0000000..ef6230e --- /dev/null +++ b/M5/Part5/DuplicateRoomException.java @@ -0,0 +1,13 @@ + +package M5.Part5; + +public class DuplicateRoomException extends CustomIT114Exception { + public DuplicateRoomException(String message) { + super(message); + } + + public DuplicateRoomException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/M5/Part5/Payload.java b/M5/Part5/Payload.java new file mode 100644 index 0000000..1191be1 --- /dev/null +++ b/M5/Part5/Payload.java @@ -0,0 +1,57 @@ + +package M5.Part5; + +import java.io.Serializable; + +public class Payload implements Serializable { + private PayloadType payloadType; + private long clientId; + private String message; + + /** + * @return the payloadType + */ + public PayloadType getPayloadType() { + return payloadType; + } + + /** + * @param payloadType the payloadType to set + */ + public void setPayloadType(PayloadType payloadType) { + this.payloadType = payloadType; + } + + /** + * @return the clientId + */ + public long getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(long clientId) { + this.clientId = clientId; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * @param message the message to set + */ + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return String.format("Payload[%s] Client Id [%s] Message: [%s]", getPayloadType(), getClientId(), getMessage()); + } +} diff --git a/M5/Part5/PayloadType.java b/M5/Part5/PayloadType.java new file mode 100644 index 0000000..6fd7e8a --- /dev/null +++ b/M5/Part5/PayloadType.java @@ -0,0 +1,15 @@ + +package M5.Part5; + +public enum PayloadType { + CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data + // [name]) + CLIENT_ID, // server sending client id + SYNC_CLIENT, // silent syncing of clients in room + DISCONNECT, // distinct disconnect action + ROOM_CREATE, + ROOM_JOIN, + ROOM_LEAVE, + REVERSE, + MESSAGE // sender and message +} diff --git a/M5/Part5/Room.java b/M5/Part5/Room.java new file mode 100644 index 0000000..18c7c53 --- /dev/null +++ b/M5/Part5/Room.java @@ -0,0 +1,264 @@ + +package M5.Part5; + +import java.util.concurrent.ConcurrentHashMap; + +import M5.Part5.TextFX.Color; + +public class Room implements AutoCloseable { + private final String name;// unique name of the Room + private volatile boolean isRunning = false; + private final ConcurrentHashMap clientsInRoom = new ConcurrentHashMap(); + + public final static String LOBBY = "lobby"; + + private void info(String message) { + System.out.println(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); + } + + public Room(String name) { + this.name = name; + isRunning = true; + info("Created"); + } + + public String getName() { + return this.name; + } + + protected synchronized void addClient(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + if (clientsInRoom.containsKey(client.getClientId())) { + info("Attempting to add a client that already exists in the room"); + return; + } + clientsInRoom.put(client.getClientId(), client); + client.setCurrentRoom(this); + client.sendResetUserList(); + syncExistingClients(client); + // notify clients of someone joining + joinStatusRelay(client, true); + + } + + protected synchronized void removeClient(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + if (!clientsInRoom.containsKey(client.getClientId())) { + info("Attempting to remove a client that doesn't exist in the room"); + return; + } + ServerThread removedClient = clientsInRoom.get(client.getClientId()); + if (removedClient != null) { + // notify clients of someone joining + joinStatusRelay(removedClient, false); + clientsInRoom.remove(client.getClientId()); + autoCleanup(); + } + } + + private void syncExistingClients(ServerThread incomingClient) { + clientsInRoom.values().forEach(serverThread -> { + if (serverThread.getClientId() != incomingClient.getClientId()) { + boolean failedToSync = !incomingClient.sendClientInfo(serverThread.getClientId(), + serverThread.getClientName(), RoomAction.JOIN, true); + if (failedToSync) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + } + }); + } + + private void joinStatusRelay(ServerThread client, boolean didJoin) { + clientsInRoom.values().removeIf(serverThread -> { + String formattedMessage = String.format("Room[%s] %s %s the room", + getName(), + client.getClientId() == serverThread.getClientId() ? "You" + : client.getDisplayName(), + didJoin ? "joined" : "left"); + final long senderId = client == null ? Constants.DEFAULT_CLIENT_ID : client.getClientId(); + // Share info of the client joining or leaving the room + boolean failedToSync = !serverThread.sendClientInfo(client.getClientId(), + client.getClientName(), didJoin ? RoomAction.JOIN : RoomAction.LEAVE); + // Send the server generated message to the current client + boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); + if (failedToSend || failedToSync) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + /** + * Sends a basic String message from the sender to all connectedClients + * Internally calls processCommand and evaluates as necessary. + * Note: Clients that fail to receive a message get removed from + * connectedClients. + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + protected synchronized void relay(ServerThread sender, String message) { + if (!isRunning) { // block action if Room isn't running + return; + } + + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? String.format("Room[%s]", getName()) + : sender.getDisplayName(); + final long senderId = sender == null ? Constants.DEFAULT_CLIENT_ID : sender.getClientId(); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't be changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + + // loop over clients and send out the message; remove client if message failed + // to be sent + // Note: this uses a lambda expression for each item in the values() collection, + // it's one way we can safely remove items during iteration + info(String.format("sending message to %s recipients: %s", clientsInRoom.size(), formattedMessage)); + + clientsInRoom.values().removeIf(serverThread -> { + boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + /** + * Takes a ServerThread and removes them from the Server + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param client + */ + private synchronized void disconnect(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + ServerThread disconnectingServerThread = clientsInRoom.remove(client.getClientId()); + if (disconnectingServerThread != null) { + + clientsInRoom.values().removeIf(serverThread -> { + if (serverThread.getClientId() == disconnectingServerThread.getClientId()) { + return true; + } + boolean failedToSend = !serverThread.sendClientInfo(disconnectingServerThread.getClientId(), + disconnectingServerThread.getClientName(), RoomAction.LEAVE); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + return failedToSend; + }); + relay(null, disconnectingServerThread.getDisplayName() + " disconnected"); + disconnectingServerThread.disconnect(); + } + autoCleanup(); + } + + protected synchronized void disconnectAll() { + info("Disconnect All triggered"); + if (!isRunning) { + return; + } + clientsInRoom.values().removeIf(client -> { + disconnect(client); + return true; + }); + info("Disconnect All finished"); + } + + /** + * Attempts to close the room to free up resources if it's empty + */ + private void autoCleanup() { + if (!Room.LOBBY.equalsIgnoreCase(name) && clientsInRoom.isEmpty()) { + close(); + } + } + + @Override + public void close() { + // attempt to gracefully close and migrate clients + if (!clientsInRoom.isEmpty()) { + relay(null, "Room is shutting down, migrating to lobby"); + info(String.format("migrating %s clients", clientsInRoom.size())); + clientsInRoom.values().removeIf(client -> { + try { + Server.INSTANCE.joinRoom(Room.LOBBY, client); + } catch (RoomNotFoundException e) { + e.printStackTrace(); + // TODO, fill in, this shouldn't happen though + } + return true; + }); + } + Server.INSTANCE.removeRoom(this); + isRunning = false; + clientsInRoom.clear(); + info(String.format("closed")); + } + + // start handle methods + public void handleCreateRoom(ServerThread sender, String roomName) { + try { + Server.INSTANCE.createRoom(roomName); + Server.INSTANCE.joinRoom(roomName, sender); + } catch (RoomNotFoundException e) { + info("Room wasn't found (this shouldn't happen)"); + e.printStackTrace(); + } catch (DuplicateRoomException e) { + sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s already exists", roomName)); + } + } + + public void handleJoinRoom(ServerThread sender, String roomName) { + try { + Server.INSTANCE.joinRoom(roomName, sender); + } catch (RoomNotFoundException e) { + sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s doesn't exist", roomName)); + } + } + + protected synchronized void handleDisconnect(BaseServerThread sender) { + handleDisconnect((ServerThread) sender); + } + + /** + * Expose access to the disconnect action + * + * @param serverThread + */ + protected synchronized void handleDisconnect(ServerThread sender) { + disconnect(sender); + } + + protected synchronized void handleReverseText(ServerThread sender, String text) { + StringBuilder sb = new StringBuilder(text); + sb.reverse(); + String rev = sb.toString(); + relay(sender, rev); + } + + protected synchronized void handleMessage(ServerThread sender, String text) { + relay(sender, text); + } + // end handle methods +} diff --git a/M5/Part5/RoomAction.java b/M5/Part5/RoomAction.java new file mode 100644 index 0000000..92bde03 --- /dev/null +++ b/M5/Part5/RoomAction.java @@ -0,0 +1,6 @@ + +package M5.Part5; + +public enum RoomAction { + CREATE, JOIN, LEAVE +} diff --git a/M5/Part5/RoomNotFoundException.java b/M5/Part5/RoomNotFoundException.java new file mode 100644 index 0000000..eb90a20 --- /dev/null +++ b/M5/Part5/RoomNotFoundException.java @@ -0,0 +1,14 @@ + +package M5.Part5; + +public class RoomNotFoundException extends CustomIT114Exception { + + public RoomNotFoundException(String message) { + super(message); + } + + public RoomNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/M5/Part5/Server.java b/M5/Part5/Server.java new file mode 100644 index 0000000..7a2cee5 --- /dev/null +++ b/M5/Part5/Server.java @@ -0,0 +1,200 @@ + +package M5.Part5; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ConcurrentHashMap; + +import M5.Part5.TextFX.Color; + +public enum Server { + INSTANCE; // Singleton instance + + private int port = 3000; + // connected clients + // Use ConcurrentHashMap for thread-safe client management + // The key is the unique Room name and the Room is the instance + private final ConcurrentHashMap rooms = new ConcurrentHashMap<>(); + private boolean isRunning = true; + private long nextClientId = 0; + + private void info(String message) { + System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); + } + + private Server() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + info("JVM is shutting down. Perform cleanup tasks."); + shutdown(); + })); + } + + /** + * Gracefully disconnect clients + */ + private void shutdown() { + try { + // chose removeIf over forEach to avoid potential + // ConcurrentModificationException + // since empty rooms tell the server to remove themselves + rooms.values().removeIf(room -> { + room.disconnectAll(); + return true; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void start(int port) { + this.port = port; + // server listening + info("Listening on port " + this.port); + // Simplified client connection loop + try (ServerSocket serverSocket = new ServerSocket(port)) { + createRoom(Room.LOBBY);// create the first room (lobby) + while (isRunning) { + info("Waiting for next client"); + Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection + info("Client connected"); + // wrap socket in a ServerThread, pass a callback to notify the Server when + // they're initialized + ServerThread serverThread = new ServerThread(incomingClient, this::onServerThreadInitialized); + // start the thread (typically an external entity manages the lifecycle and we + // don't have the thread start itself) + serverThread.start(); + // Note: We don't yet add the ServerThread reference to our connectedClients map + } + } catch (DuplicateRoomException e) { + System.err.println(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); + } catch (IOException e) { + System.err.println(TextFX.colorize("Error accepting connection", Color.RED)); + e.printStackTrace(); + } finally { + info("Closing server socket"); + } + } + + /** + * Callback passed to ServerThread to inform Server they're ready to receive + * data + * + * @param serverThread + */ + private void onServerThreadInitialized(ServerThread serverThread) { + // Generate Server controlled clientId + nextClientId = Math.max(++nextClientId, 1); + serverThread.setClientId(nextClientId); + serverThread.sendClientId();// syncs the data to the Client + // add initialized client to the lobby + info(String.format("*%s initialized*", serverThread.getDisplayName())); + try { + joinRoom(Room.LOBBY, serverThread); + info(String.format("*%s added to Lobby*", serverThread.getDisplayName())); + } catch (RoomNotFoundException e) { + info(String.format("*Error adding %s to Lobby*", serverThread.getDisplayName())); + e.printStackTrace(); + } + } + + /** + * Attempts to create a new Room and add it to the tracked rooms collection + * + * @param name Unique name of the room + * @return true if it was created and false if it wasn't + * @throws DuplicateRoomException + */ + protected void createRoom(String name) throws DuplicateRoomException { + final String nameCheck = name.toLowerCase(); + if (rooms.containsKey(nameCheck)) { + throw new DuplicateRoomException(String.format("Room %s already exists", name)); + } + Room room = new Room(name); + rooms.put(nameCheck, room); + info(String.format("Created new Room %s", name)); + } + + /** + * Attempts to move a client (ServerThread) between rooms + * + * @param name the target room to join + * @param client the client moving + * @throws RoomNotFoundException + * + */ + protected void joinRoom(String name, ServerThread client) throws RoomNotFoundException { + final String nameCheck = name.toLowerCase(); + if (!rooms.containsKey(nameCheck)) { + throw new RoomNotFoundException(String.format("Room %s wasn't found", name)); + } + Room currentRoom = client.getCurrentRoom(); + if (currentRoom != null) { + info("Removing client from previous Room " + currentRoom.getName()); + currentRoom.removeClient(client); + } + Room next = rooms.get(nameCheck); + next.addClient(client); + } + + protected void removeRoom(Room room) { + rooms.remove(room.getName().toLowerCase()); + info(String.format("Removed room %s", room.getName())); + } + + /** + * + *

+ * Note: Not a common use-case; just updated for example sake. + *

+ * Relays the message from the sender to all rooms + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + private synchronized void relayToAllRooms(ServerThread sender, String message) { + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? "Server" : sender.getDisplayName(); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + // end temp identifier + + // loop over Rooms and send out the message + // Note: this uses a lambda expression for each item in the values() collection + + rooms.values().forEach(room -> { + room.relay(sender, formattedMessage); + }); + } + + /** + * Used to send a message to all Rooms. + * This is just an example and we likely won't be using this + * + * @param sender + * @param message + */ + public synchronized void broadcastMessageToAllRooms(ServerThread sender, String message) { + relayToAllRooms(sender, message); + } + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = Server.INSTANCE; + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } + +} diff --git a/M5/Part5/ServerThread.java b/M5/Part5/ServerThread.java new file mode 100644 index 0000000..24e5c08 --- /dev/null +++ b/M5/Part5/ServerThread.java @@ -0,0 +1,167 @@ + +package M5.Part5; + +import java.net.Socket; +import java.util.Objects; +import java.util.function.Consumer; +import M5.Part5.TextFX.Color; + +/** + * A server-side representation of a single client + */ +public class ServerThread extends BaseServerThread { + private Consumer onInitializationComplete; // callback to inform when this object is ready + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + protected void info(String message) { + System.out.println(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); + } + + /** + * Wraps the Socket connection and takes a Server reference and a callback + * + * @param myClient + * @param server + * @param onInitializationComplete method to inform listener that this object is + * ready + */ + protected ServerThread(Socket myClient, Consumer onInitializationComplete) { + Objects.requireNonNull(myClient, "Client socket cannot be null"); + Objects.requireNonNull(onInitializationComplete, "callback cannot be null"); + info("ServerThread created"); + // get communication channels to single client + this.client = myClient; + // this.clientId = this.threadId(); // An id associated with the thread + // instance, used as a temporary identifier + this.onInitializationComplete = onInitializationComplete; + + } + + // Start Send*() Methods + protected boolean sendDisconnect(long clientId) { + Payload payload = new Payload(); + payload.setClientId(clientId); + payload.setPayloadType(PayloadType.DISCONNECT); + return sendToClient(payload); + } + + protected boolean sendResetUserList() { + return sendClientInfo(Constants.DEFAULT_CLIENT_ID, null, RoomAction.JOIN); + } + + /** + * Syncs Client Info (id, name, join status) to the client + * + * @param clientId use -1 for reset/clear + * @param clientName + * @param action RoomAction of Join or Leave + * @return true for successful send + */ + protected boolean sendClientInfo(long clientId, String clientName, RoomAction action) { + return sendClientInfo(clientId, clientName, action, false); + } + + /** + * Syncs Client Info (id, name, join status) to the client + * + * @param clientId use -1 for reset/clear + * @param clientName + * @param action RoomAction of Join or Leave + * @param isSync True is used to not show output on the client side (silent + * sync) + * @return true for successful send + */ + protected boolean sendClientInfo(long clientId, String clientName, RoomAction action, boolean isSync) { + ConnectionPayload payload = new ConnectionPayload(); + switch (action) { + case JOIN: + payload.setPayloadType(PayloadType.ROOM_JOIN); + break; + case LEAVE: + payload.setPayloadType(PayloadType.ROOM_LEAVE); + break; + default: + break; + } + if (isSync) { + payload.setPayloadType(PayloadType.SYNC_CLIENT); + } + payload.setClientId(clientId); + payload.setClientName(clientName); + return sendToClient(payload); + } + + /** + * Sends this client's id to the client. + * This will be a successfully connection handshake + * + * @return true for successful send + */ + protected boolean sendClientId() { + ConnectionPayload payload = new ConnectionPayload(); + payload.setPayloadType(PayloadType.CLIENT_ID); + payload.setClientId(getClientId()); + payload.setClientName(getClientName());// Can be used as a Server-side override of username (i.e., profanity + // filter) + return sendToClient(payload); + } + + /** + * Sends a message to the client + * + * @param clientId who it's from + * @param message + * @return true for successful send + */ + protected boolean sendMessage(long clientId, String message) { + Payload payload = new Payload(); + payload.setPayloadType(PayloadType.MESSAGE); + payload.setMessage(message); + payload.setClientId(clientId); + return sendToClient(payload); + } + + // End Send*() Methods + @Override + protected void processPayload(Payload incoming) { + + switch (incoming.getPayloadType()) { + case CLIENT_CONNECT: + setClientName(((ConnectionPayload) incoming).getClientName().trim()); + + break; + case DISCONNECT: + currentRoom.handleDisconnect(this); + break; + case MESSAGE: + currentRoom.handleMessage(this, incoming.getMessage()); + break; + case REVERSE: + currentRoom.handleReverseText(this, incoming.getMessage()); + break; + case ROOM_CREATE: + currentRoom.handleCreateRoom(this, incoming.getMessage()); + break; + case ROOM_JOIN: + currentRoom.handleJoinRoom(this, incoming.getMessage()); + break; + case ROOM_LEAVE: + currentRoom.handleJoinRoom(this, Room.LOBBY); + break; + default: + System.out.println(TextFX.colorize("Unknown payload type received", Color.RED)); + break; + } + } + + @Override + protected void onInitialized() { + // once receiving the desired client name the object is ready + onInitializationComplete.accept(this); + } +} diff --git a/M5/Part5/TextFX.java b/M5/Part5/TextFX.java new file mode 100644 index 0000000..dc717a3 --- /dev/null +++ b/M5/Part5/TextFX.java @@ -0,0 +1,72 @@ +package M5.Part5; + +/** + * Utility to attempt to provide colored text in the terminal. + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ +public abstract class TextFX { + + /** + * TextFX.Color list of available colors + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ + public enum Color { + BLACK("\033[0;30m"), + RED("\033[0;31m"), + GREEN("\033[0;32m"), + YELLOW("\033[0;33m"), + BLUE("\033[0;34m"), + PURPLE("\033[0;35m"), + CYAN("\033[0;36m"), + WHITE("\033[0;37m"); + + private final String code; + + Color(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + } + + public static final String RESET = "\033[0m"; + + /** + * Generates a String with the original message wrapped in the ASCII of the + * color and RESET + * + *

+ * Note: May not work for all terminals + *

+ *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ * + * @param text Input text to colorize + * @param color Enum of Color choice from TextFX.Color + * @return wrapped String + */ + public static String colorize(String text, Color color) { + StringBuilder builder = new StringBuilder(); + builder.append(color.getCode()); + builder.append(text); + builder.append(RESET); + return builder.toString(); + } + + public static void main(String[] args) { + // Example usage: + System.out.println(TextFX.colorize("Hello, world!", Color.RED)); + System.out.println(TextFX.colorize("This is some blue text.", Color.BLUE)); + System.out.println(TextFX.colorize("And this is green!", Color.GREEN)); + } +} \ No newline at end of file diff --git a/M5/Part5/User.java b/M5/Part5/User.java new file mode 100644 index 0000000..a180490 --- /dev/null +++ b/M5/Part5/User.java @@ -0,0 +1,43 @@ +package M5.Part5; + +public class User { + private long clientId = Constants.DEFAULT_CLIENT_ID; + private String clientName; + + /** + * @return the clientId + */ + public long getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(long clientId) { + this.clientId = clientId; + } + + /** + * @return the username + */ + public String getClientName() { + return clientName; + } + + /** + * @param username the username to set + */ + public void setClientName(String username) { + this.clientName = username; + } + + public String getDisplayName() { + return String.format("%s#%s", this.clientName, this.clientId); + } + + public void reset() { + this.clientId = Constants.DEFAULT_CLIENT_ID; + this.clientName = null; + } +} \ No newline at end of file From dc1041a8ca87a24149558dca36e082a73ce72a54 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 16:22:34 -0500 Subject: [PATCH 23/56] project base files --- Project/BaseServerThread.java | 219 ++++++++++++ Project/Client.java | 495 ++++++++++++++++++++++++++++ Project/Command.java | 33 ++ Project/ConnectionPayload.java | 28 ++ Project/Constants.java | 8 + Project/CustomIT114Exception.java | 12 + Project/DuplicateRoomException.java | 13 + Project/Payload.java | 57 ++++ Project/PayloadType.java | 15 + Project/Room.java | 264 +++++++++++++++ Project/RoomAction.java | 6 + Project/RoomNotFoundException.java | 14 + Project/Server.java | 200 +++++++++++ Project/ServerThread.java | 167 ++++++++++ Project/TextFX.java | 72 ++++ Project/User.java | 43 +++ 16 files changed, 1646 insertions(+) create mode 100644 Project/BaseServerThread.java create mode 100644 Project/Client.java create mode 100644 Project/Command.java create mode 100644 Project/ConnectionPayload.java create mode 100644 Project/Constants.java create mode 100644 Project/CustomIT114Exception.java create mode 100644 Project/DuplicateRoomException.java create mode 100644 Project/Payload.java create mode 100644 Project/PayloadType.java create mode 100644 Project/Room.java create mode 100644 Project/RoomAction.java create mode 100644 Project/RoomNotFoundException.java create mode 100644 Project/Server.java create mode 100644 Project/ServerThread.java create mode 100644 Project/TextFX.java create mode 100644 Project/User.java diff --git a/Project/BaseServerThread.java b/Project/BaseServerThread.java new file mode 100644 index 0000000..2f2c08c --- /dev/null +++ b/Project/BaseServerThread.java @@ -0,0 +1,219 @@ +package M5.Part5; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; + +/** + * Base class the handles the underlying connection between Client and + * Server-side + */ +public abstract class BaseServerThread extends Thread { + + protected boolean isRunning = false; // control variable to stop this thread + protected ObjectOutputStream out; // exposed here for send() + protected Socket client; // communication directly to "my" client + private User user = new User(); + protected Room currentRoom; + + /** + * Returns the current Room associated with this ServerThread + * + * @return + */ + protected Room getCurrentRoom() { + return this.currentRoom; + } + + /** + * Allows the setting of a non-null Room reference to this ServerThread + * + * @param room + */ + protected void setCurrentRoom(Room room) { + if (room == null) { + throw new NullPointerException("Room argument can't be null"); + } + if (room == currentRoom) { + System.out.println( + String.format("ServerThread set to the same room [%s], was this intentional?", room.getName())); + } + currentRoom = room; + } + + /** + * Returns the status of this ServerThread + * + * @return + */ + public boolean isRunning() { + return isRunning; + } + + public void setClientId(long clientId) { + this.user.setClientId(clientId); + } + + public long getClientId() { + // Note: We return clientId instead of threadId as we'll change this identifier + // in the future + return this.user.getClientId(); + } + + /** + * Sets the client name and triggers onInitialized() + * + * @param clientName + */ + protected void setClientName(String clientName) { + this.user.setClientName(clientName); + onInitialized(); + } + + public String getClientName() { + return this.user.getClientName(); + } + + public String getDisplayName() { + return this.user.getDisplayName(); + } + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + protected abstract void info(String message); + + /** + * Triggered when object is fully initialized + */ + protected abstract void onInitialized(); + + /** + * Receives a Payload and passes data to proper handler + * + * @param payload + */ + protected abstract void processPayload(Payload payload); + + /** + * Sends the payload over the socket + * + * @param payload + * @return true if no errors were encountered + */ + protected boolean sendToClient(Payload payload) { + if (!isRunning) { + return true; + } + try { + info("Sending to client: " + payload); + out.writeObject(payload); + out.flush(); + return true; + } catch (IOException e) { + info("Error sending message to client (most likely disconnected)"); + // comment this out to inspect the stack trace + // e.printStackTrace(); + cleanup(); + return false; + } + } + + /** + * Terminates the server-side of the connection + */ + protected void disconnect() { + if (!isRunning) { + // prevent multiple triggers if this gets called consecutively + return; + } + info("Thread being disconnected by server"); + isRunning = false; + this.interrupt(); // breaks out of blocking read in the run() method + cleanup(); // good practice to ensure data is written out immediately + } + + @Override + public void run() { + info("Thread starting"); + try (ObjectOutputStream out = new ObjectOutputStream(client.getOutputStream()); + ObjectInputStream in = new ObjectInputStream(client.getInputStream());) { + this.out = out; + isRunning = true; + new java.util.Timer().schedule(new java.util.TimerTask() { + @Override + public void run() { + if (getClientName() == null || getClientName().isBlank()) { + info("Client name not received. Disconnecting"); + disconnect(); + } + } + }, 3000); + Payload fromClient; + /** + * isRunning is a flag to let us manage the loop exit condition + * fromClient (in.readObject()) is a blocking method that waits until data is + * received + * - null would likely mean a disconnect so we use a "set and check" logic to + * alternatively exit the loop + */ + while (isRunning) { + try { + fromClient = (Payload) in.readObject(); // blocking method + if (fromClient != null) { + info("Received from my client: " + fromClient); + processPayload(fromClient); + } else { + throw new IOException("Connection interrupted"); // Specific exception for a clean break + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (Thread.currentThread().isInterrupted()) { + info("Thread interrupted during read (likely from the disconnect() method)"); + break; + } + info("IO exception while reading from client"); + e.printStackTrace(); + break; + } + } // close while loop + } catch (Exception e) { + // happens when client disconnects + info("General Exception"); + e.printStackTrace(); + info("My Client disconnected"); + } finally { + if (currentRoom != null) { + currentRoom.handleDisconnect(this); + } + isRunning = false; + info("Exited thread loop. Cleaning up connection"); + cleanup(); + } + } + + /** + * Cleanup method to close the connection and reset the user object + */ + protected void cleanup() { + info("ServerThread cleanup() start"); + try { + // close server-side end of connection + currentRoom = null; + out.close(); + client.close(); + user.reset(); + info("Closed Server-side Socket"); + } catch (IOException e) { + info("Client already closed"); + } + + info("ServerThread cleanup() end"); + } +} diff --git a/Project/Client.java b/Project/Client.java new file mode 100644 index 0000000..bb912b2 --- /dev/null +++ b/Project/Client.java @@ -0,0 +1,495 @@ + +package M5.Part5; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.Socket; +import java.net.UnknownHostException; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import M5.Part5.TextFX.Color; + +/** + * Demoing bi-directional communication between client and server in a + * multi-client scenario + */ +public enum Client { + INSTANCE; + + private Socket server = null; + private ObjectOutputStream out = null; + private ObjectInputStream in = null; + final Pattern ipAddressPattern = Pattern + .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); + final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); + private volatile boolean isRunning = true; // volatile for thread-safe visibility + private final ConcurrentHashMap knownClients = new ConcurrentHashMap(); + private User myUser = new User(); + + private void error(String message) { + System.out.println(TextFX.colorize(String.format("%s", message), Color.RED)); + } + + // needs to be private now that the enum logic is handling this + private Client() { + System.out.println("Client Created"); + } + + public boolean isConnected() { + if (server == null) { + return false; + } + // https://stackoverflow.com/a/10241044 + // Note: these check the client's end of the socket connect; therefore they + // don't really help determine if the server had a problem + // and is just for lesson's sake + return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); + } + + /** + * Takes an IP address and a port to attempt a socket connection to a server. + * + * @param address + * @param port + * @return true if connection was successful + */ + private boolean connect(String address, int port) { + try { + server = new Socket(address, port); + // channel to send to server + out = new ObjectOutputStream(server.getOutputStream()); + // channel to listen to server + in = new ObjectInputStream(server.getInputStream()); + System.out.println("Client connected"); + // Use CompletableFuture to run listenToServer() in a separate thread + CompletableFuture.runAsync(this::listenToServer); + } catch (UnknownHostException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return isConnected(); + } + + /** + *

+ * Check if the string contains the connect command + * followed by an IP address and port or localhost and port. + *

+ *

+ * Example format: 123.123.123.123:3000 + *

+ *

+ * Example format: localhost:3000 + *

+ * https://www.w3schools.com/java/java_regex.asp + * + * @param text + * @return true if the text is a valid connection command + */ + private boolean isConnection(String text) { + Matcher ipMatcher = ipAddressPattern.matcher(text); + Matcher localhostMatcher = localhostPattern.matcher(text); + return ipMatcher.matches() || localhostMatcher.matches(); + } + + /** + * Controller for handling various text commands. + *

+ * Add more here as needed + *

+ * + * @param text + * @return true if the text was a command or triggered a command + * @throws IOException + */ + private boolean processClientCommand(String text) throws IOException { + boolean wasCommand = false; + if (text.startsWith(Constants.COMMAND_TRIGGER)) { + text = text.substring(1); // remove the / + // System.out.println("Checking command: " + text); + if (isConnection("/" + text)) { + if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { + System.out.println( + TextFX.colorize("Please set your name via /name before connecting", Color.RED)); + return true; + } + // replaces multiple spaces with a single space + // splits on the space after connect (gives us host and port) + // splits on : to get host as index 0 and port as index 1 + String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); + connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); + sendClientName(myUser.getClientName());// sync follow-up data (handshake) + wasCommand = true; + } else if (text.startsWith(Command.NAME.command)) { + text = text.replace(Command.NAME.command, "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a name as an argument", Color.RED)); + return true; + } + myUser.setClientName(text);// temporary until we get a response from the server + System.out.println(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), + Color.YELLOW)); + wasCommand = true; + } else if (text.equalsIgnoreCase(Command.LIST_USERS.command)) { + System.out.println(TextFX.colorize("Known clients:", Color.CYAN)); + knownClients.forEach((key, value) -> { + System.out.println(TextFX.colorize(String.format("%s%s", value.getDisplayName(), + key == myUser.getClientId() ? " (you)" : ""), Color.CYAN)); + }); + wasCommand = true; + } else if (Command.QUIT.command.equalsIgnoreCase(text)) { + close(); + wasCommand = true; + } else if (Command.DISCONNECT.command.equalsIgnoreCase(text)) { + sendDisconnect(); + wasCommand = true; + } else if (text.startsWith(Command.REVERSE.command)) { + text = text.replace(Command.REVERSE.command, "").trim(); + sendReverse(text); + wasCommand = true; + } else if (text.startsWith(Command.CREATE_ROOM.command)) { + text = text.replace(Command.CREATE_ROOM.command, "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + return true; + } + sendRoomAction(text, RoomAction.CREATE); + wasCommand = true; + } else if (text.startsWith(Command.JOIN_ROOM.command)) { + text = text.replace(Command.JOIN_ROOM.command, "").trim(); + if (text == null || text.length() == 0) { + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + return true; + } + sendRoomAction(text, RoomAction.JOIN); + wasCommand = true; + } else if (text.startsWith(Command.LEAVE_ROOM.command) || text.startsWith("leave")) { + // Note: Accounts for /leave and /leaveroom variants (or anything beginning with + // /leave) + sendRoomAction(text, RoomAction.LEAVE); + wasCommand = true; + } + } + return wasCommand; + } + + // Start Send*() methods + + /** + * Sends a room action to the server + * + * @param roomName + * @param roomAction (join, leave, create) + * @throws IOException + */ + private void sendRoomAction(String roomName, RoomAction roomAction) throws IOException { + Payload payload = new Payload(); + payload.setMessage(roomName); + switch (roomAction) { + case RoomAction.CREATE: + payload.setPayloadType(PayloadType.ROOM_CREATE); + break; + case RoomAction.JOIN: + payload.setPayloadType(PayloadType.ROOM_JOIN); + break; + case RoomAction.LEAVE: + payload.setPayloadType(PayloadType.ROOM_LEAVE); + break; + default: + System.out.println(TextFX.colorize("Invalid room action", Color.RED)); + break; + } + sendToServer(payload); + } + + /** + * Sends a reverse message action to the server + * + * @param message + * @throws IOException + */ + private void sendReverse(String message) throws IOException { + Payload payload = new Payload(); + payload.setMessage(message); + payload.setPayloadType(PayloadType.REVERSE); + sendToServer(payload); + + } + + /** + * Sends a disconnect action to the server + * + * @throws IOException + */ + private void sendDisconnect() throws IOException { + Payload payload = new Payload(); + payload.setPayloadType(PayloadType.DISCONNECT); + sendToServer(payload); + } + + /** + * Sends a message to the server + * + * @param message + * @throws IOException + */ + private void sendMessage(String message) throws IOException { + Payload payload = new Payload(); + payload.setMessage(message); + payload.setPayloadType(PayloadType.MESSAGE); + sendToServer(payload); + } + + /** + * Sends the client's name to the server (what the user desires to be called) + * + * @param name + * @throws IOException + */ + private void sendClientName(String name) throws IOException { + ConnectionPayload payload = new ConnectionPayload(); + payload.setClientName(name); + payload.setPayloadType(PayloadType.CLIENT_CONNECT); + sendToServer(payload); + } + + private void sendToServer(Payload payload) throws IOException { + if (isConnected()) { + out.writeObject(payload); + out.flush(); // good practice to ensure data is written out immediately + } else { + System.out.println( + "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); + } + } + // End Send*() methods + + public void start() throws IOException { + System.out.println("Client starting"); + + // Use CompletableFuture to run listenToInput() in a separate thread + CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); + + // Wait for inputFuture to complete to ensure proper termination + inputFuture.join(); + } + + /** + * Listens for messages from the server + */ + private void listenToServer() { + try { + while (isRunning && isConnected()) { + Payload fromServer = (Payload) in.readObject(); // blocking read + if (fromServer != null) { + processPayload(fromServer); + + } else { + System.out.println("Server disconnected"); + break; + } + } + } catch (ClassCastException | ClassNotFoundException cce) { + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); + } catch (IOException e) { + if (isRunning) { + System.out.println("Connection dropped"); + e.printStackTrace(); + } + } finally { + closeServerConnection(); + } + System.out.println("listenToServer thread stopped"); + } + + private void processPayload(Payload payload) { + switch (payload.getPayloadType()) { + case CLIENT_CONNECT:// unused + break; + case CLIENT_ID: + processClientData(payload); + break; + case DISCONNECT: + processDisconnect(payload); + break; + case MESSAGE: + processMessage(payload); + break; + case REVERSE: + processReverse(payload); + break; + case ROOM_CREATE: // unused + break; + case ROOM_JOIN: + processRoomAction(payload); + break; + case ROOM_LEAVE: + processRoomAction(payload); + break; + case SYNC_CLIENT: + processRoomAction(payload); + break; + default: + System.out.println(TextFX.colorize("Unhandled payload type", Color.YELLOW)); + break; + + } + } + + // Start process*() methods + private void processClientData(Payload payload) { + if (myUser.getClientId() != Constants.DEFAULT_CLIENT_ID) { + System.out.println(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); + + } + myUser.setClientId(payload.getClientId()); + myUser.setClientName(((ConnectionPayload) payload).getClientName());// confirmation from Server + knownClients.put(myUser.getClientId(), myUser); + System.out.println(TextFX.colorize("Connected", Color.GREEN)); + } + + private void processDisconnect(Payload payload) { + if (payload.getClientId() == myUser.getClientId()) { + knownClients.clear(); + myUser.reset(); + System.out.println(TextFX.colorize("You disconnected", Color.RED)); + } else if (knownClients.containsKey(payload.getClientId())) { + User disconnectedUser = knownClients.remove(payload.getClientId()); + if (disconnectedUser != null) { + System.out.println(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), + Color.RED)); + } + } + + } + + private void processRoomAction(Payload payload) { + if (!(payload instanceof ConnectionPayload)) { + error("Invalid payload subclass for processRoomAction"); + return; + } + ConnectionPayload connectionPayload = (ConnectionPayload) payload; + // use DEFAULT_CLIENT_ID to clear knownClients (mostly for disconnect and room + // transitions) + if (connectionPayload.getClientId() == Constants.DEFAULT_CLIENT_ID) { + knownClients.clear(); + return; + } + switch (connectionPayload.getPayloadType()) { + + case ROOM_LEAVE: + // remove from map + if (knownClients.containsKey(connectionPayload.getClientId())) { + knownClients.remove(connectionPayload.getClientId()); + } + if (connectionPayload.getMessage() != null) { + System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); + } + + break; + case ROOM_JOIN: + if (connectionPayload.getMessage() != null) { + System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); + } + // cascade to manage knownClients + case SYNC_CLIENT: + // add to map + if (!knownClients.containsKey(connectionPayload.getClientId())) { + User user = new User(); + user.setClientId(connectionPayload.getClientId()); + user.setClientName(connectionPayload.getClientName()); + knownClients.put(connectionPayload.getClientId(), user); + } + break; + default: + error("Invalid payload type for processRoomAction"); + break; + } + } + + private void processMessage(Payload payload) { + System.out.println(TextFX.colorize(payload.getMessage(), Color.BLUE)); + } + + private void processReverse(Payload payload) { + System.out.println(TextFX.colorize(payload.getMessage(), Color.PURPLE)); + } + // End process*() methods + + /** + * Listens for keyboard input from the user + */ + private void listenToInput() { + try (Scanner si = new Scanner(System.in)) { + System.out.println("Waiting for input"); // moved here to avoid console spam + while (isRunning) { // Run until isRunning is false + String userInput = si.nextLine(); + if (!processClientCommand(userInput)) { + sendMessage(userInput); + } + } + } catch (IOException ioException) { + System.out.println("Error in listentToInput()"); + ioException.printStackTrace(); + } + System.out.println("listenToInput thread stopped"); + } + + /** + * Closes the client connection and associated resources + */ + private void close() { + isRunning = false; + closeServerConnection(); + System.out.println("Client terminated"); + // System.exit(0); // Terminate the application + } + + /** + * Closes the server connection and associated resources + */ + private void closeServerConnection() { + try { + if (out != null) { + System.out.println("Closing output stream"); + out.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (in != null) { + System.out.println("Closing input stream"); + in.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + try { + if (server != null) { + System.out.println("Closing connection"); + server.close(); + System.out.println("Closed socket"); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + Client client = Client.INSTANCE; + try { + client.start(); + } catch (IOException e) { + System.out.println("Exception from main()"); + e.printStackTrace(); + } + } +} diff --git a/Project/Command.java b/Project/Command.java new file mode 100644 index 0000000..008712c --- /dev/null +++ b/Project/Command.java @@ -0,0 +1,33 @@ + +package M5.Part5; + +import java.util.HashMap; + +public enum Command { + QUIT("quit"), + DISCONNECT("disconnect"), + LOGOUT("logout"), + LOGOFF("logoff"), + REVERSE("reverse"), + CREATE_ROOM("createroom"), + LEAVE_ROOM("leaveroom"), + JOIN_ROOM("joinroom"), + NAME("name"), + LIST_USERS("users"); + + private static final HashMap BY_COMMAND = new HashMap<>(); + static { + for (Command e : values()) { + BY_COMMAND.put(e.command, e); + } + } + public final String command; + + private Command(String command) { + this.command = command; + } + + public static Command stringToCommand(String command) { + return BY_COMMAND.get(command); + } +} diff --git a/Project/ConnectionPayload.java b/Project/ConnectionPayload.java new file mode 100644 index 0000000..a5a5c9e --- /dev/null +++ b/Project/ConnectionPayload.java @@ -0,0 +1,28 @@ + +package M5.Part5; + +public class ConnectionPayload extends Payload { + private String clientName; + + /** + * @return the clientName + */ + public String getClientName() { + return clientName; + } + + /** + * @param clientName the clientName to set + */ + public void setClientName(String clientName) { + this.clientName = clientName; + } + + @Override + public String toString() { + return super.toString() + + String.format(" ClientName: [%s]", + getClientName()); + } + +} diff --git a/Project/Constants.java b/Project/Constants.java new file mode 100644 index 0000000..86aa03a --- /dev/null +++ b/Project/Constants.java @@ -0,0 +1,8 @@ + +package M5.Part5; + +public abstract class Constants { + final public static String COMMAND_TRIGGER = "/"; + final public static String SINGLE_SPACE = " "; + final public static long DEFAULT_CLIENT_ID = -1; +} diff --git a/Project/CustomIT114Exception.java b/Project/CustomIT114Exception.java new file mode 100644 index 0000000..efee261 --- /dev/null +++ b/Project/CustomIT114Exception.java @@ -0,0 +1,12 @@ + +package M5.Part5; + +public abstract class CustomIT114Exception extends Exception { + public CustomIT114Exception(String message) { + super(message); + } + + public CustomIT114Exception(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Project/DuplicateRoomException.java b/Project/DuplicateRoomException.java new file mode 100644 index 0000000..ef6230e --- /dev/null +++ b/Project/DuplicateRoomException.java @@ -0,0 +1,13 @@ + +package M5.Part5; + +public class DuplicateRoomException extends CustomIT114Exception { + public DuplicateRoomException(String message) { + super(message); + } + + public DuplicateRoomException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Project/Payload.java b/Project/Payload.java new file mode 100644 index 0000000..1191be1 --- /dev/null +++ b/Project/Payload.java @@ -0,0 +1,57 @@ + +package M5.Part5; + +import java.io.Serializable; + +public class Payload implements Serializable { + private PayloadType payloadType; + private long clientId; + private String message; + + /** + * @return the payloadType + */ + public PayloadType getPayloadType() { + return payloadType; + } + + /** + * @param payloadType the payloadType to set + */ + public void setPayloadType(PayloadType payloadType) { + this.payloadType = payloadType; + } + + /** + * @return the clientId + */ + public long getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(long clientId) { + this.clientId = clientId; + } + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + /** + * @param message the message to set + */ + public void setMessage(String message) { + this.message = message; + } + + @Override + public String toString() { + return String.format("Payload[%s] Client Id [%s] Message: [%s]", getPayloadType(), getClientId(), getMessage()); + } +} diff --git a/Project/PayloadType.java b/Project/PayloadType.java new file mode 100644 index 0000000..6fd7e8a --- /dev/null +++ b/Project/PayloadType.java @@ -0,0 +1,15 @@ + +package M5.Part5; + +public enum PayloadType { + CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data + // [name]) + CLIENT_ID, // server sending client id + SYNC_CLIENT, // silent syncing of clients in room + DISCONNECT, // distinct disconnect action + ROOM_CREATE, + ROOM_JOIN, + ROOM_LEAVE, + REVERSE, + MESSAGE // sender and message +} diff --git a/Project/Room.java b/Project/Room.java new file mode 100644 index 0000000..18c7c53 --- /dev/null +++ b/Project/Room.java @@ -0,0 +1,264 @@ + +package M5.Part5; + +import java.util.concurrent.ConcurrentHashMap; + +import M5.Part5.TextFX.Color; + +public class Room implements AutoCloseable { + private final String name;// unique name of the Room + private volatile boolean isRunning = false; + private final ConcurrentHashMap clientsInRoom = new ConcurrentHashMap(); + + public final static String LOBBY = "lobby"; + + private void info(String message) { + System.out.println(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); + } + + public Room(String name) { + this.name = name; + isRunning = true; + info("Created"); + } + + public String getName() { + return this.name; + } + + protected synchronized void addClient(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + if (clientsInRoom.containsKey(client.getClientId())) { + info("Attempting to add a client that already exists in the room"); + return; + } + clientsInRoom.put(client.getClientId(), client); + client.setCurrentRoom(this); + client.sendResetUserList(); + syncExistingClients(client); + // notify clients of someone joining + joinStatusRelay(client, true); + + } + + protected synchronized void removeClient(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + if (!clientsInRoom.containsKey(client.getClientId())) { + info("Attempting to remove a client that doesn't exist in the room"); + return; + } + ServerThread removedClient = clientsInRoom.get(client.getClientId()); + if (removedClient != null) { + // notify clients of someone joining + joinStatusRelay(removedClient, false); + clientsInRoom.remove(client.getClientId()); + autoCleanup(); + } + } + + private void syncExistingClients(ServerThread incomingClient) { + clientsInRoom.values().forEach(serverThread -> { + if (serverThread.getClientId() != incomingClient.getClientId()) { + boolean failedToSync = !incomingClient.sendClientInfo(serverThread.getClientId(), + serverThread.getClientName(), RoomAction.JOIN, true); + if (failedToSync) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + } + }); + } + + private void joinStatusRelay(ServerThread client, boolean didJoin) { + clientsInRoom.values().removeIf(serverThread -> { + String formattedMessage = String.format("Room[%s] %s %s the room", + getName(), + client.getClientId() == serverThread.getClientId() ? "You" + : client.getDisplayName(), + didJoin ? "joined" : "left"); + final long senderId = client == null ? Constants.DEFAULT_CLIENT_ID : client.getClientId(); + // Share info of the client joining or leaving the room + boolean failedToSync = !serverThread.sendClientInfo(client.getClientId(), + client.getClientName(), didJoin ? RoomAction.JOIN : RoomAction.LEAVE); + // Send the server generated message to the current client + boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); + if (failedToSend || failedToSync) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + /** + * Sends a basic String message from the sender to all connectedClients + * Internally calls processCommand and evaluates as necessary. + * Note: Clients that fail to receive a message get removed from + * connectedClients. + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + protected synchronized void relay(ServerThread sender, String message) { + if (!isRunning) { // block action if Room isn't running + return; + } + + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? String.format("Room[%s]", getName()) + : sender.getDisplayName(); + final long senderId = sender == null ? Constants.DEFAULT_CLIENT_ID : sender.getClientId(); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't be changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + + // loop over clients and send out the message; remove client if message failed + // to be sent + // Note: this uses a lambda expression for each item in the values() collection, + // it's one way we can safely remove items during iteration + info(String.format("sending message to %s recipients: %s", clientsInRoom.size(), formattedMessage)); + + clientsInRoom.values().removeIf(serverThread -> { + boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + return failedToSend; + }); + } + + /** + * Takes a ServerThread and removes them from the Server + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param client + */ + private synchronized void disconnect(ServerThread client) { + if (!isRunning) { // block action if Room isn't running + return; + } + ServerThread disconnectingServerThread = clientsInRoom.remove(client.getClientId()); + if (disconnectingServerThread != null) { + + clientsInRoom.values().removeIf(serverThread -> { + if (serverThread.getClientId() == disconnectingServerThread.getClientId()) { + return true; + } + boolean failedToSend = !serverThread.sendClientInfo(disconnectingServerThread.getClientId(), + disconnectingServerThread.getClientName(), RoomAction.LEAVE); + if (failedToSend) { + System.out.println( + String.format("Removing disconnected %s from list", serverThread.getDisplayName())); + disconnect(serverThread); + } + return failedToSend; + }); + relay(null, disconnectingServerThread.getDisplayName() + " disconnected"); + disconnectingServerThread.disconnect(); + } + autoCleanup(); + } + + protected synchronized void disconnectAll() { + info("Disconnect All triggered"); + if (!isRunning) { + return; + } + clientsInRoom.values().removeIf(client -> { + disconnect(client); + return true; + }); + info("Disconnect All finished"); + } + + /** + * Attempts to close the room to free up resources if it's empty + */ + private void autoCleanup() { + if (!Room.LOBBY.equalsIgnoreCase(name) && clientsInRoom.isEmpty()) { + close(); + } + } + + @Override + public void close() { + // attempt to gracefully close and migrate clients + if (!clientsInRoom.isEmpty()) { + relay(null, "Room is shutting down, migrating to lobby"); + info(String.format("migrating %s clients", clientsInRoom.size())); + clientsInRoom.values().removeIf(client -> { + try { + Server.INSTANCE.joinRoom(Room.LOBBY, client); + } catch (RoomNotFoundException e) { + e.printStackTrace(); + // TODO, fill in, this shouldn't happen though + } + return true; + }); + } + Server.INSTANCE.removeRoom(this); + isRunning = false; + clientsInRoom.clear(); + info(String.format("closed")); + } + + // start handle methods + public void handleCreateRoom(ServerThread sender, String roomName) { + try { + Server.INSTANCE.createRoom(roomName); + Server.INSTANCE.joinRoom(roomName, sender); + } catch (RoomNotFoundException e) { + info("Room wasn't found (this shouldn't happen)"); + e.printStackTrace(); + } catch (DuplicateRoomException e) { + sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s already exists", roomName)); + } + } + + public void handleJoinRoom(ServerThread sender, String roomName) { + try { + Server.INSTANCE.joinRoom(roomName, sender); + } catch (RoomNotFoundException e) { + sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s doesn't exist", roomName)); + } + } + + protected synchronized void handleDisconnect(BaseServerThread sender) { + handleDisconnect((ServerThread) sender); + } + + /** + * Expose access to the disconnect action + * + * @param serverThread + */ + protected synchronized void handleDisconnect(ServerThread sender) { + disconnect(sender); + } + + protected synchronized void handleReverseText(ServerThread sender, String text) { + StringBuilder sb = new StringBuilder(text); + sb.reverse(); + String rev = sb.toString(); + relay(sender, rev); + } + + protected synchronized void handleMessage(ServerThread sender, String text) { + relay(sender, text); + } + // end handle methods +} diff --git a/Project/RoomAction.java b/Project/RoomAction.java new file mode 100644 index 0000000..92bde03 --- /dev/null +++ b/Project/RoomAction.java @@ -0,0 +1,6 @@ + +package M5.Part5; + +public enum RoomAction { + CREATE, JOIN, LEAVE +} diff --git a/Project/RoomNotFoundException.java b/Project/RoomNotFoundException.java new file mode 100644 index 0000000..eb90a20 --- /dev/null +++ b/Project/RoomNotFoundException.java @@ -0,0 +1,14 @@ + +package M5.Part5; + +public class RoomNotFoundException extends CustomIT114Exception { + + public RoomNotFoundException(String message) { + super(message); + } + + public RoomNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/Project/Server.java b/Project/Server.java new file mode 100644 index 0000000..7a2cee5 --- /dev/null +++ b/Project/Server.java @@ -0,0 +1,200 @@ + +package M5.Part5; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ConcurrentHashMap; + +import M5.Part5.TextFX.Color; + +public enum Server { + INSTANCE; // Singleton instance + + private int port = 3000; + // connected clients + // Use ConcurrentHashMap for thread-safe client management + // The key is the unique Room name and the Room is the instance + private final ConcurrentHashMap rooms = new ConcurrentHashMap<>(); + private boolean isRunning = true; + private long nextClientId = 0; + + private void info(String message) { + System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); + } + + private Server() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + info("JVM is shutting down. Perform cleanup tasks."); + shutdown(); + })); + } + + /** + * Gracefully disconnect clients + */ + private void shutdown() { + try { + // chose removeIf over forEach to avoid potential + // ConcurrentModificationException + // since empty rooms tell the server to remove themselves + rooms.values().removeIf(room -> { + room.disconnectAll(); + return true; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void start(int port) { + this.port = port; + // server listening + info("Listening on port " + this.port); + // Simplified client connection loop + try (ServerSocket serverSocket = new ServerSocket(port)) { + createRoom(Room.LOBBY);// create the first room (lobby) + while (isRunning) { + info("Waiting for next client"); + Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection + info("Client connected"); + // wrap socket in a ServerThread, pass a callback to notify the Server when + // they're initialized + ServerThread serverThread = new ServerThread(incomingClient, this::onServerThreadInitialized); + // start the thread (typically an external entity manages the lifecycle and we + // don't have the thread start itself) + serverThread.start(); + // Note: We don't yet add the ServerThread reference to our connectedClients map + } + } catch (DuplicateRoomException e) { + System.err.println(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); + } catch (IOException e) { + System.err.println(TextFX.colorize("Error accepting connection", Color.RED)); + e.printStackTrace(); + } finally { + info("Closing server socket"); + } + } + + /** + * Callback passed to ServerThread to inform Server they're ready to receive + * data + * + * @param serverThread + */ + private void onServerThreadInitialized(ServerThread serverThread) { + // Generate Server controlled clientId + nextClientId = Math.max(++nextClientId, 1); + serverThread.setClientId(nextClientId); + serverThread.sendClientId();// syncs the data to the Client + // add initialized client to the lobby + info(String.format("*%s initialized*", serverThread.getDisplayName())); + try { + joinRoom(Room.LOBBY, serverThread); + info(String.format("*%s added to Lobby*", serverThread.getDisplayName())); + } catch (RoomNotFoundException e) { + info(String.format("*Error adding %s to Lobby*", serverThread.getDisplayName())); + e.printStackTrace(); + } + } + + /** + * Attempts to create a new Room and add it to the tracked rooms collection + * + * @param name Unique name of the room + * @return true if it was created and false if it wasn't + * @throws DuplicateRoomException + */ + protected void createRoom(String name) throws DuplicateRoomException { + final String nameCheck = name.toLowerCase(); + if (rooms.containsKey(nameCheck)) { + throw new DuplicateRoomException(String.format("Room %s already exists", name)); + } + Room room = new Room(name); + rooms.put(nameCheck, room); + info(String.format("Created new Room %s", name)); + } + + /** + * Attempts to move a client (ServerThread) between rooms + * + * @param name the target room to join + * @param client the client moving + * @throws RoomNotFoundException + * + */ + protected void joinRoom(String name, ServerThread client) throws RoomNotFoundException { + final String nameCheck = name.toLowerCase(); + if (!rooms.containsKey(nameCheck)) { + throw new RoomNotFoundException(String.format("Room %s wasn't found", name)); + } + Room currentRoom = client.getCurrentRoom(); + if (currentRoom != null) { + info("Removing client from previous Room " + currentRoom.getName()); + currentRoom.removeClient(client); + } + Room next = rooms.get(nameCheck); + next.addClient(client); + } + + protected void removeRoom(Room room) { + rooms.remove(room.getName().toLowerCase()); + info(String.format("Removed room %s", room.getName())); + } + + /** + * + *

+ * Note: Not a common use-case; just updated for example sake. + *

+ * Relays the message from the sender to all rooms + * Adding the synchronized keyword ensures that only one thread can execute + * these methods at a time, + * preventing concurrent modification issues and ensuring thread safety + * + * @param message + * @param sender ServerThread (client) sending the message or null if it's a + * server-generated message + */ + private synchronized void relayToAllRooms(ServerThread sender, String message) { + // Note: any desired changes to the message must be done before this line + String senderString = sender == null ? "Server" : sender.getDisplayName(); + // Note: formattedMessage must be final (or effectively final) since outside + // scope can't changed inside a callback function (see removeIf() below) + final String formattedMessage = String.format("%s: %s", senderString, message); + // end temp identifier + + // loop over Rooms and send out the message + // Note: this uses a lambda expression for each item in the values() collection + + rooms.values().forEach(room -> { + room.relay(sender, formattedMessage); + }); + } + + /** + * Used to send a message to all Rooms. + * This is just an example and we likely won't be using this + * + * @param sender + * @param message + */ + public synchronized void broadcastMessageToAllRooms(ServerThread sender, String message) { + relayToAllRooms(sender, message); + } + + public static void main(String[] args) { + System.out.println("Server Starting"); + Server server = Server.INSTANCE; + int port = 3000; + try { + port = Integer.parseInt(args[0]); + } catch (Exception e) { + // can ignore, will either be index out of bounds or type mismatch + // will default to the defined value prior to the try/catch + } + server.start(port); + System.out.println("Server Stopped"); + } + +} diff --git a/Project/ServerThread.java b/Project/ServerThread.java new file mode 100644 index 0000000..24e5c08 --- /dev/null +++ b/Project/ServerThread.java @@ -0,0 +1,167 @@ + +package M5.Part5; + +import java.net.Socket; +import java.util.Objects; +import java.util.function.Consumer; +import M5.Part5.TextFX.Color; + +/** + * A server-side representation of a single client + */ +public class ServerThread extends BaseServerThread { + private Consumer onInitializationComplete; // callback to inform when this object is ready + + /** + * A wrapper method so we don't need to keep typing out the long/complex sysout + * line inside + * + * @param message + */ + protected void info(String message) { + System.out.println(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); + } + + /** + * Wraps the Socket connection and takes a Server reference and a callback + * + * @param myClient + * @param server + * @param onInitializationComplete method to inform listener that this object is + * ready + */ + protected ServerThread(Socket myClient, Consumer onInitializationComplete) { + Objects.requireNonNull(myClient, "Client socket cannot be null"); + Objects.requireNonNull(onInitializationComplete, "callback cannot be null"); + info("ServerThread created"); + // get communication channels to single client + this.client = myClient; + // this.clientId = this.threadId(); // An id associated with the thread + // instance, used as a temporary identifier + this.onInitializationComplete = onInitializationComplete; + + } + + // Start Send*() Methods + protected boolean sendDisconnect(long clientId) { + Payload payload = new Payload(); + payload.setClientId(clientId); + payload.setPayloadType(PayloadType.DISCONNECT); + return sendToClient(payload); + } + + protected boolean sendResetUserList() { + return sendClientInfo(Constants.DEFAULT_CLIENT_ID, null, RoomAction.JOIN); + } + + /** + * Syncs Client Info (id, name, join status) to the client + * + * @param clientId use -1 for reset/clear + * @param clientName + * @param action RoomAction of Join or Leave + * @return true for successful send + */ + protected boolean sendClientInfo(long clientId, String clientName, RoomAction action) { + return sendClientInfo(clientId, clientName, action, false); + } + + /** + * Syncs Client Info (id, name, join status) to the client + * + * @param clientId use -1 for reset/clear + * @param clientName + * @param action RoomAction of Join or Leave + * @param isSync True is used to not show output on the client side (silent + * sync) + * @return true for successful send + */ + protected boolean sendClientInfo(long clientId, String clientName, RoomAction action, boolean isSync) { + ConnectionPayload payload = new ConnectionPayload(); + switch (action) { + case JOIN: + payload.setPayloadType(PayloadType.ROOM_JOIN); + break; + case LEAVE: + payload.setPayloadType(PayloadType.ROOM_LEAVE); + break; + default: + break; + } + if (isSync) { + payload.setPayloadType(PayloadType.SYNC_CLIENT); + } + payload.setClientId(clientId); + payload.setClientName(clientName); + return sendToClient(payload); + } + + /** + * Sends this client's id to the client. + * This will be a successfully connection handshake + * + * @return true for successful send + */ + protected boolean sendClientId() { + ConnectionPayload payload = new ConnectionPayload(); + payload.setPayloadType(PayloadType.CLIENT_ID); + payload.setClientId(getClientId()); + payload.setClientName(getClientName());// Can be used as a Server-side override of username (i.e., profanity + // filter) + return sendToClient(payload); + } + + /** + * Sends a message to the client + * + * @param clientId who it's from + * @param message + * @return true for successful send + */ + protected boolean sendMessage(long clientId, String message) { + Payload payload = new Payload(); + payload.setPayloadType(PayloadType.MESSAGE); + payload.setMessage(message); + payload.setClientId(clientId); + return sendToClient(payload); + } + + // End Send*() Methods + @Override + protected void processPayload(Payload incoming) { + + switch (incoming.getPayloadType()) { + case CLIENT_CONNECT: + setClientName(((ConnectionPayload) incoming).getClientName().trim()); + + break; + case DISCONNECT: + currentRoom.handleDisconnect(this); + break; + case MESSAGE: + currentRoom.handleMessage(this, incoming.getMessage()); + break; + case REVERSE: + currentRoom.handleReverseText(this, incoming.getMessage()); + break; + case ROOM_CREATE: + currentRoom.handleCreateRoom(this, incoming.getMessage()); + break; + case ROOM_JOIN: + currentRoom.handleJoinRoom(this, incoming.getMessage()); + break; + case ROOM_LEAVE: + currentRoom.handleJoinRoom(this, Room.LOBBY); + break; + default: + System.out.println(TextFX.colorize("Unknown payload type received", Color.RED)); + break; + } + } + + @Override + protected void onInitialized() { + // once receiving the desired client name the object is ready + onInitializationComplete.accept(this); + } +} diff --git a/Project/TextFX.java b/Project/TextFX.java new file mode 100644 index 0000000..dc717a3 --- /dev/null +++ b/Project/TextFX.java @@ -0,0 +1,72 @@ +package M5.Part5; + +/** + * Utility to attempt to provide colored text in the terminal. + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ +public abstract class TextFX { + + /** + * TextFX.Color list of available colors + *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ */ + public enum Color { + BLACK("\033[0;30m"), + RED("\033[0;31m"), + GREEN("\033[0;32m"), + YELLOW("\033[0;33m"), + BLUE("\033[0;34m"), + PURPLE("\033[0;35m"), + CYAN("\033[0;36m"), + WHITE("\033[0;37m"); + + private final String code; + + Color(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + } + + public static final String RESET = "\033[0m"; + + /** + * Generates a String with the original message wrapped in the ASCII of the + * color and RESET + * + *

+ * Note: May not work for all terminals + *

+ *

+ * Important: This does not satisfy the text formatting feature/requirement for + * chatroom projects. + *

+ * + * @param text Input text to colorize + * @param color Enum of Color choice from TextFX.Color + * @return wrapped String + */ + public static String colorize(String text, Color color) { + StringBuilder builder = new StringBuilder(); + builder.append(color.getCode()); + builder.append(text); + builder.append(RESET); + return builder.toString(); + } + + public static void main(String[] args) { + // Example usage: + System.out.println(TextFX.colorize("Hello, world!", Color.RED)); + System.out.println(TextFX.colorize("This is some blue text.", Color.BLUE)); + System.out.println(TextFX.colorize("And this is green!", Color.GREEN)); + } +} \ No newline at end of file diff --git a/Project/User.java b/Project/User.java new file mode 100644 index 0000000..a180490 --- /dev/null +++ b/Project/User.java @@ -0,0 +1,43 @@ +package M5.Part5; + +public class User { + private long clientId = Constants.DEFAULT_CLIENT_ID; + private String clientName; + + /** + * @return the clientId + */ + public long getClientId() { + return clientId; + } + + /** + * @param clientId the clientId to set + */ + public void setClientId(long clientId) { + this.clientId = clientId; + } + + /** + * @return the username + */ + public String getClientName() { + return clientName; + } + + /** + * @param username the username to set + */ + public void setClientName(String username) { + this.clientName = username; + } + + public String getDisplayName() { + return String.format("%s#%s", this.clientName, this.clientId); + } + + public void reset() { + this.clientId = Constants.DEFAULT_CLIENT_ID; + this.clientName = null; + } +} \ No newline at end of file From c54b1629d0150fb79b20a39ac4e2866fde7c0305 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 16:34:27 -0500 Subject: [PATCH 24/56] renamed files --- Project/{ => Client}/Client.java | 4 ++-- Project/{ => Common}/Command.java | 0 Project/{ => Common}/ConnectionPayload.java | 0 Project/{ => Common}/Constants.java | 0 Project/{ => Common}/Payload.java | 0 Project/{ => Common}/PayloadType.java | 0 Project/{ => Common}/RoomAction.java | 0 Project/{ => Common}/TextFX.java | 0 Project/{ => Common}/User.java | 0 Project/{ => Exceptions}/CustomIT114Exception.java | 0 Project/{ => Exceptions}/DuplicateRoomException.java | 0 Project/{ => Exceptions}/RoomNotFoundException.java | 0 Project/{ => Server}/BaseServerThread.java | 0 Project/{ => Server}/Room.java | 2 +- Project/{ => Server}/Server.java | 2 +- Project/{ => Server}/ServerThread.java | 2 +- 16 files changed, 5 insertions(+), 5 deletions(-) rename Project/{ => Client}/Client.java (99%) rename Project/{ => Common}/Command.java (100%) rename Project/{ => Common}/ConnectionPayload.java (100%) rename Project/{ => Common}/Constants.java (100%) rename Project/{ => Common}/Payload.java (100%) rename Project/{ => Common}/PayloadType.java (100%) rename Project/{ => Common}/RoomAction.java (100%) rename Project/{ => Common}/TextFX.java (100%) rename Project/{ => Common}/User.java (100%) rename Project/{ => Exceptions}/CustomIT114Exception.java (100%) rename Project/{ => Exceptions}/DuplicateRoomException.java (100%) rename Project/{ => Exceptions}/RoomNotFoundException.java (100%) rename Project/{ => Server}/BaseServerThread.java (100%) rename Project/{ => Server}/Room.java (99%) rename Project/{ => Server}/Server.java (99%) rename Project/{ => Server}/ServerThread.java (99%) diff --git a/Project/Client.java b/Project/Client/Client.java similarity index 99% rename from Project/Client.java rename to Project/Client/Client.java index bb912b2..55604fa 100644 --- a/Project/Client.java +++ b/Project/Client/Client.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project; import java.io.IOException; import java.io.ObjectInputStream; @@ -12,7 +12,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import M5.Part5.TextFX.Color; +import Project.Common.TextFX.Color; /** * Demoing bi-directional communication between client and server in a diff --git a/Project/Command.java b/Project/Common/Command.java similarity index 100% rename from Project/Command.java rename to Project/Common/Command.java diff --git a/Project/ConnectionPayload.java b/Project/Common/ConnectionPayload.java similarity index 100% rename from Project/ConnectionPayload.java rename to Project/Common/ConnectionPayload.java diff --git a/Project/Constants.java b/Project/Common/Constants.java similarity index 100% rename from Project/Constants.java rename to Project/Common/Constants.java diff --git a/Project/Payload.java b/Project/Common/Payload.java similarity index 100% rename from Project/Payload.java rename to Project/Common/Payload.java diff --git a/Project/PayloadType.java b/Project/Common/PayloadType.java similarity index 100% rename from Project/PayloadType.java rename to Project/Common/PayloadType.java diff --git a/Project/RoomAction.java b/Project/Common/RoomAction.java similarity index 100% rename from Project/RoomAction.java rename to Project/Common/RoomAction.java diff --git a/Project/TextFX.java b/Project/Common/TextFX.java similarity index 100% rename from Project/TextFX.java rename to Project/Common/TextFX.java diff --git a/Project/User.java b/Project/Common/User.java similarity index 100% rename from Project/User.java rename to Project/Common/User.java diff --git a/Project/CustomIT114Exception.java b/Project/Exceptions/CustomIT114Exception.java similarity index 100% rename from Project/CustomIT114Exception.java rename to Project/Exceptions/CustomIT114Exception.java diff --git a/Project/DuplicateRoomException.java b/Project/Exceptions/DuplicateRoomException.java similarity index 100% rename from Project/DuplicateRoomException.java rename to Project/Exceptions/DuplicateRoomException.java diff --git a/Project/RoomNotFoundException.java b/Project/Exceptions/RoomNotFoundException.java similarity index 100% rename from Project/RoomNotFoundException.java rename to Project/Exceptions/RoomNotFoundException.java diff --git a/Project/BaseServerThread.java b/Project/Server/BaseServerThread.java similarity index 100% rename from Project/BaseServerThread.java rename to Project/Server/BaseServerThread.java diff --git a/Project/Room.java b/Project/Server/Room.java similarity index 99% rename from Project/Room.java rename to Project/Server/Room.java index 18c7c53..980076f 100644 --- a/Project/Room.java +++ b/Project/Server/Room.java @@ -3,7 +3,7 @@ import java.util.concurrent.ConcurrentHashMap; -import M5.Part5.TextFX.Color; +import Project.Common.TextFX.Color; public class Room implements AutoCloseable { private final String name;// unique name of the Room diff --git a/Project/Server.java b/Project/Server/Server.java similarity index 99% rename from Project/Server.java rename to Project/Server/Server.java index 7a2cee5..2596e8f 100644 --- a/Project/Server.java +++ b/Project/Server/Server.java @@ -6,7 +6,7 @@ import java.net.Socket; import java.util.concurrent.ConcurrentHashMap; -import M5.Part5.TextFX.Color; +import Project.Common.TextFX.Color; public enum Server { INSTANCE; // Singleton instance diff --git a/Project/ServerThread.java b/Project/Server/ServerThread.java similarity index 99% rename from Project/ServerThread.java rename to Project/Server/ServerThread.java index 24e5c08..f4ccd0d 100644 --- a/Project/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -4,7 +4,7 @@ import java.net.Socket; import java.util.Objects; import java.util.function.Consumer; -import M5.Part5.TextFX.Color; +import Project.Common.TextFX.Color; /** * A server-side representation of a single client From e9be04d2822f8e877274e743fbe7d0ccb0d4a7c0 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 17:03:59 -0500 Subject: [PATCH 25/56] renamed packages --- Project/Common/Command.java | 2 +- Project/Common/ConnectionPayload.java | 2 +- Project/Common/Constants.java | 2 +- Project/Common/Payload.java | 2 +- Project/Common/PayloadType.java | 2 +- Project/Common/RoomAction.java | 2 +- Project/Common/TextFX.java | 2 +- Project/Common/User.java | 2 +- Project/Exceptions/CustomIT114Exception.java | 2 +- Project/Exceptions/DuplicateRoomException.java | 2 +- Project/Exceptions/RoomNotFoundException.java | 2 +- Project/Server/BaseServerThread.java | 2 +- Project/Server/Room.java | 2 +- Project/Server/Server.java | 2 +- Project/Server/ServerThread.java | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Project/Common/Command.java b/Project/Common/Command.java index 008712c..ff0f914 100644 --- a/Project/Common/Command.java +++ b/Project/Common/Command.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project import java.util.HashMap; diff --git a/Project/Common/ConnectionPayload.java b/Project/Common/ConnectionPayload.java index a5a5c9e..ea3244a 100644 --- a/Project/Common/ConnectionPayload.java +++ b/Project/Common/ConnectionPayload.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public class ConnectionPayload extends Payload { private String clientName; diff --git a/Project/Common/Constants.java b/Project/Common/Constants.java index 86aa03a..db35958 100644 --- a/Project/Common/Constants.java +++ b/Project/Common/Constants.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public abstract class Constants { final public static String COMMAND_TRIGGER = "/"; diff --git a/Project/Common/Payload.java b/Project/Common/Payload.java index 1191be1..08929a3 100644 --- a/Project/Common/Payload.java +++ b/Project/Common/Payload.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project import java.io.Serializable; diff --git a/Project/Common/PayloadType.java b/Project/Common/PayloadType.java index 6fd7e8a..02acf48 100644 --- a/Project/Common/PayloadType.java +++ b/Project/Common/PayloadType.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public enum PayloadType { CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data diff --git a/Project/Common/RoomAction.java b/Project/Common/RoomAction.java index 92bde03..bdb8cef 100644 --- a/Project/Common/RoomAction.java +++ b/Project/Common/RoomAction.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public enum RoomAction { CREATE, JOIN, LEAVE diff --git a/Project/Common/TextFX.java b/Project/Common/TextFX.java index dc717a3..56b5c71 100644 --- a/Project/Common/TextFX.java +++ b/Project/Common/TextFX.java @@ -1,4 +1,4 @@ -package M5.Part5; +package Project /** * Utility to attempt to provide colored text in the terminal. diff --git a/Project/Common/User.java b/Project/Common/User.java index a180490..f760a5d 100644 --- a/Project/Common/User.java +++ b/Project/Common/User.java @@ -1,4 +1,4 @@ -package M5.Part5; +package Project public class User { private long clientId = Constants.DEFAULT_CLIENT_ID; diff --git a/Project/Exceptions/CustomIT114Exception.java b/Project/Exceptions/CustomIT114Exception.java index efee261..84eaad6 100644 --- a/Project/Exceptions/CustomIT114Exception.java +++ b/Project/Exceptions/CustomIT114Exception.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public abstract class CustomIT114Exception extends Exception { public CustomIT114Exception(String message) { diff --git a/Project/Exceptions/DuplicateRoomException.java b/Project/Exceptions/DuplicateRoomException.java index ef6230e..386f75d 100644 --- a/Project/Exceptions/DuplicateRoomException.java +++ b/Project/Exceptions/DuplicateRoomException.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public class DuplicateRoomException extends CustomIT114Exception { public DuplicateRoomException(String message) { diff --git a/Project/Exceptions/RoomNotFoundException.java b/Project/Exceptions/RoomNotFoundException.java index eb90a20..f0074da 100644 --- a/Project/Exceptions/RoomNotFoundException.java +++ b/Project/Exceptions/RoomNotFoundException.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project public class RoomNotFoundException extends CustomIT114Exception { diff --git a/Project/Server/BaseServerThread.java b/Project/Server/BaseServerThread.java index 2f2c08c..106528e 100644 --- a/Project/Server/BaseServerThread.java +++ b/Project/Server/BaseServerThread.java @@ -1,4 +1,4 @@ -package M5.Part5; +package Project import java.io.IOException; import java.io.ObjectInputStream; diff --git a/Project/Server/Room.java b/Project/Server/Room.java index 980076f..b2fb6dd 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project import java.util.concurrent.ConcurrentHashMap; diff --git a/Project/Server/Server.java b/Project/Server/Server.java index 2596e8f..55587f5 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project import java.io.IOException; import java.net.ServerSocket; diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index f4ccd0d..6e9ca3b 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -1,5 +1,5 @@ -package M5.Part5; +package Project import java.net.Socket; import java.util.Objects; From 0bd21c73be770a2fad559b48d4961632064ebeb8 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 17:06:46 -0500 Subject: [PATCH 26/56] fixed missing colon --- Project/Client/Client.java | 2 +- Project/Common/Command.java | 2 +- Project/Common/ConnectionPayload.java | 2 +- Project/Common/Constants.java | 2 +- Project/Common/Payload.java | 2 +- Project/Common/PayloadType.java | 2 +- Project/Common/RoomAction.java | 2 +- Project/Common/TextFX.java | 2 +- Project/Common/User.java | 2 +- Project/Exceptions/CustomIT114Exception.java | 2 +- Project/Exceptions/DuplicateRoomException.java | 2 +- Project/Exceptions/RoomNotFoundException.java | 2 +- Project/Server/BaseServerThread.java | 2 +- Project/Server/Room.java | 2 +- Project/Server/Server.java | 2 +- Project/Server/ServerThread.java | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 55604fa..ffb1189 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -1,5 +1,5 @@ -package Project; +package Project;; import java.io.IOException; import java.io.ObjectInputStream; diff --git a/Project/Common/Command.java b/Project/Common/Command.java index ff0f914..c497c9c 100644 --- a/Project/Common/Command.java +++ b/Project/Common/Command.java @@ -1,5 +1,5 @@ -package Project +package Project; import java.util.HashMap; diff --git a/Project/Common/ConnectionPayload.java b/Project/Common/ConnectionPayload.java index ea3244a..a2505d6 100644 --- a/Project/Common/ConnectionPayload.java +++ b/Project/Common/ConnectionPayload.java @@ -1,5 +1,5 @@ -package Project +package Project; public class ConnectionPayload extends Payload { private String clientName; diff --git a/Project/Common/Constants.java b/Project/Common/Constants.java index db35958..c204272 100644 --- a/Project/Common/Constants.java +++ b/Project/Common/Constants.java @@ -1,5 +1,5 @@ -package Project +package Project; public abstract class Constants { final public static String COMMAND_TRIGGER = "/"; diff --git a/Project/Common/Payload.java b/Project/Common/Payload.java index 08929a3..c396027 100644 --- a/Project/Common/Payload.java +++ b/Project/Common/Payload.java @@ -1,5 +1,5 @@ -package Project +package Project; import java.io.Serializable; diff --git a/Project/Common/PayloadType.java b/Project/Common/PayloadType.java index 02acf48..b25949b 100644 --- a/Project/Common/PayloadType.java +++ b/Project/Common/PayloadType.java @@ -1,5 +1,5 @@ -package Project +package Project; public enum PayloadType { CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data diff --git a/Project/Common/RoomAction.java b/Project/Common/RoomAction.java index bdb8cef..e6942a5 100644 --- a/Project/Common/RoomAction.java +++ b/Project/Common/RoomAction.java @@ -1,5 +1,5 @@ -package Project +package Project; public enum RoomAction { CREATE, JOIN, LEAVE diff --git a/Project/Common/TextFX.java b/Project/Common/TextFX.java index 56b5c71..7d94deb 100644 --- a/Project/Common/TextFX.java +++ b/Project/Common/TextFX.java @@ -1,4 +1,4 @@ -package Project +package Project; /** * Utility to attempt to provide colored text in the terminal. diff --git a/Project/Common/User.java b/Project/Common/User.java index f760a5d..329045c 100644 --- a/Project/Common/User.java +++ b/Project/Common/User.java @@ -1,4 +1,4 @@ -package Project +package Project; public class User { private long clientId = Constants.DEFAULT_CLIENT_ID; diff --git a/Project/Exceptions/CustomIT114Exception.java b/Project/Exceptions/CustomIT114Exception.java index 84eaad6..0fce691 100644 --- a/Project/Exceptions/CustomIT114Exception.java +++ b/Project/Exceptions/CustomIT114Exception.java @@ -1,5 +1,5 @@ -package Project +package Project; public abstract class CustomIT114Exception extends Exception { public CustomIT114Exception(String message) { diff --git a/Project/Exceptions/DuplicateRoomException.java b/Project/Exceptions/DuplicateRoomException.java index 386f75d..c63866f 100644 --- a/Project/Exceptions/DuplicateRoomException.java +++ b/Project/Exceptions/DuplicateRoomException.java @@ -1,5 +1,5 @@ -package Project +package Project; public class DuplicateRoomException extends CustomIT114Exception { public DuplicateRoomException(String message) { diff --git a/Project/Exceptions/RoomNotFoundException.java b/Project/Exceptions/RoomNotFoundException.java index f0074da..edd5aea 100644 --- a/Project/Exceptions/RoomNotFoundException.java +++ b/Project/Exceptions/RoomNotFoundException.java @@ -1,5 +1,5 @@ -package Project +package Project; public class RoomNotFoundException extends CustomIT114Exception { diff --git a/Project/Server/BaseServerThread.java b/Project/Server/BaseServerThread.java index 106528e..25c01a6 100644 --- a/Project/Server/BaseServerThread.java +++ b/Project/Server/BaseServerThread.java @@ -1,4 +1,4 @@ -package Project +package Project; import java.io.IOException; import java.io.ObjectInputStream; diff --git a/Project/Server/Room.java b/Project/Server/Room.java index b2fb6dd..5a0b988 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -1,5 +1,5 @@ -package Project +package Project; import java.util.concurrent.ConcurrentHashMap; diff --git a/Project/Server/Server.java b/Project/Server/Server.java index 55587f5..c38db79 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -1,5 +1,5 @@ -package Project +package Project; import java.io.IOException; import java.net.ServerSocket; diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index 6e9ca3b..e2c7296 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -1,5 +1,5 @@ -package Project +package Project; import java.net.Socket; import java.util.Objects; From 80601cd5f6e6bd028c7f41a14b3641014179ffb7 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 17:08:04 -0500 Subject: [PATCH 27/56] removed semicolon --- Project/Client/Client.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index ffb1189..55604fa 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -1,5 +1,5 @@ -package Project;; +package Project; import java.io.IOException; import java.io.ObjectInputStream; From 78aea2d705c43f9b4a02796ebd2d72a7a406bd96 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 17:16:33 -0500 Subject: [PATCH 28/56] color package fix --- Project/Client/Client.java | 2 +- Project/Server/Room.java | 2 +- Project/Server/Server.java | 2 +- Project/Server/ServerThread.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 55604fa..8756d03 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -12,7 +12,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import Project.Common.TextFX.Color; +import Project.Common.TextFX; /** * Demoing bi-directional communication between client and server in a diff --git a/Project/Server/Room.java b/Project/Server/Room.java index 5a0b988..9896715 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -3,7 +3,7 @@ import java.util.concurrent.ConcurrentHashMap; -import Project.Common.TextFX.Color; +import Project.Common.TextFX; public class Room implements AutoCloseable { private final String name;// unique name of the Room diff --git a/Project/Server/Server.java b/Project/Server/Server.java index c38db79..449e0d3 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -6,7 +6,7 @@ import java.net.Socket; import java.util.concurrent.ConcurrentHashMap; -import Project.Common.TextFX.Color; +import Project.Common.TextFX; public enum Server { INSTANCE; // Singleton instance diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index e2c7296..daff19b 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -4,7 +4,7 @@ import java.net.Socket; import java.util.Objects; import java.util.function.Consumer; -import Project.Common.TextFX.Color; +import Project.Common.TextFX; /** * A server-side representation of a single client From 5802bbe0fc408950920f07a5b7314910d6ea2e61 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 17:23:20 -0500 Subject: [PATCH 29/56] fixing pacakge errors --- Project/Client/Client.java | 2 +- Project/Common/Command.java | 2 +- Project/Common/ConnectionPayload.java | 2 +- Project/Common/Constants.java | 2 +- Project/Common/Payload.java | 2 +- Project/Common/PayloadType.java | 2 +- Project/Common/RoomAction.java | 2 +- Project/Common/TextFX.java | 2 +- Project/Common/User.java | 2 +- Project/Exceptions/CustomIT114Exception.java | 2 +- Project/Exceptions/DuplicateRoomException.java | 2 +- Project/Exceptions/RoomNotFoundException.java | 2 +- Project/Server/BaseServerThread.java | 2 +- Project/Server/Room.java | 2 +- Project/Server/Server.java | 2 +- Project/Server/ServerThread.java | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 8756d03..0a5d372 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -1,5 +1,5 @@ -package Project; +package Project.Client; import java.io.IOException; import java.io.ObjectInputStream; diff --git a/Project/Common/Command.java b/Project/Common/Command.java index c497c9c..0c023a6 100644 --- a/Project/Common/Command.java +++ b/Project/Common/Command.java @@ -1,5 +1,5 @@ -package Project; +package Project.Common; import java.util.HashMap; diff --git a/Project/Common/ConnectionPayload.java b/Project/Common/ConnectionPayload.java index a2505d6..efea7d7 100644 --- a/Project/Common/ConnectionPayload.java +++ b/Project/Common/ConnectionPayload.java @@ -1,5 +1,5 @@ -package Project; +package Project.Common; public class ConnectionPayload extends Payload { private String clientName; diff --git a/Project/Common/Constants.java b/Project/Common/Constants.java index c204272..285ab1e 100644 --- a/Project/Common/Constants.java +++ b/Project/Common/Constants.java @@ -1,5 +1,5 @@ -package Project; +package Project.Common; public abstract class Constants { final public static String COMMAND_TRIGGER = "/"; diff --git a/Project/Common/Payload.java b/Project/Common/Payload.java index c396027..52f3c28 100644 --- a/Project/Common/Payload.java +++ b/Project/Common/Payload.java @@ -1,5 +1,5 @@ -package Project; +package Project.Common; import java.io.Serializable; diff --git a/Project/Common/PayloadType.java b/Project/Common/PayloadType.java index b25949b..8f5fd4b 100644 --- a/Project/Common/PayloadType.java +++ b/Project/Common/PayloadType.java @@ -1,5 +1,5 @@ -package Project; +package Project.Common; public enum PayloadType { CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data diff --git a/Project/Common/RoomAction.java b/Project/Common/RoomAction.java index e6942a5..8310ccb 100644 --- a/Project/Common/RoomAction.java +++ b/Project/Common/RoomAction.java @@ -1,5 +1,5 @@ -package Project; +package Project.Common; public enum RoomAction { CREATE, JOIN, LEAVE diff --git a/Project/Common/TextFX.java b/Project/Common/TextFX.java index 7d94deb..c5fe52a 100644 --- a/Project/Common/TextFX.java +++ b/Project/Common/TextFX.java @@ -1,4 +1,4 @@ -package Project; +package Project.Common; /** * Utility to attempt to provide colored text in the terminal. diff --git a/Project/Common/User.java b/Project/Common/User.java index 329045c..a64372b 100644 --- a/Project/Common/User.java +++ b/Project/Common/User.java @@ -1,4 +1,4 @@ -package Project; +package Project.Common; public class User { private long clientId = Constants.DEFAULT_CLIENT_ID; diff --git a/Project/Exceptions/CustomIT114Exception.java b/Project/Exceptions/CustomIT114Exception.java index 0fce691..fd49708 100644 --- a/Project/Exceptions/CustomIT114Exception.java +++ b/Project/Exceptions/CustomIT114Exception.java @@ -1,5 +1,5 @@ -package Project; +package Project.Exceptions; public abstract class CustomIT114Exception extends Exception { public CustomIT114Exception(String message) { diff --git a/Project/Exceptions/DuplicateRoomException.java b/Project/Exceptions/DuplicateRoomException.java index c63866f..56af638 100644 --- a/Project/Exceptions/DuplicateRoomException.java +++ b/Project/Exceptions/DuplicateRoomException.java @@ -1,5 +1,5 @@ -package Project; +package Project.Exceptions; public class DuplicateRoomException extends CustomIT114Exception { public DuplicateRoomException(String message) { diff --git a/Project/Exceptions/RoomNotFoundException.java b/Project/Exceptions/RoomNotFoundException.java index edd5aea..a558c2a 100644 --- a/Project/Exceptions/RoomNotFoundException.java +++ b/Project/Exceptions/RoomNotFoundException.java @@ -1,5 +1,5 @@ -package Project; +package Project.Exceptions; public class RoomNotFoundException extends CustomIT114Exception { diff --git a/Project/Server/BaseServerThread.java b/Project/Server/BaseServerThread.java index 25c01a6..41b432c 100644 --- a/Project/Server/BaseServerThread.java +++ b/Project/Server/BaseServerThread.java @@ -1,4 +1,4 @@ -package Project; +package Project.Server; import java.io.IOException; import java.io.ObjectInputStream; diff --git a/Project/Server/Room.java b/Project/Server/Room.java index 9896715..dc4ca80 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -1,5 +1,5 @@ -package Project; +package Project.Server; import java.util.concurrent.ConcurrentHashMap; diff --git a/Project/Server/Server.java b/Project/Server/Server.java index 449e0d3..b1b55e5 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -1,5 +1,5 @@ -package Project; +package Project.Server; import java.io.IOException; import java.net.ServerSocket; diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index daff19b..986695c 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -1,5 +1,5 @@ -package Project; +package Project.Server; import java.net.Socket; import java.util.Objects; From 697d07f145e23735b77e2824c52f26e66c9483e5 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 17:35:57 -0500 Subject: [PATCH 30/56] re added files --- Project/Client/Client.java | 122 ++++++++++++------ Project/Common/Command.java | 3 +- Project/Common/PayloadType.java | 3 +- Project/Common/RoomAction.java | 2 +- Project/Common/TextFX.java | 3 +- Project/Common/User.java | 3 +- .../Exceptions/DuplicateRoomException.java | 3 +- Project/Server/BaseServerThread.java | 4 + Project/Server/Room.java | 20 ++- Project/Server/Server.java | 43 +++++- Project/Server/ServerThread.java | 24 +++- 11 files changed, 174 insertions(+), 56 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 0a5d372..af46432 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -6,13 +6,24 @@ import java.io.ObjectOutputStream; import java.net.Socket; import java.net.UnknownHostException; +import java.util.List; import java.util.Scanner; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import Project.Common.Command; +import Project.Common.ConnectionPayload; +import Project.Common.Constants; + +import Project.Common.Payload; +import Project.Common.PayloadType; +import Project.Common.RoomAction; + import Project.Common.TextFX; +import Project.Common.User; +import Project.Common.TextFX.Color; /** * Demoing bi-directional communication between client and server in a @@ -21,6 +32,15 @@ public enum Client { INSTANCE; + { + // statically initialize the client-side LoggerUtil + LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); + config.setFileSizeLimit(2048 * 1024); // 2MB + config.setFileCount(1); + config.setLogLocation("client.log"); + // Set the logger configuration + LoggerUtil.INSTANCE.setConfig(config); + } private Socket server = null; private ObjectOutputStream out = null; private ObjectInputStream in = null; @@ -32,12 +52,12 @@ public enum Client { private User myUser = new User(); private void error(String message) { - System.out.println(TextFX.colorize(String.format("%s", message), Color.RED)); + LoggerUtil.INSTANCE.severe(TextFX.colorize(String.format("%s", message), Color.RED)); } // needs to be private now that the enum logic is handling this private Client() { - System.out.println("Client Created"); + LoggerUtil.INSTANCE.info("Client Created"); } public boolean isConnected() { @@ -65,7 +85,7 @@ private boolean connect(String address, int port) { out = new ObjectOutputStream(server.getOutputStream()); // channel to listen to server in = new ObjectInputStream(server.getInputStream()); - System.out.println("Client connected"); + LoggerUtil.INSTANCE.info("Client connected"); // Use CompletableFuture to run listenToServer() in a separate thread CompletableFuture.runAsync(this::listenToServer); } catch (UnknownHostException e) { @@ -115,7 +135,7 @@ private boolean processClientCommand(String text) throws IOException { // System.out.println("Checking command: " + text); if (isConnection("/" + text)) { if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { - System.out.println( + LoggerUtil.INSTANCE.warning( TextFX.colorize("Please set your name via /name before connecting", Color.RED)); return true; } @@ -129,17 +149,17 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.NAME.command)) { text = text.replace(Command.NAME.command, "").trim(); if (text == null || text.length() == 0) { - System.out.println(TextFX.colorize("This command requires a name as an argument", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("This command requires a name as an argument", Color.RED)); return true; } myUser.setClientName(text);// temporary until we get a response from the server - System.out.println(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), Color.YELLOW)); wasCommand = true; } else if (text.equalsIgnoreCase(Command.LIST_USERS.command)) { - System.out.println(TextFX.colorize("Known clients:", Color.CYAN)); + LoggerUtil.INSTANCE.info(TextFX.colorize("Known clients:", Color.CYAN)); knownClients.forEach((key, value) -> { - System.out.println(TextFX.colorize(String.format("%s%s", value.getDisplayName(), + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("%s%s", value.getDisplayName(), key == myUser.getClientId() ? " (you)" : ""), Color.CYAN)); }); wasCommand = true; @@ -156,7 +176,7 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.CREATE_ROOM.command)) { text = text.replace(Command.CREATE_ROOM.command, "").trim(); if (text == null || text.length() == 0) { - System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; } sendRoomAction(text, RoomAction.CREATE); @@ -164,7 +184,7 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.JOIN_ROOM.command)) { text = text.replace(Command.JOIN_ROOM.command, "").trim(); if (text == null || text.length() == 0) { - System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; } sendRoomAction(text, RoomAction.JOIN); @@ -174,6 +194,11 @@ private boolean processClientCommand(String text) throws IOException { // /leave) sendRoomAction(text, RoomAction.LEAVE); wasCommand = true; + } else if (text.startsWith(Command.LIST_ROOMS.command)) { + text = text.replace(Command.LIST_ROOMS.command, "").trim(); + + sendRoomAction(text, RoomAction.LIST); + wasCommand = true; } } return wasCommand; @@ -201,8 +226,11 @@ private void sendRoomAction(String roomName, RoomAction roomAction) throws IOExc case RoomAction.LEAVE: payload.setPayloadType(PayloadType.ROOM_LEAVE); break; + case RoomAction.LIST: + payload.setPayloadType(PayloadType.ROOM_LIST); + break; default: - System.out.println(TextFX.colorize("Invalid room action", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Invalid room action", Color.RED)); break; } sendToServer(payload); @@ -264,14 +292,14 @@ private void sendToServer(Payload payload) throws IOException { out.writeObject(payload); out.flush(); // good practice to ensure data is written out immediately } else { - System.out.println( + LoggerUtil.INSTANCE.warning( "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); } } // End Send*() methods public void start() throws IOException { - System.out.println("Client starting"); + LoggerUtil.INSTANCE.info("Client starting"); // Use CompletableFuture to run listenToInput() in a separate thread CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); @@ -291,22 +319,22 @@ private void listenToServer() { processPayload(fromServer); } else { - System.out.println("Server disconnected"); + LoggerUtil.INSTANCE.info("Server disconnected"); break; } } } catch (ClassCastException | ClassNotFoundException cce) { - System.err.println("Error reading object as specified type: " + cce.getMessage()); - cce.printStackTrace(); + LoggerUtil.INSTANCE.severe("Error reading object as specified type:",cce); + //cce.printStackTrace(); } catch (IOException e) { if (isRunning) { - System.out.println("Connection dropped"); + LoggerUtil.INSTANCE.warning("Connection dropped"); e.printStackTrace(); } } finally { closeServerConnection(); } - System.out.println("listenToServer thread stopped"); + LoggerUtil.INSTANCE.info("listenToServer thread stopped"); } private void processPayload(Payload payload) { @@ -336,34 +364,55 @@ private void processPayload(Payload payload) { case SYNC_CLIENT: processRoomAction(payload); break; + case ROOM_LIST: + processRoomsList(payload); + break; default: - System.out.println(TextFX.colorize("Unhandled payload type", Color.YELLOW)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Unhandled payload type", Color.YELLOW)); break; } } // Start process*() methods + private void processRoomsList(Payload payload) { + if (!(payload instanceof RoomResultPayload)) { + error("Invalid payload subclass for processRoomsList"); + return; + } + RoomResultPayload rrp = (RoomResultPayload) payload; + List rooms = rrp.getRooms(); + if (rooms == null || rooms.size() == 0) { + LoggerUtil.INSTANCE.warning( + TextFX.colorize("No rooms found matching your query", + Color.RED)); + return; + } + LoggerUtil.INSTANCE.info(TextFX.colorize("Room Results:", Color.PURPLE)); + LoggerUtil.INSTANCE.info( + String.join("\n", rooms)); + } + private void processClientData(Payload payload) { if (myUser.getClientId() != Constants.DEFAULT_CLIENT_ID) { - System.out.println(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); } myUser.setClientId(payload.getClientId()); myUser.setClientName(((ConnectionPayload) payload).getClientName());// confirmation from Server knownClients.put(myUser.getClientId(), myUser); - System.out.println(TextFX.colorize("Connected", Color.GREEN)); + LoggerUtil.INSTANCE.info(TextFX.colorize("Connected", Color.GREEN)); } private void processDisconnect(Payload payload) { if (payload.getClientId() == myUser.getClientId()) { knownClients.clear(); myUser.reset(); - System.out.println(TextFX.colorize("You disconnected", Color.RED)); + LoggerUtil.INSTANCE.info(TextFX.colorize("You disconnected", Color.RED)); } else if (knownClients.containsKey(payload.getClientId())) { User disconnectedUser = knownClients.remove(payload.getClientId()); if (disconnectedUser != null) { - System.out.println(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), Color.RED)); } } @@ -390,13 +439,13 @@ private void processRoomAction(Payload payload) { knownClients.remove(connectionPayload.getClientId()); } if (connectionPayload.getMessage() != null) { - System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); + LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); } break; case ROOM_JOIN: if (connectionPayload.getMessage() != null) { - System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); + LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); } // cascade to manage knownClients case SYNC_CLIENT: @@ -415,11 +464,11 @@ private void processRoomAction(Payload payload) { } private void processMessage(Payload payload) { - System.out.println(TextFX.colorize(payload.getMessage(), Color.BLUE)); + LoggerUtil.INSTANCE.info(TextFX.colorize(payload.getMessage(), Color.BLUE)); } private void processReverse(Payload payload) { - System.out.println(TextFX.colorize(payload.getMessage(), Color.PURPLE)); + LoggerUtil.INSTANCE.info(TextFX.colorize(payload.getMessage(), Color.PURPLE)); } // End process*() methods @@ -428,7 +477,7 @@ private void processReverse(Payload payload) { */ private void listenToInput() { try (Scanner si = new Scanner(System.in)) { - System.out.println("Waiting for input"); // moved here to avoid console spam + LoggerUtil.INSTANCE.info("Waiting for input"); // moved here to avoid console spam while (isRunning) { // Run until isRunning is false String userInput = si.nextLine(); if (!processClientCommand(userInput)) { @@ -436,10 +485,10 @@ private void listenToInput() { } } } catch (IOException ioException) { - System.out.println("Error in listentToInput()"); - ioException.printStackTrace(); + LoggerUtil.INSTANCE.severe("Error in listentToInput()",ioException); + //ioException.printStackTrace(); } - System.out.println("listenToInput thread stopped"); + LoggerUtil.INSTANCE.info("listenToInput thread stopped"); } /** @@ -448,7 +497,7 @@ private void listenToInput() { private void close() { isRunning = false; closeServerConnection(); - System.out.println("Client terminated"); + LoggerUtil.INSTANCE.info("Client terminated"); // System.exit(0); // Terminate the application } @@ -458,7 +507,7 @@ private void close() { private void closeServerConnection() { try { if (out != null) { - System.out.println("Closing output stream"); + LoggerUtil.INSTANCE.info("Closing output stream"); out.close(); } } catch (Exception e) { @@ -466,7 +515,7 @@ private void closeServerConnection() { } try { if (in != null) { - System.out.println("Closing input stream"); + LoggerUtil.INSTANCE.info("Closing input stream"); in.close(); } } catch (Exception e) { @@ -474,12 +523,13 @@ private void closeServerConnection() { } try { if (server != null) { - System.out.println("Closing connection"); + LoggerUtil.INSTANCE.info("Closing connection"); server.close(); - System.out.println("Closed socket"); + LoggerUtil.INSTANCE.info("Closed Socket"); } } catch (IOException e) { e.printStackTrace(); + //LoggerUtil.INSTANCE.severe("Socket Error", e); } } diff --git a/Project/Common/Command.java b/Project/Common/Command.java index 0c023a6..d98dc78 100644 --- a/Project/Common/Command.java +++ b/Project/Common/Command.java @@ -13,7 +13,8 @@ public enum Command { LEAVE_ROOM("leaveroom"), JOIN_ROOM("joinroom"), NAME("name"), - LIST_USERS("users"); + LIST_USERS("users"), + LIST_ROOMS("listrooms"); private static final HashMap BY_COMMAND = new HashMap<>(); static { diff --git a/Project/Common/PayloadType.java b/Project/Common/PayloadType.java index 8f5fd4b..64f9fde 100644 --- a/Project/Common/PayloadType.java +++ b/Project/Common/PayloadType.java @@ -11,5 +11,6 @@ public enum PayloadType { ROOM_JOIN, ROOM_LEAVE, REVERSE, - MESSAGE // sender and message + MESSAGE, // sender and message, + ROOM_LIST, // list of rooms } diff --git a/Project/Common/RoomAction.java b/Project/Common/RoomAction.java index 8310ccb..35e3076 100644 --- a/Project/Common/RoomAction.java +++ b/Project/Common/RoomAction.java @@ -2,5 +2,5 @@ package Project.Common; public enum RoomAction { - CREATE, JOIN, LEAVE + CREATE, JOIN, LEAVE, LIST } diff --git a/Project/Common/TextFX.java b/Project/Common/TextFX.java index c5fe52a..5c0c320 100644 --- a/Project/Common/TextFX.java +++ b/Project/Common/TextFX.java @@ -1,3 +1,4 @@ + package Project.Common; /** @@ -69,4 +70,4 @@ public static void main(String[] args) { System.out.println(TextFX.colorize("This is some blue text.", Color.BLUE)); System.out.println(TextFX.colorize("And this is green!", Color.GREEN)); } -} \ No newline at end of file +} diff --git a/Project/Common/User.java b/Project/Common/User.java index a64372b..6d3c1b8 100644 --- a/Project/Common/User.java +++ b/Project/Common/User.java @@ -1,3 +1,4 @@ + package Project.Common; public class User { @@ -40,4 +41,4 @@ public void reset() { this.clientId = Constants.DEFAULT_CLIENT_ID; this.clientName = null; } -} \ No newline at end of file +} diff --git a/Project/Exceptions/DuplicateRoomException.java b/Project/Exceptions/DuplicateRoomException.java index 56af638..a3eeea8 100644 --- a/Project/Exceptions/DuplicateRoomException.java +++ b/Project/Exceptions/DuplicateRoomException.java @@ -1,4 +1,3 @@ - package Project.Exceptions; public class DuplicateRoomException extends CustomIT114Exception { @@ -10,4 +9,4 @@ public DuplicateRoomException(String message, Throwable cause) { super(message, cause); } -} +} \ No newline at end of file diff --git a/Project/Server/BaseServerThread.java b/Project/Server/BaseServerThread.java index 41b432c..9a9347f 100644 --- a/Project/Server/BaseServerThread.java +++ b/Project/Server/BaseServerThread.java @@ -1,3 +1,4 @@ + package Project.Server; import java.io.IOException; @@ -5,6 +6,9 @@ import java.io.ObjectOutputStream; import java.net.Socket; +import Project.Common.Payload; +import Project.Common.User; + /** * Base class the handles the underlying connection between Client and * Server-side diff --git a/Project/Server/Room.java b/Project/Server/Room.java index dc4ca80..17b1b02 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -3,7 +3,13 @@ import java.util.concurrent.ConcurrentHashMap; +import Project.Common.Constants; +import Project.Common.LoggerUtil; +import Project.Common.RoomAction; import Project.Common.TextFX; +import Project.Common.TextFX.Color; +import Project.Exceptions.DuplicateRoomException; +import Project.Exceptions.RoomNotFoundException; public class Room implements AutoCloseable { private final String name;// unique name of the Room @@ -13,7 +19,7 @@ public class Room implements AutoCloseable { public final static String LOBBY = "lobby"; private void info(String message) { - System.out.println(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); } public Room(String name) { @@ -66,7 +72,7 @@ private void syncExistingClients(ServerThread incomingClient) { boolean failedToSync = !incomingClient.sendClientInfo(serverThread.getClientId(), serverThread.getClientName(), RoomAction.JOIN, true); if (failedToSync) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -88,7 +94,7 @@ private void joinStatusRelay(ServerThread client, boolean didJoin) { // Send the server generated message to the current client boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); if (failedToSend || failedToSync) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -131,7 +137,7 @@ protected synchronized void relay(ServerThread sender, String message) { clientsInRoom.values().removeIf(serverThread -> { boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); if (failedToSend) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -161,7 +167,7 @@ private synchronized void disconnect(ServerThread client) { boolean failedToSend = !serverThread.sendClientInfo(disconnectingServerThread.getClientId(), disconnectingServerThread.getClientName(), RoomAction.LEAVE); if (failedToSend) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -217,6 +223,10 @@ public void close() { } // start handle methods + protected void handleListRooms(ServerThread sender, String roomQuery) { + sender.sendRooms(Server.INSTANCE.listRooms(roomQuery)); + } + public void handleCreateRoom(ServerThread sender, String roomName) { try { Server.INSTANCE.createRoom(roomName); diff --git a/Project/Server/Server.java b/Project/Server/Server.java index b1b55e5..34e6b06 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -4,13 +4,29 @@ import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + + +import Project.Common.TextFX.Color; import Project.Common.TextFX; +import Project.Exceptions.DuplicateRoomException; +import Project.Exceptions.RoomNotFoundException; public enum Server { INSTANCE; // Singleton instance + { + // statically initialize the server-side LoggerUtil + LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); + config.setFileSizeLimit(2048 * 1024); // 2MB + config.setFileCount(1); + config.setLogLocation("server.log"); + // Set the logger configuration + LoggerUtil.INSTANCE.setConfig(config); + } private int port = 3000; // connected clients // Use ConcurrentHashMap for thread-safe client management @@ -20,7 +36,7 @@ public enum Server { private long nextClientId = 0; private void info(String message) { - System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); } private Server() { @@ -67,10 +83,9 @@ private void start(int port) { // Note: We don't yet add the ServerThread reference to our connectedClients map } } catch (DuplicateRoomException e) { - System.err.println(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); + LoggerUtil.INSTANCE.severe(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); } catch (IOException e) { - System.err.println(TextFX.colorize("Error accepting connection", Color.RED)); - e.printStackTrace(); + LoggerUtil.INSTANCE.severe(TextFX.colorize("Error accepting connection", Color.RED), e); } finally { info("Closing server socket"); } @@ -137,6 +152,22 @@ protected void joinRoom(String name, ServerThread client) throws RoomNotFoundExc next.addClient(client); } + /** + * Lists all rooms that partially match the given String + * + * @param roomQuery + * @return + */ + protected List listRooms(String roomQuery) { + final String nameCheck = roomQuery.toLowerCase(); + return rooms.values().stream() + .filter(room -> room.getName().toLowerCase().contains(nameCheck))// find partially matched rooms + .map(room -> room.getName())// map room to String (name) + .limit(10) // limit to 10 results + .sorted() // sort the results alphabetically + .collect(Collectors.toList()); // return a mutable list + } + protected void removeRoom(Room room) { rooms.remove(room.getName().toLowerCase()); info(String.format("Removed room %s", room.getName())); @@ -184,7 +215,7 @@ public synchronized void broadcastMessageToAllRooms(ServerThread sender, String } public static void main(String[] args) { - System.out.println("Server Starting"); + LoggerUtil.INSTANCE.info("Server Starting"); Server server = Server.INSTANCE; int port = 3000; try { @@ -194,7 +225,7 @@ public static void main(String[] args) { // will default to the defined value prior to the try/catch } server.start(port); - System.out.println("Server Stopped"); + LoggerUtil.INSTANCE.warning("Server Stopped"); } } diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index 986695c..d964970 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -2,8 +2,17 @@ package Project.Server; import java.net.Socket; +import java.util.List; import java.util.Objects; import java.util.function.Consumer; +import Project.Common.TextFX.Color; +import Project.Common.ConnectionPayload; +import Project.Common.Constants; + +import Project.Common.Payload; +import Project.Common.PayloadType; +import Project.Common.RoomAction; + import Project.Common.TextFX; /** @@ -18,8 +27,10 @@ public class ServerThread extends BaseServerThread { * * @param message */ + @Override protected void info(String message) { - System.out.println(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); + LoggerUtil.INSTANCE + .info(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); } /** @@ -43,6 +54,12 @@ protected ServerThread(Socket myClient, Consumer onInitializationC } // Start Send*() Methods + public boolean sendRooms(List rooms) { + RoomResultPayload rrp = new RoomResultPayload(); + rrp.setRooms(rooms); + return sendToClient(rrp); + } + protected boolean sendDisconnect(long clientId) { Payload payload = new Payload(); payload.setClientId(clientId); @@ -153,8 +170,11 @@ protected void processPayload(Payload incoming) { case ROOM_LEAVE: currentRoom.handleJoinRoom(this, Room.LOBBY); break; + case ROOM_LIST: + currentRoom.handleListRooms(this, incoming.getMessage()); + break; default: - System.out.println(TextFX.colorize("Unknown payload type received", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Unknown payload type received", Color.RED)); break; } } From 90de15e60379525740a22996bd0ca5bfd655ff2f Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 18:10:55 -0500 Subject: [PATCH 31/56] fixing --- Project/Client/Client.java | 115 ++++++++++--------------------- Project/Server/Room.java | 28 +++----- Project/Server/Server.java | 38 ++-------- Project/Server/ServerThread.java | 23 ++----- 4 files changed, 60 insertions(+), 144 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index af46432..93d5567 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -16,11 +16,9 @@ import Project.Common.Command; import Project.Common.ConnectionPayload; import Project.Common.Constants; - import Project.Common.Payload; import Project.Common.PayloadType; import Project.Common.RoomAction; - import Project.Common.TextFX; import Project.Common.User; import Project.Common.TextFX.Color; @@ -32,15 +30,6 @@ public enum Client { INSTANCE; - { - // statically initialize the client-side LoggerUtil - LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); - config.setFileSizeLimit(2048 * 1024); // 2MB - config.setFileCount(1); - config.setLogLocation("client.log"); - // Set the logger configuration - LoggerUtil.INSTANCE.setConfig(config); - } private Socket server = null; private ObjectOutputStream out = null; private ObjectInputStream in = null; @@ -52,12 +41,12 @@ public enum Client { private User myUser = new User(); private void error(String message) { - LoggerUtil.INSTANCE.severe(TextFX.colorize(String.format("%s", message), Color.RED)); + System.out.println(TextFX.colorize(String.format("%s", message), Color.RED)); } // needs to be private now that the enum logic is handling this private Client() { - LoggerUtil.INSTANCE.info("Client Created"); + System.out.println("Client Created"); } public boolean isConnected() { @@ -85,7 +74,7 @@ private boolean connect(String address, int port) { out = new ObjectOutputStream(server.getOutputStream()); // channel to listen to server in = new ObjectInputStream(server.getInputStream()); - LoggerUtil.INSTANCE.info("Client connected"); + System.out.println("Client connected"); // Use CompletableFuture to run listenToServer() in a separate thread CompletableFuture.runAsync(this::listenToServer); } catch (UnknownHostException e) { @@ -135,7 +124,7 @@ private boolean processClientCommand(String text) throws IOException { // System.out.println("Checking command: " + text); if (isConnection("/" + text)) { if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { - LoggerUtil.INSTANCE.warning( + System.out.println( TextFX.colorize("Please set your name via /name before connecting", Color.RED)); return true; } @@ -149,17 +138,17 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.NAME.command)) { text = text.replace(Command.NAME.command, "").trim(); if (text == null || text.length() == 0) { - LoggerUtil.INSTANCE.warning(TextFX.colorize("This command requires a name as an argument", Color.RED)); + System.out.println(TextFX.colorize("This command requires a name as an argument", Color.RED)); return true; } myUser.setClientName(text);// temporary until we get a response from the server - LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), + System.out.println(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), Color.YELLOW)); wasCommand = true; } else if (text.equalsIgnoreCase(Command.LIST_USERS.command)) { - LoggerUtil.INSTANCE.info(TextFX.colorize("Known clients:", Color.CYAN)); + System.out.println(TextFX.colorize("Known clients:", Color.CYAN)); knownClients.forEach((key, value) -> { - LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("%s%s", value.getDisplayName(), + System.out.println(TextFX.colorize(String.format("%s%s", value.getDisplayName(), key == myUser.getClientId() ? " (you)" : ""), Color.CYAN)); }); wasCommand = true; @@ -176,7 +165,7 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.CREATE_ROOM.command)) { text = text.replace(Command.CREATE_ROOM.command, "").trim(); if (text == null || text.length() == 0) { - LoggerUtil.INSTANCE.warning(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; } sendRoomAction(text, RoomAction.CREATE); @@ -184,7 +173,7 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.JOIN_ROOM.command)) { text = text.replace(Command.JOIN_ROOM.command, "").trim(); if (text == null || text.length() == 0) { - LoggerUtil.INSTANCE.warning(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; } sendRoomAction(text, RoomAction.JOIN); @@ -194,11 +183,6 @@ private boolean processClientCommand(String text) throws IOException { // /leave) sendRoomAction(text, RoomAction.LEAVE); wasCommand = true; - } else if (text.startsWith(Command.LIST_ROOMS.command)) { - text = text.replace(Command.LIST_ROOMS.command, "").trim(); - - sendRoomAction(text, RoomAction.LIST); - wasCommand = true; } } return wasCommand; @@ -226,11 +210,8 @@ private void sendRoomAction(String roomName, RoomAction roomAction) throws IOExc case RoomAction.LEAVE: payload.setPayloadType(PayloadType.ROOM_LEAVE); break; - case RoomAction.LIST: - payload.setPayloadType(PayloadType.ROOM_LIST); - break; default: - LoggerUtil.INSTANCE.warning(TextFX.colorize("Invalid room action", Color.RED)); + System.out.println(TextFX.colorize("Invalid room action", Color.RED)); break; } sendToServer(payload); @@ -292,14 +273,14 @@ private void sendToServer(Payload payload) throws IOException { out.writeObject(payload); out.flush(); // good practice to ensure data is written out immediately } else { - LoggerUtil.INSTANCE.warning( + System.out.println( "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); } } // End Send*() methods public void start() throws IOException { - LoggerUtil.INSTANCE.info("Client starting"); + System.out.println("Client starting"); // Use CompletableFuture to run listenToInput() in a separate thread CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); @@ -319,22 +300,22 @@ private void listenToServer() { processPayload(fromServer); } else { - LoggerUtil.INSTANCE.info("Server disconnected"); + System.out.println("Server disconnected"); break; } } } catch (ClassCastException | ClassNotFoundException cce) { - LoggerUtil.INSTANCE.severe("Error reading object as specified type:",cce); - //cce.printStackTrace(); + System.err.println("Error reading object as specified type: " + cce.getMessage()); + cce.printStackTrace(); } catch (IOException e) { if (isRunning) { - LoggerUtil.INSTANCE.warning("Connection dropped"); + System.out.println("Connection dropped"); e.printStackTrace(); } } finally { closeServerConnection(); } - LoggerUtil.INSTANCE.info("listenToServer thread stopped"); + System.out.println("listenToServer thread stopped"); } private void processPayload(Payload payload) { @@ -364,55 +345,34 @@ private void processPayload(Payload payload) { case SYNC_CLIENT: processRoomAction(payload); break; - case ROOM_LIST: - processRoomsList(payload); - break; default: - LoggerUtil.INSTANCE.warning(TextFX.colorize("Unhandled payload type", Color.YELLOW)); + System.out.println(TextFX.colorize("Unhandled payload type", Color.YELLOW)); break; } } // Start process*() methods - private void processRoomsList(Payload payload) { - if (!(payload instanceof RoomResultPayload)) { - error("Invalid payload subclass for processRoomsList"); - return; - } - RoomResultPayload rrp = (RoomResultPayload) payload; - List rooms = rrp.getRooms(); - if (rooms == null || rooms.size() == 0) { - LoggerUtil.INSTANCE.warning( - TextFX.colorize("No rooms found matching your query", - Color.RED)); - return; - } - LoggerUtil.INSTANCE.info(TextFX.colorize("Room Results:", Color.PURPLE)); - LoggerUtil.INSTANCE.info( - String.join("\n", rooms)); - } - private void processClientData(Payload payload) { if (myUser.getClientId() != Constants.DEFAULT_CLIENT_ID) { - LoggerUtil.INSTANCE.warning(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); + System.out.println(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); } myUser.setClientId(payload.getClientId()); myUser.setClientName(((ConnectionPayload) payload).getClientName());// confirmation from Server knownClients.put(myUser.getClientId(), myUser); - LoggerUtil.INSTANCE.info(TextFX.colorize("Connected", Color.GREEN)); + System.out.println(TextFX.colorize("Connected", Color.GREEN)); } private void processDisconnect(Payload payload) { if (payload.getClientId() == myUser.getClientId()) { knownClients.clear(); myUser.reset(); - LoggerUtil.INSTANCE.info(TextFX.colorize("You disconnected", Color.RED)); + System.out.println(TextFX.colorize("You disconnected", Color.RED)); } else if (knownClients.containsKey(payload.getClientId())) { User disconnectedUser = knownClients.remove(payload.getClientId()); if (disconnectedUser != null) { - LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), + System.out.println(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), Color.RED)); } } @@ -439,13 +399,13 @@ private void processRoomAction(Payload payload) { knownClients.remove(connectionPayload.getClientId()); } if (connectionPayload.getMessage() != null) { - LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); + System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); } break; case ROOM_JOIN: if (connectionPayload.getMessage() != null) { - LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); + System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); } // cascade to manage knownClients case SYNC_CLIENT: @@ -464,11 +424,11 @@ private void processRoomAction(Payload payload) { } private void processMessage(Payload payload) { - LoggerUtil.INSTANCE.info(TextFX.colorize(payload.getMessage(), Color.BLUE)); + System.out.println(TextFX.colorize(payload.getMessage(), Color.BLUE)); } private void processReverse(Payload payload) { - LoggerUtil.INSTANCE.info(TextFX.colorize(payload.getMessage(), Color.PURPLE)); + System.out.println(TextFX.colorize(payload.getMessage(), Color.PURPLE)); } // End process*() methods @@ -477,7 +437,7 @@ private void processReverse(Payload payload) { */ private void listenToInput() { try (Scanner si = new Scanner(System.in)) { - LoggerUtil.INSTANCE.info("Waiting for input"); // moved here to avoid console spam + System.out.println("Waiting for input"); // moved here to avoid console spam while (isRunning) { // Run until isRunning is false String userInput = si.nextLine(); if (!processClientCommand(userInput)) { @@ -485,10 +445,10 @@ private void listenToInput() { } } } catch (IOException ioException) { - LoggerUtil.INSTANCE.severe("Error in listentToInput()",ioException); - //ioException.printStackTrace(); + System.out.println("Error in listentToInput()"); + ioException.printStackTrace(); } - LoggerUtil.INSTANCE.info("listenToInput thread stopped"); + System.out.println("listenToInput thread stopped"); } /** @@ -497,7 +457,7 @@ private void listenToInput() { private void close() { isRunning = false; closeServerConnection(); - LoggerUtil.INSTANCE.info("Client terminated"); + System.out.println("Client terminated"); // System.exit(0); // Terminate the application } @@ -507,7 +467,7 @@ private void close() { private void closeServerConnection() { try { if (out != null) { - LoggerUtil.INSTANCE.info("Closing output stream"); + System.out.println("Closing output stream"); out.close(); } } catch (Exception e) { @@ -515,7 +475,7 @@ private void closeServerConnection() { } try { if (in != null) { - LoggerUtil.INSTANCE.info("Closing input stream"); + System.out.println("Closing input stream"); in.close(); } } catch (Exception e) { @@ -523,13 +483,12 @@ private void closeServerConnection() { } try { if (server != null) { - LoggerUtil.INSTANCE.info("Closing connection"); + System.out.println("Closing connection"); server.close(); - LoggerUtil.INSTANCE.info("Closed Socket"); + System.out.println("Closed socket"); } } catch (IOException e) { e.printStackTrace(); - //LoggerUtil.INSTANCE.severe("Socket Error", e); } } @@ -542,4 +501,4 @@ public static void main(String[] args) { e.printStackTrace(); } } -} +} \ No newline at end of file diff --git a/Project/Server/Room.java b/Project/Server/Room.java index 17b1b02..ffdcf43 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -4,7 +4,7 @@ import java.util.concurrent.ConcurrentHashMap; import Project.Common.Constants; -import Project.Common.LoggerUtil; + import Project.Common.RoomAction; import Project.Common.TextFX; import Project.Common.TextFX.Color; @@ -19,7 +19,7 @@ public class Room implements AutoCloseable { public final static String LOBBY = "lobby"; private void info(String message) { - LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); + System.out.println(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); } public Room(String name) { @@ -72,7 +72,7 @@ private void syncExistingClients(ServerThread incomingClient) { boolean failedToSync = !incomingClient.sendClientInfo(serverThread.getClientId(), serverThread.getClientName(), RoomAction.JOIN, true); if (failedToSync) { - LoggerUtil.INSTANCE.warning( + System.out.println( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -87,14 +87,13 @@ private void joinStatusRelay(ServerThread client, boolean didJoin) { client.getClientId() == serverThread.getClientId() ? "You" : client.getDisplayName(), didJoin ? "joined" : "left"); - final long senderId = client == null ? Constants.DEFAULT_CLIENT_ID : client.getClientId(); // Share info of the client joining or leaving the room boolean failedToSync = !serverThread.sendClientInfo(client.getClientId(), client.getClientName(), didJoin ? RoomAction.JOIN : RoomAction.LEAVE); // Send the server generated message to the current client - boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); + boolean failedToSend = !serverThread.sendMessage(formattedMessage); if (failedToSend || failedToSync) { - LoggerUtil.INSTANCE.warning( + System.out.println( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -123,7 +122,6 @@ protected synchronized void relay(ServerThread sender, String message) { // Note: any desired changes to the message must be done before this line String senderString = sender == null ? String.format("Room[%s]", getName()) : sender.getDisplayName(); - final long senderId = sender == null ? Constants.DEFAULT_CLIENT_ID : sender.getClientId(); // Note: formattedMessage must be final (or effectively final) since outside // scope can't be changed inside a callback function (see removeIf() below) final String formattedMessage = String.format("%s: %s", senderString, message); @@ -135,9 +133,9 @@ protected synchronized void relay(ServerThread sender, String message) { info(String.format("sending message to %s recipients: %s", clientsInRoom.size(), formattedMessage)); clientsInRoom.values().removeIf(serverThread -> { - boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); + boolean failedToSend = !serverThread.sendMessage(formattedMessage); if (failedToSend) { - LoggerUtil.INSTANCE.warning( + System.out.println( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -167,7 +165,7 @@ private synchronized void disconnect(ServerThread client) { boolean failedToSend = !serverThread.sendClientInfo(disconnectingServerThread.getClientId(), disconnectingServerThread.getClientName(), RoomAction.LEAVE); if (failedToSend) { - LoggerUtil.INSTANCE.warning( + System.out.println( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -223,10 +221,6 @@ public void close() { } // start handle methods - protected void handleListRooms(ServerThread sender, String roomQuery) { - sender.sendRooms(Server.INSTANCE.listRooms(roomQuery)); - } - public void handleCreateRoom(ServerThread sender, String roomName) { try { Server.INSTANCE.createRoom(roomName); @@ -235,7 +229,7 @@ public void handleCreateRoom(ServerThread sender, String roomName) { info("Room wasn't found (this shouldn't happen)"); e.printStackTrace(); } catch (DuplicateRoomException e) { - sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s already exists", roomName)); + sender.sendMessage(String.format("Room %s already exists", roomName)); } } @@ -243,7 +237,7 @@ public void handleJoinRoom(ServerThread sender, String roomName) { try { Server.INSTANCE.joinRoom(roomName, sender); } catch (RoomNotFoundException e) { - sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s doesn't exist", roomName)); + sender.sendMessage(String.format("Room %s doesn't exist", roomName)); } } @@ -271,4 +265,4 @@ protected synchronized void handleMessage(ServerThread sender, String text) { relay(sender, text); } // end handle methods -} +} \ No newline at end of file diff --git a/Project/Server/Server.java b/Project/Server/Server.java index 34e6b06..8f843e1 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -18,15 +18,6 @@ public enum Server { INSTANCE; // Singleton instance - { - // statically initialize the server-side LoggerUtil - LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); - config.setFileSizeLimit(2048 * 1024); // 2MB - config.setFileCount(1); - config.setLogLocation("server.log"); - // Set the logger configuration - LoggerUtil.INSTANCE.setConfig(config); - } private int port = 3000; // connected clients // Use ConcurrentHashMap for thread-safe client management @@ -36,7 +27,7 @@ public enum Server { private long nextClientId = 0; private void info(String message) { - LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); + System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); } private Server() { @@ -83,9 +74,10 @@ private void start(int port) { // Note: We don't yet add the ServerThread reference to our connectedClients map } } catch (DuplicateRoomException e) { - LoggerUtil.INSTANCE.severe(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); + System.err.println(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); } catch (IOException e) { - LoggerUtil.INSTANCE.severe(TextFX.colorize("Error accepting connection", Color.RED), e); + System.err.println(TextFX.colorize("Error accepting connection", Color.RED)); + e.printStackTrace(); } finally { info("Closing server socket"); } @@ -152,22 +144,6 @@ protected void joinRoom(String name, ServerThread client) throws RoomNotFoundExc next.addClient(client); } - /** - * Lists all rooms that partially match the given String - * - * @param roomQuery - * @return - */ - protected List listRooms(String roomQuery) { - final String nameCheck = roomQuery.toLowerCase(); - return rooms.values().stream() - .filter(room -> room.getName().toLowerCase().contains(nameCheck))// find partially matched rooms - .map(room -> room.getName())// map room to String (name) - .limit(10) // limit to 10 results - .sorted() // sort the results alphabetically - .collect(Collectors.toList()); // return a mutable list - } - protected void removeRoom(Room room) { rooms.remove(room.getName().toLowerCase()); info(String.format("Removed room %s", room.getName())); @@ -215,7 +191,7 @@ public synchronized void broadcastMessageToAllRooms(ServerThread sender, String } public static void main(String[] args) { - LoggerUtil.INSTANCE.info("Server Starting"); + System.out.println("Server Starting"); Server server = Server.INSTANCE; int port = 3000; try { @@ -225,7 +201,7 @@ public static void main(String[] args) { // will default to the defined value prior to the try/catch } server.start(port); - LoggerUtil.INSTANCE.warning("Server Stopped"); + System.out.println("Server Stopped"); } -} +} \ No newline at end of file diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index d964970..8a6a293 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -27,10 +27,8 @@ public class ServerThread extends BaseServerThread { * * @param message */ - @Override protected void info(String message) { - LoggerUtil.INSTANCE - .info(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); + System.out.println(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); } /** @@ -54,12 +52,6 @@ protected ServerThread(Socket myClient, Consumer onInitializationC } // Start Send*() Methods - public boolean sendRooms(List rooms) { - RoomResultPayload rrp = new RoomResultPayload(); - rrp.setRooms(rooms); - return sendToClient(rrp); - } - protected boolean sendDisconnect(long clientId) { Payload payload = new Payload(); payload.setClientId(clientId); @@ -131,15 +123,13 @@ protected boolean sendClientId() { /** * Sends a message to the client * - * @param clientId who it's from * @param message * @return true for successful send */ - protected boolean sendMessage(long clientId, String message) { + protected boolean sendMessage(String message) { Payload payload = new Payload(); payload.setPayloadType(PayloadType.MESSAGE); payload.setMessage(message); - payload.setClientId(clientId); return sendToClient(payload); } @@ -150,7 +140,7 @@ protected void processPayload(Payload incoming) { switch (incoming.getPayloadType()) { case CLIENT_CONNECT: setClientName(((ConnectionPayload) incoming).getClientName().trim()); - + break; case DISCONNECT: currentRoom.handleDisconnect(this); @@ -170,11 +160,8 @@ protected void processPayload(Payload incoming) { case ROOM_LEAVE: currentRoom.handleJoinRoom(this, Room.LOBBY); break; - case ROOM_LIST: - currentRoom.handleListRooms(this, incoming.getMessage()); - break; default: - LoggerUtil.INSTANCE.warning(TextFX.colorize("Unknown payload type received", Color.RED)); + System.out.println(TextFX.colorize("Unknown payload type received", Color.RED)); break; } } @@ -184,4 +171,4 @@ protected void onInitialized() { // once receiving the desired client name the object is ready onInitializationComplete.accept(this); } -} +} \ No newline at end of file From b1fc732d85074629d0f7889cf10d2d9c9d70fb3b Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 3 Nov 2025 19:40:53 -0500 Subject: [PATCH 32/56] changes --- Project/Client/Client.java | 8 ++++++++ Project/Server/Server.java | 8 +++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 93d5567..571ec6d 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -122,6 +122,7 @@ private boolean processClientCommand(String text) throws IOException { if (text.startsWith(Constants.COMMAND_TRIGGER)) { text = text.substring(1); // remove the / // System.out.println("Checking command: " + text); + // rk975- 11/3/25 if (isConnection("/" + text)) { if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { System.out.println( @@ -131,6 +132,7 @@ private boolean processClientCommand(String text) throws IOException { // replaces multiple spaces with a single space // splits on the space after connect (gives us host and port) // splits on : to get host as index 0 and port as index 1 + // rk975 - 11/3/25 relevant snippet of code String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); sendClientName(myUser.getClientName());// sync follow-up data (handshake) @@ -158,12 +160,14 @@ private boolean processClientCommand(String text) throws IOException { } else if (Command.DISCONNECT.command.equalsIgnoreCase(text)) { sendDisconnect(); wasCommand = true; + // rk975 - 11/3/25 } else if (text.startsWith(Command.REVERSE.command)) { text = text.replace(Command.REVERSE.command, "").trim(); sendReverse(text); wasCommand = true; } else if (text.startsWith(Command.CREATE_ROOM.command)) { text = text.replace(Command.CREATE_ROOM.command, "").trim(); + // rk975 - 11/3/25 if (text == null || text.length() == 0) { System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; @@ -197,6 +201,7 @@ private boolean processClientCommand(String text) throws IOException { * @param roomAction (join, leave, create) * @throws IOException */ + // rk975 - 11/3/25 private void sendRoomAction(String roomName, RoomAction roomAction) throws IOException { Payload payload = new Payload(); payload.setMessage(roomName); @@ -249,6 +254,7 @@ private void sendDisconnect() throws IOException { * @throws IOException */ private void sendMessage(String message) throws IOException { + // rk975 11/3/25 Payload payload = new Payload(); payload.setMessage(message); payload.setPayloadType(PayloadType.MESSAGE); @@ -271,6 +277,7 @@ private void sendClientName(String name) throws IOException { private void sendToServer(Payload payload) throws IOException { if (isConnected()) { out.writeObject(payload); + // rk975 11/3/25 out.flush(); // good practice to ensure data is written out immediately } else { System.out.println( @@ -438,6 +445,7 @@ private void processReverse(Payload payload) { private void listenToInput() { try (Scanner si = new Scanner(System.in)) { System.out.println("Waiting for input"); // moved here to avoid console spam + // rk975-11/3/25 while (isRunning) { // Run until isRunning is false String userInput = si.nextLine(); if (!processClientCommand(userInput)) { diff --git a/Project/Server/Server.java b/Project/Server/Server.java index 8f843e1..e36573a 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -29,7 +29,7 @@ public enum Server { private void info(String message) { System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); } - +// rk975 11/3/25 private Server() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { info("JVM is shutting down. Perform cleanup tasks."); @@ -53,7 +53,8 @@ private void shutdown() { e.printStackTrace(); } } - +// rk975-11/3/25 +// private void start(int port) { this.port = port; // server listening @@ -63,6 +64,7 @@ private void start(int port) { createRoom(Room.LOBBY);// create the first room (lobby) while (isRunning) { info("Waiting for next client"); + // rk975 - 11/3/25. Handling new incoming connections Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection info("Client connected"); // wrap socket in a ServerThread, pass a callback to notify the Server when @@ -121,7 +123,7 @@ protected void createRoom(String name) throws DuplicateRoomException { rooms.put(nameCheck, room); info(String.format("Created new Room %s", name)); } - +// rk975 - 11/3/25 relevant code snippet /** * Attempts to move a client (ServerThread) between rooms * From 1479391d8720c6e983351c21dc51beb6280f39d2 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 13:40:16 -0500 Subject: [PATCH 33/56] Added for milestone2 --- Project/Client/Client.java | 306 +++++++++++++++++++++++++++++++------ 1 file changed, 257 insertions(+), 49 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 571ec6d..88311fc 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -1,4 +1,3 @@ - package Project.Client; import java.io.IOException; @@ -16,9 +15,15 @@ import Project.Common.Command; import Project.Common.ConnectionPayload; import Project.Common.Constants; +import Project.Common.LoggerUtil; import Project.Common.Payload; import Project.Common.PayloadType; +import Project.Common.Phase; +import Project.Common.ReadyPayload; import Project.Common.RoomAction; +import Project.Common.RoomResultPayload; +import Project.Common.PickPayload; +import Project.Common.PointsPayload; import Project.Common.TextFX; import Project.Common.User; import Project.Common.TextFX.Color; @@ -30,6 +35,15 @@ public enum Client { INSTANCE; + { + // statically initialize the client-side LoggerUtil + LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); + config.setFileSizeLimit(2048 * 1024); // 2MB + config.setFileCount(1); + config.setLogLocation("client.log"); + // Set the logger configuration + LoggerUtil.INSTANCE.setConfig(config); + } private Socket server = null; private ObjectOutputStream out = null; private ObjectInputStream in = null; @@ -39,14 +53,15 @@ public enum Client { private volatile boolean isRunning = true; // volatile for thread-safe visibility private final ConcurrentHashMap knownClients = new ConcurrentHashMap(); private User myUser = new User(); + private Phase currentPhase = Phase.READY; private void error(String message) { - System.out.println(TextFX.colorize(String.format("%s", message), Color.RED)); + LoggerUtil.INSTANCE.severe(TextFX.colorize(String.format("%s", message), Color.RED)); } // needs to be private now that the enum logic is handling this private Client() { - System.out.println("Client Created"); + LoggerUtil.INSTANCE.info("Client Created"); } public boolean isConnected() { @@ -74,7 +89,7 @@ private boolean connect(String address, int port) { out = new ObjectOutputStream(server.getOutputStream()); // channel to listen to server in = new ObjectInputStream(server.getInputStream()); - System.out.println("Client connected"); + LoggerUtil.INSTANCE.info("Client connected"); // Use CompletableFuture to run listenToServer() in a separate thread CompletableFuture.runAsync(this::listenToServer); } catch (UnknownHostException e) { @@ -122,17 +137,15 @@ private boolean processClientCommand(String text) throws IOException { if (text.startsWith(Constants.COMMAND_TRIGGER)) { text = text.substring(1); // remove the / // System.out.println("Checking command: " + text); - // rk975- 11/3/25 if (isConnection("/" + text)) { if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { - System.out.println( + LoggerUtil.INSTANCE.warning( TextFX.colorize("Please set your name via /name before connecting", Color.RED)); return true; } // replaces multiple spaces with a single space // splits on the space after connect (gives us host and port) // splits on : to get host as index 0 and port as index 1 - // rk975 - 11/3/25 relevant snippet of code String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); sendClientName(myUser.getClientName());// sync follow-up data (handshake) @@ -140,19 +153,25 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.NAME.command)) { text = text.replace(Command.NAME.command, "").trim(); if (text == null || text.length() == 0) { - System.out.println(TextFX.colorize("This command requires a name as an argument", Color.RED)); + LoggerUtil.INSTANCE + .warning(TextFX.colorize("This command requires a name as an argument", Color.RED)); return true; } myUser.setClientName(text);// temporary until we get a response from the server - System.out.println(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), Color.YELLOW)); wasCommand = true; } else if (text.equalsIgnoreCase(Command.LIST_USERS.command)) { - System.out.println(TextFX.colorize("Known clients:", Color.CYAN)); - knownClients.forEach((key, value) -> { - System.out.println(TextFX.colorize(String.format("%s%s", value.getDisplayName(), - key == myUser.getClientId() ? " (you)" : ""), Color.CYAN)); - }); + String message = TextFX.colorize("Known clients:\n", Color.CYAN); + LoggerUtil.INSTANCE.info(TextFX.colorize("Known clients:", Color.CYAN)); + message += String.join("\n", knownClients.values().stream() + .map(c -> String.format("%s %s %s %s", + c.getDisplayName(), + c.getClientId() == myUser.getClientId() ? " (you)" : "", + c.isReady() ? "[x]" : "[ ]", + c.didTakeTurn() ? "[T]" : "[ ]")) + .toList()); + LoggerUtil.INSTANCE.info(message); wasCommand = true; } else if (Command.QUIT.command.equalsIgnoreCase(text)) { close(); @@ -160,16 +179,15 @@ private boolean processClientCommand(String text) throws IOException { } else if (Command.DISCONNECT.command.equalsIgnoreCase(text)) { sendDisconnect(); wasCommand = true; - // rk975 - 11/3/25 } else if (text.startsWith(Command.REVERSE.command)) { text = text.replace(Command.REVERSE.command, "").trim(); sendReverse(text); wasCommand = true; } else if (text.startsWith(Command.CREATE_ROOM.command)) { text = text.replace(Command.CREATE_ROOM.command, "").trim(); - // rk975 - 11/3/25 if (text == null || text.length() == 0) { - System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + LoggerUtil.INSTANCE + .warning(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; } sendRoomAction(text, RoomAction.CREATE); @@ -177,7 +195,8 @@ private boolean processClientCommand(String text) throws IOException { } else if (text.startsWith(Command.JOIN_ROOM.command)) { text = text.replace(Command.JOIN_ROOM.command, "").trim(); if (text == null || text.length() == 0) { - System.out.println(TextFX.colorize("This command requires a room name as an argument", Color.RED)); + LoggerUtil.INSTANCE + .warning(TextFX.colorize("This command requires a room name as an argument", Color.RED)); return true; } sendRoomAction(text, RoomAction.JOIN); @@ -187,12 +206,77 @@ private boolean processClientCommand(String text) throws IOException { // /leave) sendRoomAction(text, RoomAction.LEAVE); wasCommand = true; + } else if (text.startsWith(Command.LIST_ROOMS.command)) { + text = text.replace(Command.LIST_ROOMS.command, "").trim(); + + sendRoomAction(text, RoomAction.LIST); + wasCommand = true; + } else if (text.equalsIgnoreCase(Command.READY.command)) { + sendReady(); + wasCommand = true; + } else if (text.startsWith(Command.EXAMPLE_TURN.command)) { + text = text.replace(Command.EXAMPLE_TURN.command, "").trim(); + + sendDoTurn(text); + wasCommand = true; + } else if (text.startsWith("pick")) { + text = text.replace("pick", "").trim(); + if (text == null || text.length() == 0) { + LoggerUtil.INSTANCE + .warning(TextFX.colorize("This command requires a choice r, p, or s", Color.RED)); + return true; + } + sendPick(text.trim()); + wasCommand = true; + } else if (text.equalsIgnoreCase(Command.SCOREBOARD.command)) { + sendScoreboardRequest(); + wasCommand = true; } } return wasCommand; } // Start Send*() methods + private void sendDoTurn(String text) throws IOException { + // NOTE for now using ReadyPayload as it has the necessary properties + // An actual turn may include other data for your project + ReadyPayload rp = new ReadyPayload(); + rp.setPayloadType(PayloadType.TURN); + rp.setReady(true); // <- technically not needed as we'll use the payload type as a trigger + rp.setMessage(text); + sendToServer(rp); + } + + private void sendPick(String text) throws IOException { + String c = text.toLowerCase(); + if (!(c.equals("r") || c.equals("p") || c.equals("s"))) { + LoggerUtil.INSTANCE.warning(TextFX.colorize("Invalid pick. Use r, p, or s", Color.RED)); + return; + } + PickPayload pp = new PickPayload(); + pp.setPayloadType(PayloadType.PICK); + pp.setChoice(c); + sendToServer(pp); + } + + private void sendScoreboardRequest() throws IOException { + Payload p = new Payload(); + p.setPayloadType(PayloadType.SCOREBOARD); + sendToServer(p); + } + + /** + * Sends the client's intent to be ready. + * Can also be used to toggle the ready state if coded on the server-side + * + * @throws IOException + */ + private void sendReady() throws IOException { + ReadyPayload rp = new ReadyPayload(); + // rp.setReady(true); // <- technically not needed as we'll use the payload type + // as a trigger + sendToServer(rp); + } /** * Sends a room action to the server @@ -201,7 +285,6 @@ private boolean processClientCommand(String text) throws IOException { * @param roomAction (join, leave, create) * @throws IOException */ - // rk975 - 11/3/25 private void sendRoomAction(String roomName, RoomAction roomAction) throws IOException { Payload payload = new Payload(); payload.setMessage(roomName); @@ -215,8 +298,11 @@ private void sendRoomAction(String roomName, RoomAction roomAction) throws IOExc case RoomAction.LEAVE: payload.setPayloadType(PayloadType.ROOM_LEAVE); break; + case RoomAction.LIST: + payload.setPayloadType(PayloadType.ROOM_LIST); + break; default: - System.out.println(TextFX.colorize("Invalid room action", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Invalid room action", Color.RED)); break; } sendToServer(payload); @@ -254,7 +340,6 @@ private void sendDisconnect() throws IOException { * @throws IOException */ private void sendMessage(String message) throws IOException { - // rk975 11/3/25 Payload payload = new Payload(); payload.setMessage(message); payload.setPayloadType(PayloadType.MESSAGE); @@ -277,17 +362,16 @@ private void sendClientName(String name) throws IOException { private void sendToServer(Payload payload) throws IOException { if (isConnected()) { out.writeObject(payload); - // rk975 11/3/25 out.flush(); // good practice to ensure data is written out immediately } else { - System.out.println( + LoggerUtil.INSTANCE.warning( "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); } } // End Send*() methods public void start() throws IOException { - System.out.println("Client starting"); + LoggerUtil.INSTANCE.info("Client starting"); // Use CompletableFuture to run listenToInput() in a separate thread CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); @@ -307,22 +391,24 @@ private void listenToServer() { processPayload(fromServer); } else { - System.out.println("Server disconnected"); + LoggerUtil.INSTANCE.info("Server disconnected"); break; } } } catch (ClassCastException | ClassNotFoundException cce) { - System.err.println("Error reading object as specified type: " + cce.getMessage()); - cce.printStackTrace(); + LoggerUtil.INSTANCE.severe("Error reading object as specified type:", cce); + // cce.printStackTrace(); } catch (IOException e) { if (isRunning) { - System.out.println("Connection dropped"); + LoggerUtil.INSTANCE.warning("Connection dropped"); e.printStackTrace(); } + } catch (Exception e) { + LoggerUtil.INSTANCE.severe("Unexpected error in listenToServer()", e); } finally { closeServerConnection(); } - System.out.println("listenToServer thread stopped"); + LoggerUtil.INSTANCE.info("listenToServer thread stopped"); } private void processPayload(Payload payload) { @@ -352,35 +438,138 @@ private void processPayload(Payload payload) { case SYNC_CLIENT: processRoomAction(payload); break; + case ROOM_LIST: + processRoomsList(payload); + break; + case PayloadType.READY: + processReadyStatus(payload, false); + break; + case PayloadType.SYNC_READY: + processReadyStatus(payload, true); + break; + case PayloadType.RESET_READY: + // note no data necessary as this is just a trigger + processResetReady(); + break; + case PayloadType.PHASE: + processPhase(payload); + break; + case PayloadType.TURN: + case PayloadType.SYNC_TURN: + processTurn(payload); + break; + case PayloadType.RESET_TURN: + // note no data necessary as this is just a trigger + processResetTurn(); + break; + case PayloadType.POINTS: + processPoints(payload); + break; default: - System.out.println(TextFX.colorize("Unhandled payload type", Color.YELLOW)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Unhandled payload type", Color.YELLOW)); break; } } // Start process*() methods + private void processResetTurn() { + knownClients.values().forEach(cp -> cp.setTookTurn(false)); + System.out.println("Turn status reset for everyone"); + } + + private void processTurn(Payload payload) { + // Note: For now assuming ReadyPayload (this may be changed later) + if (!(payload instanceof ReadyPayload)) { + error("Invalid payload subclass for processTurn"); + return; + } + ReadyPayload rp = (ReadyPayload) payload; + if (!knownClients.containsKey(rp.getClientId())) { + LoggerUtil.INSTANCE.severe(String.format("Received turn status for client id %s who is not known", + rp.getClientId())); + return; + } + User cp = knownClients.get(rp.getClientId()); + cp.setTookTurn(rp.isReady()); + if (payload.getPayloadType() != PayloadType.SYNC_TURN) { + String message = String.format("%s %s their turn", cp.getDisplayName(), + cp.didTakeTurn() ? "took" : "reset"); + LoggerUtil.INSTANCE.info(message); + } + + } + + private void processPhase(Payload payload) { + currentPhase = Enum.valueOf(Phase.class, payload.getMessage()); + System.out.println(TextFX.colorize("Current phase is " + currentPhase.name(), Color.YELLOW)); + } + + private void processResetReady() { + knownClients.values().forEach(cp -> cp.setReady(false)); + System.out.println("Ready status reset for everyone"); + } + + private void processReadyStatus(Payload payload, boolean isQuiet) { + if (!(payload instanceof ReadyPayload)) { + error("Invalid payload subclass for processRoomsList"); + return; + } + ReadyPayload rp = (ReadyPayload) payload; + if (!knownClients.containsKey(rp.getClientId())) { + LoggerUtil.INSTANCE.severe(String.format("Received ready status [%s] for client id %s who is not known", + rp.isReady() ? "ready" : "not ready", rp.getClientId())); + return; + } + User cp = knownClients.get(rp.getClientId()); + cp.setReady(rp.isReady()); + if (!isQuiet) { + System.out.println( + String.format("%s is %s", cp.getDisplayName(), + rp.isReady() ? "ready" : "not ready")); + } + } + + private void processRoomsList(Payload payload) { + if (!(payload instanceof RoomResultPayload)) { + error("Invalid payload subclass for processRoomsList"); + return; + } + RoomResultPayload rrp = (RoomResultPayload) payload; + List rooms = rrp.getRooms(); + if (rooms == null || rooms.size() == 0) { + LoggerUtil.INSTANCE.warning( + TextFX.colorize("No rooms found matching your query", + Color.RED)); + return; + } + LoggerUtil.INSTANCE.info(TextFX.colorize("Room Results:", Color.PURPLE)); + LoggerUtil.INSTANCE.info( + String.join(System.lineSeparator(), rooms)); + } + private void processClientData(Payload payload) { if (myUser.getClientId() != Constants.DEFAULT_CLIENT_ID) { - System.out.println(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Client ID already set, this shouldn't happen", Color.YELLOW)); } myUser.setClientId(payload.getClientId()); myUser.setClientName(((ConnectionPayload) payload).getClientName());// confirmation from Server knownClients.put(myUser.getClientId(), myUser); - System.out.println(TextFX.colorize("Connected", Color.GREEN)); + LoggerUtil.INSTANCE.info(TextFX.colorize("Connected", Color.GREEN)); } private void processDisconnect(Payload payload) { if (payload.getClientId() == myUser.getClientId()) { knownClients.clear(); myUser.reset(); - System.out.println(TextFX.colorize("You disconnected", Color.RED)); + LoggerUtil.INSTANCE.info(TextFX.colorize("You disconnected", Color.RED)); } else if (knownClients.containsKey(payload.getClientId())) { User disconnectedUser = knownClients.remove(payload.getClientId()); if (disconnectedUser != null) { - System.out.println(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), - Color.RED)); + LoggerUtil.INSTANCE + .info(TextFX.colorize(String.format("%s disconnected", disconnectedUser.getDisplayName()), + Color.RED)); } } @@ -406,13 +595,13 @@ private void processRoomAction(Payload payload) { knownClients.remove(connectionPayload.getClientId()); } if (connectionPayload.getMessage() != null) { - System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); + LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.YELLOW)); } break; case ROOM_JOIN: if (connectionPayload.getMessage() != null) { - System.out.println(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); + LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); } // cascade to manage knownClients case SYNC_CLIENT: @@ -431,11 +620,30 @@ private void processRoomAction(Payload payload) { } private void processMessage(Payload payload) { - System.out.println(TextFX.colorize(payload.getMessage(), Color.BLUE)); + LoggerUtil.INSTANCE.info(TextFX.colorize(payload.getMessage(), Color.BLUE)); } private void processReverse(Payload payload) { - System.out.println(TextFX.colorize(payload.getMessage(), Color.PURPLE)); + LoggerUtil.INSTANCE.info(TextFX.colorize(payload.getMessage(), Color.PURPLE)); + } + + private void processPoints(Payload payload) { + if (!(payload instanceof PointsPayload)) { + error("Invalid payload subclass for processPoints"); + return; + } + PointsPayload pp = (PointsPayload) payload; + long id = pp.getClientId(); + int pts = pp.getPoints(); + if (!knownClients.containsKey(id)) { + // create a placeholder user entry + User user = new User(); + user.setClientId(id); + knownClients.put(id, user); + } + User u = knownClients.get(id); + u.setPoints(pts); + System.out.println(String.format("%s has %d points", u.getDisplayName(), pts)); } // End process*() methods @@ -444,8 +652,7 @@ private void processReverse(Payload payload) { */ private void listenToInput() { try (Scanner si = new Scanner(System.in)) { - System.out.println("Waiting for input"); // moved here to avoid console spam - // rk975-11/3/25 + LoggerUtil.INSTANCE.info("Waiting for input"); // moved here to avoid console spam while (isRunning) { // Run until isRunning is false String userInput = si.nextLine(); if (!processClientCommand(userInput)) { @@ -453,10 +660,10 @@ private void listenToInput() { } } } catch (IOException ioException) { - System.out.println("Error in listentToInput()"); - ioException.printStackTrace(); + LoggerUtil.INSTANCE.severe("Error in listenToInput()", ioException); + // ioException.printStackTrace(); } - System.out.println("listenToInput thread stopped"); + LoggerUtil.INSTANCE.info("listenToInput thread stopped"); } /** @@ -465,7 +672,7 @@ private void listenToInput() { private void close() { isRunning = false; closeServerConnection(); - System.out.println("Client terminated"); + LoggerUtil.INSTANCE.info("Client terminated"); // System.exit(0); // Terminate the application } @@ -475,7 +682,7 @@ private void close() { private void closeServerConnection() { try { if (out != null) { - System.out.println("Closing output stream"); + LoggerUtil.INSTANCE.info("Closing output stream"); out.close(); } } catch (Exception e) { @@ -483,7 +690,7 @@ private void closeServerConnection() { } try { if (in != null) { - System.out.println("Closing input stream"); + LoggerUtil.INSTANCE.info("Closing input stream"); in.close(); } } catch (Exception e) { @@ -491,12 +698,13 @@ private void closeServerConnection() { } try { if (server != null) { - System.out.println("Closing connection"); + LoggerUtil.INSTANCE.info("Closing connection"); server.close(); - System.out.println("Closed socket"); + LoggerUtil.INSTANCE.info("Closed Socket"); } } catch (IOException e) { e.printStackTrace(); + // LoggerUtil.INSTANCE.severe("Socket Error", e); } } From ad13dd1aed77dbbb2959ba29c5d9ca74e7909b37 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 13:43:28 -0500 Subject: [PATCH 34/56] Added few comments --- Project/Client/Client.java | 170 +++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 84 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index 88311fc..da282aa 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -29,19 +29,19 @@ import Project.Common.TextFX.Color; /** - * Demoing bi-directional communication between client and server in a - * multi-client scenario + * Demonstrates two-way communication between a client and server + * in a multi-user environment. */ public enum Client { INSTANCE; { - // statically initialize the client-side LoggerUtil + // Configure the client-side logger when the enum is initialized LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); - config.setFileSizeLimit(2048 * 1024); // 2MB + config.setFileSizeLimit(2048 * 1024); // max log file size: 2MB config.setFileCount(1); config.setLogLocation("client.log"); - // Set the logger configuration + // Apply the logger settings LoggerUtil.INSTANCE.setConfig(config); } private Socket server = null; @@ -50,7 +50,7 @@ public enum Client { final Pattern ipAddressPattern = Pattern .compile("/connect\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}:\\d{3,5})"); final Pattern localhostPattern = Pattern.compile("/connect\\s+(localhost:\\d{3,5})"); - private volatile boolean isRunning = true; // volatile for thread-safe visibility + private volatile boolean isRunning = true; // volatile for proper visibility across threads private final ConcurrentHashMap knownClients = new ConcurrentHashMap(); private User myUser = new User(); private Phase currentPhase = Phase.READY; @@ -59,7 +59,7 @@ private void error(String message) { LoggerUtil.INSTANCE.severe(TextFX.colorize(String.format("%s", message), Color.RED)); } - // needs to be private now that the enum logic is handling this + // Private constructor since enum handles instance management private Client() { LoggerUtil.INSTANCE.info("Client Created"); } @@ -69,28 +69,29 @@ public boolean isConnected() { return false; } // https://stackoverflow.com/a/10241044 - // Note: these check the client's end of the socket connect; therefore they - // don't really help determine if the server had a problem - // and is just for lesson's sake + // Note: these checks only verify the client's side of the socket; + // they don't reliably indicate server-side issues and are mainly + // included as an instructional example. return server.isConnected() && !server.isClosed() && !server.isInputShutdown() && !server.isOutputShutdown(); } /** - * Takes an IP address and a port to attempt a socket connection to a server. + * Attempts to open a socket connection to a server using the given + * IP address and port number. * - * @param address - * @param port - * @return true if connection was successful + * @param address server IP or hostname + * @param port server port + * @return true if the socket successfully connects */ private boolean connect(String address, int port) { try { server = new Socket(address, port); - // channel to send to server + // stream used to send objects to the server out = new ObjectOutputStream(server.getOutputStream()); - // channel to listen to server + // stream used to receive objects from the server in = new ObjectInputStream(server.getInputStream()); LoggerUtil.INSTANCE.info("Client connected"); - // Use CompletableFuture to run listenToServer() in a separate thread + // Run listenToServer() asynchronously on a separate thread CompletableFuture.runAsync(this::listenToServer); } catch (UnknownHostException e) { e.printStackTrace(); @@ -102,8 +103,8 @@ private boolean connect(String address, int port) { /** *

- * Check if the string contains the connect command - * followed by an IP address and port or localhost and port. + * Checks whether the input text contains the connect command + * followed by a valid IP address and port or localhost and port. *

*

* Example format: 123.123.123.123:3000 @@ -113,8 +114,8 @@ private boolean connect(String address, int port) { *

* https://www.w3schools.com/java/java_regex.asp * - * @param text - * @return true if the text is a valid connection command + * @param text user input to evaluate + * @return true if the string represents a well-formed connection command */ private boolean isConnection(String text) { Matcher ipMatcher = ipAddressPattern.matcher(text); @@ -123,19 +124,19 @@ private boolean isConnection(String text) { } /** - * Controller for handling various text commands. + * Central handler for user-entered commands. *

- * Add more here as needed + * Extend this with additional command handling as needed. *

* - * @param text - * @return true if the text was a command or triggered a command - * @throws IOException + * @param text full text entered by the user + * @return true if the input was recognized as a command or caused a command to run + * @throws IOException if sending data to the server fails */ private boolean processClientCommand(String text) throws IOException { boolean wasCommand = false; if (text.startsWith(Constants.COMMAND_TRIGGER)) { - text = text.substring(1); // remove the / + text = text.substring(1); // strip the leading '/' // System.out.println("Checking command: " + text); if (isConnection("/" + text)) { if (myUser.getClientName() == null || myUser.getClientName().isEmpty()) { @@ -143,12 +144,12 @@ private boolean processClientCommand(String text) throws IOException { TextFX.colorize("Please set your name via /name before connecting", Color.RED)); return true; } - // replaces multiple spaces with a single space - // splits on the space after connect (gives us host and port) - // splits on : to get host as index 0 and port as index 1 + // collapse repeated spaces into a single space + // split on the space after "connect" to isolate host and port + // then split on ":" to separate host (index 0) and port (index 1) String[] parts = text.trim().replaceAll(" +", " ").split(" ")[1].split(":"); connect(parts[0].trim(), Integer.parseInt(parts[1].trim())); - sendClientName(myUser.getClientName());// sync follow-up data (handshake) + sendClientName(myUser.getClientName()); // send follow-up identification (handshake) wasCommand = true; } else if (text.startsWith(Command.NAME.command)) { text = text.replace(Command.NAME.command, "").trim(); @@ -157,7 +158,7 @@ private boolean processClientCommand(String text) throws IOException { .warning(TextFX.colorize("This command requires a name as an argument", Color.RED)); return true; } - myUser.setClientName(text);// temporary until we get a response from the server + myUser.setClientName(text); // temporary value until server confirms LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), Color.YELLOW)); wasCommand = true; @@ -202,8 +203,7 @@ private boolean processClientCommand(String text) throws IOException { sendRoomAction(text, RoomAction.JOIN); wasCommand = true; } else if (text.startsWith(Command.LEAVE_ROOM.command) || text.startsWith("leave")) { - // Note: Accounts for /leave and /leaveroom variants (or anything beginning with - // /leave) + // Note: Handles /leave, /leaveroom, and any command that begins with "/leave" sendRoomAction(text, RoomAction.LEAVE); wasCommand = true; } else if (text.startsWith(Command.LIST_ROOMS.command)) { @@ -236,13 +236,13 @@ private boolean processClientCommand(String text) throws IOException { return wasCommand; } - // Start Send*() methods + // Begin Send*() helper methods private void sendDoTurn(String text) throws IOException { - // NOTE for now using ReadyPayload as it has the necessary properties - // An actual turn may include other data for your project + // NOTE: currently reusing ReadyPayload since it already contains the fields we need + // A dedicated turn payload could include more details specific to your project ReadyPayload rp = new ReadyPayload(); rp.setPayloadType(PayloadType.TURN); - rp.setReady(true); // <- technically not needed as we'll use the payload type as a trigger + rp.setReady(true); // <- technically unnecessary since payload type is the main trigger rp.setMessage(text); sendToServer(rp); } @@ -266,24 +266,24 @@ private void sendScoreboardRequest() throws IOException { } /** - * Sends the client's intent to be ready. - * Can also be used to toggle the ready state if coded on the server-side + * Informs the server that this client is ready. + * On the server side, this could also be interpreted as a toggle + * depending on implementation. * - * @throws IOException + * @throws IOException if sending the payload fails */ private void sendReady() throws IOException { ReadyPayload rp = new ReadyPayload(); - // rp.setReady(true); // <- technically not needed as we'll use the payload type - // as a trigger + // rp.setReady(true); // <- not required if server only checks payload type sendToServer(rp); } /** - * Sends a room action to the server + * Sends an action related to room management to the server. * - * @param roomName - * @param roomAction (join, leave, create) - * @throws IOException + * @param roomName the name of the room being targeted + * @param roomAction type of room operation (join, leave, create, list) + * @throws IOException if there is a problem communicating with the server */ private void sendRoomAction(String roomName, RoomAction roomAction) throws IOException { Payload payload = new Payload(); @@ -309,10 +309,10 @@ private void sendRoomAction(String roomName, RoomAction roomAction) throws IOExc } /** - * Sends a reverse message action to the server + * Sends a "reverse message" request to the server. * - * @param message - * @throws IOException + * @param message the original string to be reversed or processed + * @throws IOException if sending fails */ private void sendReverse(String message) throws IOException { Payload payload = new Payload(); @@ -323,9 +323,9 @@ private void sendReverse(String message) throws IOException { } /** - * Sends a disconnect action to the server + * Notifies the server that this client wishes to disconnect. * - * @throws IOException + * @throws IOException if sending fails */ private void sendDisconnect() throws IOException { Payload payload = new Payload(); @@ -334,10 +334,10 @@ private void sendDisconnect() throws IOException { } /** - * Sends a message to the server + * Sends a general chat/message payload to the server. * - * @param message - * @throws IOException + * @param message content to send to other clients via the server + * @throws IOException if writing to the output stream fails */ private void sendMessage(String message) throws IOException { Payload payload = new Payload(); @@ -347,10 +347,11 @@ private void sendMessage(String message) throws IOException { } /** - * Sends the client's name to the server (what the user desires to be called) + * Sends the user's preferred display name to the server so it knows + * how to refer to this client. * - * @param name - * @throws IOException + * @param name desired display name + * @throws IOException if the payload cannot be sent */ private void sendClientName(String name) throws IOException { ConnectionPayload payload = new ConnectionPayload(); @@ -362,31 +363,32 @@ private void sendClientName(String name) throws IOException { private void sendToServer(Payload payload) throws IOException { if (isConnected()) { out.writeObject(payload); - out.flush(); // good practice to ensure data is written out immediately + out.flush(); // ensures data is pushed out immediately } else { LoggerUtil.INSTANCE.warning( "Not connected to server (hint: type `/connect host:port` without the quotes and replace host/port with the necessary info)"); } } - // End Send*() methods + // End Send*() helper methods public void start() throws IOException { LoggerUtil.INSTANCE.info("Client starting"); - // Use CompletableFuture to run listenToInput() in a separate thread + // Run listenToInput() on a separate thread using CompletableFuture CompletableFuture inputFuture = CompletableFuture.runAsync(this::listenToInput); - // Wait for inputFuture to complete to ensure proper termination + // Block until the input-handling thread finishes to allow a clean shutdown inputFuture.join(); } /** - * Listens for messages from the server + * Continuously listens for incoming data from the server and + * dispatches it to the appropriate handler. */ private void listenToServer() { try { while (isRunning && isConnected()) { - Payload fromServer = (Payload) in.readObject(); // blocking read + Payload fromServer = (Payload) in.readObject(); // blocking read until an object arrives if (fromServer != null) { processPayload(fromServer); @@ -413,7 +415,7 @@ private void listenToServer() { private void processPayload(Payload payload) { switch (payload.getPayloadType()) { - case CLIENT_CONNECT:// unused + case CLIENT_CONNECT: // unused break; case CLIENT_ID: processClientData(payload); @@ -448,7 +450,7 @@ private void processPayload(Payload payload) { processReadyStatus(payload, true); break; case PayloadType.RESET_READY: - // note no data necessary as this is just a trigger + // no payload body required; this acts purely as a reset signal processResetReady(); break; case PayloadType.PHASE: @@ -459,7 +461,7 @@ private void processPayload(Payload payload) { processTurn(payload); break; case PayloadType.RESET_TURN: - // note no data necessary as this is just a trigger + // no extra data required; this is purely a reset trigger processResetTurn(); break; case PayloadType.POINTS: @@ -472,14 +474,14 @@ private void processPayload(Payload payload) { } } - // Start process*() methods + // Begin process*() handler methods private void processResetTurn() { knownClients.values().forEach(cp -> cp.setTookTurn(false)); System.out.println("Turn status reset for everyone"); } private void processTurn(Payload payload) { - // Note: For now assuming ReadyPayload (this may be changed later) + // Note: Currently assuming ReadyPayload (may be replaced with a custom payload later) if (!(payload instanceof ReadyPayload)) { error("Invalid payload subclass for processTurn"); return; @@ -554,7 +556,7 @@ private void processClientData(Payload payload) { } myUser.setClientId(payload.getClientId()); - myUser.setClientName(((ConnectionPayload) payload).getClientName());// confirmation from Server + myUser.setClientName(((ConnectionPayload) payload).getClientName()); // confirmation from Server knownClients.put(myUser.getClientId(), myUser); LoggerUtil.INSTANCE.info(TextFX.colorize("Connected", Color.GREEN)); } @@ -581,8 +583,7 @@ private void processRoomAction(Payload payload) { return; } ConnectionPayload connectionPayload = (ConnectionPayload) payload; - // use DEFAULT_CLIENT_ID to clear knownClients (mostly for disconnect and room - // transitions) + // use DEFAULT_CLIENT_ID to clear knownClients (typically on disconnect or when changing rooms) if (connectionPayload.getClientId() == Constants.DEFAULT_CLIENT_ID) { knownClients.clear(); return; @@ -590,7 +591,7 @@ private void processRoomAction(Payload payload) { switch (connectionPayload.getPayloadType()) { case ROOM_LEAVE: - // remove from map + // remove departing user from the tracking map if (knownClients.containsKey(connectionPayload.getClientId())) { knownClients.remove(connectionPayload.getClientId()); } @@ -603,9 +604,9 @@ private void processRoomAction(Payload payload) { if (connectionPayload.getMessage() != null) { LoggerUtil.INSTANCE.info(TextFX.colorize(connectionPayload.getMessage(), Color.GREEN)); } - // cascade to manage knownClients + // fall-through to keep the client list synchronized case SYNC_CLIENT: - // add to map + // add or update client information in the map if (!knownClients.containsKey(connectionPayload.getClientId())) { User user = new User(); user.setClientId(connectionPayload.getClientId()); @@ -636,7 +637,7 @@ private void processPoints(Payload payload) { long id = pp.getClientId(); int pts = pp.getPoints(); if (!knownClients.containsKey(id)) { - // create a placeholder user entry + // create a placeholder entry if the user isn't already tracked User user = new User(); user.setClientId(id); knownClients.put(id, user); @@ -645,15 +646,16 @@ private void processPoints(Payload payload) { u.setPoints(pts); System.out.println(String.format("%s has %d points", u.getDisplayName(), pts)); } - // End process*() methods + // End process*() handler methods /** - * Listens for keyboard input from the user + * Watches for keyboard input from the local user and forwards + * it either as commands or chat messages to the server. */ private void listenToInput() { try (Scanner si = new Scanner(System.in)) { - LoggerUtil.INSTANCE.info("Waiting for input"); // moved here to avoid console spam - while (isRunning) { // Run until isRunning is false + LoggerUtil.INSTANCE.info("Waiting for input"); // placed here to avoid repeated log spam + while (isRunning) { // continue loop until isRunning is flipped to false String userInput = si.nextLine(); if (!processClientCommand(userInput)) { sendMessage(userInput); @@ -667,17 +669,17 @@ private void listenToInput() { } /** - * Closes the client connection and associated resources + * Shuts down the client and cleans up all related resources. */ private void close() { isRunning = false; closeServerConnection(); LoggerUtil.INSTANCE.info("Client terminated"); - // System.exit(0); // Terminate the application + // System.exit(0); // Optionally terminate the entire application } /** - * Closes the server connection and associated resources + * Closes the connection to the server along with input/output streams. */ private void closeServerConnection() { try { @@ -717,4 +719,4 @@ public static void main(String[] args) { e.printStackTrace(); } } -} \ No newline at end of file +} From ce37a9318a477192f26a8900a7b97c0c21651f85 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:01:15 -0500 Subject: [PATCH 35/56] All payload files --- Project/Common/PayloadType.java | 42 ++++++++++++++++++--------- Project/Common/PickPayload.java | 19 ++++++++++++ Project/Common/PointsPayload.java | 19 ++++++++++++ Project/Common/ReadyPayload.java | 22 ++++++++++++++ Project/Common/RoomResultPayload.java | 25 ++++++++++++++++ 5 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 Project/Common/PickPayload.java create mode 100644 Project/Common/PointsPayload.java create mode 100644 Project/Common/ReadyPayload.java create mode 100644 Project/Common/RoomResultPayload.java diff --git a/Project/Common/PayloadType.java b/Project/Common/PayloadType.java index 64f9fde..c96f9e0 100644 --- a/Project/Common/PayloadType.java +++ b/Project/Common/PayloadType.java @@ -1,16 +1,32 @@ - package Project.Common; public enum PayloadType { - CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data - // [name]) - CLIENT_ID, // server sending client id - SYNC_CLIENT, // silent syncing of clients in room - DISCONNECT, // distinct disconnect action - ROOM_CREATE, - ROOM_JOIN, - ROOM_LEAVE, - REVERSE, - MESSAGE, // sender and message, - ROOM_LIST, // list of rooms -} + CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data + // [name]) + CLIENT_ID, // server sending client id + SYNC_CLIENT, // silent syncing of clients in room + DISCONNECT, // distinct disconnect action + ROOM_CREATE, + ROOM_JOIN, + ROOM_LEAVE, + REVERSE, + MESSAGE, // sender and message, + ROOM_LIST, // list of rooms + READY, // client to trigger themselves as ready, server to sync the related status of a + // particular client + SYNC_READY, // quiet version of READY, used to sync existing ready status of clients in a + // GameRoom + RESET_READY, // trigger to tell the client to reset their whole local list's ready status + // (saves network requests) + PHASE, // syncs current phase of session (used as a switch to only allow certain logic + // to execute) + TURN, // example of taking a turn and syncing a turn action + SYNC_TURN, // quiet version of TURN, used to sync existing turn status of clients in a + // GameRoom + RESET_TURN, // trigger to tell client to reset their local list turn status + PICK, // client picks rock/paper/scissors + POINTS, // server sync of player points + ROUND_START, + ROUND_END, + SCOREBOARD, +} \ No newline at end of file diff --git a/Project/Common/PickPayload.java b/Project/Common/PickPayload.java new file mode 100644 index 0000000..86ae5b7 --- /dev/null +++ b/Project/Common/PickPayload.java @@ -0,0 +1,19 @@ +package Project.Common; + +public class PickPayload extends Payload { + private String choice; // "r", "p", or "s" + + public String getChoice() { + return choice; + } + + public void setChoice(String choice) { + this.choice = choice; + } + + @Override + public String toString() { + return String.format("PickPayload{clientId=%d, choice=%s, type=%s}", getClientId(), choice, + getPayloadType()); + } +} \ No newline at end of file diff --git a/Project/Common/PointsPayload.java b/Project/Common/PointsPayload.java new file mode 100644 index 0000000..679e6c6 --- /dev/null +++ b/Project/Common/PointsPayload.java @@ -0,0 +1,19 @@ +package Project.Common; + +public class PointsPayload extends Payload { + private int points = 0; + + public int getPoints() { + return points; + } + + public void setPoints(int points) { + this.points = points; + } + + @Override + public String toString() { + return String.format("PointsPayload{clientId=%d, points=%d, type=%s}", getClientId(), points, + getPayloadType()); + } +} \ No newline at end of file diff --git a/Project/Common/ReadyPayload.java b/Project/Common/ReadyPayload.java new file mode 100644 index 0000000..1f7fa50 --- /dev/null +++ b/Project/Common/ReadyPayload.java @@ -0,0 +1,22 @@ +package Project.Common; + +public class ReadyPayload extends Payload { + private boolean isReady; + + public ReadyPayload() { + setPayloadType(PayloadType.READY); + } + + public boolean isReady() { + return isReady; + } + + public void setReady(boolean isReady) { + this.isReady = isReady; + } + + @Override + public String toString() { + return super.toString() + String.format(" isReady [%s]", isReady ? "ready" : "not ready"); + } +} \ No newline at end of file diff --git a/Project/Common/RoomResultPayload.java b/Project/Common/RoomResultPayload.java new file mode 100644 index 0000000..8f7fb8e --- /dev/null +++ b/Project/Common/RoomResultPayload.java @@ -0,0 +1,25 @@ +package Project.Common; + +import java.util.ArrayList; +import java.util.List; + +public class RoomResultPayload extends Payload { + private List rooms = new ArrayList(); + + public RoomResultPayload() { + setPayloadType(PayloadType.ROOM_LIST); + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + @Override + public String toString() { + return super.toString() + "Rooms [" + String.join(",", rooms) + "]"; + } +} \ No newline at end of file From 9db6d881d01a66131db69d6ee92b071fd29d8a1c Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:02:57 -0500 Subject: [PATCH 36/56] comments added --- Project/Common/PayloadType.java | 52 +++++++++++++++------------------ 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/Project/Common/PayloadType.java b/Project/Common/PayloadType.java index c96f9e0..7a3d475 100644 --- a/Project/Common/PayloadType.java +++ b/Project/Common/PayloadType.java @@ -1,32 +1,26 @@ package Project.Common; public enum PayloadType { - CLIENT_CONNECT, // client requesting to connect to server (passing of initialization data - // [name]) - CLIENT_ID, // server sending client id - SYNC_CLIENT, // silent syncing of clients in room - DISCONNECT, // distinct disconnect action - ROOM_CREATE, - ROOM_JOIN, - ROOM_LEAVE, - REVERSE, - MESSAGE, // sender and message, - ROOM_LIST, // list of rooms - READY, // client to trigger themselves as ready, server to sync the related status of a - // particular client - SYNC_READY, // quiet version of READY, used to sync existing ready status of clients in a - // GameRoom - RESET_READY, // trigger to tell the client to reset their whole local list's ready status - // (saves network requests) - PHASE, // syncs current phase of session (used as a switch to only allow certain logic - // to execute) - TURN, // example of taking a turn and syncing a turn action - SYNC_TURN, // quiet version of TURN, used to sync existing turn status of clients in a - // GameRoom - RESET_TURN, // trigger to tell client to reset their local list turn status - PICK, // client picks rock/paper/scissors - POINTS, // server sync of player points - ROUND_START, - ROUND_END, - SCOREBOARD, -} \ No newline at end of file + CLIENT_CONNECT, // client initiating a connection to the server (sending initial data like name) + CLIENT_ID, // server providing a unique identifier back to the client + SYNC_CLIENT, // quietly updates/synchronizes the list of clients in a room + DISCONNECT, // explicit request to disconnect from the server + ROOM_CREATE, // request to create a new room + ROOM_JOIN, // request to join an existing room + ROOM_LEAVE, // request to leave the current room + REVERSE, // reverse-text style operation + MESSAGE, // standard chat payload containing sender info and message text + ROOM_LIST, // response or request that involves listing available rooms + READY, // client signaling they are ready; server uses it to update one client's ready status + SYNC_READY, // non-verbose READY update, used to synchronize ready flags for clients in a GameRoom + RESET_READY, // instruction to reset all local ready flags on the client side (reduces extra network calls) + PHASE, // communicates the current phase/state of the game session (used as a gate for allowed actions) + TURN, // indicates a player has taken a turn and that action should be propagated + SYNC_TURN, // silent TURN update, used to align each client's view of who has taken a turn in a GameRoom + RESET_TURN, // directive for clients to clear local turn-tracking state + PICK, // client selection for rock/paper/scissors choice + POINTS, // server update with current player score information + ROUND_START, // notification that a new round has begun + ROUND_END, // notification that the current round has concluded + SCOREBOARD, // request or response for overall scoring/leaderboard data +} From 28e6df19c83daf27fb18c08628f8bc7fe7215ce9 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:20:03 -0500 Subject: [PATCH 37/56] New files added and old ones modififed --- Project/Common/Command.java | 8 +- Project/Common/LoggerUtil.java | 612 +++++++++++++++++++++++++++++++++ Project/Common/Phase.java | 7 + Project/Common/RoomAction.java | 3 +- Project/Common/TimedEvent.java | 110 ++++++ Project/Common/User.java | 59 +++- 6 files changed, 792 insertions(+), 7 deletions(-) create mode 100644 Project/Common/LoggerUtil.java create mode 100644 Project/Common/Phase.java create mode 100644 Project/Common/TimedEvent.java diff --git a/Project/Common/Command.java b/Project/Common/Command.java index d98dc78..ea455ed 100644 --- a/Project/Common/Command.java +++ b/Project/Common/Command.java @@ -1,4 +1,3 @@ - package Project.Common; import java.util.HashMap; @@ -14,7 +13,10 @@ public enum Command { JOIN_ROOM("joinroom"), NAME("name"), LIST_USERS("users"), - LIST_ROOMS("listrooms"); + LIST_ROOMS("listrooms"), + READY("ready"), + SCOREBOARD("scoreboard"), + EXAMPLE_TURN("exampleturn"),; private static final HashMap BY_COMMAND = new HashMap<>(); static { @@ -31,4 +33,4 @@ private Command(String command) { public static Command stringToCommand(String command) { return BY_COMMAND.get(command); } -} +} \ No newline at end of file diff --git a/Project/Common/LoggerUtil.java b/Project/Common/LoggerUtil.java new file mode 100644 index 0000000..d75c18c --- /dev/null +++ b/Project/Common/LoggerUtil.java @@ -0,0 +1,612 @@ +package Project.Common; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.logging.ConsoleHandler; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +/** + * Utility class for logging messages to a log file. + * This class provides methods to log messages at various levels and ensures + * thread-safe logging to an appropriate log file. + */ +public enum LoggerUtil { + INSTANCE; + + private Logger logger; + private LoggerConfig config; + private boolean isConfigured = false; + + LoggerUtil() { + } + + /** + * Sets the configuration for the logger. + * + * @param config the LoggerConfig object containing all the settings + */ + public void setConfig(LoggerConfig config) { + this.config = config; + setupLogger(); + } + + /** + * CustomFormatter class for formatting the log messages. + * This class formats the log messages to include the date, log level, source, + * and message. + */ + private static class CustomFormatter extends Formatter { + private static final String PATTERN = "MM/dd/yyyy HH:mm:ss"; + private static final String RESET = "\u001B[0m"; + private static final String RED = "\u001B[31m"; + private static final String GREEN = "\u001B[32m"; + private static final String YELLOW = "\u001B[33m"; + private static final String BLUE = "\u001B[34m"; + private static final String PURPLE = "\u001B[35m"; + private static final String CYAN = "\u001B[36m"; + private static final String WHITE = "\u001B[37m"; + + @Override + public String format(LogRecord record) { + SimpleDateFormat dateFormat = new SimpleDateFormat(PATTERN); + String date = dateFormat.format(new Date(record.getMillis())); + String callingClass = getCallingClassName(); + String source = callingClass != null ? callingClass + : record.getSourceClassName() != null ? record.getSourceClassName() : "unknown"; + + String message = formatMessage(record); + if (message == null) + message = "null"; + String level = getColoredLevel(record.getLevel()); + String throwable = ""; + if (record.getThrown() != null) { + // Use stackTraceLimit from LoggerConfig to truncate stack trace + throwable = "\n" + + getFormattedStackTrace(record.getThrown(), LoggerUtil.INSTANCE.config.getStackTraceLimit()); + } + return String.format("%s [%s] (%s):\n> %s%s\n", date, source, level, message, throwable); + } + + /** + * Determines the name of the class that called the logging method. + * + * @return the name of the calling class + */ + private static String getCallingClassName() { + String loggerUtilPackage = LoggerUtil.class.getPackage().getName(); + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + for (StackTraceElement element : stackTrace) { + String className = element.getClassName(); + // Skip all classes in the logging framework and the package of LoggerUtil + if (!className.startsWith("java.util.logging") && + !className.startsWith(loggerUtilPackage) && + !className.equals(Thread.class.getName())) { + return className; + } + } + return null; + } + + /** + * Returns a colored log level string based on the severity. + * + * @param level the log level + * @return the colored log level string + */ + private static String getColoredLevel(Level level) { + switch (level.getName()) { + case "SEVERE": + return RED + level.getName() + RESET; + case "WARNING": + return YELLOW + level.getName() + RESET; + case "INFO": + return GREEN + level.getName() + RESET; + case "CONFIG": + return CYAN + level.getName() + RESET; + case "FINE": + return BLUE + level.getName() + RESET; + case "FINER": + return PURPLE + level.getName() + RESET; + case "FINEST": + return WHITE + level.getName() + RESET; + default: + return level.getName(); + } + } + + /** + * Generates the stack trace string from the given Throwable. + * The format includes the exception class name, message (if any), + * and the stack trace elements up to the specified maxElements. + * + * @param throwable the throwable to extract the stack trace from + * @param maxElements the maximum number of stack trace elements to show + * @return the formatted stack trace as a string + */ + private static String getFormattedStackTrace(Throwable throwable, int maxElements) { + StringBuilder sb = new StringBuilder(); + + // Add the exception class name and message + sb.append(throwable.getClass().getName()); + if (throwable.getMessage() != null) { + sb.append(": ").append(throwable.getMessage()); + } + sb.append("\n"); + + // Stack trace elements (limit output) + StackTraceElement[] stackTrace = throwable.getStackTrace(); + int length = stackTrace.length; + int displayLimit = Math.min(maxElements, length); + + for (int i = 0; i < displayLimit; i++) { + sb.append("\tat ").append(stackTrace[i]).append("\n"); + } + + if (length > maxElements) { + sb.append("\t... ").append(length - maxElements).append(" more elements truncated ...\n"); + } + + // Recursively log suppressed exceptions + for (Throwable suppressed : throwable.getSuppressed()) { + sb.append("Suppressed: ").append(getFormattedStackTrace(suppressed, maxElements)); + } + + // Recursively log the cause + Throwable cause = throwable.getCause(); + if (cause != null && cause != throwable) { + sb.append("Caused by: ").append(getFormattedStackTrace(cause, maxElements)); + } + + return sb.toString(); + } + + } + + /** + * Ensures the logger is configured only once. + */ + private synchronized void setupLogger() { + if (isConfigured) { + return; + } + if (config == null) { + throw new IllegalStateException("LoggerUtil configuration must be set before use."); + } + try { + logger = Logger.getLogger("ApplicationLogger"); + + // Remove default console handlers + Logger rootLogger = Logger.getLogger(""); + for (var handler : rootLogger.getHandlers()) { + rootLogger.removeHandler(handler); + } + + // Customize the file naming pattern + String logPattern = config.getLogLocation().replace(".log", "-%g.log"); + // FileHandler writes log messages to a specified file, with support for + // rotating log files + FileHandler fileHandler = new FileHandler( + logPattern, + config.getFileSizeLimit(), + config.getFileCount(), + true); + fileHandler.setFormatter(new CustomFormatter()); + fileHandler.setLevel(config.getFileLogLevel()); + logger.addHandler(fileHandler); + + // ConsoleHandler prints log messages to the console + ConsoleHandler consoleHandler = new ConsoleHandler(); + consoleHandler.setFormatter(new CustomFormatter()); + consoleHandler.setLevel(config.getConsoleLogLevel()); + logger.addHandler(consoleHandler); + + logger.setLevel(Level.ALL); + isConfigured = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Logs a message at the specified level. + * + * @param level the level of the log message + * @param message the log message + */ + public void log(Level level, String message) { + if (!isConfigured) + setupLogger(); + logger.log(level, message); + } + + /** + * Logs a message at the specified level, overloaded to accept an Object. + * If the Object is a String, logs it as a message. + * If the Object is a Throwable (Exception), logs its message and stack trace. + * Otherwise, logs the Object's toString(). + * + * @param level the level of the log message + * @param message the Object to log + */ + public void log(Level level, Object message) { + if (!isConfigured) { + setupLogger(); + } + + if (message instanceof String) { + logger.log(level, (String) message); + + } else if (message instanceof Throwable) { + Throwable t = (Throwable) message; + String msg = (t.getMessage() != null) ? t.getMessage() : t.getClass().getName(); + logger.log(level, msg, t); + + } else if (message != null) { + try { + logger.log(level, message.toString()); + } catch (Exception ex) { + logger.log(level, "Error during toString(): " + ex.getMessage(), ex); + } + + } else { + logger.log(level, "null"); + } + } + + /** + * Logs an exception at the specified level. + * + * @param level the level of the log message + * @param message the log message + * @param throwable the exception to log + */ + public void log(Level level, String message, Throwable throwable) { + if (!isConfigured) + setupLogger(); + logger.log(level, message, throwable); + } + + /** + * Logs an informational message. + * + * @param message the log message + */ + public void info(String message) { + log(Level.INFO, message); + } + + /** + * Logs an informational message, overloaded to accept an Object. + * + * @param message the Object to log + */ + public void info(Object message) { + log(Level.INFO, message); + } + + /** + * Logs an exception with an INFO level. + * + * @param message the log message + * @param throwable the exception to log + */ + public void info(String message, Throwable throwable) { + log(Level.INFO, message, throwable); + } + + /** + * Logs a warning message. + * + * @param message the log message + */ + public void warning(String message) { + log(Level.WARNING, message); + } + + /** + * Logs a warning message, overloaded to accept an Object. + * + * @param message the Object to log + */ + public void warning(Object message) { + log(Level.WARNING, message); + } + + /** + * Logs an exception with a WARNING level. + * + * @param message the log message + * @param throwable the exception to log + */ + public void warning(String message, Throwable throwable) { + log(Level.WARNING, message, throwable); + } + + /** + * Logs a severe error message. + * + * @param message the log message + */ + public void severe(String message) { + log(Level.SEVERE, message); + } + + /** + * Logs a severe error message, overloaded to accept an Object. + * + * @param message the Object to log + */ + public void severe(Object message) { + log(Level.SEVERE, message); + } + + /** + * Logs an exception with a SEVERE level. + * + * @param message the log message + * @param throwable the exception to log + */ + public void severe(String message, Throwable throwable) { + log(Level.SEVERE, message, throwable); + } + + /** + * Logs a fine-grained informational message. + * + * @param message the log message + */ + public void fine(String message) { + log(Level.FINE, message); + } + + /** + * Logs a fine-grained informational message, overloaded to accept an Object. + * + * @param message the Object to log + */ + public void fine(Object message) { + log(Level.FINE, message); + } + + /** + * Logs a finer-grained informational message. + * + * @param message the log message + */ + public void finer(String message) { + log(Level.FINER, message); + } + + /** + * Logs a finer-grained informational message, overloaded to accept an Object. + * + * @param message the Object to log + */ + public void finer(Object message) { + log(Level.FINER, message); + } + + /** + * Logs the finest-grained informational message. + * + * @param message the log message + */ + public void finest(String message) { + log(Level.FINEST, message); + } + + /** + * Logs the finest-grained informational message, overloaded to accept an + * Object. + * + * @param message the Object to log + */ + public void finest(Object message) { + log(Level.FINEST, message); + } + + /** + * Configuration class for the LoggerUtil. + * This class encapsulates all the properties for configuring the logger. + */ + public static class LoggerConfig { + private int fileSizeLimit = 1024 * 1024; // 1MB default file size + private int fileCount = 5; // default number of rotating log files + private String logLocation = "application.log"; + private Level fileLogLevel = Level.ALL; // default log level for file + private Level consoleLogLevel = Level.ALL; // default log level for console + private int stackTraceLimit = 10; // default maximum number of stack trace elements + + // Getters and Setters for each property + + /** + * Gets the file limit for the log files. + * + * @return the maximum size of each log file in bytes + */ + public int getFileSizeLimit() { + return fileSizeLimit; + } + + /** + * Sets the file limit for the log files. + * + * @param fileLimit the maximum size of each log file in bytes + */ + public void setFileSizeLimit(int fileLimit) { + this.fileSizeLimit = fileLimit; + } + + /** + * Gets the number of rotating log files. + * + * @return the number of log files + */ + public int getFileCount() { + return fileCount; + } + + /** + * Sets the number of rotating log files. + * + * @param fileCount the number of log files + */ + public void setFileCount(int fileCount) { + this.fileCount = fileCount; + } + + /** + * Gets the file location for log files. + * + * @return the file location for logs + */ + public String getLogLocation() { + return logLocation; + } + + /** + * Sets the file location for log files. + * + * @param logLocation the file location for logs + */ + public void setLogLocation(String logLocation) { + this.logLocation = logLocation; + } + + /** + * Gets the log level for file logging. + * + * @return the log level for file logging + */ + public Level getFileLogLevel() { + return fileLogLevel; + } + + /** + * Sets the log level for file logging. + * + * @param fileLogLevel the log level for file logging + */ + public void setFileLogLevel(Level fileLogLevel) { + this.fileLogLevel = fileLogLevel; + } + + /** + * Gets the log level for console logging. + * + * @return the log level for console logging + */ + public Level getConsoleLogLevel() { + return consoleLogLevel; + } + + /** + * Sets the log level for console logging. + * + * @param consoleLogLevel the log level for console logging + */ + public void setConsoleLogLevel(Level consoleLogLevel) { + this.consoleLogLevel = consoleLogLevel; + } + + /** + * Gets the stack trace limit for logging. + * + * @return the maximum number of stack trace elements to show + */ + public int getStackTraceLimit() { + return stackTraceLimit; + } + + /** + * Sets the stack trace limit for logging. + * + * @param stackTraceLimit the maximum number of stack trace elements to show + */ + public void setStackTraceLimit(int stackTraceLimit) { + this.stackTraceLimit = stackTraceLimit; + } + } + + /** + * Example usage + * + * @param args + */ + public static void main(String[] args) { + // Create a LoggerConfig instance and set the desired configurations + LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); + config.setFileSizeLimit(2048 * 1024); // 2MB file size limit + config.setFileCount(10); // 10 rotating log files + config.setLogLocation("example.log"); // Log file location + config.setFileLogLevel(Level.ALL); // Log level for file + config.setConsoleLogLevel(Level.ALL); // Log level for console + + // Set the logger configuration + LoggerUtil.INSTANCE.setConfig(config); + + // Original examples + LoggerUtil.INSTANCE.info("This is an info message."); + LoggerUtil.INSTANCE.warning("This is a warning message."); + LoggerUtil.INSTANCE.severe("This is a severe error message."); + LoggerUtil.INSTANCE.fine("This is a fine-grained informational message."); + LoggerUtil.INSTANCE.finer("This is a finer-grained informational message."); + LoggerUtil.INSTANCE.finest("This is the finest-grained informational message."); + + // Simulate logging from a thread + new Thread(() -> { + LoggerUtil.INSTANCE.info("This is a message from a separate thread."); + }).start(); + + // Simulate an exception + LoggerUtil.INSTANCE.warning("This is a simulated warning exception.", new IOException("Simulated IOException")); + LoggerUtil.INSTANCE.severe("This is a simulated severe error", new Exception("Simulated Exception")); + + // New examples to test Object overloads + LoggerUtil.INSTANCE.info(new Object() { + @Override + public String toString() { + return "Logging a custom object using info"; + } + }); + + LoggerUtil.INSTANCE.warning(new Exception("Logging a Throwable object using warning")); + + LoggerUtil.INSTANCE.severe(new Object() { + @Override + public String toString() { + return "Logging a custom object using severe"; + } + }); + + LoggerUtil.INSTANCE.fine(new Object() { + @Override + public String toString() { + return "Logging a custom object using fine"; + } + }); + + // Logging a null value to test edge cases + try { + LoggerUtil.INSTANCE.info((Object) null); + } catch (Exception e) { + LoggerUtil.INSTANCE.severe("A NullPointerException occurred!", e); + } + // New example to trigger a larger stack trace (StackOverflowError) + try { + recursiveMethod(0); + } catch (StackOverflowError e) { + LoggerUtil.INSTANCE.severe("A StackOverflowError occurred!", e); + } + } + + private static void recursiveMethod(int depth) { + // Keep calling itself to cause a StackOverflowError + recursiveMethod(depth + 1); + } +} \ No newline at end of file diff --git a/Project/Common/Phase.java b/Project/Common/Phase.java new file mode 100644 index 0000000..d46fba8 --- /dev/null +++ b/Project/Common/Phase.java @@ -0,0 +1,7 @@ +package Project.Common; + +public enum Phase { + READY, // pre-setup phase + CHOOSING, // players pick their choice + IN_PROGRESS, // example phase that'll be renamed/removed later +} \ No newline at end of file diff --git a/Project/Common/RoomAction.java b/Project/Common/RoomAction.java index 35e3076..b61712b 100644 --- a/Project/Common/RoomAction.java +++ b/Project/Common/RoomAction.java @@ -1,6 +1,5 @@ - package Project.Common; public enum RoomAction { CREATE, JOIN, LEAVE, LIST -} +} \ No newline at end of file diff --git a/Project/Common/TimedEvent.java b/Project/Common/TimedEvent.java new file mode 100644 index 0000000..1495d45 --- /dev/null +++ b/Project/Common/TimedEvent.java @@ -0,0 +1,110 @@ +package Project.Common; + +/* Originally based off of https://gist.github.com/MattToegel/c55747f26c5092d6362678d5b1729ec6 */ + +import java.util.Timer; +import java.util.TimerTask; +import java.util.function.Consumer; + +/** + * Simple countdown timer demo of java.util.Timer facility. + * Formerly called Countdown + */ + +public class TimedEvent { + private int secondsRemaining; + private Runnable expireCallback = null; + private Consumer tickCallback = null; + final private Timer timer; + + /** + * Create a TimedEvent to trigger the passed in callback after a set duration + * + * @param durationInSeconds + * @param callback + */ + public TimedEvent(int durationInSeconds, Runnable callback) { + this(durationInSeconds); + this.expireCallback = callback; + } + + /** + * Create a TimedEvent to trigger after a set duration. + * Note: Requires expireCallback and/or tickCallback to be set otherwise it'll + * do nothing + * + * @param durationInSeconds + */ + public TimedEvent(int durationInSeconds) { + timer = new Timer(); + secondsRemaining = durationInSeconds; + timer.scheduleAtFixedRate(new TimerTask() { + public void run() { + secondsRemaining--; + if (tickCallback != null) { + tickCallback.accept(secondsRemaining); + } + if (secondsRemaining <= 0) { + timer.cancel(); + secondsRemaining = 0; + if (expireCallback != null) { + expireCallback.run(); + } + } + } + }, 1000, 1000); + } + + /** + * Set a method to be called every timer tick; it'll receive the current time of + * the timer. + * + * @param callback + */ + public void setTickCallback(Consumer callback) { + tickCallback = callback; + } + + /** + * Set a method to be called when the timer expires + * + * @param callback + */ + public void setExpireCallback(Runnable callback) { + expireCallback = callback; + } + + /** + * Removes all callback references and cancels the timer + */ + public void cancel() { + expireCallback = null; + tickCallback = null; + timer.cancel(); + } + + /** + * Used to override the remaining countdown durationInSeconds + */ + public void setDurationInSeconds(int d) { + secondsRemaining = d; + } + + public int getRemainingTime() { + return secondsRemaining; + } + + /** + * This is just for testing/demo + * + * @param args + */ + public static void main(String args[]) { + TimedEvent cd = new TimedEvent(30, () -> { + System.out.println("Time expired"); + }); + cd.setTickCallback((tick) -> { + System.out.println("Tick: " + tick); + }); + } +} \ No newline at end of file diff --git a/Project/Common/User.java b/Project/Common/User.java index 6d3c1b8..91d7e0e 100644 --- a/Project/Common/User.java +++ b/Project/Common/User.java @@ -1,9 +1,13 @@ - package Project.Common; public class User { private long clientId = Constants.DEFAULT_CLIENT_ID; private String clientName; + private boolean isReady = false; + private boolean tookTurn = false; + private int points = 0; + private boolean eliminated = false; + private String choice = null; // "r","p","s" or null /** * @return the clientId @@ -37,8 +41,59 @@ public String getDisplayName() { return String.format("%s#%s", this.clientName, this.clientId); } + public boolean isReady() { + return isReady; + } + + public void setReady(boolean isReady) { + this.isReady = isReady; + } + public void reset() { this.clientId = Constants.DEFAULT_CLIENT_ID; this.clientName = null; + this.isReady = false; + this.tookTurn = false; + this.points = 0; + this.eliminated = false; + this.choice = null; + } + + public int getPoints() { + return points; + } + + public void setPoints(int points) { + this.points = points; + } + + public boolean isEliminated() { + return eliminated; + } + + public void setEliminated(boolean eliminated) { + this.eliminated = eliminated; + } + + public String getChoice() { + return choice; + } + + public void setChoice(String choice) { + this.choice = choice; + } + + /** + * @return the tookTurn + */ + public boolean didTakeTurn() { + return tookTurn; + } + + /** + * @param tookTurn the tookTurn to set + */ + public void setTookTurn(boolean tookTurn) { + this.tookTurn = tookTurn; } -} +} \ No newline at end of file From bad915be251f1e618476943a5961e22ac170b804 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:22:11 -0500 Subject: [PATCH 38/56] comments made --- Project/Common/LoggerUtil.java | 310 ++++++++++++++++----------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/Project/Common/LoggerUtil.java b/Project/Common/LoggerUtil.java index d75c18c..190779a 100644 --- a/Project/Common/LoggerUtil.java +++ b/Project/Common/LoggerUtil.java @@ -11,9 +11,9 @@ import java.util.logging.Logger; /** - * Utility class for logging messages to a log file. - * This class provides methods to log messages at various levels and ensures - * thread-safe logging to an appropriate log file. + * Helper utility for writing log output to files and the console. + * Provides convenience methods for logging at different levels + * and coordinates thread-safe access to the underlying logger. */ public enum LoggerUtil { INSTANCE; @@ -26,9 +26,9 @@ public enum LoggerUtil { } /** - * Sets the configuration for the logger. - * - * @param config the LoggerConfig object containing all the settings + * Applies the given logger configuration. + * + * @param config the LoggerConfig instance containing all logger settings */ public void setConfig(LoggerConfig config) { this.config = config; @@ -36,9 +36,8 @@ public void setConfig(LoggerConfig config) { } /** - * CustomFormatter class for formatting the log messages. - * This class formats the log messages to include the date, log level, source, - * and message. + * Formatter implementation that controls the layout of log entries. + * Adds timestamp, log level, originating class, and the formatted message. */ private static class CustomFormatter extends Formatter { private static final String PATTERN = "MM/dd/yyyy HH:mm:ss"; @@ -65,7 +64,7 @@ public String format(LogRecord record) { String level = getColoredLevel(record.getLevel()); String throwable = ""; if (record.getThrown() != null) { - // Use stackTraceLimit from LoggerConfig to truncate stack trace + // Use stackTraceLimit from LoggerConfig to shorten the printed stack trace throwable = "\n" + getFormattedStackTrace(record.getThrown(), LoggerUtil.INSTANCE.config.getStackTraceLimit()); } @@ -73,16 +72,17 @@ public String format(LogRecord record) { } /** - * Determines the name of the class that called the logging method. - * - * @return the name of the calling class + * Attempts to determine the external class that invoked the logger. + * + * @return the fully-qualified name of the caller class, or null if not found */ private static String getCallingClassName() { String loggerUtilPackage = LoggerUtil.class.getPackage().getName(); StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); for (StackTraceElement element : stackTrace) { String className = element.getClassName(); - // Skip all classes in the logging framework and the package of LoggerUtil + // Skip entries from the Java logging framework, LoggerUtil's own package, + // and the Thread class to locate the actual caller. if (!className.startsWith("java.util.logging") && !className.startsWith(loggerUtilPackage) && !className.equals(Thread.class.getName())) { @@ -93,10 +93,10 @@ private static String getCallingClassName() { } /** - * Returns a colored log level string based on the severity. - * - * @param level the log level - * @return the colored log level string + * Wraps the log level name in an ANSI color code based on severity. + * + * @param level the logging Level + * @return a string representing the colored log level */ private static String getColoredLevel(Level level) { switch (level.getName()) { @@ -120,25 +120,24 @@ private static String getColoredLevel(Level level) { } /** - * Generates the stack trace string from the given Throwable. - * The format includes the exception class name, message (if any), - * and the stack trace elements up to the specified maxElements. - * - * @param throwable the throwable to extract the stack trace from - * @param maxElements the maximum number of stack trace elements to show - * @return the formatted stack trace as a string + * Builds a string representation of a Throwable's stack trace. + * It includes the exception type, message, and up to maxElements of the stack. + * + * @param throwable the Throwable to inspect + * @param maxElements maximum number of stack frames to output + * @return a nicely formatted stack trace string */ private static String getFormattedStackTrace(Throwable throwable, int maxElements) { StringBuilder sb = new StringBuilder(); - // Add the exception class name and message + // Add the exception class name and message details sb.append(throwable.getClass().getName()); if (throwable.getMessage() != null) { sb.append(": ").append(throwable.getMessage()); } sb.append("\n"); - // Stack trace elements (limit output) + // Output stack frames up to the configured limit StackTraceElement[] stackTrace = throwable.getStackTrace(); int length = stackTrace.length; int displayLimit = Math.min(maxElements, length); @@ -151,12 +150,12 @@ private static String getFormattedStackTrace(Throwable throwable, int maxElement sb.append("\t... ").append(length - maxElements).append(" more elements truncated ...\n"); } - // Recursively log suppressed exceptions + // Recursively include any suppressed exceptions for (Throwable suppressed : throwable.getSuppressed()) { sb.append("Suppressed: ").append(getFormattedStackTrace(suppressed, maxElements)); } - // Recursively log the cause + // Recursively include the root cause chain Throwable cause = throwable.getCause(); if (cause != null && cause != throwable) { sb.append("Caused by: ").append(getFormattedStackTrace(cause, maxElements)); @@ -168,7 +167,8 @@ private static String getFormattedStackTrace(Throwable throwable, int maxElement } /** - * Ensures the logger is configured only once. + * Initializes and configures the logger once. + * Subsequent calls are ignored after the first successful setup. */ private synchronized void setupLogger() { if (isConfigured) { @@ -180,16 +180,15 @@ private synchronized void setupLogger() { try { logger = Logger.getLogger("ApplicationLogger"); - // Remove default console handlers + // Strip existing default console handlers from the root logger Logger rootLogger = Logger.getLogger(""); for (var handler : rootLogger.getHandlers()) { rootLogger.removeHandler(handler); } - // Customize the file naming pattern + // Generate the log file pattern with index suffix support for rotation String logPattern = config.getLogLocation().replace(".log", "-%g.log"); - // FileHandler writes log messages to a specified file, with support for - // rotating log files + // FileHandler writes logs to disk, with rollover supported based on size/count FileHandler fileHandler = new FileHandler( logPattern, config.getFileSizeLimit(), @@ -199,7 +198,7 @@ private synchronized void setupLogger() { fileHandler.setLevel(config.getFileLogLevel()); logger.addHandler(fileHandler); - // ConsoleHandler prints log messages to the console + // ConsoleHandler outputs log entries to stdout/stderr ConsoleHandler consoleHandler = new ConsoleHandler(); consoleHandler.setFormatter(new CustomFormatter()); consoleHandler.setLevel(config.getConsoleLogLevel()); @@ -213,10 +212,10 @@ private synchronized void setupLogger() { } /** - * Logs a message at the specified level. - * - * @param level the level of the log message - * @param message the log message + * Generic logging method that logs a text message at the given level. + * + * @param level the severity level + * @param message the text to write to the log */ public void log(Level level, String message) { if (!isConfigured) @@ -225,13 +224,15 @@ public void log(Level level, String message) { } /** - * Logs a message at the specified level, overloaded to accept an Object. - * If the Object is a String, logs it as a message. - * If the Object is a Throwable (Exception), logs its message and stack trace. - * Otherwise, logs the Object's toString(). - * - * @param level the level of the log message - * @param message the Object to log + * Overloaded logging method that accepts an Object. + *
    + *
  • If the Object is a String, logs it directly.
  • + *
  • If it's a Throwable, logs both its message and stack trace.
  • + *
  • Otherwise, logs the result of message.toString().
  • + *
+ * + * @param level the severity level + * @param message arbitrary object to be logged */ public void log(Level level, Object message) { if (!isConfigured) { @@ -259,11 +260,11 @@ public void log(Level level, Object message) { } /** - * Logs an exception at the specified level. + * Logs an exception or error along with a message at the specified level. * - * @param level the level of the log message - * @param message the log message - * @param throwable the exception to log + * @param level the severity level + * @param message context message to accompany the exception + * @param throwable the Throwable being recorded */ public void log(Level level, String message, Throwable throwable) { if (!isConfigured) @@ -272,261 +273,260 @@ public void log(Level level, String message, Throwable throwable) { } /** - * Logs an informational message. - * - * @param message the log message + * Convenience method for INFO-level logging. + * + * @param message the message to write */ public void info(String message) { log(Level.INFO, message); } /** - * Logs an informational message, overloaded to accept an Object. - * - * @param message the Object to log + * INFO-level logging overload that accepts any Object. + * + * @param message the object to log */ public void info(Object message) { log(Level.INFO, message); } /** - * Logs an exception with an INFO level. + * Logs an INFO-level message along with a Throwable. * - * @param message the log message - * @param throwable the exception to log + * @param message the context message + * @param throwable the associated exception or error */ public void info(String message, Throwable throwable) { log(Level.INFO, message, throwable); } /** - * Logs a warning message. - * - * @param message the log message + * Convenience wrapper for WARNING-level logs. + * + * @param message the warning text */ public void warning(String message) { log(Level.WARNING, message); } /** - * Logs a warning message, overloaded to accept an Object. - * - * @param message the Object to log + * WARNING-level variant that accepts a generic Object. + * + * @param message object to be rendered in the log */ public void warning(Object message) { log(Level.WARNING, message); } /** - * Logs an exception with a WARNING level. + * Logs a WARNING-level message together with a Throwable. * - * @param message the log message - * @param throwable the exception to log + * @param message explanatory message + * @param throwable related exception */ public void warning(String message, Throwable throwable) { log(Level.WARNING, message, throwable); } /** - * Logs a severe error message. - * - * @param message the log message + * Convenience API for logging critical failures at SEVERE level. + * + * @param message description of the error condition */ public void severe(String message) { log(Level.SEVERE, message); } /** - * Logs a severe error message, overloaded to accept an Object. - * - * @param message the Object to log + * SEVERE-level overload for arbitrary objects. + * + * @param message the object whose contents should be logged */ public void severe(Object message) { log(Level.SEVERE, message); } /** - * Logs an exception with a SEVERE level. + * Logs a SEVERE-level entry with a Throwable and message. * - * @param message the log message - * @param throwable the exception to log + * @param message context or description + * @param throwable the underlying cause to be recorded */ public void severe(String message, Throwable throwable) { log(Level.SEVERE, message, throwable); } /** - * Logs a fine-grained informational message. - * - * @param message the log message + * Logs at FINE level, typically for detailed informational output. + * + * @param message the message payload */ public void fine(String message) { log(Level.FINE, message); } /** - * Logs a fine-grained informational message, overloaded to accept an Object. - * - * @param message the Object to log + * FINE-level logging of an arbitrary Object. + * + * @param message the object to be converted and logged */ public void fine(Object message) { log(Level.FINE, message); } /** - * Logs a finer-grained informational message. - * - * @param message the log message + * Logs at FINER level for more granular debugging information. + * + * @param message the detail message */ public void finer(String message) { log(Level.FINER, message); } /** - * Logs a finer-grained informational message, overloaded to accept an Object. - * - * @param message the Object to log + * FINER-level logging variant that works with an Object. + * + * @param message object whose string form will be logged */ public void finer(Object message) { log(Level.FINER, message); } /** - * Logs the finest-grained informational message. - * - * @param message the log message + * Logs at the FINEST level for extremely detailed diagnostic output. + * + * @param message descriptive message text */ public void finest(String message) { log(Level.FINEST, message); } /** - * Logs the finest-grained informational message, overloaded to accept an - * Object. - * - * @param message the Object to log + * FINEST-level method that can accept any Object. + * + * @param message object to serialize into the log */ public void finest(Object message) { log(Level.FINEST, message); } /** - * Configuration class for the LoggerUtil. - * This class encapsulates all the properties for configuring the logger. + * Configuration holder for LoggerUtil. + * Encapsulates all tuning parameters for both file and console logging. */ public static class LoggerConfig { - private int fileSizeLimit = 1024 * 1024; // 1MB default file size - private int fileCount = 5; // default number of rotating log files + private int fileSizeLimit = 1024 * 1024; // 1MB default maximum size per file + private int fileCount = 5; // default number of rotated log files private String logLocation = "application.log"; - private Level fileLogLevel = Level.ALL; // default log level for file - private Level consoleLogLevel = Level.ALL; // default log level for console - private int stackTraceLimit = 10; // default maximum number of stack trace elements + private Level fileLogLevel = Level.ALL; // default file logging threshold + private Level consoleLogLevel = Level.ALL; // default console logging threshold + private int stackTraceLimit = 10; // default limit for stack trace depth // Getters and Setters for each property /** - * Gets the file limit for the log files. - * - * @return the maximum size of each log file in bytes + * Returns the size threshold for individual log files. + * + * @return maximum allowed size in bytes for one log file */ public int getFileSizeLimit() { return fileSizeLimit; } /** - * Sets the file limit for the log files. - * - * @param fileLimit the maximum size of each log file in bytes + * Updates the size cap for each log file in the rotation set. + * + * @param fileLimit new file size limit in bytes */ public void setFileSizeLimit(int fileLimit) { this.fileSizeLimit = fileLimit; } /** - * Gets the number of rotating log files. - * - * @return the number of log files + * Retrieves how many log files are maintained in the rotation. + * + * @return number of log files used for rotation */ public int getFileCount() { return fileCount; } /** - * Sets the number of rotating log files. - * - * @param fileCount the number of log files + * Specifies how many rolling log files should be kept. + * + * @param fileCount desired number of log files in the cycle */ public void setFileCount(int fileCount) { this.fileCount = fileCount; } /** - * Gets the file location for log files. - * - * @return the file location for logs + * Gets the base path/filename for the log output. + * + * @return path string for the log file destination */ public String getLogLocation() { return logLocation; } /** - * Sets the file location for log files. - * - * @param logLocation the file location for logs + * Sets the base file path used by the FileHandler. + * + * @param logLocation the log file name or path */ public void setLogLocation(String logLocation) { this.logLocation = logLocation; } /** - * Gets the log level for file logging. - * - * @return the log level for file logging + * Retrieves the logging Level applied to file output. + * + * @return file logging threshold */ public Level getFileLogLevel() { return fileLogLevel; } /** - * Sets the log level for file logging. - * - * @param fileLogLevel the log level for file logging + * Adjusts the minimum Level of messages written to file. + * + * @param fileLogLevel new file logging level */ public void setFileLogLevel(Level fileLogLevel) { this.fileLogLevel = fileLogLevel; } /** - * Gets the log level for console logging. - * - * @return the log level for console logging + * Retrieves the logging Level used for console output. + * + * @return console logging Level */ public Level getConsoleLogLevel() { return consoleLogLevel; } /** - * Sets the log level for console logging. - * - * @param consoleLogLevel the log level for console logging + * Sets the minimum severity for messages printed to the console. + * + * @param consoleLogLevel desired console logging Level */ public void setConsoleLogLevel(Level consoleLogLevel) { this.consoleLogLevel = consoleLogLevel; } /** - * Gets the stack trace limit for logging. - * - * @return the maximum number of stack trace elements to show + * Returns the current maximum stack frames shown in logged traces. + * + * @return stack trace length cap */ public int getStackTraceLimit() { return stackTraceLimit; } /** - * Sets the stack trace limit for logging. - * - * @param stackTraceLimit the maximum number of stack trace elements to show + * Configures how many stack trace elements are included when logging errors. + * + * @param stackTraceLimit upper bound for stack frames to output */ public void setStackTraceLimit(int stackTraceLimit) { this.stackTraceLimit = stackTraceLimit; @@ -534,20 +534,20 @@ public void setStackTraceLimit(int stackTraceLimit) { } /** - * Example usage - * - * @param args + * Simple demonstration entry point. + * + * @param args command-line arguments (unused) */ public static void main(String[] args) { - // Create a LoggerConfig instance and set the desired configurations + // Build a LoggerConfig instance and adjust sample settings LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); - config.setFileSizeLimit(2048 * 1024); // 2MB file size limit - config.setFileCount(10); // 10 rotating log files - config.setLogLocation("example.log"); // Log file location - config.setFileLogLevel(Level.ALL); // Log level for file - config.setConsoleLogLevel(Level.ALL); // Log level for console + config.setFileSizeLimit(2048 * 1024); // adjust file size cap to 2MB + config.setFileCount(10); // maintain 10 rotated log files + config.setLogLocation("example.log"); // sample log file name + config.setFileLogLevel(Level.ALL); // capture all messages to file + config.setConsoleLogLevel(Level.ALL); // show all messages on console - // Set the logger configuration + // Register the logger configuration LoggerUtil.INSTANCE.setConfig(config); // Original examples @@ -558,12 +558,12 @@ public static void main(String[] args) { LoggerUtil.INSTANCE.finer("This is a finer-grained informational message."); LoggerUtil.INSTANCE.finest("This is the finest-grained informational message."); - // Simulate logging from a thread + // Demonstrate logging from a separate thread context new Thread(() -> { LoggerUtil.INSTANCE.info("This is a message from a separate thread."); }).start(); - // Simulate an exception + // Try logging exceptions at different levels LoggerUtil.INSTANCE.warning("This is a simulated warning exception.", new IOException("Simulated IOException")); LoggerUtil.INSTANCE.severe("This is a simulated severe error", new Exception("Simulated Exception")); @@ -591,13 +591,13 @@ public String toString() { } }); - // Logging a null value to test edge cases + // Log a null reference to cover edge behavior try { LoggerUtil.INSTANCE.info((Object) null); } catch (Exception e) { LoggerUtil.INSTANCE.severe("A NullPointerException occurred!", e); } - // New example to trigger a larger stack trace (StackOverflowError) + // Example to deliberately trigger a deeper stack trace (StackOverflowError) try { recursiveMethod(0); } catch (StackOverflowError e) { @@ -606,7 +606,7 @@ public String toString() { } private static void recursiveMethod(int depth) { - // Keep calling itself to cause a StackOverflowError + // Recursively call itself until the stack overflows recursiveMethod(depth + 1); } -} \ No newline at end of file +} From 2119e8a9e4ecc27ed3fe325134c11afda2e8d747 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:23:50 -0500 Subject: [PATCH 39/56] comments added --- Project/Common/TimedEvent.java | 54 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/Project/Common/TimedEvent.java b/Project/Common/TimedEvent.java index 1495d45..3419891 100644 --- a/Project/Common/TimedEvent.java +++ b/Project/Common/TimedEvent.java @@ -1,16 +1,15 @@ package Project.Common; -/* Originally based off of https://gist.github.com/MattToegel/c55747f26c5092d6362678d5b1729ec6 */ +/* Initially inspired by https://gist.github.com/MattToegel/c55747f26c5092d6362678d5b1729ec6 */ import java.util.Timer; import java.util.TimerTask; import java.util.function.Consumer; /** - * Simple countdown timer demo of java.util.Timer facility. - * Formerly called Countdown + * Lightweight countdown-style timer using java.util.Timer. + * Previously referred to as "Countdown". */ - public class TimedEvent { private int secondsRemaining; private Runnable expireCallback = null; @@ -18,10 +17,11 @@ public class TimedEvent { final private Timer timer; /** - * Create a TimedEvent to trigger the passed in callback after a set duration - * - * @param durationInSeconds - * @param callback + * Builds a TimedEvent that will execute the provided callback once the + * specified time interval has elapsed. + * + * @param durationInSeconds number of seconds before expiration + * @param callback code to run when the timer finishes */ public TimedEvent(int durationInSeconds, Runnable callback) { this(durationInSeconds); @@ -29,11 +29,11 @@ public TimedEvent(int durationInSeconds, Runnable callback) { } /** - * Create a TimedEvent to trigger after a set duration. - * Note: Requires expireCallback and/or tickCallback to be set otherwise it'll - * do nothing - * - * @param durationInSeconds + * Constructs a TimedEvent that counts down for the given number of seconds. + * Note: you must assign an expireCallback and/or tickCallback, otherwise the + * timer will simply count down without triggering any actions. + * + * @param durationInSeconds initial countdown length in seconds */ public TimedEvent(int durationInSeconds) { timer = new Timer(); @@ -56,26 +56,26 @@ public void run() { } /** - * Set a method to be called every timer tick; it'll receive the current time of - * the timer. - * - * @param callback + * Registers a callback to be invoked on every timer tick. + * The callback receives the current remaining time in seconds. + * + * @param callback function to call once per second with the current countdown value */ public void setTickCallback(Consumer callback) { tickCallback = callback; } /** - * Set a method to be called when the timer expires - * - * @param callback + * Registers a callback to be executed when the countdown reaches zero. + * + * @param callback runnable to invoke when the timer expires */ public void setExpireCallback(Runnable callback) { expireCallback = callback; } /** - * Removes all callback references and cancels the timer + * Clears all callback references and halts the underlying timer. */ public void cancel() { expireCallback = null; @@ -84,7 +84,9 @@ public void cancel() { } /** - * Used to override the remaining countdown durationInSeconds + * Overrides the remaining countdown time in seconds. + * + * @param d new remaining duration, in seconds */ public void setDurationInSeconds(int d) { secondsRemaining = d; @@ -95,9 +97,9 @@ public int getRemainingTime() { } /** - * This is just for testing/demo - * - * @param args + * Basic usage demonstration / sanity check. + * + * @param args ignored */ public static void main(String args[]) { TimedEvent cd = new TimedEvent(30, () -> { @@ -107,4 +109,4 @@ public static void main(String args[]) { System.out.println("Tick: " + tick); }); } -} \ No newline at end of file +} From f96db7fe8b4f31e27c1f74dcd90260c0e9219197 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:25:08 -0500 Subject: [PATCH 40/56] Milstone2 ready --- Project/Common/User.java | 75 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/Project/Common/User.java b/Project/Common/User.java index 91d7e0e..413a4f6 100644 --- a/Project/Common/User.java +++ b/Project/Common/User.java @@ -7,9 +7,11 @@ public class User { private boolean tookTurn = false; private int points = 0; private boolean eliminated = false; - private String choice = null; // "r","p","s" or null + private String choice = null; // holds "r", "p", "s", or null depending on current selection /** + * Returns the unique identifier assigned to this client. + * * @return the clientId */ public long getClientId() { @@ -17,38 +19,63 @@ public long getClientId() { } /** - * @param clientId the clientId to set + * Updates the internal client identifier. + * + * @param clientId the id value to apply */ public void setClientId(long clientId) { this.clientId = clientId; } /** - * @return the username + * Retrieves the client's chosen display name. + * + * @return the clientName string */ public String getClientName() { return clientName; } /** - * @param username the username to set + * Assigns a display name for this client. + * + * @param username the name to store */ public void setClientName(String username) { this.clientName = username; } + /** + * Returns a formatted label combining name and ID for easy identification. + * + * @return a formatted string "name#id" + */ public String getDisplayName() { return String.format("%s#%s", this.clientName, this.clientId); } + /** + * Indicates whether this user has marked themselves as ready. + * + * @return true if ready, false otherwise + */ public boolean isReady() { return isReady; } + /** + * Toggles or sets the user's ready state. + * + * @param isReady new ready flag + */ public void setReady(boolean isReady) { this.isReady = isReady; } + /** + * Resets this user back to all default values. + * Useful after disconnecting or switching rooms. + */ public void reset() { this.clientId = Constants.DEFAULT_CLIENT_ID; this.clientName = null; @@ -59,41 +86,75 @@ public void reset() { this.choice = null; } + /** + * Retrieves the total points accumulated by this user. + * + * @return the current point count + */ public int getPoints() { return points; } + /** + * Updates the user's point total. + * + * @param points new score to assign + */ public void setPoints(int points) { this.points = points; } + /** + * Indicates whether this user has been removed or knocked out of a round. + * + * @return true if eliminated, false if still active + */ public boolean isEliminated() { return eliminated; } + /** + * Marks or unmarks the user as eliminated. + * + * @param eliminated elimination state to apply + */ public void setEliminated(boolean eliminated) { this.eliminated = eliminated; } + /** + * Retrieves the user's current selection (e.g., rock/paper/scissors). + * + * @return the stored choice value + */ public String getChoice() { return choice; } + /** + * Stores the user's chosen option (e.g., "r", "p", "s"). + * + * @param choice the value to store + */ public void setChoice(String choice) { this.choice = choice; } /** - * @return the tookTurn + * Identifies whether this user has already taken their turn. + * + * @return true if the turn has been taken */ public boolean didTakeTurn() { return tookTurn; } /** - * @param tookTurn the tookTurn to set + * Sets the internal flag indicating the user's turn state. + * + * @param tookTurn true if the user has acted this turn */ public void setTookTurn(boolean tookTurn) { this.tookTurn = tookTurn; } -} \ No newline at end of file +} From 7b300c2fa47580b9b6f0c3d11182728063441d3d Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:37:39 -0500 Subject: [PATCH 41/56] Added exception files for milestone2 --- Project/Exceptions/NotReadyException.java | 12 ++++++++++++ Project/Exceptions/PhaseMismatchException.java | 12 ++++++++++++ Project/Exceptions/PlayNotFoundException.java | 12 ++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 Project/Exceptions/NotReadyException.java create mode 100644 Project/Exceptions/PhaseMismatchException.java create mode 100644 Project/Exceptions/PlayNotFoundException.java diff --git a/Project/Exceptions/NotReadyException.java b/Project/Exceptions/NotReadyException.java new file mode 100644 index 0000000..d7267f6 --- /dev/null +++ b/Project/Exceptions/NotReadyException.java @@ -0,0 +1,12 @@ + +package Project.Exceptions; + +public class NotReadyException extends CustomIT114Exception { + public NotReadyException(String message) { + super(message); + } + + public NotReadyException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/Project/Exceptions/PhaseMismatchException.java b/Project/Exceptions/PhaseMismatchException.java new file mode 100644 index 0000000..5c469ee --- /dev/null +++ b/Project/Exceptions/PhaseMismatchException.java @@ -0,0 +1,12 @@ +package Project.Exceptions; + +public class PhaseMismatchException extends CustomIT114Exception { + public PhaseMismatchException(String message) { + super(message); + } + + public PhaseMismatchException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file diff --git a/Project/Exceptions/PlayNotFoundException.java b/Project/Exceptions/PlayNotFoundException.java new file mode 100644 index 0000000..92d9c65 --- /dev/null +++ b/Project/Exceptions/PlayNotFoundException.java @@ -0,0 +1,12 @@ +package Project.Exceptions; + +public class PlayerNotFoundException extends CustomIT114Exception { + public PlayerNotFoundException(String message) { + super(message); + } + + public PlayerNotFoundException(String message, Throwable cause) { + super(message, cause); + } + +} \ No newline at end of file From c60eb27406ea5eb24334db4c71978b45e995e128 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:51:10 -0500 Subject: [PATCH 42/56] new room files for milestone2 --- Project/Server/BaseGameRoom.java | 300 +++++++++++++++++++ Project/Server/GameRoom.java | 476 +++++++++++++++++++++++++++++++ Project/Server/Room.java | 37 ++- 3 files changed, 799 insertions(+), 14 deletions(-) create mode 100644 Project/Server/BaseGameRoom.java create mode 100644 Project/Server/GameRoom.java diff --git a/Project/Server/BaseGameRoom.java b/Project/Server/BaseGameRoom.java new file mode 100644 index 0000000..c9544d5 --- /dev/null +++ b/Project/Server/BaseGameRoom.java @@ -0,0 +1,300 @@ +package Project.Server; + +import Project.Common.Constants; +import Project.Common.LoggerUtil; +import Project.Common.Phase; +import Project.Common.TimedEvent; +import Project.Exceptions.NotReadyException; +import Project.Exceptions.PhaseMismatchException; +import Project.Exceptions.PlayerNotFoundException; + +/** + * No edits should be needed in this file, this prepares the core logic for the + * GameRoom + */ +public abstract class BaseGameRoom extends Room { + + private TimedEvent readyTimer = null; + + protected final int MINIMUM_REQUIRED_TO_START = 2; + + protected Phase currentPhase = Phase.READY; + + protected boolean allowToggleReady = false; + + public BaseGameRoom(String name) { + super(name); + } + + /** + * Project session initialization step (triggered from readyCheck) + */ + protected abstract void onSessionStart(); + + /** + * Round initialization step (in some cases can be used in place of turns if + * simple enough) + */ + protected abstract void onRoundStart(); + + /** + * Turn initialization step (if there are distinct turns) + */ + protected abstract void onTurnStart(); + + /** + * Turn cleanup step + */ + protected abstract void onTurnEnd(); + + /** + * Round cleanup step + */ + protected abstract void onRoundEnd(); + + /** + * Session cleanup step + */ + protected abstract void onSessionEnd(); + + /** + * Triggered when a client is successfully added to the base-Room map and the + * GameRoom map + * + * @param client the client who joined + */ + protected abstract void onClientAdded(ServerThread client); + + /** + * Triggered when a client is removed from the base-Room map and the GameRoom + * map + * + * @param client the client who was removed (can be null if removal already + * occurred) + */ + protected abstract void onClientRemoved(ServerThread client); + + @Override + protected synchronized void addClient(ServerThread client) { + if (!isRunning()) { // block action if Room isn't running + return; + } + // do the base Room class logic + super.addClient(client); + onClientAdded(client); + } + + @Override + protected synchronized void removeClient(ServerThread client) { + if (!isRunning()) { // block action if Room isn't running + return; + } + LoggerUtil.INSTANCE.info("Players in room: " + clientsInRoom.size()); + // do the base-class logic + super.removeClient(client); + onClientRemoved(client); + } + + @Override + protected synchronized void disconnect(ServerThread client) { + super.disconnect(client); + LoggerUtil.INSTANCE.info("Players in room: " + clientsInRoom.size()); + onClientRemoved(client); + } + + /** + * Cancels any in progress readyTimer + */ + protected void resetReadyTimer() { + if (readyTimer != null) { + readyTimer.cancel(); + readyTimer = null; + } + } + + /** + * Starts the ready timer + * + * @param resetOnTry when true, will cancel any active readyTimer + */ + protected void startReadyTimer(boolean resetOnTry) { + if (resetOnTry) { + resetReadyTimer(); + } + if (readyTimer == null) { + readyTimer = new TimedEvent(30, () -> { + // callback to trigger when ready expires + checkReadyStatus(); + }); + readyTimer.setTickCallback((time) -> System.out.println("Ready Timer: " + time)); + } + } + + /** + * Rules to begin a session: At least MINIMUM_REQUIRED_TO_START must be joined + * and ready + */ + private void checkReadyStatus() { + long numReady = clientsInRoom.values().stream().filter(p -> p.isReady()).count(); + if (numReady >= MINIMUM_REQUIRED_TO_START) { + resetReadyTimer(); + onSessionStart(); + } else { + onSessionEnd(); + } + } + + protected void resetReadyStatus() { + clientsInRoom.values().forEach(p -> p.setReady(false)); + sendResetReadyTrigger(); + } + + /** + * Attempts to change the current phase if the passed phase differs. + * If it changes, sends the update to all Clients + * + * @param phase + */ + protected void changePhase(Phase phase) { + if (currentPhase != phase) { + currentPhase = phase; + sendCurrentPhase(); + } + } + + // send/sync data to ServerThread(s) + + /** + * Syncs the current phase to a single client + * + * @param sp + */ + protected void syncCurrentPhase(ServerThread sp) { + sp.sendCurrentPhase(currentPhase); + } + + /** + * Sends the current phase to all clients + */ + protected void sendCurrentPhase() { + clientsInRoom.values().removeIf(spInRoom -> { + boolean failedToSend = !spInRoom.sendCurrentPhase(currentPhase); + if (failedToSend) { + removeClient(spInRoom); + } + return failedToSend; + }); + } + + /** + * A shorthand way of telling all clients to reset their local list's ready + * status + */ + protected void sendResetReadyTrigger() { + clientsInRoom.values().removeIf(spInRoom -> { + boolean failedToSend = !spInRoom.sendResetReady(); + if (failedToSend) { + removeClient(spInRoom); + } + return failedToSend; + }); + } + + /** + * Sends the ready status of each ServerThread to one client + * + * @param incomingSP + */ + protected void syncReadyStatus(ServerThread incomingSP) { + clientsInRoom.values().removeIf(spInRoom -> { + boolean failedToSend = !incomingSP.sendReadyStatus(spInRoom.getClientId(), spInRoom.isReady(), true); + if (failedToSend) { + removeClient(spInRoom); + } + return failedToSend && spInRoom.getClientId() == incomingSP.getClientId(); + }); + } + + /** + * Sends the ready status of one ServerThread to all clients + * + * @param incomingSP + * @param isReady + */ + protected void sendReadyStatus(ServerThread incomingSP, boolean isReady) { + clientsInRoom.values().removeIf(spInRoom -> { + boolean failedToSend = !spInRoom.sendReadyStatus(incomingSP.getClientId(), incomingSP.isReady()); + if (failedToSend) { + removeClient(spInRoom); + } + return failedToSend; + }); + } + // end send data to ServerThread(s) + + // receive data from ServerThread (GameRoom specific) + protected void handleReady(ServerThread sender) { + try { + // early exit checks + checkPlayerInRoom(sender); + checkCurrentPhase(sender, Phase.READY); + + ServerThread sp = null; + // option 1: simply just mark ready + if (!allowToggleReady) { + sp = clientsInRoom.get(sender.getClientId()); + sp.setReady(true); + } + // option 2: toggle + else { + sp = clientsInRoom.get(sender.getClientId()); + sp.setReady(!sp.isReady()); + } + startReadyTimer(false); // <-- triggers the next step when it expires + + sendReadyStatus(sp, sp.isReady()); + } catch (Exception e) { + LoggerUtil.INSTANCE.severe("handleReady exception", e); + } + + } + // end receive data from ServerThread (GameRoom specific) + + // Logic Checks + + /** + * Early exit (via exception throwing) if it's not the proper phase + * + * @param client + * @param check + * @throws Exception + */ + protected void checkCurrentPhase(ServerThread client, Phase check) throws Exception { + if (currentPhase != check) { + client.sendMessage(Constants.DEFAULT_CLIENT_ID, + String.format("Current phase is %s, please try again later", currentPhase.name())); + throw new PhaseMismatchException("Invalid Phase"); + } + } + + protected void checkIsReady(ServerThread client) throws NotReadyException { + if (!client.isReady()) { + client.sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be marked 'ready' to do this action"); + throw new NotReadyException("Not ready"); + } + } + + /** + * Early exit (via exception throwing) if the user isn't in the room + * + * @param client + * @throws Exception + */ + protected void checkPlayerInRoom(ServerThread client) throws Exception { + if (!clientsInRoom.containsKey(client.getClientId())) { + LoggerUtil.INSTANCE.severe("Player isn't in room"); + throw new PlayerNotFoundException("Player isn't in room"); + } + } + // end Logic Checks +} \ No newline at end of file diff --git a/Project/Server/GameRoom.java b/Project/Server/GameRoom.java new file mode 100644 index 0000000..1ff4681 --- /dev/null +++ b/Project/Server/GameRoom.java @@ -0,0 +1,476 @@ +package Project.Server; + +import Project.Common.Constants; +import Project.Common.LoggerUtil; +import Project.Common.Phase; +import Project.Common.TimedEvent; +import Project.Exceptions.NotReadyException; +import Project.Exceptions.PhaseMismatchException; +import Project.Exceptions.PlayerNotFoundException; + +public class GameRoom extends BaseGameRoom { + + // used for general rounds (usually phase-based turns) + private TimedEvent roundTimer = null; + + // used for granular turn handling (usually turn-order turns) + private TimedEvent turnTimer = null; + private int round = 0; + // If true, a single loss (attack or defense) eliminates a player. + // If false, player must lose both battles in the round to be eliminated. + private final boolean ELIMINATE_ON_SINGLE_LOSS = true; + + public GameRoom(String name) { + super(name); + } + + /** {@inheritDoc} */ + @Override + protected void onClientAdded(ServerThread sp) { + // sync GameRoom state to new client + syncCurrentPhase(sp); + syncReadyStatus(sp); + syncTurnStatus(sp); + } + + /** {@inheritDoc} */ + @Override + protected void onClientRemoved(ServerThread sp) { + // added after Summer 2024 Demo + // Stops the timers so room can clean up + LoggerUtil.INSTANCE.info("Player Removed, remaining: " + clientsInRoom.size()); + + if (clientsInRoom.isEmpty()) { + resetReadyTimer(); + resetTurnTimer(); + resetRoundTimer(); + onSessionEnd(); + } + } + + // timer handlers + private void startRoundTimer() { + roundTimer = new TimedEvent(30, () -> onRoundEnd()); + roundTimer.setTickCallback((time) -> System.out.println("Round Time: " + time)); + } + + private void resetRoundTimer() { + if (roundTimer != null) { + roundTimer.cancel(); + roundTimer = null; + } + } + + private void startTurnTimer() { + turnTimer = new TimedEvent(30, () -> onTurnEnd()); + turnTimer.setTickCallback((time) -> System.out.println("Turn Time: " + time)); + } + + private void resetTurnTimer() { + if (turnTimer != null) { + turnTimer.cancel(); + turnTimer = null; + } + } + // end timer handlers + + // lifecycle methods + + /** {@inheritDoc} */ + @Override + protected void onSessionStart() { + LoggerUtil.INSTANCE.info("onSessionStart() start"); + round = 0; + // Begin session in choosing phase for the RPS rounds + changePhase(Phase.CHOOSING); + LoggerUtil.INSTANCE.info("onSessionStart() end"); + onRoundStart(); + } + + /** {@inheritDoc} */ + @Override + protected void onRoundStart() { + LoggerUtil.INSTANCE.info("onRoundStart() start"); + resetRoundTimer(); + resetTurnStatus(); + round++; + // set phase to choosing and initialize choices for active players + changePhase(Phase.CHOOSING); + clientsInRoom.values().forEach(sp -> { + // reset choice only for non-eliminated players + sp.user.setChoice(null); + }); + relay(null, "Round " + round + " started. Pick with /pick "); + startRoundTimer(); + LoggerUtil.INSTANCE.info("onRoundStart() end"); + // Note: no turn lifecycle used here + // Users do their actions in between roundStart and roundEnd + } + + /** {@inheritDoc} */ + @Override + protected void onTurnStart() { + LoggerUtil.INSTANCE.info("onTurnStart() start"); + resetTurnTimer(); + + startTurnTimer(); + LoggerUtil.INSTANCE.info("onTurnStart() end"); + } + + // Note: logic between Turn Start and Turn End is typically handled via timers + // and user interaction + /** {@inheritDoc} */ + @Override + protected void onTurnEnd() { + LoggerUtil.INSTANCE.info("onTurnEnd() start"); + resetTurnTimer(); // reset timer if turn ended without the time expiring + LoggerUtil.INSTANCE.info("onTurnEnd() end"); + } + + // Note: logic between Round Start and Round End is typically handled via timers + // and user interaction + /** {@inheritDoc} */ + @Override + protected void onRoundEnd() { + LoggerUtil.INSTANCE.info("onRoundEnd() start"); + resetRoundTimer(); // reset timer if round ended without the time expiring + LoggerUtil.INSTANCE.info("onRoundEnd() end"); + relay(null, "Round ended — processing results..."); + // Process end of round: eliminate non-pickers, resolve battles, award points + processRoundResults(); + // check end conditions + long active = clientsInRoom.values().stream().filter(sp -> !sp.user.isEliminated()).count(); + if (active <= 1) { + onSessionEnd(); + } else { + // start next round + onRoundStart(); + } + } + + /** {@inheritDoc} */ + @Override + protected void onSessionEnd() { + LoggerUtil.INSTANCE.info("onSessionEnd() start"); + resetReadyStatus(); + resetTurnStatus(); + // announce winner(s) + java.util.List alive = clientsInRoom.values().stream() + .filter(sp -> !sp.user.isEliminated()).toList(); + if (alive.size() == 1) { + relay(null, String.format("%s is the winner!", alive.get(0).getDisplayName())); + } else if (alive.size() == 0) { + relay(null, "No players remaining — tie"); + } + + // send final scoreboard sorted by points + sendFinalScoreboard(); + + // reset player data for next session (do not disconnect) + clientsInRoom.values().forEach(sp -> { + sp.user.setPoints(0); + sp.user.setEliminated(false); + sp.user.setChoice(null); + sp.setTookTurn(false); + sp.setReady(false); + }); + + // sync points reset to clients + clientsInRoom.values().forEach(sp -> { + clientsInRoom.values().forEach(target -> target.sendPointsUpdate(sp.getClientId(), sp.user.getPoints())); + }); + + changePhase(Phase.READY); + LoggerUtil.INSTANCE.info("onSessionEnd() end"); + } + // end lifecycle methods + + // send/sync data to ServerThread(s) + private void sendResetTurnStatus() { + clientsInRoom.values().forEach(spInRoom -> { + boolean failedToSend = !spInRoom.sendResetTurnStatus(); + if (failedToSend) { + removeClient(spInRoom); + } + }); + } + + private void sendTurnStatus(ServerThread client, boolean tookTurn) { + clientsInRoom.values().removeIf(spInRoom -> { + boolean failedToSend = !spInRoom.sendTurnStatus(client.getClientId(), client.didTakeTurn()); + if (failedToSend) { + removeClient(spInRoom); + } + return failedToSend; + }); + } + + private void syncTurnStatus(ServerThread incomingClient) { + clientsInRoom.values().forEach(serverUser -> { + if (serverUser.getClientId() != incomingClient.getClientId()) { + boolean failedToSync = !incomingClient.sendTurnStatus(serverUser.getClientId(), + serverUser.didTakeTurn(), true); + if (failedToSync) { + LoggerUtil.INSTANCE.warning( + String.format("Removing disconnected %s from list", serverUser.getDisplayName())); + disconnect(serverUser); + } + } + }); + } + + // end send data to ServerThread(s) + + // misc methods + private void resetTurnStatus() { + clientsInRoom.values().forEach(sp -> { + sp.setTookTurn(false); + }); + sendResetTurnStatus(); + } + + private void checkAllTookTurn() { + int numReady = clientsInRoom.values().stream() + .filter(sp -> sp.isReady()) + .toList().size(); + int numTookTurn = clientsInRoom.values().stream() + // ensure to verify the isReady part since it's against the original list + .filter(sp -> sp.isReady() && sp.didTakeTurn()) + .toList().size(); + if (numReady == numTookTurn) { + relay(null, + String.format("All players have taken their turn (%d/%d) ending the round", numTookTurn, numReady)); + onRoundEnd(); + } + } + + // start check methods + + // end check methods + + // receive data from ServerThread (GameRoom specific) + + /** + * Handles the turn action from the client. + * + * @param currentUser + * @param exampleText (arbitrary text from the client, can be used for + * additional actions or information) + */ + protected void handleTurnAction(ServerThread currentUser, String exampleText) { + // check if the client is in the room + try { + checkPlayerInRoom(currentUser); + checkCurrentPhase(currentUser, Phase.IN_PROGRESS); + checkIsReady(currentUser); + if (currentUser.didTakeTurn()) { + currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, "You have already taken your turn this round"); + return; + } + currentUser.setTookTurn(true); + sendTurnStatus(currentUser, currentUser.didTakeTurn()); + // TODO handle example text possibly or other turn related intention from client + // finished processing the turn + checkAllTookTurn(); + } catch (NotReadyException e) { + // The check method already informs the currentUser + LoggerUtil.INSTANCE.severe("handleTurnAction exception", e); + } catch (PlayerNotFoundException e) { + currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to do the ready check"); + LoggerUtil.INSTANCE.severe("handleTurnAction exception", e); + } catch (PhaseMismatchException e) { + currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, + "You can only take a turn during the IN_PROGRESS phase"); + LoggerUtil.INSTANCE.severe("handleTurnAction exception", e); + } catch (Exception e) { + LoggerUtil.INSTANCE.severe("handleTurnAction exception", e); + } + } + + // end receive data from ServerThread (GameRoom specific) + + /** + * Override generic message handling so raw single-letter picks (r/p/s) + * submitted during CHOOSING are treated as picks and not broadcast. + */ + @Override + protected synchronized void handleMessage(ServerThread sender, String text) { + try { + if (currentPhase == Phase.CHOOSING && text != null) { + String t = text.trim(); + String tl = t.toLowerCase(); + // single-letter quick pick (e.g., "r") + if (tl.length() == 1 && (tl.equals("r") || tl.equals("p") || tl.equals("s"))) { + handlePick(sender, tl); + return; + } + // command form possibly sent as a MESSAGE (some clients may not parse commands correctly) + // accept "/pick p", "pick p", or similar variations + String withoutSlash = tl.startsWith("/") ? tl.substring(1).trim() : tl; + if (withoutSlash.startsWith("pick")) { + String[] parts = withoutSlash.split(" +"); + if (parts.length >= 2) { + String choice = parts[1].trim(); + if (choice.length() == 1 && (choice.equals("r") || choice.equals("p") || choice.equals("s"))) { + handlePick(sender, choice); + return; + } + } + } + } + } catch (Exception e) { + LoggerUtil.INSTANCE.severe("handleMessage override error", e); + } + // fallback to base behavior + super.handleMessage(sender, text); + } + + /** + * Handles a player's pick (r/p/s) + * + * @param currentUser + * @param choiceStr expected "r","p","s" + */ + protected void handlePick(ServerThread currentUser, String choiceStr) { + try { + checkPlayerInRoom(currentUser); + checkCurrentPhase(currentUser, Phase.CHOOSING); + checkIsReady(currentUser); + if (currentUser.user.isEliminated()) { + currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, "You are eliminated and cannot pick"); + return; + } + if (choiceStr == null || choiceStr.isBlank()) { + currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, "Invalid choice"); + return; + } + String c = choiceStr.trim().toLowerCase(); + if (!(c.equals("r") || c.equals("p") || c.equals("s"))) { + currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, "Choice must be r, p, or s"); + return; + } + currentUser.user.setChoice(c); + relay(currentUser, String.format("%s picked their choice", currentUser.getDisplayName())); + // check if all active (non-eliminated) players have chosen + boolean allChosen = clientsInRoom.values().stream() + .filter(sp -> !sp.user.isEliminated()) + .allMatch(sp -> sp.user.getChoice() != null); + if (allChosen) { + onRoundEnd(); + } + } catch (Exception e) { + LoggerUtil.INSTANCE.severe("handlePick exception", e); + } + } + + private boolean beats(String a, String b) { + // returns true if a beats b in RPS + if (a == null || b == null) + return false; + if (a.equals("r") && b.equals("s")) + return true; + if (a.equals("s") && b.equals("p")) + return true; + if (a.equals("p") && b.equals("r")) + return true; + return false; + } + + private void processRoundResults() { + // eliminate non-pickers + clientsInRoom.values().forEach(sp -> { + if (!sp.user.isEliminated() && sp.user.getChoice() == null) { + sp.user.setEliminated(true); + relay(null, String.format("%s did not pick and is eliminated", sp.getDisplayName())); + } + }); + + // gather active players (non-eliminated) + var active = clientsInRoom.values().stream().filter(sp -> !sp.user.isEliminated()).toList(); + int n = active.size(); + if (n <= 1) { + // nothing to resolve + return; + } + + // compute round-robin battles and accumulate point awards and loss counts + java.util.Map addPoints = new java.util.HashMap<>(); + java.util.Map lossCount = new java.util.HashMap<>(); + + for (int i = 0; i < n; i++) { + ServerThread attacker = active.get(i); + ServerThread defender = active.get((i + 1) % n); + String aChoice = attacker.user.getChoice(); + String dChoice = defender.user.getChoice(); + if (aChoice == null || dChoice == null) { + continue; // skip incomplete + } + + String resultMsg; + if (beats(aChoice, dChoice)) { + addPoints.put(attacker, addPoints.getOrDefault(attacker, 0) + 1); + lossCount.put(defender, lossCount.getOrDefault(defender, 0) + 1); + resultMsg = String.format("Battle: %s (%s) vs %s (%s) -> %s wins", attacker.getDisplayName(), aChoice, + defender.getDisplayName(), dChoice, attacker.getDisplayName()); + } else if (beats(dChoice, aChoice)) { + addPoints.put(defender, addPoints.getOrDefault(defender, 0) + 1); + lossCount.put(attacker, lossCount.getOrDefault(attacker, 0) + 1); + resultMsg = String.format("Battle: %s (%s) vs %s (%s) -> %s wins", attacker.getDisplayName(), aChoice, + defender.getDisplayName(), dChoice, defender.getDisplayName()); + } else { + resultMsg = String.format("Battle: %s (%s) vs %s (%s) -> tie", attacker.getDisplayName(), aChoice, + defender.getDisplayName(), dChoice); + } + // relay the matchup, choices, and result (visible at round resolution) + relay(null, resultMsg); + } + + // apply points and notify clients + addPoints.forEach((sp, pts) -> { + sp.user.setPoints(sp.user.getPoints() + pts); + // sync to everyone + clientsInRoom.values().forEach(sendTo -> { + boolean ok = sendTo.sendPointsUpdate(sp.getClientId(), sp.user.getPoints()); + if (!ok) { + removeClient(sendTo); + } + }); + }); + + // determine eliminations based on loss counts and config + java.util.Set toEliminate = new java.util.HashSet<>(); + for (ServerThread sp : active) { + int losses = lossCount.getOrDefault(sp, 0); + boolean eliminate = ELIMINATE_ON_SINGLE_LOSS ? losses >= 1 : losses >= 2; + if (eliminate) { + toEliminate.add(sp); + } + } + + // apply eliminations + toEliminate.forEach(sp -> { + sp.user.setEliminated(true); + relay(null, String.format("%s has been eliminated", sp.getDisplayName())); + }); + + } + + /** + * Builds and sends a final scoreboard message to all clients sorted by points + */ + private void sendFinalScoreboard() { + java.util.List sorted = clientsInRoom.values().stream() + .sorted((a, b) -> Integer.compare(b.user.getPoints(), a.user.getPoints())).toList(); + StringBuilder sb = new StringBuilder(); + sb.append("Final Scoreboard:\n"); + sorted.forEach(sp -> sb.append(String.format("%s : %d\n", sp.getDisplayName(), sp.user.getPoints()))); + relay(null, sb.toString()); + } + + /** + * Handle a scoreboard request from a client (send current scoreboard) + */ + protected void handleScoreboard(ServerThread requester) { + sendFinalScoreboard(); + } +} \ No newline at end of file diff --git a/Project/Server/Room.java b/Project/Server/Room.java index ffdcf43..c5f64b8 100644 --- a/Project/Server/Room.java +++ b/Project/Server/Room.java @@ -1,10 +1,9 @@ - package Project.Server; import java.util.concurrent.ConcurrentHashMap; import Project.Common.Constants; - +import Project.Common.LoggerUtil; import Project.Common.RoomAction; import Project.Common.TextFX; import Project.Common.TextFX.Color; @@ -14,12 +13,12 @@ public class Room implements AutoCloseable { private final String name;// unique name of the Room private volatile boolean isRunning = false; - private final ConcurrentHashMap clientsInRoom = new ConcurrentHashMap(); + protected final ConcurrentHashMap clientsInRoom = new ConcurrentHashMap(); public final static String LOBBY = "lobby"; private void info(String message) { - System.out.println(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Room[%s]: %s", name, message), Color.PURPLE)); } public Room(String name) { @@ -32,6 +31,10 @@ public String getName() { return this.name; } + protected boolean isRunning() { + return isRunning; + } + protected synchronized void addClient(ServerThread client) { if (!isRunning) { // block action if Room isn't running return; @@ -72,7 +75,7 @@ private void syncExistingClients(ServerThread incomingClient) { boolean failedToSync = !incomingClient.sendClientInfo(serverThread.getClientId(), serverThread.getClientName(), RoomAction.JOIN, true); if (failedToSync) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -87,13 +90,14 @@ private void joinStatusRelay(ServerThread client, boolean didJoin) { client.getClientId() == serverThread.getClientId() ? "You" : client.getDisplayName(), didJoin ? "joined" : "left"); + final long senderId = client == null ? Constants.DEFAULT_CLIENT_ID : client.getClientId(); // Share info of the client joining or leaving the room boolean failedToSync = !serverThread.sendClientInfo(client.getClientId(), client.getClientName(), didJoin ? RoomAction.JOIN : RoomAction.LEAVE); // Send the server generated message to the current client - boolean failedToSend = !serverThread.sendMessage(formattedMessage); + boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); if (failedToSend || failedToSync) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -120,8 +124,9 @@ protected synchronized void relay(ServerThread sender, String message) { } // Note: any desired changes to the message must be done before this line - String senderString = sender == null ? String.format("Room[%s]", getName()) + final String senderString = sender == null ? String.format("Room[%s]", getName()) : sender.getDisplayName(); + final long senderId = sender == null ? Constants.DEFAULT_CLIENT_ID : sender.getClientId(); // Note: formattedMessage must be final (or effectively final) since outside // scope can't be changed inside a callback function (see removeIf() below) final String formattedMessage = String.format("%s: %s", senderString, message); @@ -133,9 +138,9 @@ protected synchronized void relay(ServerThread sender, String message) { info(String.format("sending message to %s recipients: %s", clientsInRoom.size(), formattedMessage)); clientsInRoom.values().removeIf(serverThread -> { - boolean failedToSend = !serverThread.sendMessage(formattedMessage); + boolean failedToSend = !serverThread.sendMessage(senderId, formattedMessage); if (failedToSend) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -151,7 +156,7 @@ protected synchronized void relay(ServerThread sender, String message) { * * @param client */ - private synchronized void disconnect(ServerThread client) { + protected synchronized void disconnect(ServerThread client) { if (!isRunning) { // block action if Room isn't running return; } @@ -165,7 +170,7 @@ private synchronized void disconnect(ServerThread client) { boolean failedToSend = !serverThread.sendClientInfo(disconnectingServerThread.getClientId(), disconnectingServerThread.getClientName(), RoomAction.LEAVE); if (failedToSend) { - System.out.println( + LoggerUtil.INSTANCE.warning( String.format("Removing disconnected %s from list", serverThread.getDisplayName())); disconnect(serverThread); } @@ -221,6 +226,10 @@ public void close() { } // start handle methods + protected void handleListRooms(ServerThread sender, String roomQuery) { + sender.sendRooms(Server.INSTANCE.listRooms(roomQuery)); + } + public void handleCreateRoom(ServerThread sender, String roomName) { try { Server.INSTANCE.createRoom(roomName); @@ -229,7 +238,7 @@ public void handleCreateRoom(ServerThread sender, String roomName) { info("Room wasn't found (this shouldn't happen)"); e.printStackTrace(); } catch (DuplicateRoomException e) { - sender.sendMessage(String.format("Room %s already exists", roomName)); + sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s already exists", roomName)); } } @@ -237,7 +246,7 @@ public void handleJoinRoom(ServerThread sender, String roomName) { try { Server.INSTANCE.joinRoom(roomName, sender); } catch (RoomNotFoundException e) { - sender.sendMessage(String.format("Room %s doesn't exist", roomName)); + sender.sendMessage(Constants.DEFAULT_CLIENT_ID, String.format("Room %s doesn't exist", roomName)); } } From 4e3602d26b59b07abd52900e7f0ef59727da63af Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:52:28 -0500 Subject: [PATCH 43/56] new comments added --- Project/Server/BaseGameRoom.java | 137 +++++++++++++++++-------------- 1 file changed, 75 insertions(+), 62 deletions(-) diff --git a/Project/Server/BaseGameRoom.java b/Project/Server/BaseGameRoom.java index c9544d5..99f1141 100644 --- a/Project/Server/BaseGameRoom.java +++ b/Project/Server/BaseGameRoom.java @@ -9,17 +9,19 @@ import Project.Exceptions.PlayerNotFoundException; /** - * No edits should be needed in this file, this prepares the core logic for the - * GameRoom + * Core base class that wires up the main game flow for a GameRoom. + * Concrete game rooms extend this and fill in the abstract lifecycle hooks. */ public abstract class BaseGameRoom extends Room { private TimedEvent readyTimer = null; + // Minimum number of players that must be ready for a session to start protected final int MINIMUM_REQUIRED_TO_START = 2; protected Phase currentPhase = Phase.READY; + // If true, /ready can be used to toggle; if false, it only sets to ready protected boolean allowToggleReady = false; public BaseGameRoom(String name) { @@ -27,70 +29,69 @@ public BaseGameRoom(String name) { } /** - * Project session initialization step (triggered from readyCheck) + * Called once at the beginning of the game session (kicked off via readyCheck). */ protected abstract void onSessionStart(); /** - * Round initialization step (in some cases can be used in place of turns if - * simple enough) + * Invoked when a new round begins. + * In simpler games this may be used instead of per-turn logic. */ protected abstract void onRoundStart(); /** - * Turn initialization step (if there are distinct turns) + * Invoked when an individual turn starts (for games with explicit turns). */ protected abstract void onTurnStart(); /** - * Turn cleanup step + * Invoked after a turn completes for cleanup or state transitions. */ protected abstract void onTurnEnd(); /** - * Round cleanup step + * Invoked after a round is finished (cleanup, scoring, etc.). */ protected abstract void onRoundEnd(); /** - * Session cleanup step + * Called when the overall session is ending (e.g., game over or aborted). */ protected abstract void onSessionEnd(); /** - * Triggered when a client is successfully added to the base-Room map and the - * GameRoom map - * - * @param client the client who joined + * Fires when a client is successfully added to both the base Room map + * and this GameRoom's client tracking. + * + * @param client the newly joined client */ protected abstract void onClientAdded(ServerThread client); /** - * Triggered when a client is removed from the base-Room map and the GameRoom - * map - * - * @param client the client who was removed (can be null if removal already - * occurred) + * Fires when a client is removed from both the base Room map + * and this GameRoom's client tracking. + * + * @param client the departing client (may be null if already removed) */ protected abstract void onClientRemoved(ServerThread client); @Override protected synchronized void addClient(ServerThread client) { - if (!isRunning()) { // block action if Room isn't running + if (!isRunning()) { // ignore join attempts when the Room is inactive return; } - // do the base Room class logic + // invoke the shared Room-level add logic super.addClient(client); onClientAdded(client); } @Override protected synchronized void removeClient(ServerThread client) { - if (!isRunning()) { // block action if Room isn't running + if (!isRunning()) { // ignore removals if the Room isn't active return; } LoggerUtil.INSTANCE.info("Players in room: " + clientsInRoom.size()); - // do the base-class logic + // perform the base Room removal logic super.removeClient(client); onClientRemoved(client); } @@ -103,7 +104,7 @@ protected synchronized void disconnect(ServerThread client) { } /** - * Cancels any in progress readyTimer + * Stops any active ready timer and clears its reference. */ protected void resetReadyTimer() { if (readyTimer != null) { @@ -113,9 +114,9 @@ protected void resetReadyTimer() { } /** - * Starts the ready timer - * - * @param resetOnTry when true, will cancel any active readyTimer + * Starts a new "ready" countdown timer if one isn't already active. + * + * @param resetOnTry if true, cancels a currently running ready timer first */ protected void startReadyTimer(boolean resetOnTry) { if (resetOnTry) { @@ -123,7 +124,7 @@ protected void startReadyTimer(boolean resetOnTry) { } if (readyTimer == null) { readyTimer = new TimedEvent(30, () -> { - // callback to trigger when ready expires + // callback executed when the ready timer expires checkReadyStatus(); }); readyTimer.setTickCallback((time) -> System.out.println("Ready Timer: " + time)); @@ -131,8 +132,8 @@ protected void startReadyTimer(boolean resetOnTry) { } /** - * Rules to begin a session: At least MINIMUM_REQUIRED_TO_START must be joined - * and ready + * Determines whether enough players are ready to begin the session. + * If the requirement is met, the session begins; otherwise, the session ends. */ private void checkReadyStatus() { long numReady = clientsInRoom.values().stream().filter(p -> p.isReady()).count(); @@ -144,16 +145,19 @@ private void checkReadyStatus() { } } + /** + * Clears all ready flags for players and notifies clients to reset their local state. + */ protected void resetReadyStatus() { clientsInRoom.values().forEach(p -> p.setReady(false)); sendResetReadyTrigger(); } /** - * Attempts to change the current phase if the passed phase differs. - * If it changes, sends the update to all Clients - * - * @param phase + * Attempts to move the game into a new phase if it differs from the current one. + * When changed, the updated phase is broadcast to all connected clients. + * + * @param phase the new phase being requested */ protected void changePhase(Phase phase) { if (currentPhase != phase) { @@ -165,16 +169,16 @@ protected void changePhase(Phase phase) { // send/sync data to ServerThread(s) /** - * Syncs the current phase to a single client - * - * @param sp + * Sends the current phase value down to a single client. + * + * @param sp the ServerThread to sync with */ protected void syncCurrentPhase(ServerThread sp) { sp.sendCurrentPhase(currentPhase); } /** - * Sends the current phase to all clients + * Broadcasts the current phase to all active clients in this room. */ protected void sendCurrentPhase() { clientsInRoom.values().removeIf(spInRoom -> { @@ -187,8 +191,7 @@ protected void sendCurrentPhase() { } /** - * A shorthand way of telling all clients to reset their local list's ready - * status + * Convenience helper to tell all clients to reset their ready status locally. */ protected void sendResetReadyTrigger() { clientsInRoom.values().removeIf(spInRoom -> { @@ -201,9 +204,9 @@ protected void sendResetReadyTrigger() { } /** - * Sends the ready status of each ServerThread to one client - * - * @param incomingSP + * Sends the ready state for each ServerThread in this room to a single client. + * + * @param incomingSP the client that needs the current ready snapshot */ protected void syncReadyStatus(ServerThread incomingSP) { clientsInRoom.values().removeIf(spInRoom -> { @@ -211,15 +214,16 @@ protected void syncReadyStatus(ServerThread incomingSP) { if (failedToSend) { removeClient(spInRoom); } + // only mark for removal if sending failed AND it's the same client return failedToSend && spInRoom.getClientId() == incomingSP.getClientId(); }); } /** - * Sends the ready status of one ServerThread to all clients - * - * @param incomingSP - * @param isReady + * Notifies all clients about a single player's ready status change. + * + * @param incomingSP the player whose ready status changed + * @param isReady the new ready value (kept for clarity, even though we read from incomingSP) */ protected void sendReadyStatus(ServerThread incomingSP, boolean isReady) { clientsInRoom.values().removeIf(spInRoom -> { @@ -232,25 +236,26 @@ protected void sendReadyStatus(ServerThread incomingSP, boolean isReady) { } // end send data to ServerThread(s) - // receive data from ServerThread (GameRoom specific) + // receive data from ServerThread (GameRoom-specific entry points) protected void handleReady(ServerThread sender) { try { - // early exit checks + // early validation checks checkPlayerInRoom(sender); checkCurrentPhase(sender, Phase.READY); ServerThread sp = null; - // option 1: simply just mark ready + // option 1: simply set ready to true if (!allowToggleReady) { sp = clientsInRoom.get(sender.getClientId()); sp.setReady(true); } - // option 2: toggle + // option 2: flip the current ready state else { sp = clientsInRoom.get(sender.getClientId()); sp.setReady(!sp.isReady()); } - startReadyTimer(false); // <-- triggers the next step when it expires + // kicks off or reuses a timer that will trigger the next stage when it expires + startReadyTimer(false); sendReadyStatus(sp, sp.isReady()); } catch (Exception e) { @@ -258,16 +263,17 @@ protected void handleReady(ServerThread sender) { } } - // end receive data from ServerThread (GameRoom specific) + // end receive data from ServerThread (GameRoom-specific entry points) // Logic Checks /** - * Early exit (via exception throwing) if it's not the proper phase - * - * @param client - * @param check - * @throws Exception + * Guard clause to ensure the requested action matches the current phase. + * Throws a PhaseMismatchException and informs the client if there's a mismatch. + * + * @param client the requesting client + * @param check required phase for this action + * @throws Exception if the phase is not correct */ protected void checkCurrentPhase(ServerThread client, Phase check) throws Exception { if (currentPhase != check) { @@ -277,6 +283,12 @@ protected void checkCurrentPhase(ServerThread client, Phase check) throws Except } } + /** + * Ensures the client is marked as ready before proceeding. + * + * @param client the client to validate + * @throws NotReadyException if the client is not ready + */ protected void checkIsReady(ServerThread client) throws NotReadyException { if (!client.isReady()) { client.sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be marked 'ready' to do this action"); @@ -285,10 +297,11 @@ protected void checkIsReady(ServerThread client) throws NotReadyException { } /** - * Early exit (via exception throwing) if the user isn't in the room - * - * @param client - * @throws Exception + * Validates that the client is actually present in this room. + * Throws PlayerNotFoundException if not found. + * + * @param client the client to look up + * @throws Exception if the client is missing from the room */ protected void checkPlayerInRoom(ServerThread client) throws Exception { if (!clientsInRoom.containsKey(client.getClientId())) { @@ -297,4 +310,4 @@ protected void checkPlayerInRoom(ServerThread client) throws Exception { } } // end Logic Checks -} \ No newline at end of file +} From 430825f5cd9d0e41a19645f209fd7a23ec68dab0 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 14:54:11 -0500 Subject: [PATCH 44/56] Milestone 2 Done --- Project/Server/GameRoom.java | 122 ++++++++++++++++++----------------- 1 file changed, 63 insertions(+), 59 deletions(-) diff --git a/Project/Server/GameRoom.java b/Project/Server/GameRoom.java index 1ff4681..326288d 100644 --- a/Project/Server/GameRoom.java +++ b/Project/Server/GameRoom.java @@ -10,14 +10,16 @@ public class GameRoom extends BaseGameRoom { - // used for general rounds (usually phase-based turns) + // Timer that governs full rounds (often tied to phase-based actions) private TimedEvent roundTimer = null; - // used for granular turn handling (usually turn-order turns) + // Timer for per-player turns (typically used when enforcing turn order) private TimedEvent turnTimer = null; + private int round = 0; - // If true, a single loss (attack or defense) eliminates a player. - // If false, player must lose both battles in the round to be eliminated. + + // If true, a single defeat (on attack or defense) removes a player from the game. + // If false, a player must lose both encounters in the round before they are out. private final boolean ELIMINATE_ON_SINGLE_LOSS = true; public GameRoom(String name) { @@ -27,7 +29,7 @@ public GameRoom(String name) { /** {@inheritDoc} */ @Override protected void onClientAdded(ServerThread sp) { - // sync GameRoom state to new client + // align the newly-joined client with current GameRoom state syncCurrentPhase(sp); syncReadyStatus(sp); syncTurnStatus(sp); @@ -37,7 +39,7 @@ protected void onClientAdded(ServerThread sp) { @Override protected void onClientRemoved(ServerThread sp) { // added after Summer 2024 Demo - // Stops the timers so room can clean up + // Stop active timers when the room is empty so things can fully reset LoggerUtil.INSTANCE.info("Player Removed, remaining: " + clientsInRoom.size()); if (clientsInRoom.isEmpty()) { @@ -81,7 +83,7 @@ private void resetTurnTimer() { protected void onSessionStart() { LoggerUtil.INSTANCE.info("onSessionStart() start"); round = 0; - // Begin session in choosing phase for the RPS rounds + // Kick off the session in the CHOOSING phase for the RPS rounds changePhase(Phase.CHOOSING); LoggerUtil.INSTANCE.info("onSessionStart() end"); onRoundStart(); @@ -94,17 +96,17 @@ protected void onRoundStart() { resetRoundTimer(); resetTurnStatus(); round++; - // set phase to choosing and initialize choices for active players + // move into the choosing phase and prep player choices for active participants changePhase(Phase.CHOOSING); clientsInRoom.values().forEach(sp -> { - // reset choice only for non-eliminated players + // clear choice only for players still in the game sp.user.setChoice(null); }); relay(null, "Round " + round + " started. Pick with /pick "); startRoundTimer(); LoggerUtil.INSTANCE.info("onRoundStart() end"); - // Note: no turn lifecycle used here - // Users do their actions in between roundStart and roundEnd + // Note: per-turn lifecycle hooks are not used here + // Players perform their actions within the window between roundStart and roundEnd } /** {@inheritDoc} */ @@ -117,33 +119,34 @@ protected void onTurnStart() { LoggerUtil.INSTANCE.info("onTurnStart() end"); } - // Note: logic between Turn Start and Turn End is typically handled via timers - // and user interaction + // Note: The actual gameplay logic between Turn Start and Turn End + // is usually driven by incoming player actions and the timers above. /** {@inheritDoc} */ @Override protected void onTurnEnd() { LoggerUtil.INSTANCE.info("onTurnEnd() start"); - resetTurnTimer(); // reset timer if turn ended without the time expiring + // clear the turn timer if the turn concluded before timeout + resetTurnTimer(); LoggerUtil.INSTANCE.info("onTurnEnd() end"); } - // Note: logic between Round Start and Round End is typically handled via timers - // and user interaction + // Note: Round-to-round logic is similarly handled via timers and player input. /** {@inheritDoc} */ @Override protected void onRoundEnd() { LoggerUtil.INSTANCE.info("onRoundEnd() start"); - resetRoundTimer(); // reset timer if round ended without the time expiring + // reset the round timer if the round ended normally + resetRoundTimer(); LoggerUtil.INSTANCE.info("onRoundEnd() end"); relay(null, "Round ended — processing results..."); - // Process end of round: eliminate non-pickers, resolve battles, award points + // Handle end-of-round logic: auto-eliminate non-pickers, resolve battles, assign points processRoundResults(); - // check end conditions + // Evaluate whether the game should end or continue long active = clientsInRoom.values().stream().filter(sp -> !sp.user.isEliminated()).count(); if (active <= 1) { onSessionEnd(); } else { - // start next round + // proceed to the next round onRoundStart(); } } @@ -154,7 +157,7 @@ protected void onSessionEnd() { LoggerUtil.INSTANCE.info("onSessionEnd() start"); resetReadyStatus(); resetTurnStatus(); - // announce winner(s) + // determine and announce winner(s) java.util.List alive = clientsInRoom.values().stream() .filter(sp -> !sp.user.isEliminated()).toList(); if (alive.size() == 1) { @@ -163,10 +166,10 @@ protected void onSessionEnd() { relay(null, "No players remaining — tie"); } - // send final scoreboard sorted by points + // push out final scores sorted by total points sendFinalScoreboard(); - // reset player data for next session (do not disconnect) + // clear per-session player state in preparation for a fresh game (clients remain connected) clientsInRoom.values().forEach(sp -> { sp.user.setPoints(0); sp.user.setEliminated(false); @@ -175,7 +178,7 @@ protected void onSessionEnd() { sp.setReady(false); }); - // sync points reset to clients + // sync point resets to all clients clientsInRoom.values().forEach(sp -> { clientsInRoom.values().forEach(target -> target.sendPointsUpdate(sp.getClientId(), sp.user.getPoints())); }); @@ -234,7 +237,7 @@ private void checkAllTookTurn() { .filter(sp -> sp.isReady()) .toList().size(); int numTookTurn = clientsInRoom.values().stream() - // ensure to verify the isReady part since it's against the original list + // Must still be marked ready, based on the original group .filter(sp -> sp.isReady() && sp.didTakeTurn()) .toList().size(); if (numReady == numTookTurn) { @@ -251,14 +254,14 @@ private void checkAllTookTurn() { // receive data from ServerThread (GameRoom specific) /** - * Handles the turn action from the client. - * - * @param currentUser - * @param exampleText (arbitrary text from the client, can be used for - * additional actions or information) + * Processes a turn request from a client. + * + * @param currentUser the ServerThread for the acting client + * @param exampleText an arbitrary string from the client that could be used + * for additional per-turn data */ protected void handleTurnAction(ServerThread currentUser, String exampleText) { - // check if the client is in the room + // validate that the caller is in the room and that conditions are correct try { checkPlayerInRoom(currentUser); checkCurrentPhase(currentUser, Phase.IN_PROGRESS); @@ -269,11 +272,11 @@ protected void handleTurnAction(ServerThread currentUser, String exampleText) { } currentUser.setTookTurn(true); sendTurnStatus(currentUser, currentUser.didTakeTurn()); - // TODO handle example text possibly or other turn related intention from client - // finished processing the turn + // TODO: incorporate exampleText for richer turn handling logic if desired + // completion of the current user's turn checkAllTookTurn(); } catch (NotReadyException e) { - // The check method already informs the currentUser + // NotReady check already sends a message to currentUser LoggerUtil.INSTANCE.severe("handleTurnAction exception", e); } catch (PlayerNotFoundException e) { currentUser.sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to do the ready check"); @@ -290,8 +293,9 @@ protected void handleTurnAction(ServerThread currentUser, String exampleText) { // end receive data from ServerThread (GameRoom specific) /** - * Override generic message handling so raw single-letter picks (r/p/s) - * submitted during CHOOSING are treated as picks and not broadcast. + * Overrides the default message handler so that plain single-letter + * choices (r/p/s) during CHOOSING are interpreted as game picks instead + * of being broadcast as chat text. */ @Override protected synchronized void handleMessage(ServerThread sender, String text) { @@ -299,13 +303,13 @@ protected synchronized void handleMessage(ServerThread sender, String text) { if (currentPhase == Phase.CHOOSING && text != null) { String t = text.trim(); String tl = t.toLowerCase(); - // single-letter quick pick (e.g., "r") + // direct one-character selection like "r" if (tl.length() == 1 && (tl.equals("r") || tl.equals("p") || tl.equals("s"))) { handlePick(sender, tl); return; } - // command form possibly sent as a MESSAGE (some clients may not parse commands correctly) - // accept "/pick p", "pick p", or similar variations + // Some clients may not interpret slash commands correctly and send them as messages. + // Accept patterns like "/pick p", "pick p", etc. String withoutSlash = tl.startsWith("/") ? tl.substring(1).trim() : tl; if (withoutSlash.startsWith("pick")) { String[] parts = withoutSlash.split(" +"); @@ -321,15 +325,15 @@ protected synchronized void handleMessage(ServerThread sender, String text) { } catch (Exception e) { LoggerUtil.INSTANCE.severe("handleMessage override error", e); } - // fallback to base behavior + // fall back to the parent implementation for non-pick cases super.handleMessage(sender, text); } /** - * Handles a player's pick (r/p/s) + * Handles a player's rock/paper/scissors choice. * - * @param currentUser - * @param choiceStr expected "r","p","s" + * @param currentUser the ServerThread for the client choosing + * @param choiceStr expecting "r", "p", or "s" */ protected void handlePick(ServerThread currentUser, String choiceStr) { try { @@ -351,8 +355,8 @@ protected void handlePick(ServerThread currentUser, String choiceStr) { } currentUser.user.setChoice(c); relay(currentUser, String.format("%s picked their choice", currentUser.getDisplayName())); - // check if all active (non-eliminated) players have chosen - boolean allChosen = clientsInRoom.values().stream() + // check whether all still-active players have submitted a choice + boolean allChosen = clientsInRoom.values().stream() .filter(sp -> !sp.user.isEliminated()) .allMatch(sp -> sp.user.getChoice() != null); if (allChosen) { @@ -364,7 +368,7 @@ protected void handlePick(ServerThread currentUser, String choiceStr) { } private boolean beats(String a, String b) { - // returns true if a beats b in RPS + // returns true if the first choice wins against the second in RPS if (a == null || b == null) return false; if (a.equals("r") && b.equals("s")) @@ -377,7 +381,7 @@ private boolean beats(String a, String b) { } private void processRoundResults() { - // eliminate non-pickers + // first remove any active players who never submitted a choice clientsInRoom.values().forEach(sp -> { if (!sp.user.isEliminated() && sp.user.getChoice() == null) { sp.user.setEliminated(true); @@ -385,15 +389,15 @@ private void processRoundResults() { } }); - // gather active players (non-eliminated) + // assemble a list of still-active (non-eliminated) participants var active = clientsInRoom.values().stream().filter(sp -> !sp.user.isEliminated()).toList(); int n = active.size(); if (n <= 1) { - // nothing to resolve + // with 0 or 1 player remaining, there is nothing to resolve return; } - // compute round-robin battles and accumulate point awards and loss counts + // iterate over players in a round-robin style and track both points and losses java.util.Map addPoints = new java.util.HashMap<>(); java.util.Map lossCount = new java.util.HashMap<>(); @@ -403,7 +407,7 @@ private void processRoundResults() { String aChoice = attacker.user.getChoice(); String dChoice = defender.user.getChoice(); if (aChoice == null || dChoice == null) { - continue; // skip incomplete + continue; // skip incomplete pairs } String resultMsg; @@ -421,14 +425,14 @@ private void processRoundResults() { resultMsg = String.format("Battle: %s (%s) vs %s (%s) -> tie", attacker.getDisplayName(), aChoice, defender.getDisplayName(), dChoice); } - // relay the matchup, choices, and result (visible at round resolution) + // announce the matchup and outcome after the round finishes relay(null, resultMsg); } - // apply points and notify clients + // award accumulated points and push updates to all clients addPoints.forEach((sp, pts) -> { sp.user.setPoints(sp.user.getPoints() + pts); - // sync to everyone + // broadcast updated scores clientsInRoom.values().forEach(sendTo -> { boolean ok = sendTo.sendPointsUpdate(sp.getClientId(), sp.user.getPoints()); if (!ok) { @@ -437,7 +441,7 @@ private void processRoundResults() { }); }); - // determine eliminations based on loss counts and config + // figure out who should be eliminated based on how many losses they took java.util.Set toEliminate = new java.util.HashSet<>(); for (ServerThread sp : active) { int losses = lossCount.getOrDefault(sp, 0); @@ -447,7 +451,7 @@ private void processRoundResults() { } } - // apply eliminations + // mark players as eliminated and let everyone know toEliminate.forEach(sp -> { sp.user.setEliminated(true); relay(null, String.format("%s has been eliminated", sp.getDisplayName())); @@ -456,7 +460,7 @@ private void processRoundResults() { } /** - * Builds and sends a final scoreboard message to all clients sorted by points + * Assembles and broadcasts a final scoreboard, ordered by total points. */ private void sendFinalScoreboard() { java.util.List sorted = clientsInRoom.values().stream() @@ -468,9 +472,9 @@ private void sendFinalScoreboard() { } /** - * Handle a scoreboard request from a client (send current scoreboard) + * Handles a live scoreboard request from a client by sending the current standings. */ protected void handleScoreboard(ServerThread requester) { sendFinalScoreboard(); } -} \ No newline at end of file +} From c6d523895b42781c55636aa87bedc4b4632aeb94 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 15:03:02 -0500 Subject: [PATCH 45/56] MileStone 2 Server files --- Project/Server/BaseServerThread.java | 10 +- Project/Server/Server.java | 58 ++++++---- Project/Server/ServerThread.java | 154 ++++++++++++++++++++++++--- 3 files changed, 185 insertions(+), 37 deletions(-) diff --git a/Project/Server/BaseServerThread.java b/Project/Server/BaseServerThread.java index 9a9347f..9bfd825 100644 --- a/Project/Server/BaseServerThread.java +++ b/Project/Server/BaseServerThread.java @@ -1,14 +1,12 @@ - package Project.Server; +import Project.Common.Payload; +import Project.Common.User; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.Socket; -import Project.Common.Payload; -import Project.Common.User; - /** * Base class the handles the underlying connection between Client and * Server-side @@ -18,7 +16,7 @@ public abstract class BaseServerThread extends Thread { protected boolean isRunning = false; // control variable to stop this thread protected ObjectOutputStream out; // exposed here for send() protected Socket client; // communication directly to "my" client - private User user = new User(); + protected User user = new User(); protected Room currentRoom; /** @@ -220,4 +218,4 @@ protected void cleanup() { info("ServerThread cleanup() end"); } -} +} \ No newline at end of file diff --git a/Project/Server/Server.java b/Project/Server/Server.java index e36573a..0c0792b 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -1,6 +1,10 @@ - package Project.Server; +import Project.Common.LoggerUtil; +import Project.Common.TextFX; +import Project.Common.TextFX.Color; +import Project.Exceptions.DuplicateRoomException; +import Project.Exceptions.RoomNotFoundException; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; @@ -8,16 +12,18 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; - - -import Project.Common.TextFX.Color; -import Project.Common.TextFX; -import Project.Exceptions.DuplicateRoomException; -import Project.Exceptions.RoomNotFoundException; - public enum Server { INSTANCE; // Singleton instance + { + // statically initialize the server-side LoggerUtil + LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); + config.setFileSizeLimit(2048 * 1024); // 2MB + config.setFileCount(1); + config.setLogLocation("server.log"); + // Set the logger configuration + LoggerUtil.INSTANCE.setConfig(config); + } private int port = 3000; // connected clients // Use ConcurrentHashMap for thread-safe client management @@ -27,9 +33,9 @@ public enum Server { private long nextClientId = 0; private void info(String message) { - System.out.println(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); + LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Server: %s", message), Color.YELLOW)); } -// rk975 11/3/25 + private Server() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { info("JVM is shutting down. Perform cleanup tasks."); @@ -53,8 +59,7 @@ private void shutdown() { e.printStackTrace(); } } -// rk975-11/3/25 -// + private void start(int port) { this.port = port; // server listening @@ -64,7 +69,6 @@ private void start(int port) { createRoom(Room.LOBBY);// create the first room (lobby) while (isRunning) { info("Waiting for next client"); - // rk975 - 11/3/25. Handling new incoming connections Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection info("Client connected"); // wrap socket in a ServerThread, pass a callback to notify the Server when @@ -76,9 +80,9 @@ private void start(int port) { // Note: We don't yet add the ServerThread reference to our connectedClients map } } catch (DuplicateRoomException e) { - System.err.println(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); + LoggerUtil.INSTANCE.severe(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); } catch (IOException e) { - System.err.println(TextFX.colorize("Error accepting connection", Color.RED)); + LoggerUtil.INSTANCE.severe(TextFX.colorize("Error accepting connection", Color.RED)); e.printStackTrace(); } finally { info("Closing server socket"); @@ -119,11 +123,11 @@ protected void createRoom(String name) throws DuplicateRoomException { if (rooms.containsKey(nameCheck)) { throw new DuplicateRoomException(String.format("Room %s already exists", name)); } - Room room = new Room(name); + Room room = Room.LOBBY.equalsIgnoreCase(nameCheck) ? new Room(name) : new GameRoom(name); rooms.put(nameCheck, room); info(String.format("Created new Room %s", name)); } -// rk975 - 11/3/25 relevant code snippet + /** * Attempts to move a client (ServerThread) between rooms * @@ -146,6 +150,22 @@ protected void joinRoom(String name, ServerThread client) throws RoomNotFoundExc next.addClient(client); } + /** + * Lists all rooms that partially match the given String + * + * @param roomQuery + * @return + */ + protected List listRooms(String roomQuery) { + final String nameCheck = roomQuery.toLowerCase(); + return rooms.values().stream() + .filter(room -> room.getName().toLowerCase().contains(nameCheck))// find partially matched rooms + .map(room -> room.getName())// map room to String (name) + .limit(10) // limit to 10 results + .sorted() // sort the results alphabetically + .collect(Collectors.toList()); // return a mutable list + } + protected void removeRoom(Room room) { rooms.remove(room.getName().toLowerCase()); info(String.format("Removed room %s", room.getName())); @@ -193,7 +213,7 @@ public synchronized void broadcastMessageToAllRooms(ServerThread sender, String } public static void main(String[] args) { - System.out.println("Server Starting"); + LoggerUtil.INSTANCE.info("Server Starting"); Server server = Server.INSTANCE; int port = 3000; try { @@ -203,7 +223,7 @@ public static void main(String[] args) { // will default to the defined value prior to the try/catch } server.start(port); - System.out.println("Server Stopped"); + LoggerUtil.INSTANCE.warning("Server Stopped"); } } \ No newline at end of file diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index 8a6a293..0eabd0e 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -1,19 +1,22 @@ - package Project.Server; -import java.net.Socket; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import Project.Common.TextFX.Color; import Project.Common.ConnectionPayload; import Project.Common.Constants; - +import Project.Common.LoggerUtil; import Project.Common.Payload; import Project.Common.PayloadType; +import Project.Common.Phase; +import Project.Common.PickPayload; +import Project.Common.PointsPayload; +import Project.Common.ReadyPayload; import Project.Common.RoomAction; - +import Project.Common.RoomResultPayload; import Project.Common.TextFX; +import Project.Common.TextFX.Color; +import java.net.Socket; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; /** * A server-side representation of a single client @@ -27,8 +30,10 @@ public class ServerThread extends BaseServerThread { * * @param message */ + @Override protected void info(String message) { - System.out.println(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); + LoggerUtil.INSTANCE + .info(TextFX.colorize(String.format("Thread[%s]: %s", this.getClientId(), message), Color.CYAN)); } /** @@ -52,6 +57,67 @@ protected ServerThread(Socket myClient, Consumer onInitializationC } // Start Send*() Methods + public boolean sendResetTurnStatus() { + ReadyPayload rp = new ReadyPayload(); + rp.setPayloadType(PayloadType.RESET_TURN); + return sendToClient(rp); + } + + public boolean sendTurnStatus(long clientId, boolean didTakeTurn) { + return sendTurnStatus(clientId, didTakeTurn, false); + } + + public boolean sendTurnStatus(long clientId, boolean didTakeTurn, boolean quiet) { + // NOTE for now using ReadyPayload as it has the necessary properties + // An actual turn may include other data for your project + ReadyPayload rp = new ReadyPayload(); + rp.setPayloadType(quiet ? PayloadType.SYNC_TURN : PayloadType.TURN); + rp.setClientId(clientId); + rp.setReady(didTakeTurn); + return sendToClient(rp); + } + + public boolean sendCurrentPhase(Phase phase) { + Payload p = new Payload(); + p.setPayloadType(PayloadType.PHASE); + p.setMessage(phase.name()); + return sendToClient(p); + } + + public boolean sendResetReady() { + ReadyPayload rp = new ReadyPayload(); + rp.setPayloadType(PayloadType.RESET_READY); + return sendToClient(rp); + } + + public boolean sendReadyStatus(long clientId, boolean isReady) { + return sendReadyStatus(clientId, isReady, false); + } + + /** + * Sync ready status of client id + * + * @param clientId who + * @param isReady ready or not + * @param quiet silently mark ready + * @return + */ + public boolean sendReadyStatus(long clientId, boolean isReady, boolean quiet) { + ReadyPayload rp = new ReadyPayload(); + rp.setClientId(clientId); + rp.setReady(isReady); + if (quiet) { + rp.setPayloadType(PayloadType.SYNC_READY); + } + return sendToClient(rp); + } + + public boolean sendRooms(List rooms) { + RoomResultPayload rrp = new RoomResultPayload(); + rrp.setRooms(rooms); + return sendToClient(rrp); + } + protected boolean sendDisconnect(long clientId) { Payload payload = new Payload(); payload.setClientId(clientId); @@ -123,16 +189,26 @@ protected boolean sendClientId() { /** * Sends a message to the client * + * @param clientId who it's from * @param message * @return true for successful send */ - protected boolean sendMessage(String message) { + protected boolean sendMessage(long clientId, String message) { Payload payload = new Payload(); payload.setPayloadType(PayloadType.MESSAGE); payload.setMessage(message); + payload.setClientId(clientId); return sendToClient(payload); } + protected boolean sendPointsUpdate(long clientId, int points) { + PointsPayload pp = new PointsPayload(); + pp.setPayloadType(PayloadType.POINTS); + pp.setClientId(clientId); + pp.setPoints(points); + return sendToClient(pp); + } + // End Send*() Methods @Override protected void processPayload(Payload incoming) { @@ -140,7 +216,7 @@ protected void processPayload(Payload incoming) { switch (incoming.getPayloadType()) { case CLIENT_CONNECT: setClientName(((ConnectionPayload) incoming).getClientName().trim()); - + break; case DISCONNECT: currentRoom.handleDisconnect(this); @@ -160,12 +236,66 @@ protected void processPayload(Payload incoming) { case ROOM_LEAVE: currentRoom.handleJoinRoom(this, Room.LOBBY); break; + case ROOM_LIST: + currentRoom.handleListRooms(this, incoming.getMessage()); + break; + case READY: + // no data needed as the intent will be used as the trigger + try { + // cast to GameRoom as the subclass will handle all Game logic + ((GameRoom) currentRoom).handleReady(this); + } catch (Exception e) { + sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to do the ready check"); + } + break; + case SCOREBOARD: + try { + ((GameRoom) currentRoom).handleScoreboard(this); + } catch (Exception e) { + sendMessage(Constants.DEFAULT_CLIENT_ID, "Unable to provide scoreboard"); + } + break; + case PICK: + try { + PickPayload pp = (PickPayload) incoming; + LoggerUtil.INSTANCE.info(String.format("Received pick from %s -> %s", pp.getClientId(), pp.getChoice())); + ((GameRoom) currentRoom).handlePick(this, pp.getChoice()); + } catch (Exception e) { + sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to pick"); + } + break; + case TURN: + // no data needed as the intent will be used as the trigger + try { + // cast to GameRoom as the subclass will handle all Game logic + ((GameRoom) currentRoom).handleTurnAction(this, incoming.getMessage()); + } catch (Exception e) { + sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to do a turn"); + } + break; default: - System.out.println(TextFX.colorize("Unknown payload type received", Color.RED)); + LoggerUtil.INSTANCE.warning(TextFX.colorize("Unknown payload type received", Color.RED)); break; } } + // limited user data exposer + protected boolean isReady() { + return this.user.isReady(); + } + + protected void setReady(boolean isReady) { + this.user.setReady(isReady); + } + + protected boolean didTakeTurn() { + return this.user.didTakeTurn(); + } + + protected void setTookTurn(boolean tookTurn) { + this.user.setTookTurn(tookTurn); + } + @Override protected void onInitialized() { // once receiving the desired client name the object is ready From b2c8778485c646158a3e70f78d17e0c9ce7f3851 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 15:04:17 -0500 Subject: [PATCH 46/56] Comments added --- Project/Server/BaseServerThread.java | 95 ++++++++++++++-------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/Project/Server/BaseServerThread.java b/Project/Server/BaseServerThread.java index 9bfd825..bf006f6 100644 --- a/Project/Server/BaseServerThread.java +++ b/Project/Server/BaseServerThread.java @@ -8,30 +8,30 @@ import java.net.Socket; /** - * Base class the handles the underlying connection between Client and - * Server-side + * Abstract base class that manages the low-level connection between a Client + * and the server side. */ public abstract class BaseServerThread extends Thread { - protected boolean isRunning = false; // control variable to stop this thread - protected ObjectOutputStream out; // exposed here for send() - protected Socket client; // communication directly to "my" client + protected boolean isRunning = false; // control flag used to stop this thread + protected ObjectOutputStream out; // exposed here so sendToClient() can use it + protected Socket client; // socket tied directly to this specific client protected User user = new User(); protected Room currentRoom; /** - * Returns the current Room associated with this ServerThread - * - * @return + * Returns the Room currently associated with this ServerThread. + * + * @return the active Room reference */ protected Room getCurrentRoom() { return this.currentRoom; } /** - * Allows the setting of a non-null Room reference to this ServerThread - * - * @param room + * Assigns a non-null Room reference to this ServerThread. + * + * @param room target Room to bind this thread to */ protected void setCurrentRoom(Room room) { if (room == null) { @@ -45,9 +45,9 @@ protected void setCurrentRoom(Room room) { } /** - * Returns the status of this ServerThread - * - * @return + * Indicates whether this ServerThread is currently active. + * + * @return true if the thread is running, false otherwise */ public boolean isRunning() { return isRunning; @@ -58,15 +58,16 @@ public void setClientId(long clientId) { } public long getClientId() { - // Note: We return clientId instead of threadId as we'll change this identifier - // in the future + // Note: We return clientId instead of the thread id since this identifier + // may later change independently of the thread. return this.user.getClientId(); } /** - * Sets the client name and triggers onInitialized() - * - * @param clientName + * Sets the client's name and then invokes onInitialized() once + * basic identity information is available. + * + * @param clientName the name to associate with this client */ protected void setClientName(String clientName) { this.user.setClientName(clientName); @@ -82,30 +83,29 @@ public String getDisplayName() { } /** - * A wrapper method so we don't need to keep typing out the long/complex sysout - * line inside - * - * @param message + * Convenience method to abstract away the logging/printing implementation. + * + * @param message text to log/display */ protected abstract void info(String message); /** - * Triggered when object is fully initialized + * Called when this object has finished its initialization sequence. */ protected abstract void onInitialized(); /** - * Receives a Payload and passes data to proper handler - * - * @param payload + * Receives an incoming Payload and routes it to the correct handler method. + * + * @param payload data object sent from the client */ protected abstract void processPayload(Payload payload); /** - * Sends the payload over the socket - * - * @param payload - * @return true if no errors were encountered + * Serializes and sends a Payload over the socket to the client. + * + * @param payload object to send + * @return true if the send operation completed successfully */ protected boolean sendToClient(Payload payload) { if (!isRunning) { @@ -118,7 +118,7 @@ protected boolean sendToClient(Payload payload) { return true; } catch (IOException e) { info("Error sending message to client (most likely disconnected)"); - // comment this out to inspect the stack trace + // uncomment to inspect full stack trace // e.printStackTrace(); cleanup(); return false; @@ -126,17 +126,18 @@ protected boolean sendToClient(Payload payload) { } /** - * Terminates the server-side of the connection + * Shuts down the server side of this connection. + * Safe to call multiple times; subsequent calls are ignored. */ protected void disconnect() { if (!isRunning) { - // prevent multiple triggers if this gets called consecutively + // avoid executing disconnect logic more than once return; } info("Thread being disconnected by server"); isRunning = false; - this.interrupt(); // breaks out of blocking read in the run() method - cleanup(); // good practice to ensure data is written out immediately + this.interrupt(); // breaks out of the blocking read loop in run() + cleanup(); // finalize connections and release resources } @Override @@ -157,20 +158,19 @@ public void run() { }, 3000); Payload fromClient; /** - * isRunning is a flag to let us manage the loop exit condition - * fromClient (in.readObject()) is a blocking method that waits until data is - * received - * - null would likely mean a disconnect so we use a "set and check" logic to - * alternatively exit the loop + * isRunning acts as a flag controlling when to exit the main loop. + * fromClient (in.readObject()) is a blocking call that waits for incoming data. + * - null generally indicates some sort of disconnect, so we use "set then check" + * logic to decide when to break out of the loop. */ while (isRunning) { try { - fromClient = (Payload) in.readObject(); // blocking method + fromClient = (Payload) in.readObject(); // this call blocks until data arrives if (fromClient != null) { info("Received from my client: " + fromClient); processPayload(fromClient); } else { - throw new IOException("Connection interrupted"); // Specific exception for a clean break + throw new IOException("Connection interrupted"); // explicit exception for a clean exit path } } catch (ClassCastException | ClassNotFoundException cce) { System.err.println("Error reading object as specified type: " + cce.getMessage()); @@ -186,7 +186,7 @@ public void run() { } } // close while loop } catch (Exception e) { - // happens when client disconnects + // typically occurs when the client disconnects unexpectedly info("General Exception"); e.printStackTrace(); info("My Client disconnected"); @@ -201,12 +201,13 @@ public void run() { } /** - * Cleanup method to close the connection and reset the user object + * Cleans up this ServerThread's resources by closing the socket, clearing + * references, and resetting the user state. */ protected void cleanup() { info("ServerThread cleanup() start"); try { - // close server-side end of connection + // close the server-side end of the socket connection currentRoom = null; out.close(); client.close(); @@ -218,4 +219,4 @@ protected void cleanup() { info("ServerThread cleanup() end"); } -} \ No newline at end of file +} From d1eb5d5cb325d820b35eb5c247af8758082d8f50 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 15:06:01 -0500 Subject: [PATCH 47/56] MileStone 2 ready --- Project/Server/Server.java | 146 ++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 74 deletions(-) diff --git a/Project/Server/Server.java b/Project/Server/Server.java index 0c0792b..72d9b48 100644 --- a/Project/Server/Server.java +++ b/Project/Server/Server.java @@ -13,21 +13,22 @@ import java.util.stream.Collectors; public enum Server { - INSTANCE; // Singleton instance + INSTANCE; // Singleton instance representing the one and only Server { - // statically initialize the server-side LoggerUtil + // initialize the server-side LoggerUtil configuration once LoggerUtil.LoggerConfig config = new LoggerUtil.LoggerConfig(); - config.setFileSizeLimit(2048 * 1024); // 2MB - config.setFileCount(1); - config.setLogLocation("server.log"); - // Set the logger configuration + config.setFileSizeLimit(2048 * 1024); // cap each log file at 2MB + config.setFileCount(1); // keep a single rotating log file + config.setLogLocation("server.log"); // write server logs to this file + // apply the logger configuration LoggerUtil.INSTANCE.setConfig(config); } + private int port = 3000; - // connected clients - // Use ConcurrentHashMap for thread-safe client management - // The key is the unique Room name and the Room is the instance + // actively tracked rooms + // ConcurrentHashMap ensures thread-safe modifications when rooms are added/removed + // The map key is the lowercase room name, the value is the associated Room instance private final ConcurrentHashMap rooms = new ConcurrentHashMap<>(); private boolean isRunning = true; private long nextClientId = 0; @@ -44,13 +45,12 @@ private Server() { } /** - * Gracefully disconnect clients + * Attempts to gracefully disconnect clients and clean up Rooms. */ private void shutdown() { try { - // chose removeIf over forEach to avoid potential - // ConcurrentModificationException - // since empty rooms tell the server to remove themselves + // use removeIf instead of forEach to avoid potential ConcurrentModificationException, + // since empty rooms will request removal from this map rooms.values().removeIf(room -> { room.disconnectAll(); return true; @@ -62,22 +62,20 @@ private void shutdown() { private void start(int port) { this.port = port; - // server listening + // begin listening for incoming client connections info("Listening on port " + this.port); - // Simplified client connection loop + // simplified loop for accepting and wiring new client connections try (ServerSocket serverSocket = new ServerSocket(port)) { - createRoom(Room.LOBBY);// create the first room (lobby) + createRoom(Room.LOBBY); // ensure the lobby exists as the initial/default room while (isRunning) { info("Waiting for next client"); - Socket incomingClient = serverSocket.accept(); // blocking action, waits for a client connection + Socket incomingClient = serverSocket.accept(); // blocking call until a client connects info("Client connected"); - // wrap socket in a ServerThread, pass a callback to notify the Server when - // they're initialized + // wrap each client socket in a ServerThread; provide callback to notify when ready ServerThread serverThread = new ServerThread(incomingClient, this::onServerThreadInitialized); - // start the thread (typically an external entity manages the lifecycle and we - // don't have the thread start itself) + // start thread execution (lifecycle typically managed by the server, not the thread itself) serverThread.start(); - // Note: We don't yet add the ServerThread reference to our connectedClients map + // Note: we don't yet insert the ServerThread into any room-specific collection here } } catch (DuplicateRoomException e) { LoggerUtil.INSTANCE.severe(TextFX.colorize("Lobby already exists (this shouldn't happen)", Color.RED)); @@ -90,17 +88,17 @@ private void start(int port) { } /** - * Callback passed to ServerThread to inform Server they're ready to receive - * data - * - * @param serverThread + * Callback invoked by ServerThread when it has finished its basic initialization + * and is ready for use by the Server. + * + * @param serverThread the newly initialized ServerThread instance */ private void onServerThreadInitialized(ServerThread serverThread) { - // Generate Server controlled clientId + // Generate a server-controlled unique clientId nextClientId = Math.max(++nextClientId, 1); serverThread.setClientId(nextClientId); - serverThread.sendClientId();// syncs the data to the Client - // add initialized client to the lobby + serverThread.sendClientId(); // synchronize the identifier back to the client + // place newly initialized client into the lobby info(String.format("*%s initialized*", serverThread.getDisplayName())); try { joinRoom(Room.LOBBY, serverThread); @@ -112,29 +110,30 @@ private void onServerThreadInitialized(ServerThread serverThread) { } /** - * Attempts to create a new Room and add it to the tracked rooms collection - * - * @param name Unique name of the room - * @return true if it was created and false if it wasn't - * @throws DuplicateRoomException + * Attempts to create and register a new Room with the server. + * + * @param name Unique name identifying the room + * @return true if room creation succeeds + * @throws DuplicateRoomException if a room with the same name already exists */ protected void createRoom(String name) throws DuplicateRoomException { final String nameCheck = name.toLowerCase(); if (rooms.containsKey(nameCheck)) { throw new DuplicateRoomException(String.format("Room %s already exists", name)); } + // special case: lobby uses the basic Room implementation; other rooms use GameRoom Room room = Room.LOBBY.equalsIgnoreCase(nameCheck) ? new Room(name) : new GameRoom(name); rooms.put(nameCheck, room); info(String.format("Created new Room %s", name)); } /** - * Attempts to move a client (ServerThread) between rooms - * - * @param name the target room to join - * @param client the client moving - * @throws RoomNotFoundException - * + * Moves a client (ServerThread) to the specified room, removing them from any + * previous room first. + * + * @param name room name the client should join + * @param client the client being moved + * @throws RoomNotFoundException if the target room does not exist */ protected void joinRoom(String name, ServerThread client) throws RoomNotFoundException { final String nameCheck = name.toLowerCase(); @@ -151,62 +150,61 @@ protected void joinRoom(String name, ServerThread client) throws RoomNotFoundExc } /** - * Lists all rooms that partially match the given String - * - * @param roomQuery - * @return + * Returns a list of room names that contain the provided substring. + * + * @param roomQuery case-insensitive text used to filter room names + * @return a list of matching room names (up to 10, sorted alphabetically) */ protected List listRooms(String roomQuery) { final String nameCheck = roomQuery.toLowerCase(); return rooms.values().stream() - .filter(room -> room.getName().toLowerCase().contains(nameCheck))// find partially matched rooms - .map(room -> room.getName())// map room to String (name) - .limit(10) // limit to 10 results - .sorted() // sort the results alphabetically - .collect(Collectors.toList()); // return a mutable list + .filter(room -> room.getName().toLowerCase().contains(nameCheck)) // partial name matches + .map(room -> room.getName()) // convert Room to its name + .limit(10) // restrict result set size + .sorted() // sort names alphabetically + .collect(Collectors.toList()); // collect into a standard mutable List } + /** + * Removes the specified room from the server registry. + * + * @param room the Room instance to deregister + */ protected void removeRoom(Room room) { rooms.remove(room.getName().toLowerCase()); info(String.format("Removed room %s", room.getName())); } /** - * *

- * Note: Not a common use-case; just updated for example sake. + * Note: Not a typical production use-case; mostly present as a sample. *

- * Relays the message from the sender to all rooms - * Adding the synchronized keyword ensures that only one thread can execute - * these methods at a time, - * preventing concurrent modification issues and ensuring thread safety - * - * @param message - * @param sender ServerThread (client) sending the message or null if it's a - * server-generated message + * Relays a message from the given sender to every Room managed by the Server. + * The synchronized keyword ensures that only one thread executes this at + * a time, helping avoid concurrent modification problems. + * + * @param message text to distribute + * @param sender ServerThread (client) that originated the message, or null + * if the message is generated by the server itself */ private synchronized void relayToAllRooms(ServerThread sender, String message) { - // Note: any desired changes to the message must be done before this line + // Make sure any formatting or decoration of the message is done before this point String senderString = sender == null ? "Server" : sender.getDisplayName(); - // Note: formattedMessage must be final (or effectively final) since outside - // scope can't changed inside a callback function (see removeIf() below) + // formattedMessage must be effectively final for use within the lambda below final String formattedMessage = String.format("%s: %s", senderString, message); - // end temp identifier - - // loop over Rooms and send out the message - // Note: this uses a lambda expression for each item in the values() collection + // iterate over Rooms and forward the message through each one rooms.values().forEach(room -> { room.relay(sender, formattedMessage); }); } /** - * Used to send a message to all Rooms. - * This is just an example and we likely won't be using this - * - * @param sender - * @param message + * Public helper to broadcast a message out to every room. + * Primarily a demonstration method; may not be heavily used in practice. + * + * @param sender originator of the message (may be null for server-originated messages) + * @param message the text to be broadcast */ public synchronized void broadcastMessageToAllRooms(ServerThread sender, String message) { relayToAllRooms(sender, message); @@ -219,11 +217,11 @@ public static void main(String[] args) { try { port = Integer.parseInt(args[0]); } catch (Exception e) { - // can ignore, will either be index out of bounds or type mismatch - // will default to the defined value prior to the try/catch + // can safely ignore: either an index error or a parsing issue + // in either case, we fall back to the default port value defined above } server.start(port); LoggerUtil.INSTANCE.warning("Server Stopped"); } -} \ No newline at end of file +} From 6cdfeeaf671a315b3ebda2e1606d68667d56d8d2 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 15:08:20 -0500 Subject: [PATCH 48/56] MileStone2 Done --- Project/Server/ServerThread.java | 134 ++++++++++++++++++------------- 1 file changed, 76 insertions(+), 58 deletions(-) diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index 0eabd0e..b7b5439 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -19,16 +19,17 @@ import java.util.function.Consumer; /** - * A server-side representation of a single client + * Server-side wrapper representing a single connected client. */ public class ServerThread extends BaseServerThread { - private Consumer onInitializationComplete; // callback to inform when this object is ready + // callback used to notify listeners when this thread has finished initialization + private Consumer onInitializationComplete; /** - * A wrapper method so we don't need to keep typing out the long/complex sysout - * line inside - * - * @param message + * Convenience logger wrapper so we don't have to repeat the formatted output + * syntax everywhere. + * + * @param message text to log */ @Override protected void info(String message) { @@ -37,26 +38,24 @@ protected void info(String message) { } /** - * Wraps the Socket connection and takes a Server reference and a callback - * - * @param myClient - * @param server - * @param onInitializationComplete method to inform listener that this object is - * ready + * Wraps the socket for a single client and accepts a callback to notify when + * this ServerThread is fully initialized. + * + * @param myClient underlying client socket (must not be null) + * @param onInitializationComplete function to invoke once this object is ready */ protected ServerThread(Socket myClient, Consumer onInitializationComplete) { Objects.requireNonNull(myClient, "Client socket cannot be null"); Objects.requireNonNull(onInitializationComplete, "callback cannot be null"); info("ServerThread created"); - // get communication channels to single client + // keep reference to the client connection this.client = myClient; - // this.clientId = this.threadId(); // An id associated with the thread - // instance, used as a temporary identifier + // client id is assigned later by the Server callback this.onInitializationComplete = onInitializationComplete; - } // Start Send*() Methods + public boolean sendResetTurnStatus() { ReadyPayload rp = new ReadyPayload(); rp.setPayloadType(PayloadType.RESET_TURN); @@ -68,8 +67,8 @@ public boolean sendTurnStatus(long clientId, boolean didTakeTurn) { } public boolean sendTurnStatus(long clientId, boolean didTakeTurn, boolean quiet) { - // NOTE for now using ReadyPayload as it has the necessary properties - // An actual turn may include other data for your project + // NOTE: using ReadyPayload here since it already carries the needed fields + // A real "turn" payload could be introduced later for more complex projects ReadyPayload rp = new ReadyPayload(); rp.setPayloadType(quiet ? PayloadType.SYNC_TURN : PayloadType.TURN); rp.setClientId(clientId); @@ -95,12 +94,12 @@ public boolean sendReadyStatus(long clientId, boolean isReady) { } /** - * Sync ready status of client id - * - * @param clientId who - * @param isReady ready or not - * @param quiet silently mark ready - * @return + * Synchronizes the ready flag for a particular client id. + * + * @param clientId id of the player being updated + * @param isReady whether they are marked ready or not + * @param quiet if true, performs a silent sync without extra client output + * @return true if the send operation succeeded */ public boolean sendReadyStatus(long clientId, boolean isReady, boolean quiet) { ReadyPayload rp = new ReadyPayload(); @@ -125,31 +124,36 @@ protected boolean sendDisconnect(long clientId) { return sendToClient(payload); } + /** + * Instructs the client to clear its local user list. + * + * @return true if the message is delivered successfully + */ protected boolean sendResetUserList() { return sendClientInfo(Constants.DEFAULT_CLIENT_ID, null, RoomAction.JOIN); } /** - * Syncs Client Info (id, name, join status) to the client - * - * @param clientId use -1 for reset/clear - * @param clientName - * @param action RoomAction of Join or Leave - * @return true for successful send + * Sends client identity info (id, name, join/leave status) to this client. + * + * @param clientId use -1 to indicate a reset or clear operation + * @param clientName name to associate with that id + * @param action RoomAction.JOIN or RoomAction.LEAVE + * @return true if the payload is sent without error */ protected boolean sendClientInfo(long clientId, String clientName, RoomAction action) { return sendClientInfo(clientId, clientName, action, false); } /** - * Syncs Client Info (id, name, join status) to the client - * - * @param clientId use -1 for reset/clear - * @param clientName - * @param action RoomAction of Join or Leave - * @param isSync True is used to not show output on the client side (silent - * sync) - * @return true for successful send + * Sends client identity info (id, name, join/leave status) to this client. + * + * @param clientId use -1 to indicate a reset or clear operation + * @param clientName name belonging to the given id + * @param action RoomAction of JOIN or LEAVE + * @param isSync when true, uses a "silent" sync-type payload so the client + * doesn't show additional feedback + * @return true if the send completed successfully */ protected boolean sendClientInfo(long clientId, String clientName, RoomAction action, boolean isSync) { ConnectionPayload payload = new ConnectionPayload(); @@ -172,26 +176,26 @@ protected boolean sendClientInfo(long clientId, String clientName, RoomAction ac } /** - * Sends this client's id to the client. - * This will be a successfully connection handshake - * - * @return true for successful send + * Sends this thread's current client id to the connected client. + * Serves as part of the connection handshake. + * + * @return true if the handshake payload is sent */ protected boolean sendClientId() { ConnectionPayload payload = new ConnectionPayload(); payload.setPayloadType(PayloadType.CLIENT_ID); payload.setClientId(getClientId()); - payload.setClientName(getClientName());// Can be used as a Server-side override of username (i.e., profanity - // filter) + // Can be used for server-side name normalization or profanity filtering if needed + payload.setClientName(getClientName()); return sendToClient(payload); } /** - * Sends a message to the client - * - * @param clientId who it's from - * @param message - * @return true for successful send + * Sends a chat/message payload back to the client. + * + * @param clientId id of the sender (could be another client or server) + * @param message text of the message + * @return true if the send is successful */ protected boolean sendMessage(long clientId, String message) { Payload payload = new Payload(); @@ -210,44 +214,53 @@ protected boolean sendPointsUpdate(long clientId, int points) { } // End Send*() Methods + @Override protected void processPayload(Payload incoming) { switch (incoming.getPayloadType()) { case CLIENT_CONNECT: setClientName(((ConnectionPayload) incoming).getClientName().trim()); - break; + case DISCONNECT: currentRoom.handleDisconnect(this); break; + case MESSAGE: currentRoom.handleMessage(this, incoming.getMessage()); break; + case REVERSE: currentRoom.handleReverseText(this, incoming.getMessage()); break; + case ROOM_CREATE: currentRoom.handleCreateRoom(this, incoming.getMessage()); break; + case ROOM_JOIN: currentRoom.handleJoinRoom(this, incoming.getMessage()); break; + case ROOM_LEAVE: currentRoom.handleJoinRoom(this, Room.LOBBY); break; + case ROOM_LIST: currentRoom.handleListRooms(this, incoming.getMessage()); break; + case READY: - // no data needed as the intent will be used as the trigger + // no additional fields are required; the type alone signals the intent try { - // cast to GameRoom as the subclass will handle all Game logic + // Game-specific behavior is handled in the GameRoom subclass ((GameRoom) currentRoom).handleReady(this); } catch (Exception e) { sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to do the ready check"); } break; + case SCOREBOARD: try { ((GameRoom) currentRoom).handleScoreboard(this); @@ -255,31 +268,36 @@ protected void processPayload(Payload incoming) { sendMessage(Constants.DEFAULT_CLIENT_ID, "Unable to provide scoreboard"); } break; + case PICK: try { PickPayload pp = (PickPayload) incoming; - LoggerUtil.INSTANCE.info(String.format("Received pick from %s -> %s", pp.getClientId(), pp.getChoice())); + LoggerUtil.INSTANCE.info( + String.format("Received pick from %s -> %s", pp.getClientId(), pp.getChoice())); ((GameRoom) currentRoom).handlePick(this, pp.getChoice()); } catch (Exception e) { sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to pick"); } break; + case TURN: - // no data needed as the intent will be used as the trigger + // like READY, this relies on the type to represent the action intent try { - // cast to GameRoom as the subclass will handle all Game logic + // delegate turn processing logic to the GameRoom ((GameRoom) currentRoom).handleTurnAction(this, incoming.getMessage()); } catch (Exception e) { sendMessage(Constants.DEFAULT_CLIENT_ID, "You must be in a GameRoom to do a turn"); } break; + default: LoggerUtil.INSTANCE.warning(TextFX.colorize("Unknown payload type received", Color.RED)); break; } } - // limited user data exposer + // limited user data exposure helpers + protected boolean isReady() { return this.user.isReady(); } @@ -298,7 +316,7 @@ protected void setTookTurn(boolean tookTurn) { @Override protected void onInitialized() { - // once receiving the desired client name the object is ready + // once we've received and stored the client name, consider this thread ready onInitializationComplete.accept(this); } -} \ No newline at end of file +} From 20f5d27744b3cf0a931764cbaae3c54928c4b81c Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 17:15:36 -0500 Subject: [PATCH 49/56] bash files --- build.sh | 9 +++++++++ run.sh | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 build.sh create mode 100644 run.sh diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..8f1ec7a --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/bin/bash +wd=$(pwd) +cd $1 +# delete all .class files +find . -name "*.class" -type f -delete +find . -name "*.java" > sources.txt # generate list of files +javac @sources.txt # compile from list of files +rm sources.txt # cleanup +cd "$wd" \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..c0bbb96 --- /dev/null +++ b/run.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# Convert input to lowercase +input=$(echo "${2:-client}" | tr '[:upper:]' '[:lower:]') +port=${3:-3000} # Default port to 3000 if not +# Default debug mode to false +debug=false +debugArg="" +# Check for -d flag +if [[ " $@ " =~ " -d " ]]; then + debug=true +fi +if $debug; then + # Used for binding to vs code debug mode + debugArg="-agentlib:jdwp=transport=dt_socket,server=y,address=5005" + echo "Debug mode is ON" +fi + +if [ "$input" = "server" ]; then + java $debugArg $1.Server.Server $port +elif [ "$input" = "client" ]; then + java $debugArg $1.Client.Client + # In Milestone3 changes Client to ClientUI +elif [ "$input" = "ui" ]; then + java $debugArg $1.Client.ClientUI + # Milestone 3's new entry point +else + echo "Must specify client or server for MS2 or ui or server for MS3" +fi \ No newline at end of file From 462c350349e0a73bc90f15ad83b8602080c81080 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 17:16:00 -0500 Subject: [PATCH 50/56] ucid comments --- Project/Server/BaseGameRoom.java | 21 +++++++++++++++++++-- Project/Server/GameRoom.java | 28 ++++++++++++++++++++++++++++ Project/Server/ServerThread.java | 4 ++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/Project/Server/BaseGameRoom.java b/Project/Server/BaseGameRoom.java index 99f1141..12818b6 100644 --- a/Project/Server/BaseGameRoom.java +++ b/Project/Server/BaseGameRoom.java @@ -32,12 +32,18 @@ public BaseGameRoom(String name) { * Called once at the beginning of the game session (kicked off via readyCheck). */ protected abstract void onSessionStart(); + // rk975 - 11/26/25 + // Initializes game session state, resets round counter, + // and transitions to the CHOOSING phase to start the first round. /** * Invoked when a new round begins. * In simpler games this may be used instead of per-turn logic. */ protected abstract void onRoundStart(); + // rk975 - 11/26/25 + // Advances the round counter, resets per-round state, + // and transitions to the CHOOSING phase for the new round. /** * Invoked when an individual turn starts (for games with explicit turns). @@ -53,12 +59,18 @@ public BaseGameRoom(String name) { * Invoked after a round is finished (cleanup, scoring, etc.). */ protected abstract void onRoundEnd(); + // rk975 - 11/26/25 + // Advances the round counter, resets per-round state, + // and transitions to the CHOOSING phase for the new round. /** * Called when the overall session is ending (e.g., game over or aborted). */ protected abstract void onSessionEnd(); - + // rk975 - 11/26/25 + // Stops active timers when the room is empty so things can fully reset + // determine and announce winner(s) + /** * Fires when a client is successfully added to both the base Room map * and this GameRoom's client tracking. @@ -66,7 +78,9 @@ public BaseGameRoom(String name) { * @param client the newly joined client */ protected abstract void onClientAdded(ServerThread client); - +// rk975 - 11/26/25 +// Fires when a client is successfully added to both the base Room map +// and this GameRoom's client tracking. /** * Fires when a client is removed from both the base Room map * and this GameRoom's client tracking. @@ -74,6 +88,9 @@ public BaseGameRoom(String name) { * @param client the departing client (may be null if already removed) */ protected abstract void onClientRemoved(ServerThread client); +// rk975 - 11/26/25 +// Fires when a client is removed from both the base Room map +// and this GameRoom's client tracking. @Override protected synchronized void addClient(ServerThread client) { diff --git a/Project/Server/GameRoom.java b/Project/Server/GameRoom.java index 326288d..1a4f9c5 100644 --- a/Project/Server/GameRoom.java +++ b/Project/Server/GameRoom.java @@ -27,6 +27,10 @@ public GameRoom(String name) { } /** {@inheritDoc} */ + // rk975 - 11/26/25 + // Fires when a client is successfully added to both the base Room map + // and this GameRoom's client tracking. + // Aligns the newly-joined client with the current GameRoom state @Override protected void onClientAdded(ServerThread sp) { // align the newly-joined client with current GameRoom state @@ -36,6 +40,10 @@ protected void onClientAdded(ServerThread sp) { } /** {@inheritDoc} */ + // rk975 - 11/26/25 + // Fires when a client is removed from both the base Room map + // and this GameRoom's client tracking. + // Stops active timers when the room is empty so things can fully reset @Override protected void onClientRemoved(ServerThread sp) { // added after Summer 2024 Demo @@ -79,6 +87,9 @@ private void resetTurnTimer() { // lifecycle methods /** {@inheritDoc} */ + // rk975 - 11/26/25 + // Initializes game session state, resets round counter, + // and transitions to the CHOOSING phase to start the first round. @Override protected void onSessionStart() { LoggerUtil.INSTANCE.info("onSessionStart() start"); @@ -90,6 +101,10 @@ protected void onSessionStart() { } /** {@inheritDoc} */ + // rk975 - 11/26/25 + // Advances the round counter, resets per-round state, + // and transitions to the CHOOSING phase for the new round. + // Starts the round timer to limit the duration of the round. @Override protected void onRoundStart() { LoggerUtil.INSTANCE.info("onRoundStart() start"); @@ -132,6 +147,10 @@ protected void onTurnEnd() { // Note: Round-to-round logic is similarly handled via timers and player input. /** {@inheritDoc} */ + // rk975 - 11/26/25 + // Advances the round counter, resets per-round state, + // and transitions to the CHOOSING phase for the new round. + // Starts the round timer to limit the duration of the round. @Override protected void onRoundEnd() { LoggerUtil.INSTANCE.info("onRoundEnd() start"); @@ -152,6 +171,11 @@ protected void onRoundEnd() { } /** {@inheritDoc} */ + // rk975 - 11/26/25 + // Stops active timers when the room is empty so things can fully reset + // determine and announce winner(s) + // push out final scores sorted by total points + // clear per-session player state in preparation for a fresh game (clients @Override protected void onSessionEnd() { LoggerUtil.INSTANCE.info("onSessionEnd() start"); @@ -335,6 +359,10 @@ protected synchronized void handleMessage(ServerThread sender, String text) { * @param currentUser the ServerThread for the client choosing * @param choiceStr expecting "r", "p", or "s" */ + // rk975 - 11/26/25 + // Handles a player's pick choice during the game. + // Expects a PickPayload containing the choice ("r", "p", or "s"). + // Delegates the processing to the GameRoom's handlePick() method. protected void handlePick(ServerThread currentUser, String choiceStr) { try { checkPlayerInRoom(currentUser); diff --git a/Project/Server/ServerThread.java b/Project/Server/ServerThread.java index b7b5439..5ba4d0a 100644 --- a/Project/Server/ServerThread.java +++ b/Project/Server/ServerThread.java @@ -268,6 +268,10 @@ protected void processPayload(Payload incoming) { sendMessage(Constants.DEFAULT_CLIENT_ID, "Unable to provide scoreboard"); } break; + // rk975 - 11/26/25 + // Handles a player's pick choice during the game. + // Expects a PickPayload containing the choice ("r", "p", or "s"). + // Delegates the processing to the GameRoom's handlePick() method. case PICK: try { From f63b8e88e26389e23e1216723e3b30bb6b66e166 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 17:16:28 -0500 Subject: [PATCH 51/56] ucid added --- .../{PlayNotFoundException.java => PlayerNotFoundException.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Project/Exceptions/{PlayNotFoundException.java => PlayerNotFoundException.java} (100%) diff --git a/Project/Exceptions/PlayNotFoundException.java b/Project/Exceptions/PlayerNotFoundException.java similarity index 100% rename from Project/Exceptions/PlayNotFoundException.java rename to Project/Exceptions/PlayerNotFoundException.java From d34501e410a6c8516d011ed6c71da538d43247d7 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 17:17:02 -0500 Subject: [PATCH 52/56] ucid added --- Project/Common/ConnectionPayload.java | 4 ++++ Project/Common/Payload.java | 2 ++ Project/Common/PickPayload.java | 6 +++++- Project/Common/PointsPayload.java | 6 +++++- Project/Common/ReadyPayload.java | 6 +++++- Project/Common/RoomResultPayload.java | 6 +++++- 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Project/Common/ConnectionPayload.java b/Project/Common/ConnectionPayload.java index efea7d7..9a7dd27 100644 --- a/Project/Common/ConnectionPayload.java +++ b/Project/Common/ConnectionPayload.java @@ -26,3 +26,7 @@ public String toString() { } } +// rk975/11/26/25 +//Overrides toString() to add extra information to the parent class's string output. +//Calls super.toString() to include base class details. +//Appends the client’s name in a formatted ClientName: [name] section. \ No newline at end of file diff --git a/Project/Common/Payload.java b/Project/Common/Payload.java index 52f3c28..a02aa3d 100644 --- a/Project/Common/Payload.java +++ b/Project/Common/Payload.java @@ -55,3 +55,5 @@ public String toString() { return String.format("Payload[%s] Client Id [%s] Message: [%s]", getPayloadType(), getClientId(), getMessage()); } } +// rk975 - 11/26/25 +// Overrides toString() to provide a readable text version of the object. \ No newline at end of file diff --git a/Project/Common/PickPayload.java b/Project/Common/PickPayload.java index 86ae5b7..2d18cbe 100644 --- a/Project/Common/PickPayload.java +++ b/Project/Common/PickPayload.java @@ -16,4 +16,8 @@ public String toString() { return String.format("PickPayload{clientId=%d, choice=%s, type=%s}", getClientId(), choice, getPayloadType()); } -} \ No newline at end of file +} +// rk975 - 11/26/25 +// Overrides toString() to provide a readable text version of the object. +// Uses String.format() to include clientId, choice, and the payload type. +//Helps with debugging and logging by showing object data in one line. \ No newline at end of file diff --git a/Project/Common/PointsPayload.java b/Project/Common/PointsPayload.java index 679e6c6..a008c22 100644 --- a/Project/Common/PointsPayload.java +++ b/Project/Common/PointsPayload.java @@ -16,4 +16,8 @@ public String toString() { return String.format("PointsPayload{clientId=%d, points=%d, type=%s}", getClientId(), points, getPayloadType()); } -} \ No newline at end of file +} +// rk975 - 11/26/25 +// Overrides toString() to provide a readable text version of the object. +// Uses String.format() to include clientId, points, and the payload type. +//Helps with debugging and logging by showing object data in one line. \ No newline at end of file diff --git a/Project/Common/ReadyPayload.java b/Project/Common/ReadyPayload.java index 1f7fa50..9e38888 100644 --- a/Project/Common/ReadyPayload.java +++ b/Project/Common/ReadyPayload.java @@ -19,4 +19,8 @@ public void setReady(boolean isReady) { public String toString() { return super.toString() + String.format(" isReady [%s]", isReady ? "ready" : "not ready"); } -} \ No newline at end of file +} +// rk975 - 11/26/25 +// Overrides toString() to add readiness status to the base class's string output. +// Calls super.toString() to include base class details. +// Appends isReady status in a formatted isReady [ready/not ready] section. \ No newline at end of file diff --git a/Project/Common/RoomResultPayload.java b/Project/Common/RoomResultPayload.java index 8f7fb8e..c1723cb 100644 --- a/Project/Common/RoomResultPayload.java +++ b/Project/Common/RoomResultPayload.java @@ -22,4 +22,8 @@ public void setRooms(List rooms) { public String toString() { return super.toString() + "Rooms [" + String.join(",", rooms) + "]"; } -} \ No newline at end of file +} +// rk975 - 11/26/25 +// Overrides toString() to add room list information to the base class's string output. +// Calls super.toString() to include base class details. +// Appends the list of rooms in a formatted Rooms [room1,room2,...] section. \ No newline at end of file From 82a93267d177d11588f8ec1763333037d12a7c06 Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 17:17:31 -0500 Subject: [PATCH 53/56] ucid added --- Project/Client/Client.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index da282aa..d8ae221 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -247,6 +247,10 @@ private void sendDoTurn(String text) throws IOException { sendToServer(rp); } + // rk975 - 11/26/25 + // Sends the player's choice of "r", "p", or "s" to the server. + // Validates input before sending. + // Throws IOException if sending fails. private void sendPick(String text) throws IOException { String c = text.toLowerCase(); if (!(c.equals("r") || c.equals("p") || c.equals("s"))) { From e9d491f1dc1a9dee401bfd63f54226b569d28d6c Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Wed, 26 Nov 2025 20:44:39 -0500 Subject: [PATCH 54/56] changes --- Project/Common/TimedEvent.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project/Common/TimedEvent.java b/Project/Common/TimedEvent.java index 3419891..eaef2cc 100644 --- a/Project/Common/TimedEvent.java +++ b/Project/Common/TimedEvent.java @@ -109,4 +109,4 @@ public static void main(String args[]) { System.out.println("Tick: " + tick); }); } -} +} \ No newline at end of file From be5e0e7aa67427daefdb9aa88c1fd4c20330e57e Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Mon, 1 Dec 2025 16:13:10 -0500 Subject: [PATCH 55/56] fixed the ready function --- Project/Server/BaseGameRoom.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Project/Server/BaseGameRoom.java b/Project/Server/BaseGameRoom.java index 12818b6..6d56796 100644 --- a/Project/Server/BaseGameRoom.java +++ b/Project/Server/BaseGameRoom.java @@ -153,9 +153,10 @@ protected void startReadyTimer(boolean resetOnTry) { * If the requirement is met, the session begins; otherwise, the session ends. */ private void checkReadyStatus() { + // Always reset the timer after it fires so a new one can be started + resetReadyTimer(); long numReady = clientsInRoom.values().stream().filter(p -> p.isReady()).count(); if (numReady >= MINIMUM_REQUIRED_TO_START) { - resetReadyTimer(); onSessionStart(); } else { onSessionEnd(); From 8a675d8bc6d1dfb6aa966dd8d3b8c3c30cb7036e Mon Sep 17 00:00:00 2001 From: rutgershealthhackathon67-ops Date: Sun, 7 Dec 2025 18:33:55 -0500 Subject: [PATCH 56/56] changes --- Project/Client/Client.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Project/Client/Client.java b/Project/Client/Client.java index d8ae221..daf9295 100644 --- a/Project/Client/Client.java +++ b/Project/Client/Client.java @@ -162,7 +162,7 @@ private boolean processClientCommand(String text) throws IOException { LoggerUtil.INSTANCE.info(TextFX.colorize(String.format("Name set to %s", myUser.getClientName()), Color.YELLOW)); wasCommand = true; - } else if (text.equalsIgnoreCase(Command.LIST_USERS.command)) { + } else if (text.trim().equalsIgnoreCase(Command.LIST_USERS.command)) { String message = TextFX.colorize("Known clients:\n", Color.CYAN); LoggerUtil.INSTANCE.info(TextFX.colorize("Known clients:", Color.CYAN)); message += String.join("\n", knownClients.values().stream() @@ -174,10 +174,10 @@ private boolean processClientCommand(String text) throws IOException { .toList()); LoggerUtil.INSTANCE.info(message); wasCommand = true; - } else if (Command.QUIT.command.equalsIgnoreCase(text)) { + } else if (Command.QUIT.command.equalsIgnoreCase(text.trim())) { close(); wasCommand = true; - } else if (Command.DISCONNECT.command.equalsIgnoreCase(text)) { + } else if (Command.DISCONNECT.command.equalsIgnoreCase(text.trim())) { sendDisconnect(); wasCommand = true; } else if (text.startsWith(Command.REVERSE.command)) { @@ -211,7 +211,7 @@ private boolean processClientCommand(String text) throws IOException { sendRoomAction(text, RoomAction.LIST); wasCommand = true; - } else if (text.equalsIgnoreCase(Command.READY.command)) { + } else if (text.trim().equalsIgnoreCase(Command.READY.command)) { sendReady(); wasCommand = true; } else if (text.startsWith(Command.EXAMPLE_TURN.command)) { @@ -228,7 +228,7 @@ private boolean processClientCommand(String text) throws IOException { } sendPick(text.trim()); wasCommand = true; - } else if (text.equalsIgnoreCase(Command.SCOREBOARD.command)) { + } else if (text.trim().equalsIgnoreCase(Command.SCOREBOARD.command)) { sendScoreboardRequest(); wasCommand = true; }