diff --git a/main/src/main/java/client/Client.java b/main/src/main/java/client/Client.java new file mode 100644 index 0000000..37c6928 --- /dev/null +++ b/main/src/main/java/client/Client.java @@ -0,0 +1,147 @@ +package client; + +import java.util.Scanner; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import client.structs.NetworkManager; +import client.handlers.UnicastHandler; +import client.handlers.MulticastHandler; +import client.handlers.BroadcastHandler; +import client.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * Client application that manages network communications and thread handling. + * Created by: 0x1eo + * Last modified: 2024-12-12 + */ +public class Client implements AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(Client.class.getName()); + + public static final String SERVER_ADDRESS = "localhost"; + public static final String BROADCAST_ADDRESS = "255.255.255.255"; + public static final int SERVER_PORT = 7500; + public static int CLIENT_PORT = 7501; // Made non-final to allow dynamic assignment + public static final int MULTICAST_PORT = 7502; + public static final int BUFFER_SIZE = 1024; + + private final ExecutorService executorService; + private final NetworkManager networkManager; + private static Client instance; + + private Client() { + this.executorService = Executors.newFixedThreadPool(3); + this.networkManager = NetworkManager.getInstance(); + } + + public static synchronized Client getInstance() { + if (instance == null) { + instance = new Client(); + } + return instance; + } + + /** + * Initializes and starts the client application. + */ + public void start() { + LOGGER.info("Initializing client application..."); + try { + networkManager.initializePrimaryConnection(); + setupShutdownHook(); + initializeAuthenticatedState(); + startInputLoop(); + LOGGER.info("Client initialization completed successfully"); + } catch (Exception e) { + LOGGER.severe("Failed to initialize client: " + e.getMessage()); + close(); + } + } + + /** + * Initializes authenticated state and starts network handlers. + */ + public void initializeAuthenticatedState() { + try { + NetworkManager networkManager = NetworkManager.getInstance(); + networkManager.initializeAuthenticatedConnections(); + + if (networkManager.isAuthenticated()) { + startNetworkHandlers(); + LOGGER.info("Authenticated state initialized successfully on port " + CLIENT_PORT); + } else { + LOGGER.severe("Authentication failed"); + close(); + } + } catch (NetworkManager.NetworkInitializationException e) { + LOGGER.log(Level.SEVERE, "Failed to initialize authenticated state", e); + close(); + } + } + + private void startNetworkHandlers() { + executorService.execute(new UnicastHandler()); + executorService.execute(new MulticastHandler()); + executorService.execute(new BroadcastHandler()); + } + + private void setupShutdownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + LOGGER.info("Shutdown hook triggered"); + close(); + })); + } + + @Override + public void close() { + LOGGER.info("Initiating client shutdown sequence..."); + shutdownExecutors(); + networkManager.close(); + LOGGER.info("Client shutdown completed"); + System.exit(0); + } + + private void shutdownExecutors() { + try { + UnicastHandler.getExecutorService().shutdown(); + executorService.shutdown(); + + // Wait for termination + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException e) { + LOGGER.warning("Executor service shutdown interrupted: " + e.getMessage()); + Thread.currentThread().interrupt(); + } catch (Exception e) { + LOGGER.severe("Error during executor service shutdown: " + e.getMessage()); + } + } + + private void startInputLoop() { + Thread inputThread = new Thread(() -> { + try (Scanner scanner = new Scanner(System.in)) { + while (!Thread.currentThread().isInterrupted()) { + String input = scanner.nextLine(); + String response = InputCommandRouter.processInput(ConnType.UNICAST, input); + if (response != null) { + networkManager.getUnicastOut().println(response); + } + } + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error in input loop", e); + } + }); + inputThread.setName("InputProcessor"); + inputThread.start(); + } + + public static void main(String[] args) { + Client client = Client.getInstance(); + client.start(); + } +} \ No newline at end of file diff --git a/main/src/main/java/client/handlers/BroadcastHandler.java b/main/src/main/java/client/handlers/BroadcastHandler.java new file mode 100644 index 0000000..83131b3 --- /dev/null +++ b/main/src/main/java/client/handlers/BroadcastHandler.java @@ -0,0 +1,164 @@ +package client.handlers; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +import client.Client; +import client.structs.NetworkManager; +import client.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * Handles broadcast communication across the network. + * Receives and processes broadcast packets, excluding packets from the local host. + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class BroadcastHandler implements Runnable, AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(BroadcastHandler.class.getName()); + private static final int SOCKET_TIMEOUT_MS = 5000; // 5 seconds + private static final long TIMEOUT_LOG_INTERVAL = 300000; // Log timeouts every 5 minutes + private static final long HEARTBEAT_INTERVAL = 60000; // 1 minute + + private final NetworkManager networkManager; + private volatile boolean isRunning; + private DatagramSocket socket; + private long lastTimeoutLog; + private long lastHeartbeat; + + public BroadcastHandler() { + this.networkManager = NetworkManager.getInstance(); + this.isRunning = true; + this.lastTimeoutLog = System.currentTimeMillis(); + this.lastHeartbeat = System.currentTimeMillis(); + } + + @Override + public void run() { + try { + initializeSocket(); + LOGGER.info("Broadcast handler started successfully on port " + socket.getLocalPort()); + processBroadcastMessages(); + } catch (IOException e) { + if (isRunning) { + LOGGER.log(Level.SEVERE, "Fatal error in broadcast handler", e); + } + } finally { + close(); + } + } + + private void initializeSocket() throws IOException { + this.socket = networkManager.getBroadcastSocket(); + if (socket == null) { + throw new IOException("Failed to initialize broadcast socket"); + } + socket.setSoTimeout(SOCKET_TIMEOUT_MS); + LOGGER.fine("Broadcast socket timeout set to " + SOCKET_TIMEOUT_MS + "ms"); + } + + private void processBroadcastMessages() throws IOException { + byte[] buffer = new byte[Client.BUFFER_SIZE]; + InetAddress localhost = InetAddress.getLocalHost(); + + while (isRunning) { + checkHeartbeat(); + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + + try { + receiveAndProcessPacket(packet, localhost); + } catch (IOException e) { + handleReceiveException(e); + } + } + } + + private void handleReceiveException(IOException e) { + if (!isRunning) return; + + if (e instanceof java.net.SocketTimeoutException) { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastTimeoutLog > TIMEOUT_LOG_INTERVAL) { + LOGGER.fine("No broadcast messages received in the last " + + (SOCKET_TIMEOUT_MS / 1000) + " seconds"); + lastTimeoutLog = currentTime; + } + } else { + LOGGER.log(Level.WARNING, "Error receiving broadcast packet", e); + } + } + + private void checkHeartbeat() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastHeartbeat > HEARTBEAT_INTERVAL) { + if (socket != null && !socket.isClosed()) { + LOGGER.fine("Broadcast connection alive - listening for messages"); + } + lastHeartbeat = currentTime; + } + } + + private void receiveAndProcessPacket(DatagramPacket packet, InetAddress localhost) throws IOException { + socket.receive(packet); + + if (packet.getAddress().equals(localhost)) { + return; // Skip localhost packets + } + + String input = extractMessage(packet); + String output = processMessage(input); + + if (output != null) { + sendResponse(output, packet.getAddress(), packet.getPort()); + } + } + + private String extractMessage(DatagramPacket packet) { + return new String( + packet.getData(), + 0, + packet.getLength(), + StandardCharsets.UTF_8 + ).trim(); + } + + private String processMessage(String input) { + try { + return InputCommandRouter.processInput(ConnType.BROADCAST, input); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error processing message", e); + return null; + } + } + + + private void sendResponse(String message, InetAddress address, int port) { + try { + byte[] responseData = message.getBytes(StandardCharsets.UTF_8); + DatagramPacket response = new DatagramPacket( + responseData, + responseData.length, + address, + port + ); + socket.send(response); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to send broadcast response", e); + } + } + + @Override + public void close() { + isRunning = false; + if (socket != null && !socket.isClosed()) { + socket.close(); + LOGGER.info("Broadcast handler closed"); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/client/handlers/MulticastHandler.java b/main/src/main/java/client/handlers/MulticastHandler.java new file mode 100644 index 0000000..2a4d5dc --- /dev/null +++ b/main/src/main/java/client/handlers/MulticastHandler.java @@ -0,0 +1,163 @@ +package client.handlers; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +import client.Client; +import client.structs.NetworkManager; +import client.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * Handles multicast communication between network nodes. + * Receives and processes multicast packets, excluding packets from the local host. + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class MulticastHandler implements Runnable, AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(MulticastHandler.class.getName()); + private static final int SOCKET_TIMEOUT_MS = 5000; // 5 seconds + private static final long TIMEOUT_LOG_INTERVAL = 300000; // Log timeouts every 5 minutes + private static final long HEARTBEAT_INTERVAL = 60000; // 1 minute + + private final NetworkManager networkManager; + private volatile boolean isRunning; + private DatagramSocket socket; + private long lastTimeoutLog; + private long lastHeartbeat; + + public MulticastHandler() { + this.networkManager = NetworkManager.getInstance(); + this.isRunning = true; + this.lastTimeoutLog = System.currentTimeMillis(); + this.lastHeartbeat = System.currentTimeMillis(); + } + + @Override + public void run() { + try { + initializeSocket(); + LOGGER.info("Multicast handler started successfully on port " + socket.getLocalPort()); + processMulticastMessages(); + } catch (IOException e) { + if (isRunning) { + LOGGER.log(Level.SEVERE, "Fatal error in multicast handler", e); + } + } finally { + close(); + } + } + + private void initializeSocket() throws IOException { + this.socket = networkManager.getMulticastSocket(); + if (socket == null) { + throw new IOException("Failed to initialize multicast socket"); + } + socket.setSoTimeout(SOCKET_TIMEOUT_MS); + LOGGER.fine("Multicast socket timeout set to " + SOCKET_TIMEOUT_MS + "ms"); + } + + private void processMulticastMessages() throws IOException { + byte[] buffer = new byte[Client.BUFFER_SIZE]; + InetAddress localhost = InetAddress.getLocalHost(); + + while (isRunning) { + checkHeartbeat(); + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + + try { + receiveAndProcessPacket(packet, localhost); + } catch (IOException e) { + handleReceiveException(e); + } + } + } + + private void handleReceiveException(IOException e) { + if (!isRunning) return; + + if (e instanceof java.net.SocketTimeoutException) { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastTimeoutLog > TIMEOUT_LOG_INTERVAL) { + LOGGER.fine("No multicast messages received in the last " + + (SOCKET_TIMEOUT_MS / 1000) + " seconds"); + lastTimeoutLog = currentTime; + } + } else { + LOGGER.log(Level.WARNING, "Error receiving multicast packet", e); + } + } + + private void checkHeartbeat() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastHeartbeat > HEARTBEAT_INTERVAL) { + if (socket != null && !socket.isClosed()) { + LOGGER.fine("Multicast connection alive - listening for messages"); + } + lastHeartbeat = currentTime; + } + } + + private void receiveAndProcessPacket(DatagramPacket packet, InetAddress localhost) throws IOException { + socket.receive(packet); + + if (packet.getAddress().equals(localhost)) { + return; // Skip localhost packets + } + + String input = extractMessage(packet); + String output = processMessage(input); + + if (output != null) { + sendResponse(output, packet.getAddress(), packet.getPort()); + } + } + + private String extractMessage(DatagramPacket packet) { + return new String( + packet.getData(), + 0, + packet.getLength(), + StandardCharsets.UTF_8 + ).trim(); + } + + private String processMessage(String input) { + try { + return InputCommandRouter.processInput(ConnType.MULTICAST, input); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error processing message", e); + return null; + } + } + + private void sendResponse(String message, InetAddress address, int port) { + try { + byte[] responseData = message.getBytes(StandardCharsets.UTF_8); + DatagramPacket response = new DatagramPacket( + responseData, + responseData.length, + address, + port + ); + socket.send(response); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Failed to send multicast response", e); + } + } + + @Override + public void close() { + isRunning = false; + if (socket != null && !socket.isClosed()) { + socket.close(); + LOGGER.info("Multicast handler closed"); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/client/handlers/UnicastHandler.java b/main/src/main/java/client/handlers/UnicastHandler.java new file mode 100644 index 0000000..573e879 --- /dev/null +++ b/main/src/main/java/client/handlers/UnicastHandler.java @@ -0,0 +1,143 @@ +package client.handlers; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +import client.structs.NetworkManager; +import client.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * Handles unicast (point-to-point) connections between clients. + * Manages incoming connections and processes their messages in separate threads. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public class UnicastHandler implements Runnable, AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(UnicastHandler.class.getName()); + private static final int MAX_THREADS = 50; + private static final int SOCKET_TIMEOUT_MS = 30000; // 30 seconds + private static final int SHUTDOWN_TIMEOUT_SECONDS = 5; + + private static ExecutorService connectionPool = null; + private final NetworkManager networkManager; + private volatile boolean isRunning; + + /** + * Creates a new UnicastHandler with a fixed thread pool. + */ + public UnicastHandler() { + connectionPool = Executors.newFixedThreadPool(MAX_THREADS); + this.networkManager = NetworkManager.getInstance(); + this.isRunning = true; + } + + @Override + public void run() { + ServerSocket serverSocket = networkManager.getServerSocket(); + if (serverSocket == null) { + LOGGER.severe("Server socket is null. Cannot start UnicastHandler."); + return; + } + + while (isRunning) { + try { + Socket clientSocket = serverSocket.accept(); + configureSocket(clientSocket); + connectionPool.execute(new ClientHandler(clientSocket)); + } catch (IOException e) { + if (isRunning) { + LOGGER.log(Level.SEVERE, "Error accepting connection", e); + } + } + } + } + + private void configureSocket(Socket socket) throws IOException { + socket.setSoTimeout(SOCKET_TIMEOUT_MS); + socket.setKeepAlive(true); + } + + @Override + public void close() { + isRunning = false; + shutdownConnectionPool(); + } + + private void shutdownConnectionPool() { + connectionPool.shutdown(); + try { + if (!connectionPool.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) { + connectionPool.shutdownNow(); + } + } catch (InterruptedException e) { + connectionPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + + /** + * Gets the executor service managing client connections. + * + * @return the executor service + */ + public static ExecutorService getExecutorService() { + return connectionPool; + } + + /** + * Handles individual client connections in separate threads. + */ + private static class ClientHandler implements Runnable { + private final Socket clientSocket; + private final String clientInfo; + + public ClientHandler(Socket socket) { + this.clientSocket = socket; + this.clientInfo = String.format("%s:%d", + socket.getInetAddress().getHostAddress(), + socket.getPort()); + } + + @Override + public void run() { + LOGGER.info(() -> "New connection established with " + clientInfo); + + try (Socket socket = clientSocket; + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) { + + handleClientCommunication(in, out); + + } catch (IOException e) { + LOGGER.log(Level.SEVERE, "Error handling client " + clientInfo, e); + } finally { + LOGGER.info(() -> "Connection closed with " + clientInfo); + } + } + + private void handleClientCommunication(BufferedReader in, PrintWriter out) throws IOException { + String input; + while ((input = in.readLine()) != null) { + try { + String response = InputCommandRouter.processInput(ConnType.UNICAST, input); + if (response != null) { + out.println(response); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error processing message from " + clientInfo, e); + } + } + } + } +} \ No newline at end of file diff --git a/main/src/main/java/client/structs/NetworkManager.java b/main/src/main/java/client/structs/NetworkManager.java new file mode 100644 index 0000000..17fe67d --- /dev/null +++ b/main/src/main/java/client/structs/NetworkManager.java @@ -0,0 +1,222 @@ +package client.structs; + +import java.io.*; +import java.net.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; +import client.Client; + +/** + * Network connection manager for the emergency communication client. + * Implements thread-safe singleton pattern and manages all network resources. + * + * Features: + * - Connection retry mechanism + * - Multiple communication channels (Unicast, Multicast, Broadcast) + * - Automatic resource cleanup + * - Connection state monitoring + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class NetworkManager implements AutoCloseable { + private static final Logger LOGGER = Logger.getLogger(NetworkManager.class.getName()); + private static volatile NetworkManager instance; + private static final Object LOCK = new Object(); + + private static final int MAX_CONNECTION_RETRIES = 3; + private static final int RETRY_DELAY_MS = 2000; + private static final int SOCKET_TIMEOUT_MS = 5000; + + private final AtomicBoolean isAuthenticated = new AtomicBoolean(false); + private volatile String username; + private final SocketContainer sockets; + + private static final int PORT_RANGE_START = 7501; + private static final int PORT_RANGE_END = 7600; + private static final int MAX_PORT_ATTEMPTS = 10; + + private NetworkManager() { + this.sockets = new SocketContainer(); + } + + public static NetworkManager getInstance() { + NetworkManager result = instance; + if (result == null) { + synchronized (LOCK) { + result = instance; + if (result == null) { + instance = result = new NetworkManager(); + } + } + } + return result; + } + + /** + * Initializes primary connection with retry mechanism. + * + * @throws NetworkInitializationException if connection fails after retries + */ + public void initializePrimaryConnection() { + for (int attempt = 1; attempt <= MAX_CONNECTION_RETRIES; attempt++) { + try { + establishPrimaryConnection(); + LOGGER.info("Primary connection initialized successfully on attempt " + attempt); + return; + } catch (IOException e) { + handleConnectionRetry(attempt, e); + } + } + throw new NetworkInitializationException("Failed to initialize after " + MAX_CONNECTION_RETRIES + " attempts", null); + } + + private void establishPrimaryConnection() throws IOException { + Socket unicastSocket = new Socket(Client.SERVER_ADDRESS, Client.SERVER_PORT); + unicastSocket.setSoTimeout(SOCKET_TIMEOUT_MS); + + sockets.unicastSocket = unicastSocket; + sockets.unicastIn = new BufferedReader( + new InputStreamReader(unicastSocket.getInputStream()) + ); + sockets.unicastOut = new PrintWriter( + unicastSocket.getOutputStream(), + true + ); + } + + private void handleConnectionRetry(int attempt, IOException e) { + LOGGER.log(Level.WARNING, + String.format("Connection attempt %d/%d failed", attempt, MAX_CONNECTION_RETRIES), + e + ); + + if (attempt < MAX_CONNECTION_RETRIES) { + try { + Thread.sleep(RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new NetworkInitializationException("Connection retry interrupted", ie); + } + } + } + + /** + * Initializes authenticated connections for multicast and broadcast. + * + * @throws NetworkInitializationException if initialization fails + */ + public void initializeAuthenticatedConnections() { + for (int port = PORT_RANGE_START; port <= PORT_RANGE_END; port++) { + try { + setupAuthenticatedSockets(port); + isAuthenticated.set(true); + LOGGER.info(String.format( + "Authenticated connections initialized for user %s on port %d", + username, port + )); + return; + } catch (IOException e) { + if (port >= PORT_RANGE_END) { + isAuthenticated.set(false); + throw new NetworkInitializationException( + "Failed to find available ports in range " + + PORT_RANGE_START + "-" + PORT_RANGE_END, + e + ); + } + LOGGER.warning(String.format( + "Port %d in use, trying next port", port + )); + } + } + } + + private void setupAuthenticatedSockets(int basePort) throws IOException { + // Server socket uses the base port + sockets.serverSocket = new ServerSocket(basePort); + + // Multicast socket uses the multicast port + sockets.multicastSocket = new MulticastSocket(Client.MULTICAST_PORT); + + // Broadcast socket uses base port + 1 to avoid conflict + sockets.broadcastSocket = createBroadcastSocket(basePort + 1); + + // Update the client port in the Client class + Client.CLIENT_PORT = basePort; + } + + private DatagramSocket createBroadcastSocket(int port) throws IOException { + DatagramSocket socket = new DatagramSocket( + port, + InetAddress.getByName(Client.BROADCAST_ADDRESS) + ); + socket.setBroadcast(true); + return socket; + } + + @Override + public void close() { + sockets.closeAll(); + isAuthenticated.set(false); + LOGGER.info("Network manager closed successfully"); + } + + // Thread-safe getters + public boolean isAuthenticated() { return isAuthenticated.get(); } + public String getUsername() { return username; } + public Socket getUnicastSocket() { return sockets.unicastSocket; } + public BufferedReader getUnicastIn() { return sockets.unicastIn; } + public PrintWriter getUnicastOut() { return sockets.unicastOut; } + public ServerSocket getServerSocket() { return sockets.serverSocket; } + public MulticastSocket getMulticastSocket() { return sockets.multicastSocket; } + public DatagramSocket getBroadcastSocket() { return sockets.broadcastSocket; } + + // Thread-safe setter + public void setUsername(String username) { + this.username = username; + LOGGER.info("Username set to: " + username); + } + + /** + * Thread-safe container for network resources. + */ + private static class SocketContainer { + private volatile Socket unicastSocket; + private volatile BufferedReader unicastIn; + private volatile PrintWriter unicastOut; + private volatile ServerSocket serverSocket; + private volatile MulticastSocket multicastSocket; + private volatile DatagramSocket broadcastSocket; + + private void closeAll() { + closeQuietly(broadcastSocket, "BroadcastSocket"); + closeQuietly(multicastSocket, "MulticastSocket"); + closeQuietly(serverSocket, "ServerSocket"); + closeQuietly(unicastSocket, "UnicastSocket"); + closeQuietly(unicastOut, "UnicastWriter"); + closeQuietly(unicastIn, "UnicastReader"); + } + + private void closeQuietly(AutoCloseable resource, String resourceName) { + if (resource != null) { + try { + resource.close(); + LOGGER.fine(resourceName + " closed successfully"); + } catch (Exception e) { + LOGGER.warning(resourceName + " close failed: " + e.getMessage()); + } + } + } + } + + /** + * Custom exception for network initialization failures. + */ + public static class NetworkInitializationException extends RuntimeException { + public NetworkInitializationException(String message, Throwable cause) { + super(message, cause); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/client/structs/TerminalMessageHandler.java b/main/src/main/java/client/structs/TerminalMessageHandler.java new file mode 100644 index 0000000..b6c85cf --- /dev/null +++ b/main/src/main/java/client/structs/TerminalMessageHandler.java @@ -0,0 +1,18 @@ +package client.structs; + +public class TerminalMessageHandler { + public static void displayMessage(String from, String to, String content, String date) { + System.out.printf("[%s] %s -> %s: %s%n", date, from, to, content); + } + + public static void displayRequest(String from, String to, String content, String date, String accepter) { + String accepterStatus = accepter.isEmpty() ? "PENDING" : "ACCEPTED by " + accepter; + System.out.printf("[%s] REQUEST from %s to %s: %s (%s)%n", date, from, to, content, accepterStatus); + } + + public static boolean promptRequestAcceptance(String from, String to, String content) { + System.out.printf("Accept request from %s to %s: %s%n", from, to, content); + System.out.print("Accept? (y/n): "); + return System.console().readLine().trim().toLowerCase().startsWith("y"); + } +} \ No newline at end of file diff --git a/main/src/main/java/client/utils/ChatMessageHandler.java b/main/src/main/java/client/utils/ChatMessageHandler.java new file mode 100644 index 0000000..9b1d055 --- /dev/null +++ b/main/src/main/java/client/utils/ChatMessageHandler.java @@ -0,0 +1,162 @@ +package client.utils; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.logging.Logger; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import client.structs.NetworkManager; +import client.structs.TerminalMessageHandler; + +/** + * Handles chat message operations including creation, receiving, and processing of messages and requests. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public class ChatMessageHandler { + private static final Logger LOGGER = Logger.getLogger(ChatMessageHandler.class.getName()); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy HH:mm"); + private static final NetworkManager networkManager = NetworkManager.getInstance(); + + private ChatMessageHandler() {} // Prevent instantiation + + /** + * Message types supported by the chat system. + */ + public enum MessageType { + MESSAGE("message"), + REQUEST("request"), + JOIN_GROUP("joinGroup"); + + private final String command; + + MessageType(String command) { + this.command = command; + } + + public String getCommand() { + return command; + } + } + + /** + * Creates a basic message structure with common fields. + */ + private static JSONObject createBaseMessage(MessageType type, String destination, String content) { + JSONObject json = new JSONObject(); + json.put("command", type.getCommand()); + json.put("from", networkManager.getUsername()); + json.put("to", destination); + json.put("content", content); + json.put("date", LocalDateTime.now().format(DATE_FORMATTER)); + return json; + } + + public static JSONObject createMessage(String destination, String content) { + return createBaseMessage(MessageType.MESSAGE, destination, content); + } + + public static JSONObject createRequest(String destination, String content) { + JSONObject json = createBaseMessage(MessageType.REQUEST, destination, content); + json.put("accepter", ""); + return json; + } + + /** + * Validates if a JSON object contains all required fields. + */ + private static boolean validateMessageFields(JSONObject json, String... requiredFields) { + for (String field : requiredFields) { + if (!json.has(field)) { + LOGGER.warning("Missing required field: " + field); + return false; + } + } + return true; + } + + public static void receiveMessage(JSONObject json) { + if (!validateMessageFields(json, "from", "to", "content", "date")) { + return; + } + + TerminalMessageHandler.displayMessage( + json.getString("from"), + json.getString("to"), + json.getString("content"), + json.getString("date") + ); + } + + public static void receiveRequest(JSONObject json) { + if (!validateMessageFields(json, "from", "to", "content", "date", "accepter")) { + return; + } + + TerminalMessageHandler.displayRequest( + json.getString("from"), + json.getString("to"), + json.getString("content"), + json.getString("date"), + json.getString("accepter") + ); + } + + public static String handleAnswerRequest(JSONObject json) { + if (!validateMessageFields(json, "from", "to", "content")) { + return null; + } + + boolean accepted = TerminalMessageHandler.promptRequestAcceptance( + json.getString("from"), + json.getString("to"), + json.getString("content") + ); + + return new JSONObject().put("response", accepted ? "YES" : "NO").toString(); + } + + public static void processEvents(JSONObject json) { + if (!json.has("events")) { + LOGGER.warning("No events field in JSON"); + return; + } + + JSONArray events = json.getJSONArray("events"); + events.forEach(event -> { + JSONObject eventObj = (JSONObject) event; + if (!eventObj.has("command")) { + LOGGER.warning("Event missing command field"); + return; + } + + String command = eventObj.getString("command"); + try { + switch (MessageType.valueOf(command.toUpperCase())) { + case MESSAGE -> receiveMessage(eventObj); + case REQUEST -> receiveRequest(eventObj); + default -> LOGGER.warning("Unknown command: " + command); + } + } catch (IllegalArgumentException e) { + LOGGER.warning("Invalid command type: " + command); + } + }); + } + + public static void announceJoinGroup(String ip) { + try { + JSONObject json = new JSONObject() + .put("command", MessageType.JOIN_GROUP.getCommand()) + .put("group", ip) + .put("username", networkManager.getUsername()); + + networkManager.getUnicastOut().println(json); + } catch (JSONException e) { + LOGGER.severe("Failed to announce group join: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/client/utils/InputCommandRouter.java b/main/src/main/java/client/utils/InputCommandRouter.java new file mode 100644 index 0000000..30a1091 --- /dev/null +++ b/main/src/main/java/client/utils/InputCommandRouter.java @@ -0,0 +1,157 @@ +package client.utils; + +import java.util.logging.Level; +import java.util.logging.Logger; +import org.json.JSONObject; +import shared.enums.ConnType; +import client.structs.NetworkManager; +import shared.enums.Hierarchy; + +/** + * Client-side command router that processes and formats commands according to server protocol. + * + * @author 0x1eo + * @since 2024-12-13 16:41:03 + */ +public class InputCommandRouter { + private static final Logger LOGGER = Logger.getLogger(InputCommandRouter.class.getName()); + private static final NetworkManager networkManager = NetworkManager.getInstance(); + + /** + * Processes input and formats it according to server protocol. + * + * @param connType The type of connection (UNICAST, MULTICAST, BROADCAST) + * @param input The user's input string + * @return Formatted JSON string or null if no response needed + */ + public static String processInput(ConnType connType, String input) { + if (input == null || input.trim().isEmpty()) { + return null; + } + + String trimmedInput = input.trim(); + + // Handle client-side commands + if (trimmedInput.startsWith("{command:")) { + return handleCommand(connType, trimmedInput); + } + + // Handle messages and requests + return handleMessage(connType, trimmedInput); + } + + private static String handleCommand(ConnType connType, String command) { + if (connType != ConnType.UNICAST) { + return null; + } + + // Split by : but keep quotes intact + String[] parts = command.substring(9, command.length() - 1).split(":"); + String cmd = parts[0].toLowerCase(); + + switch (cmd) { + case "help": + displayHelp(); + return null; + case "register": + if (parts.length != 4) { + System.out.println("Invalid register format. Use: {command:register:username:name:password}"); + return null; + } + JSONObject registerJson = new JSONObject() + .put("command", "register") + .put("username", parts[1]) + .put("name", parts[2]) + .put("password", parts[3]) + .put("role", Hierarchy.LOW.name()); + + // Debug log + LOGGER.info("Sending registration JSON: " + registerJson.toString()); + + return registerJson.toString(); + default: + LOGGER.warning("Unknown command: " + command); + System.out.println("Unknown command. Type {command:help} for available commands."); + return null; + } + } + + private static void displayHelp() { + System.out.println("\nAvailable Commands:"); + System.out.println("------------------"); + System.out.println("{command:register:username:name:password} - Register with the server"); + System.out.println("Example: {command:register:leo:Leandro:0808wq21}"); + System.out.println("{command:help} - Display this help message"); + System.out.println("\nMessage Formats:"); + System.out.println("---------------"); + System.out.println("Regular message: text"); + System.out.println("Direct message: @username: message"); + System.out.println("Group message: #groupname: message"); + System.out.println("Request: !request @username: content"); + System.out.println(); + } + + private static String createRegistrationJson() { + JSONObject json = new JSONObject() + .put("command", "register") + .put("username", networkManager.getUsername()) + .put("name", networkManager.getUsername()) // Using username as name for now + .put("password", "default") // Should be handled properly in production + .put("role", Hierarchy.LOW.name()); // Using LOW as the default hierarchy level + + return json.toString(); + } + + private static String createLoginJson() { + JSONObject json = new JSONObject() + .put("command", "login") + .put("username", networkManager.getUsername()) + .put("password", "default"); // Should be handled properly in production + + return json.toString(); + } + + private static String handleMessage(ConnType connType, String input) { + try { + JSONObject messageJson; + + if (input.startsWith("!request")) { + // Handle request + String[] parts = input.substring(9).split(":", 2); + if (parts.length != 2 || !parts[0].startsWith("@")) { + System.out.println("Invalid request format. Use: !request @username: content"); + return null; + } + String destination = parts[0].substring(1).trim(); + messageJson = ChatMessageHandler.createRequest(destination, parts[1].trim()); + } else if (input.startsWith("@")) { + // Handle direct message + String[] parts = input.substring(1).split(":", 2); + if (parts.length != 2) { + System.out.println("Invalid direct message format. Use: @username: message"); + return null; + } + messageJson = ChatMessageHandler.createMessage(parts[0].trim(), parts[1].trim()); + } else if (input.startsWith("#")) { + // Handle group message + String[] parts = input.substring(1).split(":", 2); + if (parts.length != 2) { + System.out.println("Invalid group message format. Use: #groupname: message"); + return null; + } + String groupName = parts[0].trim(); + ChatMessageHandler.announceJoinGroup(groupName); // Join group first + messageJson = ChatMessageHandler.createMessage(groupName, parts[1].trim()); + } else { + // Handle broadcast message + messageJson = ChatMessageHandler.createMessage("", input); + } + + return messageJson.toString(); + + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Failed to process message: " + e.getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/main/src/main/java/sd/CruzamentoServer.java b/main/src/main/java/sd/CruzamentoServer.java deleted file mode 100644 index 1d84a00..0000000 --- a/main/src/main/java/sd/CruzamentoServer.java +++ /dev/null @@ -1,34 +0,0 @@ -package sd; - -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; - -public class CruzamentoServer { - public static void main(String[] args) { - // ... Inicializa Semáforos (Threads) ... - // ... Inicializa as Estruturas de Dados ... - - try (ServerSocket serverSocket = new ServerSocket(portaDoCruzamento)) { - while (true) { - Socket clienteSocket = serverSocket.accept(); - // Cria uma Thread de atendimento para lidar com o Veículo/Cliente - new Thread(new AtendenteVeiculo(clienteSocket)).start(); - } - } catch (IOException e) { /* ... */ } - } - - // Método chamado pelo AtendenteVeiculo para gerenciar o tráfego - public synchronized boolean tentarPassar(Veiculo veiculo, String direcao) { - // 1. Veículo entra na fila da direção - // 2. Verifica o estado do semáforo da direção: - Semaforo semaforo = getSemaforo(direcao); - semaforo.esperarPeloVerde(); // O Veículo fica bloqueado se for vermelho - - // 3. Após o verde: - // - Remove da fila - // - Permite a passagem (envia resposta de volta ao Veículo cliente) - // 4. Envia estatística de passagem ao Simulador Principal (Cliente TCP) - return true; - } -} diff --git a/main/src/main/java/sd/Main.java b/main/src/main/java/sd/Main.java deleted file mode 100644 index 137fc4e..0000000 --- a/main/src/main/java/sd/Main.java +++ /dev/null @@ -1,7 +0,0 @@ -package sd; - -public class Main { - public static void main(String[] args) { - System.out.println("Hello world!"); - } -} \ No newline at end of file diff --git a/main/src/main/java/sd/Semaforo.java b/main/src/main/java/sd/Semaforo.java deleted file mode 100644 index 8f1d8ad..0000000 --- a/main/src/main/java/sd/Semaforo.java +++ /dev/null @@ -1,47 +0,0 @@ -package sd; - -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; - -public class Semaforo extends Thread { - // ... atributos ... - private final Lock semaforoLock; // Para sincronizar acesso ao estado - private final Condition verdeCondition; // Para Veículos esperarem pelo verde - - public Semaforo(...) { - this.semaforoLock = new ReentrantLock(); - this.verdeCondition = semaforoLock.newCondition(); - } - - @Override - public void run() { - while (true) { - // Ciclo de tempo (ajustável para controle) - estado = Estado.VERMELHO; - // Notificar o Cruzamento sobre o estado - try { - Thread.sleep(tempoVermelho); - estado = Estado.VERDE; - // Ao ficar VERDE, notifica as threads Veículo que estão esperando - semaforoLock.lock(); - try { - verdeCondition.signalAll(); - } finally { - semaforoLock.unlock(); - } - Thread.sleep(tempoVerde); - } catch (InterruptedException e) { /* ... */ } - } - } - // Método para a thread Veículo esperar - public void esperarPeloVerde() throws InterruptedException { - semaforoLock.lock(); - try { - if (estado == Estado.VERMELHO) { - verdeCondition.await(); - } - } finally { - semaforoLock.unlock(); - } - } -} \ No newline at end of file diff --git a/main/src/main/java/sd/Veiculo.java b/main/src/main/java/sd/Veiculo.java deleted file mode 100644 index 2a55ec9..0000000 --- a/main/src/main/java/sd/Veiculo.java +++ /dev/null @@ -1,35 +0,0 @@ -package sd; - -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.net.Socket; - -public class Veiculo implements Runnable { - // ... - private String proximoCruzamentoIP; - private int proximoCruzamentoPorta; - - public void run() { - // Simular o movimento na rua (Thread.sleep(t)) - - // 1. Tenta se conectar ao próximo Cruzamento - try (Socket socket = new Socket(proximoCruzamentoIP, proximoCruzamentoPorta); - ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); - ObjectInputStream in = new ObjectInputStream(socket.getInputStream())) { - - // Envia o objeto Veículo com a solicitação de passagem - out.writeObject(this); - - // 2. BLOQUEIA a Thread, esperando a resposta do Servidor/Cruzamento - String permissao = (String) in.readObject(); - - if ("OK_PASSAR".equals(permissao)) { - // Simular tempo de travessia do cruzamento (pequeno Thread.sleep()) - // Atualiza a rota (próximo nó) - } - - } catch (IOException | ClassNotFoundException e) { /* ... */ } - // ... continua o loop da rota até a Saída (S) ... - } -} diff --git a/main/src/main/java/server/Server.java b/main/src/main/java/server/Server.java new file mode 100644 index 0000000..4b406e7 --- /dev/null +++ b/main/src/main/java/server/Server.java @@ -0,0 +1,95 @@ +package server; + +import java.io.IOException; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.net.ServerSocket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import server.structs.SystemStateManager; +import server.handlers.ActiveUsersHandler; +import server.handlers.BroadcastHandler; +import server.handlers.UnicastHandler; +import server.handlers.DataPersistence; +import server.handlers.ThingHandler; +import server.handlers.MulticastHandler; +//import server.handlers.RequestsStatsThread; + +/** + * The Server class represents the main server application. + * It handles direct connections, broadcasts, multicasts, events, active users, request statistics, + * data persistence, and provides methods for starting and closing the server. + */ +public class Server { + private static final Logger logger = Logger.getLogger(Server.class.getName()); + private static final ExecutorService executorService = Executors.newFixedThreadPool(100); + + public static final int BUFFER_SIZE = 1024; + public static final int SERVER_PORT = 7500; + public static final int USER_PORT = 7501; + public static final int MULTICAST_PORT = 7502; + public static final String BROADCAST_ADDRESS = "255.255.255.255"; + + /** + * The main method of the Server class. + * It loads the shared data, creates sockets, and starts various threads for server operations. + * + * @param args The command line arguments. + */ + public static void main(String[] args) { + try { + SystemStateManager.loadData(); + } catch (Exception ignored) { + } + + executorService.execute(() -> handleUnicast(SERVER_PORT)); + + try { + SystemStateManager.setMulticastSocket(new MulticastSocket(MULTICAST_PORT)); + DatagramSocket broadcastSocket = new DatagramSocket(USER_PORT, InetAddress.getByName(BROADCAST_ADDRESS)); + broadcastSocket.setBroadcast(true); + SystemStateManager.setBroadcastSocket(broadcastSocket); + } catch (IOException io) { + logger.severe("Error Creating Sockets! " + io.getMessage()); + close(); + } + + executorService.execute(new BroadcastHandler()); + executorService.execute(new MulticastHandler()); + executorService.execute(new ThingHandler()); + executorService.execute(new ActiveUsersHandler()); + //executorService.execute(new RequestsStats()); + executorService.execute(new DataPersistence()); + } + + /** + * Handles direct connections on the specified port. + * + * @param port The port number for direct connections. + */ + public static void handleUnicast(int port) { + try (ServerSocket serverSocket = new ServerSocket(port)) { + while (true) { + executorService.execute(new UnicastHandler(serverSocket.accept())); + } + } catch (Exception e) { + logger.severe("Error Handling Unicast Connection! " + e.getMessage()); + close(); + } + } + + /** + * Closes the server by shutting down the executor service, saving the shared data, and exiting the application. + */ + public static void close() { + try { + executorService.shutdown(); + SystemStateManager.saveData(); + System.exit(0); + } catch (Exception ignored) { + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/AcceptRequestHandler.java b/main/src/main/java/server/handlers/AcceptRequestHandler.java new file mode 100644 index 0000000..c76e0c1 --- /dev/null +++ b/main/src/main/java/server/handlers/AcceptRequestHandler.java @@ -0,0 +1,195 @@ +package server.handlers; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import server.Server; +import server.structs.SystemStateManager; +import server.structs.intfaces.Request; +import server.structs.intfaces.User; +import server.utils.MessageProtocolHandler; +import server.utils.UserHandler; +import shared.enums.ConnType; + +/** + * Handles asynchronous request acceptance in the emergency communication system. + * Manages the workflow of request acceptance based on connection type and user hierarchy. + * + * Features: + * - Multi-mode communication support (unicast, multicast, broadcast) + * - Hierarchy-based request handling + * - Asynchronous operation + * - Request acknowledgment tracking + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class AcceptRequestHandler implements Runnable { + private static final Logger logger = Logger.getLogger(AcceptRequestHandler.class.getName()); + private static final String MULTICAST_ADDRESS_PATTERN = + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + private final ConnType connectionType; + private final Request request; + + /** + * Creates a new request acceptance handler. + * + * @param connectionType the type of network connection to use + * @param request the request to be processed + */ + public AcceptRequestHandler(ConnType connectionType, Request request) { + this.connectionType = connectionType; + this.request = request; + } + + @Override + public void run() { + try { + JSONObject requestJson = createRequestJson(); + handleRequestByConnectionType(requestJson); + } catch (IOException e) { + logger.log(Level.SEVERE, "Failed to send request answer", e); + } catch (JSONException e) { + logger.log(Level.SEVERE, "Failed to create request JSON", e); + } + } + + private JSONObject createRequestJson() throws JSONException { + JSONObject json = new JSONObject(); + json.put("command", "requestAnswer"); + json.put("from", request.getSender()); + json.put("content", request.getMessage()); + json.put("to", determineReceiver()); + return json; + } + + private String determineReceiver() { + Object receiver = request.getReceiver(); + if (receiver instanceof User) { + return ((User) receiver).getUsername(); + } + if (receiver instanceof String) { + String receiverStr = (String) receiver; + if ("broadcast".equals(receiverStr) || receiverStr.matches(MULTICAST_ADDRESS_PATTERN)) { + return receiverStr; + } + logger.severe("Invalid receiver string format"); + throw new IllegalStateException("Invalid receiver format"); + } + logger.severe("Invalid receiver type"); + throw new IllegalStateException("Invalid receiver type"); + } + + private void handleRequestByConnectionType(JSONObject requestJson) throws IOException, JSONException { + switch (connectionType) { + case UNICAST: + handleUnicastRequest(requestJson); + break; + case MULTICAST: + handleMulticastRequest(requestJson); + break; + case BROADCAST: + handleBroadcastRequest(requestJson); + break; + default: + logger.warning("Unsupported connection type: " + connectionType); + } + } + + private void handleUnicastRequest(JSONObject requestJson) throws IOException, JSONException { + User receiver = request.getReceiver(); + String response = UserHandler.sendAndReceiveSomething(receiver, requestJson.toString()); + + if (response != null && new JSONObject(response).getString("response").equals("YES")) { + request.setTruster(receiver); + notifyUsers(receiver); + } + } + + private void handleMulticastRequest(JSONObject requestJson) throws IOException, JSONException { + User group = request.getReceiver(); // Now correctly returns a User + List eligibleUsers = getEligibleUsers( + new ArrayList<>(SystemStateManager.getUsersFromGroup(group.getUsername())), // Assuming you want to get users from the group name + request.getSender() + ); + + for (User user : eligibleUsers) { + if (tryAcceptRequest(user, requestJson)) { + sendMulticastNotification(group.toString()); + break; + } + } + } + + private void handleBroadcastRequest(JSONObject requestJson) throws IOException, JSONException { + List eligibleUsers = getEligibleUsers( + new ArrayList<>(SystemStateManager.getUsers()), + request.getSender() + ); + + for (User user : eligibleUsers) { + if (tryAcceptRequest(user, requestJson)) { + sendBroadcastNotification(); + break; + } + } + } + + private List getEligibleUsers(List users, User sender) { + users.remove(sender); + users.removeIf(user -> !SystemStateManager.getOnlineUsers().contains(user)); + users.removeIf(user -> !user.getHierarchy().isHigherThan(sender.getHierarchy())); + users.sort((u1, u2) -> u2.getHierarchy().getValue() - u1.getHierarchy().getValue()); + return users; + } + + private boolean tryAcceptRequest(User user, JSONObject requestJson) throws IOException, JSONException { + String response = UserHandler.sendAndReceiveSomething(user, requestJson.toString()); + if (response != null && new JSONObject(response).getString("response").equals("YES")) { + request.setTruster(user); + return true; + } + return false; + } + + private void notifyUsers(User receiver) throws IOException { + UserHandler.sendSomething(request.getSender(), + MessageProtocolHandler.notificationToJson(request).toString()); + UserHandler.sendSomething(receiver, + MessageProtocolHandler.notificationToJson(request).toString()); + } + + private void sendMulticastNotification(String group) throws IOException { + String eventJson = MessageProtocolHandler.notificationToJson(request).toString(); + DatagramPacket packet = new DatagramPacket( + eventJson.getBytes(), + eventJson.length(), + InetAddress.getByName(group), + Server.MULTICAST_PORT + ); + SystemStateManager.getMulticastSocket().send(packet); + } + + private void sendBroadcastNotification() throws IOException { + String eventJson = MessageProtocolHandler.notificationToJson(request).toString(); + DatagramPacket packet = new DatagramPacket( + eventJson.getBytes(), + eventJson.length(), + InetAddress.getByName(Server.BROADCAST_ADDRESS), + Server.USER_PORT + ); + SystemStateManager.getBroadcastSocket().send(packet); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/ActiveUsersHandler.java b/main/src/main/java/server/handlers/ActiveUsersHandler.java new file mode 100644 index 0000000..a41181b --- /dev/null +++ b/main/src/main/java/server/handlers/ActiveUsersHandler.java @@ -0,0 +1,61 @@ +package server.handlers; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import server.structs.SystemStateManager; +import server.structs.intfaces.User; +import server.utils.UserHandler; + +/** + * This class represents a thread that periodically checks the number of online users and sends a message to the user with the highest role. + */ +public class ActiveUsersHandler implements Runnable { + + private static final Logger logger = Logger.getLogger(ActiveUsersHandler.class.getName()); + + /** + * The run method of the ActiveUsersThread class. + * This method is executed when the thread starts. + * It periodically checks the number of online users and sends a message to the user with the highest role. + */ + @Override + public void run() { + // Number of Online Users Only for the highest role + while (true) { + try { + Thread.sleep(10000); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + List onlineUsers = SystemStateManager.getOnlineUsers(); + logger.info("Number of Online Users: " + onlineUsers.size()); + if (onlineUsers.size() == 0) { + continue; + } + User highestRoleUser = SystemStateManager.getHighestHierarchyUser(onlineUsers); + if (highestRoleUser == null) { + logger.severe("Highest Role User is null!"); + continue; + } + try { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("command", "message"); + jsonObject.put("from", "server"); + jsonObject.put("to", highestRoleUser.getUsername()); + jsonObject.put("content", "Number of Online Users: " + onlineUsers.size()); + SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm"); + jsonObject.put("date", sdf.format(new Date())); + UserHandler.sendSomething(highestRoleUser, jsonObject.toString()); + } catch (IOException | JSONException ignored) { + logger.severe("Error Sending Active Users! " + ignored.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/BroadcastHandler.java b/main/src/main/java/server/handlers/BroadcastHandler.java new file mode 100644 index 0000000..e5ad6e8 --- /dev/null +++ b/main/src/main/java/server/handlers/BroadcastHandler.java @@ -0,0 +1,57 @@ +package server.handlers; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.logging.Logger; + +import server.Server; +import server.structs.SystemStateManager; +import server.utils.InputCommandRouter; +import server.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * The BroadcastThread class represents a thread that handles broadcasting messages to all connected clients. + */ +public class BroadcastHandler implements Runnable { + private static final Logger logger = Logger.getLogger(BroadcastHandler.class.getName()); + + /** + * Constructs a new BroadcastThread. + */ + public BroadcastHandler() {} + + /** + * Runs the broadcast thread. + */ + @Override + public void run() { + try ( + DatagramSocket broadcastSocket = SystemStateManager.getBroadcastSocket(); + ) { + while (true) { + byte[] buffer = new byte[Server.BUFFER_SIZE]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + broadcastSocket.receive(packet); + if (packet.getAddress().equals(InetAddress.getLocalHost())) { + continue; + } + String input = new String(packet.getData()); + String output = InputCommandRouter.processInput(ConnType.BROADCAST, packet, input); + if (output == null) { + continue; + } + DatagramPacket response = new DatagramPacket(output.getBytes(), output.length(), packet.getAddress(), packet.getPort()); + try { + broadcastSocket.send(response); + } catch (IOException io) { + logger.severe("Error Sending Broadcast Response: " + io.getMessage()); + } + } + } catch (IOException io) { + logger.severe("Error Handling Broadcast Connection! " + io.getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/DataPersistence.java b/main/src/main/java/server/handlers/DataPersistence.java new file mode 100644 index 0000000..b33f138 --- /dev/null +++ b/main/src/main/java/server/handlers/DataPersistence.java @@ -0,0 +1,30 @@ +package server.handlers; + +import java.util.logging.Logger; + +import server.structs.SystemStateManager; + +/** + * This class represents a thread responsible for persisting data at regular intervals. + */ +public class DataPersistence implements Runnable { + + private static final Logger logger = Logger.getLogger(DataPersistence.class.getName()); + + /** + * The run method of the DataPersistenceThread. + * This method is responsible for saving data at regular intervals. + */ + @Override + public void run() { + while (true) { + try { + Thread.sleep(10000); + SystemStateManager.saveData(); + logger.info("Data Saved"); + } catch (Exception e) { + logger.severe("Error Saving Data! " + e.getMessage()); + } + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/MessageHistoryHandler.java b/main/src/main/java/server/handlers/MessageHistoryHandler.java new file mode 100644 index 0000000..a8a0976 --- /dev/null +++ b/main/src/main/java/server/handlers/MessageHistoryHandler.java @@ -0,0 +1,57 @@ +package server.handlers; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import server.structs.SystemStateManager; +import server.structs.intfaces.Notification; +import server.structs.intfaces.User; +import server.utils.MessageProtocolHandler; +import server.utils.UserHandler; + + +/** + * This class represents a thread that retrieves and sends message history for a user. + */ +public class MessageHistoryHandler implements Runnable { + private static final Logger logger = Logger.getLogger(MessageHistoryHandler.class.getName()); + + private User user; + + /** + * Constructs a new MessageHistoryThread object. + * + * @param user the user for whom the message history will be retrieved and sent + */ + public MessageHistoryHandler(User user) { + this.user = user; + } + + /** + * Runs the thread, retrieving and sending the message history for the user. + */ + @Override + public void run() { + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + List notifications = SystemStateManager.getUserNotifications(user); + if (notifications.isEmpty()) { + return; + } + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("command", "history"); + jsonObject.put("notifications", MessageProtocolHandler.notificationsToJson(notifications)); + UserHandler.sendSomething(user, jsonObject.toString()); + } catch (JSONException | IOException error) { + logger.severe("Error Sending Message History! " + error.getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/MulticastHandler.java b/main/src/main/java/server/handlers/MulticastHandler.java new file mode 100644 index 0000000..cc3635a --- /dev/null +++ b/main/src/main/java/server/handlers/MulticastHandler.java @@ -0,0 +1,58 @@ +package server.handlers; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.net.MulticastSocket; +import java.util.logging.Logger; + +import server.Server; +import server.structs.SystemStateManager; +import server.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * MulticastThread class. + * This class is responsible for handling the multicast connection. + * It receives multicast packets, processes them, and sends responses back to the clients. + */ +public class MulticastHandler implements Runnable { + private static final Logger logger = Logger.getLogger(MulticastHandler.class.getName()); + + /** + * MulticastThread constructor. + */ + public MulticastHandler() {} + + /** + * Runs the multicast thread. + * It continuously receives multicast packets, processes them, and sends responses back to the clients. + */ + @Override + public void run() { + try ( + MulticastSocket multicastSocket = SystemStateManager.getMulticastSocket()) { + while (true) { + byte[] buffer = new byte[Server.BUFFER_SIZE]; + DatagramPacket packet = new DatagramPacket(buffer, buffer.length); + multicastSocket.receive(packet); + if (packet.getAddress().equals(InetAddress.getLocalHost())) { + continue; + } + String input = new String(packet.getData()); + String output = InputCommandRouter.processInput(ConnType.MULTICAST, packet, input); + if (output == null) { + continue; + } + DatagramPacket response = new DatagramPacket(output.getBytes(), output.length(), packet.getAddress(), packet.getPort()); + try { + multicastSocket.send(response); + } catch (IOException io) { + logger.severe("Error sending Multicast Response: " + io.getMessage()); + } + } + } catch (IOException io) { + logger.severe("Error Handling Multicast Connection! " + io.getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/ThingHandler.java b/main/src/main/java/server/handlers/ThingHandler.java new file mode 100644 index 0000000..fbb5ae1 --- /dev/null +++ b/main/src/main/java/server/handlers/ThingHandler.java @@ -0,0 +1,119 @@ +package server.handlers; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.List; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import server.Server; +import server.structs.SystemStateManager; +import server.structs.intfaces.Notification; +import server.structs.intfaces.Message; +import server.structs.intfaces.Request; +import server.structs.intfaces.User; +import server.utils.MessageProtocolHandler; + +/** + * Handles the delivery of notifications to users in a server-side communication system. + * + * This class is a runnable thread responsible for continuously processing and delivering + * notifications from the system state manager to their intended recipients. It manages + * different types of notifications such as messages and requests, ensuring they are + * transmitted to the appropriate users via network sockets. + * + * Key responsibilities: + *
    + *
  • Periodically retrieves pending notifications from the system state manager
  • + *
  • Validates and delivers notifications to their intended users
  • + *
  • Handles socket connections and communication protocols
  • + *
  • Manages error scenarios such as closed sockets or failed JSON conversions
  • + *
+ * + * The handler operates in an infinite loop, sleeping briefly between notification checks + * to prevent excessive CPU usage. It supports different notification types and logs + * critical events for monitoring and debugging purposes. + * + * @author 0x1eo + * @since 2024-12-13 + * @see SystemStateManager + * @see Notification + * @see Message + * @see Request + */ +public class ThingHandler implements Runnable { + + private static final Logger logger = Logger.getLogger(ThingHandler.class.getName()); + + /** + * The run method is the entry point for the thread. + * It continuously checks for events in the shared object and delivers them to the appropriate users. + */ + @Override + public void run() { + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + List notifications = SystemStateManager.getNotificationsToDeliver(); + logger.info("Notifications to deliver: " + notifications.size()); + for (int i = 0; i < notifications.size(); i++) { + Notification notification = notifications.get(i); + + Object receiver = notification.getReceiver(); + + if (receiver instanceof User) { + User user = ((User) receiver); + logger.info("Event to deliver to " + user.getUsername()); + Socket socket = SystemStateManager.getUserSocket(user); + if (socket == null || socket.isClosed() || !socket.isConnected()) { + logger.severe("User Socket is Null or Closed"); + SystemStateManager.removeNotificationDelivered(notification); + continue; + } + try ( + Socket newSocket = new Socket(socket.getInetAddress(), Server.USER_PORT); + BufferedReader in = new BufferedReader(new InputStreamReader(newSocket.getInputStream())); + PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true)) { + if (notification instanceof Message) { + Message message = ((Message) notification); + JSONObject json = MessageProtocolHandler.notificationToJson(message); + if (json == null) { + logger.severe("Event to JSON returned null!"); + SystemStateManager.removeNotificationDelivered(notification); + continue; + } + out.println(MessageProtocolHandler.notificationToJson(message).toString()); + logger.info("Message delivered to " + user.getUsername()); + } else if (notification instanceof Request) { + Request request = ((Request) notification); + JSONObject json = MessageProtocolHandler.notificationToJson(request); + if (json == null) { + logger.severe("Event to JSON returned null!"); + SystemStateManager.removeNotificationDelivered(notification); + continue; + } + out.println(MessageProtocolHandler.notificationToJson(request).toString()); + logger.info("Request delivered to " + user.getUsername()); + } + SystemStateManager.removeNotificationDelivered(notification); + } catch (IOException io) { + SystemStateManager.removeUserSocket(user); + } catch (JSONException json) { + SystemStateManager.removeNotificationDelivered(notification); + } + } else { + logger.severe("Receiver is not a user!"); + SystemStateManager.removeNotificationDelivered(notification); + } + } + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/handlers/UnicastHandler.java b/main/src/main/java/server/handlers/UnicastHandler.java new file mode 100644 index 0000000..2261e12 --- /dev/null +++ b/main/src/main/java/server/handlers/UnicastHandler.java @@ -0,0 +1,60 @@ +package server.handlers; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.logging.Logger; + +import server.utils.InputCommandRouter; +import shared.enums.ConnType; + +/** + * Represents a thread that handles direct connections with clients. + */ +public class UnicastHandler implements Runnable { + private static final Logger logger = Logger.getLogger(UnicastHandler.class.getName()); + + private Socket socket; + + /** + * Constructs a DirectThread object with the specified socket. + * + * @param socket the socket representing the client connection + */ + public UnicastHandler(Socket socket) { + this.socket = socket; + } + + /** + * Runs the thread and handles the communication with the client. + */ + @Override + public void run() { + try ( + BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + PrintWriter out = new PrintWriter(socket.getOutputStream(), true);) { + try { + while (true) { + String input = in.readLine(); + if (input == null) { + in.close(); + out.close(); + if (!socket.isClosed()) socket.close(); + return; + } + String output = InputCommandRouter.processInput(ConnType.UNICAST, socket, input); + if (output == null) { + continue; + } + out.println(output); + } + } catch (IOException io) { + logger.severe("Error Handling Direct Message! " + io.getMessage()); + } + } catch (IOException io) { + logger.severe("Error Handling Direct Connection! " + io.getMessage()); + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/SystemStateManager.java b/main/src/main/java/server/structs/SystemStateManager.java new file mode 100644 index 0000000..5f75581 --- /dev/null +++ b/main/src/main/java/server/structs/SystemStateManager.java @@ -0,0 +1,453 @@ +package server.structs; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.stream.Collectors; +import server.structs.intfaces.*; +import shared.enums.Hierarchy; + +/** + * Manages network communications and user data for the emergency chat server. + * Implements thread-safe operations for managing users, connections, and messages. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public class SystemStateManager { + // Maps usernames to their corresponding User objects for quick lookup + private static final Map users = new HashMap<>(); + // Maintains active socket connections for online users + private static final Map userSockets = new HashMap<>(); + // Stores pending notifications for each user in priority order + private static final Map> userNotifications = new HashMap<>(); + // Queue of notifications pending delivery to users + private static final List notificationsToDeliver = new ArrayList<>(); + // Maps multicast group addresses to their member users + private static final Map> groups = new HashMap<>(); + // Socket for handling multicast communication + private static MulticastSocket multicastSocket; + // Socket for handling broadcast messages + private static DatagramSocket broadcastSocket; + + // Private constructor to prevent instantiation + private SystemStateManager() { + throw new AssertionError("Utility class - do not instantiate"); + } + + //#region User Management + /** + * Adds a new user to the system. + * + * @param user the user to be added + * @throws IllegalArgumentException if user is null or if username already exists + */ + public static void addUser(User user) { + validateNotNull("User", user); + synchronized (users) { + if (users.containsKey(user.getUsername())) { + throw new IllegalArgumentException("User already exists!"); + } + users.put(user.getUsername(), user); + } + } + + /** + * Retrieves a user by their username. + * + * @param username the username to look up + * @return the User object if found, null otherwise + * @throws IllegalArgumentException if username is null or empty + */ + public static User getUser(String username) { + validateNotEmpty("Username", username); + synchronized (users) { + return users.get(username); + } + } + + /** + * Returns a list of all registered users in the system. + * + * @return new ArrayList containing all users + */ + public static List getUsers() { + synchronized (users) { + return new ArrayList<>(users.values()); + } + } + + /** + * Gets the user with the highest hierarchy level from a list of users. + * + * @param userList list of users to check + * @return user with highest hierarchy level, or null if list is empty + */ + public static User getHighestHierarchyUser(List userList) { + if (userList == null || userList.isEmpty()) { + return null; + } + + return userList.stream() + .max((u1, u2) -> { + Hierarchy h1 = u1.getHierarchy(); + Hierarchy h2 = u2.getHierarchy(); + return Integer.compare(h1.getValue(), h2.getValue()); + }) + .orElse(null); + } + //#endregion + + //#region Socket Management + /** + * Associates a socket connection with a user. + * + * @param user the user to associate the socket with + * @param socket the socket connection + * @throws IllegalArgumentException if either parameter is null + */ + public static void addUserSocket(User user, Socket socket) { + validateNotNull("User", user); + validateNotNull("Socket", socket); + synchronized (userSockets) { + userSockets.put(user, socket); + } + } + + /** + * Retrieves the active socket connection for a user. + * + * @param user the user whose socket to retrieve + * @return the Socket object if found, null otherwise + * @throws IllegalArgumentException if user is null + */ + public static Socket getUserSocket(User user) { + validateNotNull("User", user); + synchronized (userSockets) { + return userSockets.get(user); + } + } + + /** + * Returns a list of currently online users. + * A user is considered online if they have an active socket connection. + * + * @return list of users with active socket connections + */ + public static List getOnlineUsers() { + synchronized (userSockets) { + return userSockets.entrySet().stream() + .filter(entry -> isSocketActive(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } + } + + /** + * Checks if a socket connection is active and valid. + * + * @param socket the socket to check + * @return true if socket is active and operational + */ + private static boolean isSocketActive(Socket socket) { + return socket != null && + !socket.isClosed() && + socket.isConnected() && + !socket.isInputShutdown() && + !socket.isOutputShutdown(); + } + + /** + * Removes a user's socket connection. + * + * @param user the user whose socket to remove + * @throws IllegalArgumentException if user is null + */ + public static void removeUserSocket(User user) { + validateNotNull("User", user); + synchronized (userSockets) { + userSockets.remove(user); + } + } + //#endregion + + //#region Notification Management + /** + * Adds a notification for a specific user. + * + * @param user the target user + * @param notification the notification to add + * @throws IllegalArgumentException if either parameter is null + */ + public static void addUserNotification(User user, Notification notification) { + validateNotNull("User", user); + validateNotNull("Notification", notification); + synchronized (userNotifications) { + userNotifications.computeIfAbsent(user, k -> new TreeSet<>()).add(notification); + } + } + + /** + * Retrieves all notifications for a user. + * + * @param user the user whose notifications to retrieve + * @return list of notifications, empty list if none found + * @throws IllegalArgumentException if user is null + */ + public static List getUserNotifications(User user) { + validateNotNull("User", user); + synchronized (userNotifications) { + TreeSet notifications = userNotifications.get(user); + return notifications != null ? new ArrayList<>(notifications) : new ArrayList<>(); + } + } + + /** + * Gets all pending requests in the system. + * + * @return list of all Request objects + */ + public static List getRequests() { + synchronized (userNotifications) { + return userNotifications.values().stream() + .flatMap(Collection::stream) + .filter(notification -> notification instanceof Request) + .map(notification -> (Request) notification) + .collect(Collectors.toList()); + } + } + + /** + * Filters a collection of requests to return only accepted ones. + * + * @param requests collection of requests to filter + * @return list of requests that have been accepted + */ + public static List getAcceptedRequests(Collection requests) { + return requests.stream() + .filter(request -> request.getTruster() != null) + .collect(Collectors.toList()); + } + + /** + * Adds a notification to the delivery queue. + * + * @param notification the notification to queue + * @throws IllegalArgumentException if notification is null + */ + public static void addNotificationToDeliver(Notification notification) { + validateNotNull("Notification", notification); + synchronized (notificationsToDeliver) { + notificationsToDeliver.add(notification); + } + } + + /** + * Returns all notifications pending delivery. + * + * @return list of queued notifications + */ + public static List getNotificationsToDeliver() { + synchronized (notificationsToDeliver) { + return new ArrayList<>(notificationsToDeliver); + } + } + + /** + * Removes a delivered notification from the queue. + * + * @param notification the notification to remove + * @throws IllegalArgumentException if notification is null + */ + public static void removeNotificationDelivered(Notification notification) { + validateNotNull("Notification", notification); + synchronized (notificationsToDeliver) { + notificationsToDeliver.remove(notification); + } + } + //#endregion + + //#region Group Management + /** + * Adds a user to a multicast group. + * + * @param group the multicast group address + * @param user the user to add + * @throws IllegalArgumentException if group is invalid or user is null + */ + public static void addUserToGroup(String group, User user) { + validateNotEmpty("Group", group); + validateNotNull("User", user); + validateMulticastAddress(group); + + synchronized (groups) { + groups.computeIfAbsent(group, k -> new ArrayList<>()).add(user); + } + } + + /** + * Gets all users in a specific group. + * + * @param group the group address + * @return list of users in the group + * @throws IllegalArgumentException if group is null or empty + */ + public static List getUsersFromGroup(String group) { + validateNotEmpty("Group", group); + synchronized (groups) { + List groupUsers = groups.get(group); + return groupUsers != null ? new ArrayList<>(groupUsers) : new ArrayList<>(); + } + } + //#endregion + + //#region Socket Getters/Setters + /** + * Gets the system's multicast socket. + * + * @return the MulticastSocket instance + */ + public static MulticastSocket getMulticastSocket() { + return multicastSocket; + } + + /** + * Sets the system's multicast socket. + * + * @param socket the MulticastSocket to use + */ + public static void setMulticastSocket(MulticastSocket socket) { + multicastSocket = socket; + } + + /** + * Gets the system's broadcast socket. + * + * @return the DatagramSocket instance + */ + public static DatagramSocket getBroadcastSocket() { + return broadcastSocket; + } + + /** + * Sets the system's broadcast socket. + * + * @param socket the DatagramSocket to use + */ + public static void setBroadcastSocket(DatagramSocket socket) { + broadcastSocket = socket; + } + //#endregion + + //#region Data Persistence + /** + * Gets a map of all data structures for persistence. + * + * @return map of structure names to their objects + */ + private static Map getDataStructures() { + Map structures = new HashMap<>(); + structures.put("users.bin", users); + structures.put("userNotifications.bin", userNotifications); + structures.put("notificationsToDeliver.bin", notificationsToDeliver); + structures.put("groups.bin", groups); + return structures; + } + + /** + * Saves all system state to persistent storage. + * + * @throws IOException if an I/O error occurs during saving + */ + public static void saveData() throws IOException { + for (Map.Entry entry : getDataStructures().entrySet()) { + try (ObjectOutputStream out = new ObjectOutputStream( + new FileOutputStream("./" + entry.getKey()))) { + out.writeObject(entry.getValue()); + } + } + } + + /** + * Loads system state from persistent storage. + * + * @throws IOException if an I/O error occurs during loading + * @throws ClassNotFoundException if a serialized class cannot be found + */ + public static void loadData() throws IOException, ClassNotFoundException { + for (Map.Entry entry : getDataStructures().entrySet()) { + try (ObjectInputStream in = new ObjectInputStream( + new FileInputStream(entry.getKey()))) { + Object data = in.readObject(); + loadDataStructure(entry.getKey(), data); + } + } + } + + /** + * Loads a specific data structure from serialized data. + * + * @param key the identifier for the data structure + * @param data the serialized data to load + */ + private static void loadDataStructure(String key, Object data) { + switch (key) { + case "users.bin": + users.putAll((Map) data); + break; + case "userNotifications.bin": + userNotifications.putAll((Map>) data); + break; + case "notificationsToDeliver.bin": + notificationsToDeliver.addAll((List) data); + break; + case "groups.bin": + groups.putAll((Map>) data); + break; + } + } + //#endregion + + //#region Validation Helpers + /** + * Validates that an object is not null. + * + * @param field name of the field being validated + * @param value the value to check + * @throws IllegalArgumentException if value is null + */ + private static void validateNotNull(String field, Object value) { + if (value == null) { + throw new IllegalArgumentException(field + " cannot be null!"); + } + } + + /** + * Validates that a string is not null or empty. + * + * @param field name of the field being validated + * @param value the string to check + * @throws IllegalArgumentException if value is null or empty + */ + private static void validateNotEmpty(String field, String value) { + if (value == null || value.trim().isEmpty()) { + throw new IllegalArgumentException(field + " cannot be null or empty!"); + } + } + + /** + * Validates that a string represents a valid multicast address. + * + * @param address the address to validate + * @throws IllegalArgumentException if address format is invalid + */ + private static void validateMulticastAddress(String address) { + if (!address.matches("^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) { + throw new IllegalArgumentException("Invalid multicast address format!"); + } + } + //#endregion +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/abstractions/AbstractMessage.java b/main/src/main/java/server/structs/abstractions/AbstractMessage.java new file mode 100644 index 0000000..26fd97d --- /dev/null +++ b/main/src/main/java/server/structs/abstractions/AbstractMessage.java @@ -0,0 +1,28 @@ +package server.structs.abstractions; + +import server.structs.intfaces.Message; +import server.structs.intfaces.User; + +/** + * Abstract implementation of Message interface for the emergency communication system. + * Represents the base message type that can be exchanged between users. + * + * @author 0x1eo + * @since 2024-12-13 + * @see Message + * @see AbstractNotification + */ +public abstract class AbstractMessage extends AbstractNotification implements Message { + + /** + * Creates a new message. + * + * @param sender the user sending the message + * @param receiver the recipient (user, broadcast, or multicast group) + * @param content the message content + * @throws IllegalArgumentException if sender or content is null + */ + protected AbstractMessage(User sender, Object receiver, String content) { + super(sender, receiver, content); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/abstractions/AbstractNotification.java b/main/src/main/java/server/structs/abstractions/AbstractNotification.java new file mode 100644 index 0000000..50deb70 --- /dev/null +++ b/main/src/main/java/server/structs/abstractions/AbstractNotification.java @@ -0,0 +1,111 @@ +package server.structs.abstractions; + +import java.time.Instant; +import java.util.Objects; + +import server.structs.intfaces.Notification; +import server.structs.intfaces.User; + +/** + * Abstract base implementation for notifications in the emergency communication system. + * Provides common functionality for all types of communications including + * messages, requests, and alerts. + * + * Features: + * - Timestamp-based ordering + * - Sender and receiver tracking + * - Message content storage + * - Thread-safe immutable timestamp + * + * @author 0x1eo + * @since 2024-12-13 02:47:23 + * @see Notification + */ +public abstract class AbstractNotification implements Notification { + protected User sender; + protected User receiver; + protected String message; + protected final Instant timestamp; + + /** + * Creates a new notification. + * + * @param sender the sender's identifier object + * @param receiver the recipient's identifier object + * @param message the notification message content + * @throws IllegalArgumentException if sender, receiver or message is null + */ + protected AbstractNotification(Object sender, Object receiver, String message) { + setSender(sender); + setReceiver(receiver); + setMessage(message); + this.timestamp = Instant.now(); + } + + @Override + public User getSender() { + return sender; + } + + @Override + public void setSender(Object sender) { + if (sender == null) { + throw new IllegalArgumentException("Sender cannot be null"); + } + this.sender = (User)sender; + } + + @Override + public User getReceiver() { return receiver; } + + @Override + public void setReceiver(Object receiver) { + if (receiver == null) { + throw new IllegalArgumentException("Receiver cannot be null"); + } + this.receiver = (User)receiver; + } + + @Override + public String getMessage() { return message; } + + @Override + public void setMessage(String message) { + if (message == null) { + throw new IllegalArgumentException("Message cannot be null"); + } + this.message = message; + } + + @Override + public Instant getTimestamp() { return timestamp; } + + @Override + public void setTimestamp(Instant timestamp) { + throw new UnsupportedOperationException("Timestamp cannot be modified after creation"); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Notification)) return false; + + Notification other = (Notification) obj; + return Objects.equals(sender, other.getSender()) && + Objects.equals(receiver, other.getReceiver()) && + Objects.equals(message, other.getMessage()) && + Objects.equals(timestamp, other.getTimestamp()); + } + + @Override + public int hashCode() { return Objects.hash(sender, receiver, message, timestamp); } + + @Override + public String toString() { + return String.format("Notification[from=%s, to=%s, message='%s', time=%s]", + sender, + receiver, + message, + timestamp); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/abstractions/AbstractRequest.java b/main/src/main/java/server/structs/abstractions/AbstractRequest.java new file mode 100644 index 0000000..fef299c --- /dev/null +++ b/main/src/main/java/server/structs/abstractions/AbstractRequest.java @@ -0,0 +1,88 @@ +package server.structs.abstractions; + +import server.structs.intfaces.Request; +import server.structs.intfaces.User; + +/** + * Abstract base implementation for requests in the emergency communication system. + * Extends AbstractNotification to provide request-specific functionality including + * request author tracking. + * + * Features: + * - Request author (truster) tracking + * - Inherits notification base features + * - Supports emergency communication protocol + * - Immutable creation timestamp + * + * @author 0x1eo + * @since 2024-12-13 + * @see Request + * @see AbstractNotification + * @see User + */ +public abstract class AbstractRequest extends AbstractNotification implements Request { + protected User truster; + + /** + * Creates a new request in the emergency system. + * + * @param sender the user initiating the request + * @param receiver the intended recipient + * @param content the request content/message + * @throws IllegalArgumentException if any parameter is null, or if receiver is empty + */ + protected AbstractRequest(User sender, String receiver, String content) { + super(sender.getUsername(), receiver, content); + setTruster(sender); // Initialize truster with the sender + } + + /** + * Gets the author (requesting user) of this request. + * + * @return the user who authored this request + */ + @Override + public User getTruster() { + return truster; + } + + /** + * Sets the author (requesting user) of this request. + * + * @param author the user who authored this request + * @throws IllegalArgumentException if author is null + */ + @Override + public void setTruster(User author) { + if (author == null) { + throw new IllegalArgumentException("Request author cannot be null"); + } + this.truster = author; + } + + @Override + public String toString() { + return String.format("Request[from=%s, to=%s, content='%s', author=%s, time=%s]", + getSender(), + getReceiver(), + getMessage(), + truster.getUsername(), + getTimestamp()); + } + + @Override + public boolean equals(Object obj) { + if (!super.equals(obj)) return false; + if (!(obj instanceof Request)) return false; + + Request other = (Request) obj; + return truster.equals(other.getTruster()); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + truster.hashCode(); + return result; + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/abstractions/AbstractUser.java b/main/src/main/java/server/structs/abstractions/AbstractUser.java new file mode 100644 index 0000000..3f0a29a --- /dev/null +++ b/main/src/main/java/server/structs/abstractions/AbstractUser.java @@ -0,0 +1,125 @@ +package server.structs.abstractions; + +import java.util.Objects; +import server.structs.intfaces.User; +import shared.enums.Hierarchy; + +/** + * Abstract base implementation of the User interface that represents + * a user in the emergency communication system with hierarchical privileges. + * + * Features: + * - Username and full name management + * - Secure password storage + * - Hierarchical role-based access + * - Natural ordering based on hierarchy + * + * @author 0x1eo + * @since 2024-12-13 + * @see User + * @see Hierarchy + */ +public abstract class AbstractUser implements User { + private String username; + private String name; + private String password; + private Hierarchy hierarchy; + + /** + * Creates a new user with the specified credentials and hierarchy level. + * + * @param username unique identifier for the user + * @param name full name of the user + * @param password user's authentication credential + * @param hierarchy user's position in the system hierarchy + * @throws IllegalArgumentException if any parameter is null or empty strings + */ + protected AbstractUser(String username, String name, String password, Hierarchy hierarchy) { + setUsername(username); + setName(name); + setPassword(password); + setHierarchy(hierarchy); + } + + @Override + public String getUsername() { + return username; + } + + @Override + public void setUsername(String username) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("Username cannot be null or empty"); + } + this.username = username.trim(); + } + + @Override + public String getName() { + return name; + } + + @Override + public void setName(String name) { + if (name == null || name.trim().isEmpty()) { + throw new IllegalArgumentException("Name cannot be null or empty"); + } + this.name = name.trim(); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public void setPassword(String password) { + if (password == null || password.trim().isEmpty()) { + throw new IllegalArgumentException("Password cannot be null or empty"); + } + this.password = password; + } + + @Override + public Hierarchy getHierarchy() { + return hierarchy; + } + + @Override + public void setHierarchy(Hierarchy hierarchy) { + if (hierarchy == null) { + throw new IllegalArgumentException("Hierarchy cannot be null"); + } + this.hierarchy = hierarchy; + } + + @Override + public int compareTo(User other) { + return this.hierarchy.getValue() - other.getHierarchy().getValue(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof User)) return false; + + User other = (User) obj; + return Objects.equals(username, other.getUsername()) && + Objects.equals(name, other.getName()) && + Objects.equals(password, other.getPassword()) && + Objects.equals(hierarchy, other.getHierarchy()); + } + + @Override + public int hashCode() { + return Objects.hash(username, name, password, hierarchy); + } + + @Override + public String toString() { + return String.format("User[username='%s', name='%s', hierarchy=%s]", + username, + name, + hierarchy.getDisplayName()); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/implementations/StandardMessage.java b/main/src/main/java/server/structs/implementations/StandardMessage.java new file mode 100644 index 0000000..3e2620c --- /dev/null +++ b/main/src/main/java/server/structs/implementations/StandardMessage.java @@ -0,0 +1,46 @@ +package server.structs.implementations; + +import server.structs.abstractions.AbstractMessage; +import server.structs.intfaces.Message; +import server.structs.intfaces.User; + +/** + * Standard message implementation for the emergency communication system. + * Provides a concrete implementation for direct, broadcast, and group messages. + * + * Features: + * - Direct user-to-user messaging + * - Broadcast messaging support + * - Multicast group communication + * - Emergency notifications + * - Timestamp-based ordering + * + * @author 0x1eo + * @since 2024-12-13 03:42:45 UTC + * @see Message + * @see AbstractMessage + * @see User + */ +public class StandardMessage extends AbstractMessage { + + /** + * Creates a new message in the emergency system. + * + * @param sender the user initiating the message + * @param receiver the recipient (user, broadcast address, or multicast group) + * @param content the message content + * @throws IllegalArgumentException if sender or content is null + */ + public StandardMessage(User sender, Object receiver, String content) { + super(sender, receiver, content); + } + + @Override + public String toString() { + return String.format("Message[from=%s, to=%s, content='%s', time=%s]", + getSender(), + getReceiver(), + getMessage(), + getTimestamp()); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/implementations/StandardNotification.java b/main/src/main/java/server/structs/implementations/StandardNotification.java new file mode 100644 index 0000000..5919c55 --- /dev/null +++ b/main/src/main/java/server/structs/implementations/StandardNotification.java @@ -0,0 +1,44 @@ +package server.structs.implementations; + +import server.structs.abstractions.AbstractNotification; +import server.structs.intfaces.User; +import server.utils.UserHandler; + +/** + * Standard notification implementation for the emergency communication system. + * Provides a concrete implementation of AbstractNotification for general-purpose + * system notifications. + * + * Features: + * - Direct messaging support + * - Broadcast capability + * - Multicast group messaging + * - System alerts and announcements + * + * @author 0x1eo + * @since 2024-12-13 03:41:39 UTC + * @see AbstractNotification + */ +public class StandardNotification extends AbstractNotification { + + /** + * Creates a new standard notification. + * + * @param sender the sender's identifier (user, system, or service) + * @param receiver the recipient's identifier (user, broadcast, or multicast group) + * @param message the notification content + * @throws IllegalArgumentException if any parameter is null + */ + public StandardNotification(Object sender, Object receiver, String message) { + super(sender, receiver, message); + } + + public String convertToUser(Object obj) { + if (obj instanceof String) { + // Use your user lookup mechanism + return UserHandler.findUser(String.valueOf(obj)); + } + throw new IllegalArgumentException("Cannot convert to User: " + obj); + } + +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/implementations/StandardRequest.java b/main/src/main/java/server/structs/implementations/StandardRequest.java new file mode 100644 index 0000000..c556d6c --- /dev/null +++ b/main/src/main/java/server/structs/implementations/StandardRequest.java @@ -0,0 +1,17 @@ +package server.structs.implementations; + +import server.structs.abstractions.AbstractRequest; +import server.structs.intfaces.User; + +/** + * Standard request implementation for the emergency communication system. + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class StandardRequest extends AbstractRequest { + + public StandardRequest(User sender, String receiver, String content) { + super(sender, receiver, content); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/implementations/StandardUser.java b/main/src/main/java/server/structs/implementations/StandardUser.java new file mode 100644 index 0000000..d6fbd6d --- /dev/null +++ b/main/src/main/java/server/structs/implementations/StandardUser.java @@ -0,0 +1,17 @@ +package server.structs.implementations; + +import server.structs.abstractions.AbstractUser; +import shared.enums.Hierarchy; + +/** + * Standard user implementation for the emergency communication system. + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class StandardUser extends AbstractUser { + + public StandardUser(String username, String name, String password, Hierarchy hierarchy) { + super(username, name, password, hierarchy); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/intfaces/Message.java b/main/src/main/java/server/structs/intfaces/Message.java new file mode 100644 index 0000000..e9582e0 --- /dev/null +++ b/main/src/main/java/server/structs/intfaces/Message.java @@ -0,0 +1,15 @@ +package server.structs.intfaces; + +/** + * Represents a message in the communication system. + * Extends the base Notification interface to provide message-specific functionality. + * This interface serves as a marker for distinguishing message types from other notifications. + * + * @author 0x1eo + * @since 2024-12-12 + * @see Notification + */ +public interface Message extends Notification { + // Marker interface - no additional methods required + // Implementation classes should provide message-specific functionality +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/intfaces/Notification.java b/main/src/main/java/server/structs/intfaces/Notification.java new file mode 100644 index 0000000..c0575e5 --- /dev/null +++ b/main/src/main/java/server/structs/intfaces/Notification.java @@ -0,0 +1,86 @@ +package server.structs.intfaces; + +import java.io.Serializable; +import java.time.Instant; + +/** + * Represents a notification in the communication system. + * Provides methods for managing notification metadata and content. + * Implements Serializable for network transmission and Comparable for ordering. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public interface Notification extends Serializable, Comparable { + + /** + * Gets the sender of the notification. + * + * @return the sender's identifier + */ + User getSender(); + + /** + * Sets the sender of the notification. + * + * @param sender the sender's identifier + * @throws IllegalArgumentException if sender is null or empty + */ + void setSender(Object sender); + + /** + * Gets the receiver of the notification. + * + * @return the receiver's identifier + */ + User getReceiver(); + + /** + * Sets the receiver of the notification. + * + * @param receiver the receiver's identifier + * @throws IllegalArgumentException if receiver is null or empty + */ + void setReceiver(Object receiver); + + /** + * Gets the content of the notification. + * + * @return the notification message content + */ + String getMessage(); + + /** + * Sets the content of the notification. + * + * @param message the notification message content + * @throws IllegalArgumentException if message is null + */ + void setMessage(String message); + + /** + * Gets the timestamp of the notification. + * + * @return the instant when the notification was created + */ + Instant getTimestamp(); + + /** + * Sets the timestamp of the notification. + * + * @param timestamp the instant when the notification was created + * @throws IllegalArgumentException if timestamp is null + */ + void setTimestamp(Instant timestamp); + + /** + * Provides a default natural ordering for notifications based on timestamp. + * + * @param other the notification to compare with + * @return negative if this is earlier, zero if same time, positive if later + */ + @Override + default int compareTo(Notification other) { + return this.getTimestamp().compareTo(other.getTimestamp()); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/intfaces/Request.java b/main/src/main/java/server/structs/intfaces/Request.java new file mode 100644 index 0000000..73644bd --- /dev/null +++ b/main/src/main/java/server/structs/intfaces/Request.java @@ -0,0 +1,29 @@ +package server.structs.intfaces; + +/** + * Represents a request in the communication system. + * Extends the Notification interface to add request-specific functionality. + * A request is a special type of notification that includes an author (requesting user). + * + * @author 0x1eo + * @since 2024-12-12 + * @see Notification + * @see User + */ +public interface Request extends Notification { + + /** + * Gets the author (requesting user) of this request. + * + * @return the user who authored this request + */ + User getTruster(); + + /** + * Sets the author (requesting user) of this request. + * + * @param author the user who authored this request + * @throws IllegalArgumentException if author is null + */ + void setTruster(User author); +} \ No newline at end of file diff --git a/main/src/main/java/server/structs/intfaces/User.java b/main/src/main/java/server/structs/intfaces/User.java new file mode 100644 index 0000000..973ccbf --- /dev/null +++ b/main/src/main/java/server/structs/intfaces/User.java @@ -0,0 +1,101 @@ +package server.structs.intfaces; + +import java.io.Serializable; + +import shared.enums.Hierarchy; + +/** + * Represents a user in the system with their credentials and permissions. + * Implements Serializable for network transmission and Comparable for user ordering. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public interface User extends Serializable, Comparable { + + /** + * Gets the user's unique username. + * + * @return the username + */ + String getUsername(); + + /** + * Sets the user's username. + * + * @param username the username to set + * @throws IllegalArgumentException if username is null or empty + */ + void setUsername(String username); // Fixed method name from setUserName + + /** + * Gets the user's password (hashed). + * + * @return the hashed password + */ + String getPassword(); + + /** + * Sets the user's password. + * Implementation should ensure the password is properly hashed before storage. + * + * @param password the password to set + * @throws IllegalArgumentException if password is null or empty + */ + void setPassword(String password); + + /** + * Gets the user's display name. + * + * @return the user's full name + */ + String getName(); + + /** + * Sets the user's display name. + * + * @param name the name to set + * @throws IllegalArgumentException if name is null or empty + */ + void setName(String name); + + /** + * Gets the user's hierarchy level in the system. + * + * @return the user's hierarchy level + */ + Hierarchy getHierarchy(); + + /** + * Sets the user's hierarchy level. + * + * @param hierarchy the hierarchy level to set + * @throws IllegalArgumentException if hierarchy is null + */ + void setHierarchy(Hierarchy hierarchy); + + /** + * Provides a default natural ordering for users based on username. + * + * @param other the user to compare with + * @return negative if this username comes before, zero if equal, positive if after + */ + @Override + default int compareTo(User other) { + return this.getUsername().compareToIgnoreCase(other.getUsername()); + } + + /** + * Checks if the user has at least the specified hierarchy level. + * + * @param minimumHierarchy the minimum required hierarchy level + * @return true if user's hierarchy is at least the specified level + * @throws IllegalArgumentException if minimumHierarchy is null + */ + default boolean hasMinimumHierarchy(Hierarchy minimumHierarchy) { + if (minimumHierarchy == null) { + throw new IllegalArgumentException("Minimum hierarchy cannot be null"); + } + return this.getHierarchy().ordinal() >= minimumHierarchy.ordinal(); + } +} \ No newline at end of file diff --git a/main/src/main/java/server/utils/InputCommandRouter.java b/main/src/main/java/server/utils/InputCommandRouter.java new file mode 100644 index 0000000..c055db1 --- /dev/null +++ b/main/src/main/java/server/utils/InputCommandRouter.java @@ -0,0 +1,90 @@ +package server.utils; + +import java.net.Socket; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import server.structs.SystemStateManager; +import server.structs.intfaces.User; +import shared.enums.ConnType; + +/** + * This class represents the protocol used for processing input in the server. + */ +public class InputCommandRouter { + private static final Logger logger = Logger.getLogger(InputCommandRouter.class.getName()); + /** + * Processes the input based on the given connection type, socket packet, and input string. + * + * @param connType The type of connection (DIRECT or INDIRECT). + * @param socketPacket The socket packet object. + * @param input The input string to be processed. + * @return The response as a JSON string. + */ + public static String processInput(ConnType connType, Object socketPacket, String input) { + JSONObject response = new JSONObject(); + try { + JSONObject json = new JSONObject(input); + if (!json.has("command")) { + response.put("response", "Invalid command!"); + return response.toString(); + } + + // Register the user's socket if it is not already registered + Socket socket; + if (connType == ConnType.UNICAST) { + if (json.has("username") || json.has("from")) { + User User = null; + if (json.has("username")) { + User = SystemStateManager.getUser(json.getString("username")); + } + if (json.has("from")) { + User = SystemStateManager.getUser(json.getString("from")); + } + if (User != null) { + if (socketPacket instanceof Socket) { + socket = (Socket) socketPacket; + if (socket != SystemStateManager.getUserSocket(User)) { + SystemStateManager.addUserSocket(User, socket); + } + } + } + } + } + + // Process the input based on the command + switch (json.getString("command")) { + case "register": + if (connType != ConnType.UNICAST) { + return null; + } + socket = (Socket) socketPacket; + return UserHandler.register(json, socketPacket); + case "login": + if (connType != ConnType.UNICAST) { + return null; + } + socket = (Socket) socketPacket; + return UserHandler.login(json, socketPacket); + case "message": + MessageProtocolHandler.receiveMessage(connType, json); + return null; + case "request": + MessageProtocolHandler.receiveRequest(connType, json, socketPacket); + return null; + case "joinGroup": + UserHandler.joinGroup(json); + return null; + default: + logger.severe("Invalid command received! " + input); + return null; + } + + } catch (JSONException e) { + logger.severe("Invalid JSON received! " + e.getMessage()); + return null; + } + } +} \ No newline at end of file diff --git a/main/src/main/java/server/utils/MessageProtocolHandler.java b/main/src/main/java/server/utils/MessageProtocolHandler.java new file mode 100644 index 0000000..75b1515 --- /dev/null +++ b/main/src/main/java/server/utils/MessageProtocolHandler.java @@ -0,0 +1,260 @@ +package server.utils; + +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import server.structs.SystemStateManager; +import server.structs.implementations.StandardMessage; +import server.structs.implementations.StandardRequest; +import server.structs.intfaces.Notification; +import server.structs.intfaces.Message; +import server.structs.intfaces.Request; +import server.structs.intfaces.User; +import server.handlers.AcceptRequestHandler; +import shared.enums.ConnType; + +public class MessageProtocolHandler { + private static final Logger logger = Logger.getLogger(MessageProtocolHandler.class.getName()); + private static final ExecutorService executorService = Executors.newFixedThreadPool(50); + + /** + * Returns the ExecutorService used by the EventsHandler. + * + * @return the ExecutorService used by the EventsHandler + */ + public static ExecutorService getExecutorService() { + return executorService; + } + + /** + * Converts an event to a JSON object. + * + * @param notification The event to convert. + * @return The JSON object representation of the event. + */ + public static JSONObject notificationToJson(N notification) throws JSONException { + JSONObject json = new JSONObject(); + json.put("from", notification.getSender()); + Object receiver = notification.getReceiver(); + if (receiver instanceof User) { + json.put("to", ((User) receiver).getUsername()); + } else if (receiver instanceof String) { + String receiverString = (String) receiver; + if (receiverString.equals("broadcast")) { + json.put("to", "broadcast"); + } else if (receiverString.matches( + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) { + json.put("to", receiverString); + } + } else { + logger.severe("Invalid receiver type!"); + } + json.put("content", notification.getMessage()); + SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy HH:mm"); + json.put("date", sdf.format(notification.getTimestamp())); + if (notification instanceof Message) { + json.put("command", "message"); + return json; + } else if (notification instanceof Request) { + json.put("command", "request"); + Request request = (Request) notification; + if (request.getTruster() != null) { + json.put("accepter", request.getTruster().getUsername()); + } else { + json.put("accepter", ""); + } + return json; + } + throw new JSONException("Invalid event type"); + } + + + /** + * Converts a collection of events to a JSON array. + * + * @param notifications The collection of events to convert. + * @return The JSON array representation of the events. + * @throws JSONException If an error occurs while converting the events to JSON. + */ + public static JSONArray notificationsToJson(Collection notifications) throws JSONException { + JSONArray jsonArray = new JSONArray(); + for (Notification notification : notifications) { + jsonArray.put(notificationToJson(notification)); + } + return jsonArray; + } + + /** + * Converts a JSON object to a message. + * + * @param json The JSON object to convert. + * @return The message representation of the JSON object. + */ + public static Message messageFromJson(JSONObject json) { + try { + if (!json.has("from") || !json.has("to") || !json.has("content")) { + logger.severe("Invalid message received! (field missing)"); + return null; + } + if (json.getString("from").equals("server")) { + return null; + } + User from = SystemStateManager.getUser(json.getString("from")); + if (from == null) { + logger.severe("Invalid message received! (User from)"); + return null; + } + String to = json.getString("to"); + String content = json.getString("content"); + Message message = new StandardMessage(from, null, content); + if (to.equals("broadcast")) { + message.setReceiver(to); + } else if (to.matches( + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) { + message.setReceiver(to); + } else { + message.setReceiver(SystemStateManager.getUser(to)); + } + if (message.getReceiver() == null) { + logger.severe("Invalid message received! (User to)"); + return null; + } + return message; + } catch (JSONException ignored) { + logger.severe("Invalid message received! (JSONException)"); + return null; + } + } + + /** + * Converts a JSON object to a request. + * + * @param json The JSON object to convert. + * @return The request representation of the JSON object. + */ + public static Request requestFromJson(JSONObject json) { + try { + if (!json.has("from") || !json.has("to") || !json.has("content")) { + logger.severe("Invalid request received! (field missing)"); + return null; + } + User from = SystemStateManager.getUser(json.getString("from")); + if (from == null) { + logger.severe("Invalid request received! (User from)"); + return null; + } + String to = json.getString("to"); + String content = json.getString("content"); + Request request = new StandardRequest(from, null, content); + if (to.equals("broadcast")) { + request.setReceiver(to); + } else if (to.matches( + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) { + request.setReceiver(to); + } else { + request.setReceiver(SystemStateManager.getUser(to)); + } + if (request.getReceiver() == null) { + logger.severe("Invalid request received! (User to)"); + return null; + } + return request; + } catch (JSONException ignored) { + logger.severe("Invalid request received! (JSONException)"); + return null; + } + } + + /// ! Protocol methods + + /** + * Receives a message and processes it based on the connection type and the message content. + * + * @param connType the type of connection (DIRECT or BROADCAST) + * @param json the JSON object containing the message data + * @return always returns null + */ + public static String receiveMessage(ConnType connType, JSONObject json) { + Message message = messageFromJson(json); + if (message != null) { + Object Receiver = message.getReceiver(); + if (Receiver instanceof User) { + SystemStateManager.addUserNotification((User) message.getReceiver(), message); + } else if (Receiver instanceof String) { + String receiverString = (String) Receiver; + if (receiverString.equals("broadcast")) { + Collection users = SystemStateManager.getUsers(); + for (User user : users) { + SystemStateManager.addUserNotification(user, message); + } + } else if (receiverString.matches( + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) { + Collection users = SystemStateManager.getUsersFromGroup(receiverString); + for (User user : users) { + SystemStateManager.addUserNotification(user, message); + } + } else { + User user = SystemStateManager.getUser(receiverString); + if (user != null) { + SystemStateManager.addUserNotification(user, message); + } + } + } + SystemStateManager.addUserNotification(message.getSender(), message); + if (connType == ConnType.UNICAST) { + SystemStateManager.addNotificationToDeliver(message); + } + } + return null; + } + + /** + * Receives a request and processes it based on the connection type, JSON data, and socket packet. + * + * @param connType The type of connection (DIRECT or INDIRECT). + * @param json The JSON object containing the request data. + * @param socketPacket The socket packet associated with the request. + * @return The response string. + */ + public static String receiveRequest(ConnType connType, JSONObject json, Object socketPacket) { + Request request = requestFromJson(json); + if (request != null) { + Object Receiver = request.getReceiver(); + if (Receiver instanceof User) { + SystemStateManager.addUserNotification(request.getReceiver(), request); + } else if (Receiver instanceof String) { + String receiverString = (String) Receiver; + if (receiverString.equals("broadcast")) { + Collection users = SystemStateManager.getUsers(); + for (User user : users) { + SystemStateManager.addUserNotification(user, request); + } + } else if (receiverString.matches( + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")) { + Collection users = SystemStateManager.getUsersFromGroup(receiverString); + for (User user : users) { + SystemStateManager.addUserNotification(user, request); + } + } else { + User user = SystemStateManager.getUser(receiverString); + if (user != null) { + SystemStateManager.addUserNotification(user, request); + } + } + } + SystemStateManager.addUserNotification(request.getSender(), request); + if (connType == ConnType.UNICAST) { + SystemStateManager.addNotificationToDeliver(request); + } + executorService.execute(new AcceptRequestHandler(connType, request)); + } + return null; + } +} \ No newline at end of file diff --git a/main/src/main/java/server/utils/UserHandler.java b/main/src/main/java/server/utils/UserHandler.java new file mode 100644 index 0000000..03da10e --- /dev/null +++ b/main/src/main/java/server/utils/UserHandler.java @@ -0,0 +1,193 @@ +package server.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONException; +import org.json.JSONObject; + +import server.Server; +import server.handlers.MessageHistoryHandler; +import server.structs.SystemStateManager; +import server.structs.intfaces.User; +import server.structs.implementations.StandardUser; // New concrete implementation +import shared.enums.Hierarchy; + +/** + * Handles user registration, authentication, and communication in the emergency system. + * Manages user sessions and group memberships. + * + * @author 0x1eo + * @since 2024-12-13 + */ +public class UserHandler { + private static final Logger logger = Logger.getLogger(UserHandler.class.getName()); + private static final String MULTICAST_GROUP_PATTERN = + "^(22[4-9]|23[0-9]|2[4-9][0-9]|[3-9][0-9]{2}|[12][0-9]{3})" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + + "\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + // Prevent instantiation + private UserHandler() {} + + /** + * Registers a new user in the system. + * + * @param json The registration details + * @param socketPacket The connection socket + * @return JSON response indicating success or failure + * @throws JSONException If registration data is malformed + */ + public static String register(JSONObject json, Object socketPacket) throws JSONException { + try { + String username = json.getString("username"); + if (SystemStateManager.getUser(username) != null) { + logger.info("Registration failed: User already exists - " + username); + return createErrorResponse("User already exists!"); + } + + User user = createUser(json); + Socket socket = (Socket) socketPacket; + + SystemStateManager.addUser(user); + SystemStateManager.addUserSocket(user, socket); + + return createSuccessResponse(); + } catch (IllegalArgumentException e) { + logger.log(Level.WARNING, "Registration failed: Invalid role", e); + return createErrorResponse("Invalid role!"); + } catch (Exception e) { + logger.log(Level.SEVERE, "Registration failed: Unexpected error", e); + return e.getMessage(); + } + } + + /** + * Creates a new user instance from JSON data. + */ + private static User createUser(JSONObject json) throws JSONException { + return new StandardUser( + json.getString("username"), + json.getString("name"), + json.getString("password"), + Hierarchy.valueOf(json.getString("role").toUpperCase()) + ); + } + + /** + * Authenticates a user and establishes their session. + */ + public static String login(JSONObject json, Object socketPacket) throws JSONException { + User user = SystemStateManager.getUser(json.getString("username")); + + if (user == null) { + logger.info("Login failed: Invalid username"); + return createErrorResponse("Invalid username!"); + } + + if (!user.getPassword().equals(json.getString("password"))) { + logger.info("Login failed: Invalid password for user " + user.getUsername()); + return createErrorResponse("Invalid password!"); + } + + establishUserSession(user, (Socket) socketPacket); + return createSuccessResponse(); + } + + /** + * Sets up user session and starts message history handler. + */ + private static void establishUserSession(User user, Socket socket) { + SystemStateManager.addUserSocket(user, socket); + new Thread(new MessageHistoryHandler(user)).start(); + } + + /** + * Sends data to a user through their socket connection. + */ + public static void sendSomething(User user, String data) throws IOException { + try (Socket newSocket = createUserSocket(user); + PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true)) { + out.println(data); + } + } + + /** + * Sends data and waits for a response. + */ + public static String sendAndReceiveSomething(User user, String data) throws IOException { + try (Socket newSocket = createUserSocket(user); + PrintWriter out = new PrintWriter(newSocket.getOutputStream(), true); + BufferedReader in = new BufferedReader(new InputStreamReader(newSocket.getInputStream()))) { + out.println(data); + return in.readLine(); + } + } + + private static Socket createUserSocket(User user) throws IOException { + Socket userSocket = SystemStateManager.getUserSocket(user); + return new Socket(userSocket.getInetAddress(), Server.USER_PORT); + } + + /** + * Adds a user to a multicast group. + */ + public static String joinGroup(JSONObject json) throws JSONException { + if (!isValidGroupRequest(json)) { + return null; + } + + User user = SystemStateManager.getUser(json.getString("username")); + if (user == null) { + logger.info("Group join failed: Invalid username"); + return null; + } + + try { + String group = json.getString("group"); + SystemStateManager.getMulticastSocket().joinGroup(InetAddress.getByName(group)); + SystemStateManager.addUserToGroup(group, user); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to join multicast group", e); + } + return null; + } + + private static boolean isValidGroupRequest(JSONObject json) { + if (!json.has("group") || !json.has("username")) { + logger.info("Group join failed: Missing required fields"); + return false; + } + + String group = json.getString("group"); + if (!group.matches(MULTICAST_GROUP_PATTERN)) { + logger.info("Group join failed: Invalid group address - " + group); + return false; + } + + return true; + } + + private static String createSuccessResponse() { + return new JSONObject().put("response", "OK").toString(); + } + + private static String createErrorResponse(String message) { + return new JSONObject().put("response", message).toString(); + } + + public static String findUser(String username) { + User user = SystemStateManager.getUser(username); + if (user == null) { + return null; + } + return user.getUsername(); + } +} \ No newline at end of file diff --git a/main/src/main/java/shared/enums/ConnType.java b/main/src/main/java/shared/enums/ConnType.java new file mode 100644 index 0000000..e108227 --- /dev/null +++ b/main/src/main/java/shared/enums/ConnType.java @@ -0,0 +1,72 @@ +package shared.enums; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents different types of network connections used in the application. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public enum ConnType { + UNICAST("Unicast", "Point-to-point connection between two nodes"), + MULTICAST("Multicast", "One-to-many connection to a specific group of nodes"), + BROADCAST("Broadcast", "One-to-all connection reaching all nodes in the network"); + + private final String displayName; + private final String description; + + ConnType(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + /** + * Gets the user-friendly display name of the connection type. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Gets the description of the connection type. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Safely converts a string to a ConnType. + * + * @param value the string value to convert + * @return an Optional containing the ConnType if valid, empty Optional otherwise + */ + public static Optional fromString(String value) { + if (value == null || value.trim().isEmpty()) { + return Optional.empty(); + } + + return Arrays.stream(values()) + .filter(connType -> connType.name().equalsIgnoreCase(value.trim())) + .findFirst(); + } + + /** + * Checks if the connection type is suitable for group communication. + * + * @return true if the connection type supports group communication + */ + public boolean isGroupCapable() { + return this == MULTICAST || this == BROADCAST; + } + + @Override + public String toString() { + return String.format("%s (%s)", displayName, description); + } +} \ No newline at end of file diff --git a/main/src/main/java/shared/enums/Hierarchy.java b/main/src/main/java/shared/enums/Hierarchy.java new file mode 100644 index 0000000..281a9da --- /dev/null +++ b/main/src/main/java/shared/enums/Hierarchy.java @@ -0,0 +1,108 @@ +package shared.enums; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents the priority levels in the system's hierarchy. + * Used for tasks and user permissions. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public enum Hierarchy { + LOW(0, "Low Priority"), + MEDIUM(1, "Medium Priority"), + HIGH(2, "High Priority"); + + private final int value; + private final String displayName; + + Hierarchy(int value, String displayName) { + this.value = value; + this.displayName = displayName; + } + + /** + * Gets the numeric value associated with this hierarchy level. + * + * @return the numeric value of the hierarchy level + */ + public int getValue() { + return value; + } + + /** + * Gets the display name of this hierarchy level. + * + * @return the user-friendly name of the hierarchy level + */ + public String getDisplayName() { + return displayName; + } + + /** + * Finds a Hierarchy enum by its numeric value. + * + * @param value the numeric value to look up + * @return an Optional containing the Hierarchy if found, empty Optional otherwise + */ + public static Optional fromValue(int value) { + return Arrays.stream(values()) + .filter(h -> h.value == value) + .findFirst(); + } + + /** + * Finds a Hierarchy enum by its name (case-insensitive). + * + * @param name the name to look up + * @return an Optional containing the Hierarchy if found, empty Optional otherwise + */ + public static Optional fromString(String name) { + if (name == null || name.trim().isEmpty()) { + return Optional.empty(); + } + + return Arrays.stream(values()) + .filter(h -> h.name().equalsIgnoreCase(name.trim())) + .findFirst(); + } + + /** + * Gets all hierarchy values as strings. + * + * @return array of hierarchy names + */ + public static String[] getAllNames() { + return Arrays.stream(values()) + .map(Hierarchy::name) + .toArray(String[]::new); + } + + /** + * Gets all hierarchy display names. + * + * @return array of user-friendly hierarchy names + */ + public static String[] getAllDisplayNames() { + return Arrays.stream(values()) + .map(Hierarchy::getDisplayName) + .toArray(String[]::new); + } + + /** + * Compares this hierarchy level with another. + * + * @param other the hierarchy level to compare with + * @return true if this level is higher than the other + */ + public boolean isHigherThan(Hierarchy other) { + return this.value > other.value; + } + + @Override + public String toString() { + return displayName; + } +} \ No newline at end of file diff --git a/main/src/main/java/shared/enums/RecvType.java b/main/src/main/java/shared/enums/RecvType.java new file mode 100644 index 0000000..576b263 --- /dev/null +++ b/main/src/main/java/shared/enums/RecvType.java @@ -0,0 +1,90 @@ +package shared.enums; + +import java.util.Arrays; +import java.util.Optional; + +/** + * Represents different types of message receivers in the communication system. + * + * @author 0x1eo + * @since 2024-12-12 + */ +public enum RecvType { + USER("Single User", "Direct message to a specific user"), + GROUP("Group", "Message to a defined group of users"), + BROADCAST("Broadcast", "Message to all users in the network"); + + private final String displayName; + private final String description; + + RecvType(String displayName, String description) { + this.displayName = displayName; + this.description = description; + } + + /** + * Gets the user-friendly display name of the receiver type. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } + + /** + * Gets the description of the receiver type. + * + * @return the description + */ + public String getDescription() { + return description; + } + + /** + * Determines if this receiver type supports multiple recipients. + * + * @return true if the receiver type supports multiple recipients + */ + public boolean isMultiReceiver() { + return this == GROUP || this == BROADCAST; + } + + /** + * Safely converts a string to a RecvType. + * + * @param value the string value to convert + * @return an Optional containing the RecvType if valid, empty Optional otherwise + */ + public static Optional fromString(String value) { + if (value == null || value.trim().isEmpty()) { + return Optional.empty(); + } + + return Arrays.stream(values()) + .filter(type -> type.name().equalsIgnoreCase(value.trim())) + .findFirst(); + } + + /** + * Gets the appropriate receiver type for a given number of recipients. + * + * @param recipientCount the number of recipients + * @return the appropriate receiver type + */ + public static RecvType forRecipientCount(int recipientCount) { + if (recipientCount <= 0) { + throw new IllegalArgumentException("Recipient count must be positive"); + } + + return switch (recipientCount) { + case 1 -> USER; + case Integer.MAX_VALUE -> BROADCAST; + default -> GROUP; + }; + } + + @Override + public String toString() { + return String.format("%s (%s)", displayName, description); + } +} \ No newline at end of file