diff --git a/README.md b/README.md index 610afd1..46ac535 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,36 @@ -## Network File-Sharing chat System (Java) +# UniChat - Network File-Sharing chat System (Java) -### Overview -This project is a multi-client network chat system with file sharing, implemented using **Java sockets**, **MUltithreading** and **Object Serialization**. -Clients connect to a central server and can: +A modern, real-time chat application built using **JavaFX** and **Java Socket Programming**. UniChat allows users to connect to a central server, see who is online, and engage in both public and private conversations through a clean, dark-themed interface. -* send public chat message -* send private messages -* request the list of online users -* transfer files to other users +## 🚀 Key Features -communication between client and server is implemented using Java Object Streams(`ObjectInputStream` / `ObjectOutputStream`) so that structured objects can be transferred directly over the network. +* **Authentication System:** A dedicated login screen where users can authenticate with a username. +* **Real-time Public Chat:** A global communication channel to broadcast messages to all connected users. +* **Private Messaging:** Targeted, one-on-one communication capabilities between users. +* **Live User List:** A sidebar that displays all currently connected users in real-time. +* **Modern UI:** Built with JavaFX, featuring a responsive, dark-themed interface for a comfortable user experience. +## 📸 Project Interface + +| Login Screen | Authentication Success | Main Chat Interface | +| :---: | :---: | :---: | +| ![Login](images/login_screen.png) | ![Success](images/login_success.png) | ![Chat](images/chat_interface.png) | + + +## 🛠 Tech Stack + +* **Language:** Java 17+ +* **GUI Framework:** JavaFX +* **Networking:** Java Sockets (TCP) +* **Concurrency:** Multi-threaded Client-Server architecture + +## 💻 How to Run + +1. **Start the Server:** Ensure the `Server` application is running to listen for incoming connections. +2. **Launch the Client:** Run the `Launcher` class to open the UI. +3. **Connect:** Enter your desired username on the login screen and click "Connect". --- -### Architecture - -1. **ChatServer** opens a `ServerSocket` and waits for connections. -2. Each connecting client gets its own **ClientSession** thread. -3. A shared **UserManager** tracks who's online and routes messages. -4. Everything is exchanged as serialized objects (`ChatMessage` / `FileMessage`). - -``` -Client → Socket → Server → ClientSession (thread) → UserManager → Broadcast/Route → Clients -``` - ---- - -### Message Protocol -Two serializable classes carry everything over the wire: - -* **ChatMessage** – text-based: logins, public/private messages, user-list requests, server replies. -* **FileMessage** – file transfers, carrying the file as a `byte[]`. - -`MessageType` is an enum (`LOGIN`, `LOGIN_SUCCESS`, `LOGIN_FAILED`, `PUBLIC_MESSAGE`, `PRIVATE_MESSAGE`, `USER_LIST`, …) so both sides agree on what each message means. - ---- - -### Package Structure -``` -com.university.chat -├── common → shared message classes (ChatMessage, FileMessage, MessageType) -├── server → server-side logic -└── client → client application -``` - -#### `common` -Already complete — these define the protocol and don't need any changes. - -#### `server` -* **ChatServer** – entry point. Opens the server socket and creates a `ClientSession` per client. -* **ClientSession** – one thread per client. Handles login, reads incoming messages, and dispatches them (broadcast, private message, user list, file transfer). -* **UserManager** – thread-safe registry of online users (`ConcurrentHashMap`). Already complete. -* **FileManager** – manages per-user folders on disk (`server_data//sent` and `received`). Already complete. - -#### `client` -* **chatClient** – entry point. Connects to the server, logs in, starts a listener thread, and runs the input loop for user commands. -* **ServerListener** – background thread that continuously reads and displays messages/files coming from the server. -* **TransferProgress** – small helper that prints a progress bar during file uploads. Already complete. - ---- - -### Client Commands -``` -/msg → private message -/users → list online users -/sendfile → send a file - → public message -``` - ---- - -### File Transfer Flow -1. Client reads the file and converts it to `byte[]`. -2. It's wrapped in a `FileMessage` and sent to the server. -3. The server saves a copy (in `sent/` for the sender, `received/` for the receiver) and forwards it to the recipient. - ---- - -### Threading Model -* **Server**: one `ClientSession` thread per connected client. -* **Client**: main thread handles user input, a `ServerListener` thread handles incoming messages — so you can chat and receive **at the same time**. - ---- - -## Your Task - -Several core pieces are left as **TODOs** for you to implement. Follow the comments in each file alongside the structure mentioned above to complete them. - -| File | What to implement | -|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Server/ChatServer.java` | Pick a port, create a shared `UserManager`, open the server socket, accept clients in a loop, and create a `ClientSession` thread per client. | -| `Server/ClientSession.java` | Set up object streams in the constructor; handle login (success/failure); receive messages in a loop and dispatch `ChatMessage`/`FileMessage`; broadcast, private message, and user-list logic; remove user on disconnect; forward files. | -| `Client/chatClient.java` | Connect to the server, set up streams, log in, start `ServerListener`, and implement the command loop (`/msg`, `/users`, `/sendfile`, plain messages). | -| `Client/ServerListener.java` | Continuously read objects from the server and print chat messages / file-received notifications. | - -**Tips:** -* Always create the `ObjectOutputStream` *before* the `ObjectInputStream` on both ends — this avoids a stream-handshake deadlock. -* Don't forget `out.flush()` after `writeObject(...)`. -* Test with two or more client instances to verify broadcasting, private messages, and file transfer all work correctly. - -#### Running multiple clients in IntelliJ -1. Click the **⋮** icon next to the run button → **Edit Configurations** -2. Open **Modify options** (Alt+M) -3. Enable **Allow multiple instances** (Alt+U) - ---- - -## Optional Bonus: JavaFX UI - -If you want to go further replace the console client with a simple **JavaFX** GUI (chat window, online-user list, file-send button, etc.). - -1. **Add the JavaFX dependencies** to your `pom.xml` — for Maven: - ```xml - - org.openjfx - javafx-controls - 21 - - - org.openjfx - javafx-fxml - 21 - - ``` - You'll also need the `javafx-maven-plugin` to run the app from Maven. - - -2. **Create a JavaFX entry point**, e.g. `Client/ChatClientApp.java`, extending `javafx.application.Application` and implementing `start(Stage stage)`. This becomes your new launch class instead of (or alongside) `chatClient`. 3. Reuse your existing socket/streams logic — just move the "send" actions to button handlers and update the UI from `ServerListener` using `Platform.runLater(...)` (**since UI updates must happen on the JavaFX Application Thread**). diff --git a/images/chat_interface.png b/images/chat_interface.png new file mode 100644 index 0000000..72ee70b Binary files /dev/null and b/images/chat_interface.png differ diff --git a/images/login_screen.png b/images/login_screen.png new file mode 100644 index 0000000..7b22904 Binary files /dev/null and b/images/login_screen.png differ diff --git a/images/login_success.png b/images/login_success.png new file mode 100644 index 0000000..4d8842c Binary files /dev/null and b/images/login_success.png differ diff --git a/pom.xml b/pom.xml index c7c56af..76fe42b 100644 --- a/pom.xml +++ b/pom.xml @@ -13,5 +13,31 @@ 25 UTF-8 + + + org.openjfx + javafx-controls + 21 + + + org.openjfx + javafx-fxml + 21 + + + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + com.university.chat.Client.UI.Launcher + + + + \ No newline at end of file diff --git a/src/main/java/com/university/chat/Client/Net/ServerListener.java b/src/main/java/com/university/chat/Client/Net/ServerListener.java new file mode 100644 index 0000000..cfab302 --- /dev/null +++ b/src/main/java/com/university/chat/Client/Net/ServerListener.java @@ -0,0 +1,46 @@ +package com.university.chat.Client.Net; + +import java.io.ObjectInputStream; +import java.util.function.Consumer; + +public class ServerListener implements Runnable { + + private final ObjectInputStream in; + private Consumer onMessageReceived; + private final Consumer onConnectionError; + + private volatile boolean running = true; + + public ServerListener(ObjectInputStream in, + Consumer onMessageReceived, + Consumer onConnectionError) { + this.in = in; + this.onMessageReceived = onMessageReceived; + this.onConnectionError = onConnectionError; + } + + @Override + public void run() { + + try { + while (running) { + Object object = in.readObject(); + if (onMessageReceived != null) { + onMessageReceived.accept(object); + } + } + } catch (Exception e) { + if (running && onConnectionError != null) { + onConnectionError.accept(e); + } + } + } + + public void stop() { + running = false; + } + + public void setOnMessageReceived(Consumer onMessageReceived) { + this.onMessageReceived = onMessageReceived; + } +} diff --git a/src/main/java/com/university/chat/Client/Net/chatClient.java b/src/main/java/com/university/chat/Client/Net/chatClient.java new file mode 100644 index 0000000..653a978 --- /dev/null +++ b/src/main/java/com/university/chat/Client/Net/chatClient.java @@ -0,0 +1,158 @@ +package com.university.chat.Client.Net; + +import com.university.chat.Client.UI.ChatController; +import com.university.chat.Common.ChatMessage; +import com.university.chat.Common.FileMessage; +import com.university.chat.Common.MessageType; +import javafx.scene.control.ListView; + +import java.io.*; +import java.net.Socket; +import java.nio.file.Files; +import java.util.function.Consumer; + +public class chatClient { + + private Socket socket; + private ObjectOutputStream out; + private ObjectInputStream in; + + private Thread listenerThread; + private ServerListener serverListener; + + private String username; + + public void connect(String host, int port) throws IOException { + + socket = new Socket(host, port); + + out = new ObjectOutputStream(socket.getOutputStream()); + out.flush(); + + in = new ObjectInputStream(socket.getInputStream()); + } + + public void startListening(Consumer onMessageReceived, + Consumer onConnectionError) { + + serverListener = new ServerListener(in, onMessageReceived, onConnectionError); + + listenerThread = new Thread(serverListener); + listenerThread.setDaemon(true); + listenerThread.start(); + } + + public void login(String username) throws IOException { + this.username = username; + + ChatMessage loginMessage = new ChatMessage( + MessageType.LOGIN, + username, + null, + null + ); + + sendObject(loginMessage); + + } + + public void sendPublicMessage(String content) throws IOException { + ChatMessage message = new ChatMessage( + MessageType.PUBLIC_MESSAGE, + username, + null, + content + ); + + sendObject(message); + } + + public void sendPrivateMessage(String receiver, String content) throws IOException { + ChatMessage message = new ChatMessage( + MessageType.PRIVATE_MESSAGE, + username, + receiver, + content + ); + + sendObject(message); + } + + public void requestUserList() throws IOException { + ChatMessage message = new ChatMessage( + MessageType.USER_LIST, + username, + null, + null + ); + + sendObject(message); + } + + + public void sendFile(String receiver, File file) throws IOException { + if (file == null || !file.exists() || !file.isFile()) { + throw new FileNotFoundException("Invalid file."); + } + + byte[] fileData = Files.readAllBytes(file.toPath()); + + + FileMessage fileMessage = new FileMessage( + username, + receiver, + file.getName(), + fileData + ); + + sendObject(fileMessage); + } + + private synchronized void sendObject(Object object) throws IOException { + if (out == null) { + throw new IOException("Client is not connected."); + } + + out.writeObject(object); + out.flush(); + } + + public boolean isConnected() { + return socket != null && socket.isConnected() && !socket.isClosed(); + } + + public String getUsername() { + return username; + } + + public void close() { + try { + if (serverListener != null) { + serverListener.stop(); + } + + if (listenerThread != null) { + listenerThread.interrupt(); + } + + if (in != null) { + in.close(); + } + + if (out != null) { + out.close(); + } + + if (socket != null) { + socket.close(); + } + + } catch (IOException e) { + System.err.println("Error while closing client: " + e.getMessage()); + } + } + + public ServerListener getServerListener() { + return serverListener; + } +} diff --git a/src/main/java/com/university/chat/Client/ServerListener.java b/src/main/java/com/university/chat/Client/ServerListener.java deleted file mode 100644 index b609d74..0000000 --- a/src/main/java/com/university/chat/Client/ServerListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.university.chat.Client; - -public class ServerListener implements Runnable{ - // TODO: store the ObjectInputStream from the user socket - // (this should be the same input stream the - // chatClient created when connecting) - - @Override - public void run() { - try { - // TODO: In an infinite loop read objects from the server - // - if it's a ChatMessage -> print ": " - // - if it's a FileMessage -> print that a file was received - // (filename + sender), it's already - // saved to disk by the server. - } catch (Exception e){ - System.out.println("Disconnected from server"); - } - } -} diff --git a/src/main/java/com/university/chat/Client/UI/ChatApp.java b/src/main/java/com/university/chat/Client/UI/ChatApp.java new file mode 100644 index 0000000..7f3719f --- /dev/null +++ b/src/main/java/com/university/chat/Client/UI/ChatApp.java @@ -0,0 +1,28 @@ +package com.university.chat.Client.UI; + + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +import java.io.IOException; + +public class ChatApp extends Application { + + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("login.fxml")); + Scene scene = new Scene(fxmlLoader.load()); + scene.getStylesheets().add( + getClass().getResource("/chat_style.css").toExternalForm() + ); + + stage.setTitle("Login"); + stage.initStyle(StageStyle.UNDECORATED); + stage.setScene(scene); + stage.show(); + } + +} diff --git a/src/main/java/com/university/chat/Client/UI/ChatController.java b/src/main/java/com/university/chat/Client/UI/ChatController.java new file mode 100644 index 0000000..e8f31bc --- /dev/null +++ b/src/main/java/com/university/chat/Client/UI/ChatController.java @@ -0,0 +1,337 @@ +package com.university.chat.Client.UI; + +import com.university.chat.Client.Net.chatClient; +import com.university.chat.Client.TransferProgress; +import com.university.chat.Common.ChatMessage; +import com.university.chat.Common.FileMessage; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.FileChooser; +import javafx.stage.Stage; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.*; + +public class ChatController { + + @FXML + private ListView usersList; + + @FXML + private TextArea publicChatArea; + + @FXML + private AnchorPane chatAnchor; + + @FXML + private TextField messageField; + + @FXML + private BorderPane titleBar; + + private double xOffset = 0; + private double yOffset = 0; + + private chatClient client; + private String selectedUser = null; + + private final Map privateChatViews = new HashMap<>(); + private final Map privateTextAreas = new HashMap<>(); + private final Map privateInputFields = new HashMap<>(); + + @FXML + private VBox fileTransferBox; + + @FXML + private Label fileTransferLabel; + + @FXML + private ProgressBar fileTransferProgress; + + + @FXML + private void initialize() { + + titleBar.setOnMousePressed(event -> { + xOffset = event.getSceneX(); + yOffset = event.getSceneY(); + }); + + titleBar.setOnMouseDragged(event -> { + Stage stage = (Stage) titleBar.getScene().getWindow(); + stage.setX(event.getScreenX() - xOffset); + stage.setY(event.getScreenY() - yOffset); + }); + + usersList.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { + + if (newVal == null || newVal.isBlank()) { + selectedUser = null; + chatAnchor.getChildren().clear(); + return; + } + + String clickedUser = newVal.trim(); + + if (client != null && clickedUser.equals(client.getUsername())) { + selectedUser = null; + chatAnchor.getChildren().clear(); + return; + } + + selectedUser = clickedUser; + openPrivateChat(clickedUser); + }); + } + + public void setClient(chatClient client) { + + this.client = client; + + if (client.getServerListener() != null) { + client.getServerListener().setOnMessageReceived(this::handleIncomingMessage); + } + + try { + client.requestUserList(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void handleIncomingMessage(Object obj) { + + if (obj instanceof ChatMessage msg) { + + switch (msg.getType()) { + + case USER_LIST -> { + String content = msg.getContent(); + if (content != null) { + + List users = Arrays.stream(content.split(",")) + .map(String::trim) + .filter(u -> !u.isEmpty()) + .toList(); + + updateUsersList(users); + } + } + + case PUBLIC_MESSAGE -> Platform.runLater(() -> + publicChatArea.appendText(msg.getSender() + ": " + msg.getContent() + "\n")); + + case PRIVATE_MESSAGE -> Platform.runLater(() -> + appendPrivateMessage(msg.getSender(), + msg.getSender() + ": " + msg.getContent())); + + + } + } + else if (obj instanceof FileMessage msg) { + + Platform.runLater(() -> { + + FileChooser chooser = new FileChooser(); + chooser.setInitialFileName(msg.getFilename()); + + File file = chooser.showSaveDialog(usersList.getScene().getWindow()); + + if(file != null){ + try { + Files.write(file.toPath(), msg.getData()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + }); + } + } + + public void updateUsersList(List users) { + Platform.runLater(() -> + usersList.getItems().setAll(users)); + } + + @FXML + public void sendPublicMessage() { + + String text = messageField.getText(); + + if (text == null || text.trim().isEmpty()) return; + + text = text.trim(); + + try { + + client.sendPublicMessage(text); + publicChatArea.appendText("Me: " + text + "\n"); + messageField.clear(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + + private void sendPrivateMessage(String receiver) { + + TextField inputField = privateInputFields.get(receiver); + TextArea chatArea = privateTextAreas.get(receiver); + + if (inputField == null || chatArea == null) return; + + String text = inputField.getText(); + + if (text == null || text.trim().isEmpty()) return; + + text = text.trim(); + + try { + + client.sendPrivateMessage(receiver, text); + chatArea.appendText("Me: " + text + "\n"); + inputField.clear(); + + } catch (IOException e) { + e.printStackTrace(); + } + } + + @FXML + private void sendFile() { + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select file to send"); + + Stage stage = (Stage) usersList.getScene().getWindow(); + File file = fileChooser.showOpenDialog(stage); + + if (file == null) return; + + try { + + updateFileTransfer(file.length()); + client.sendFile(selectedUser,file); + publicChatArea.appendText("Me sent file: " + file.getName() + "\n"); + } + catch (Exception e){ + e.printStackTrace(); + } + } + + + private void openPrivateChat(String user) { + + BorderPane chatView = privateChatViews.get(user); + + if (chatView == null) { + chatView = createPrivateChatView(user); + privateChatViews.put(user, chatView); + } + + chatAnchor.getChildren().setAll(chatView); + + AnchorPane.setTopAnchor(chatView, 0.0); + AnchorPane.setBottomAnchor(chatView, 0.0); + AnchorPane.setLeftAnchor(chatView, 0.0); + AnchorPane.setRightAnchor(chatView, 0.0); + } + + private BorderPane createPrivateChatView(String user) { + + TextArea chatArea = new TextArea(); + chatArea.setEditable(false); + chatArea.setWrapText(true); + + TextField inputField = new TextField(); + inputField.setPromptText("Message to " + user); + + Button sendButton = new Button("Send"); + + sendButton.setOnAction(e -> sendPrivateMessage(user)); + inputField.setOnAction(e -> sendPrivateMessage(user)); + + HBox bottom = new HBox(8, inputField, sendButton); + bottom.setPadding(new Insets(8)); + inputField.setPrefWidth(300); + + BorderPane root = new BorderPane(); + root.setCenter(chatArea); + root.setBottom(bottom); + + privateTextAreas.put(user, chatArea); + privateInputFields.put(user, inputField); + + return root; + } + + + private void appendPrivateMessage(String user, String line) { + + BorderPane view = privateChatViews.get(user); + + if (view == null) { + view = createPrivateChatView(user); + privateChatViews.put(user, view); + } + + TextArea area = privateTextAreas.get(user); + + if (area != null) { + area.appendText(line + "\n"); + } + } + + + public void showFileTransfer(String fileName) { + Platform.runLater(() -> { + fileTransferBox.setVisible(true); + fileTransferBox.setManaged(true); + fileTransferProgress.setProgress(0); + fileTransferLabel.setText("Sending " + fileName + "..."); + }); + } + + public void updateFileTransfer(long length) { + Platform.runLater(() -> { + + fileTransferProgress.setProgress(length); + fileTransferLabel.setText(String.format( + "Sending... %d", + (int) (length * 100) + )); + }); + } + + public void finishFileTransfer(String fileName) { + Platform.runLater(() -> { + fileTransferProgress.setProgress(1.0); + fileTransferLabel.setText(fileName + " sent successfully"); + }); + } + + + @FXML + private void closeApp() { + System.exit(0); + } + + @FXML + private void minimize() { + Stage stage = (Stage) usersList.getScene().getWindow(); + stage.setIconified(true); + } + + @FXML + private void maximize() { + Stage stage = (Stage) usersList.getScene().getWindow(); + stage.setMaximized(!stage.isMaximized()); + } +} diff --git a/src/main/java/com/university/chat/Client/UI/Launcher.java b/src/main/java/com/university/chat/Client/UI/Launcher.java new file mode 100644 index 0000000..44f582e --- /dev/null +++ b/src/main/java/com/university/chat/Client/UI/Launcher.java @@ -0,0 +1,9 @@ +package com.university.chat.Client.UI; +import javafx.application.Application; + +public class Launcher{ + public static void main(String[] args) { + + Application.launch(ChatApp.class); + } +} diff --git a/src/main/java/com/university/chat/Client/UI/LoginController.java b/src/main/java/com/university/chat/Client/UI/LoginController.java new file mode 100644 index 0000000..ca27b11 --- /dev/null +++ b/src/main/java/com/university/chat/Client/UI/LoginController.java @@ -0,0 +1,180 @@ +package com.university.chat.Client.UI; + +import com.university.chat.Client.Net.chatClient; +import com.university.chat.Common.ChatMessage; +import com.university.chat.Common.MessageType; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; +import javafx.stage.Stage; + +public class LoginController { + + @FXML + public HBox titleBar; + + private double xOffset = 0; + private double yOffset = 0; + + @FXML + private TextField usernameField; + + @FXML + private Button nameButton; + + private final chatClient client = new chatClient(); + + private ChatController chatController; + + @FXML + public void initialize() { + titleBar.setOnMousePressed(event -> { + xOffset = event.getSceneX(); + yOffset = event.getSceneY(); + }); + + titleBar.setOnMouseDragged(event -> { + Stage stage = (Stage) titleBar.getScene().getWindow(); + stage.setX(event.getScreenX() - xOffset); + stage.setY(event.getScreenY() - yOffset); + }); + + } + + @FXML + private void onConnectClicked() { + String username = usernameField.getText().trim(); + + if (username.isEmpty()) { + showError("Username cannot be empty."); + return; + } + + nameButton.setDisable(true); + + new Thread(() -> { + try { + + client.connect("localhost", 9000); + + client.startListening( + this::handleServerMessage, + this::handleConnectionError + ); + + client.login(username); + + } catch (Exception e) { + Platform.runLater(() -> { + showError("Failed to connect to server.\nMake sure the server is running.\n\n" + e.getMessage()); + nameButton.setDisable(false); + }); + } + }).start(); + } + + + private void handleServerMessage(Object obj) { + if (obj instanceof ChatMessage msg) { + + if (msg.getType() == MessageType.LOGIN_SUCCESS) { + Platform.runLater(() -> { + showInfo("Login successful!"); + openChatWindow(); + }); + } + else if (msg.getType() == MessageType.LOGIN_FAILED) { + Platform.runLater(() -> { + showError("Login failed: username taken"); + nameButton.setDisable(false); + }); + } + else if (msg.getType() == MessageType.USER_LIST) { + Platform.runLater(() -> { + if (chatController != null) { + String content = msg.getContent(); + java.util.List users = java.util.Arrays.asList(content.split(",")); + chatController.updateUsersList(users); + } + }); + } + else { + Platform.runLater(() -> { + if (chatController != null) { + chatController.sendPublicMessage(); + } + System.out.println("Server: " + msg.getContent()); + }); + } + } + } + + private void handleConnectionError(Exception e) { + Platform.runLater(() -> showError("Connection lost: " + e.getMessage())); + } + + + private void showError(String text) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Error"); + alert.setHeaderText(null); + alert.setContentText(text); + alert.showAndWait(); + } + + private void showInfo(String text) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Info"); + alert.setHeaderText(null); + alert.setContentText(text); + alert.showAndWait(); + } + + private void openChatWindow() { + try { + + FXMLLoader loader = new FXMLLoader( + getClass().getResource("chat.fxml") + ); + + Parent root = loader.load(); + + this.chatController = loader.getController(); + this.chatController.setClient(client); + + Stage stage = (Stage) usernameField.getScene().getWindow(); + + stage.setScene(new Scene(root, 900, 600)); + + stage.getScene().getStylesheets().add( + getClass().getResource("/chat_style.css").toExternalForm() + ); + + } catch (Exception e) { + e.printStackTrace(); + } + } + + @FXML + private void closeApp() { + System.exit(0); + } + + @FXML + private void minimize() { + Stage stage = (Stage) usernameField.getScene().getWindow(); + stage.setIconified(true); + } + + @FXML + private void maximize() { + Stage stage = (Stage) usernameField.getScene().getWindow(); + stage.setMaximized(!stage.isMaximized()); + } +} diff --git a/src/main/java/com/university/chat/Client/chatClient.java b/src/main/java/com/university/chat/Client/chatClient.java deleted file mode 100644 index dae0bad..0000000 --- a/src/main/java/com/university/chat/Client/chatClient.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.university.chat.Client; - -public class chatClient { - public static void main() { - // TODO: Connecting to the server - // 1. Create a socket and connect to the server - // 2. Create an ObjectOutputStream (out) and ObjectInputStream (in) - // from the socket's streams — output FIRST, then input. - // 2. Get the username, and send a LOGIN ChatMessage with that username - // 3. Start a new Thread running a ServerListener(in) so incoming - // messages are handled concurrently. - - while (true){ - try { - // TODO: Program loop — read a line from the console and act on it: - // - "/msg " -> build & send a PRIVATE_MESSAGE - // - "/users" -> build & send a USER_LIST request - // - "/sendfile " -> read the file into a byte[] - // (you can use TransferProgress - // to show progress) - // and send it as a FileMessage - // - anything else -> send a PUBLIC_MESSAGE - // Remember to flush() the output stream after writeObject(). - } catch (Exception e){ - System.out.println("command failed: " + e.getMessage()); - } - } - } -} diff --git a/src/main/java/com/university/chat/Server/ChatServer.java b/src/main/java/com/university/chat/Server/ChatServer.java index b2a35ae..b49361d 100644 --- a/src/main/java/com/university/chat/Server/ChatServer.java +++ b/src/main/java/com/university/chat/Server/ChatServer.java @@ -1,15 +1,37 @@ package com.university.chat.Server; +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + public class ChatServer { - // TODO: declare a single shared UserManager instance (static final) - // This MUST be shared by all ClientSession threads so that - // broadcasting and private messaging work correctly. + + private static final UserManager userManager = new UserManager(); + private static ServerSocket serverSocket; public static void main(String[] args) { - // TODO: Create a ServerSocket - // TODO: In an infinite loop: - // accept an incoming client connection - // make a new thread running ClientSession for each user. + try { + serverSocket = new ServerSocket(9000); + System.out.printf("server started. port: %s\n" ,9000); + } catch (IOException e) { + throw new RuntimeException(e); + } + + try { + while (true) { + + Socket s = serverSocket.accept(); + System.out.println("new client accepted."); + ClientSession clientSession = new ClientSession(s, userManager); + Thread thread = new Thread(clientSession); + thread.start(); + + } + } + catch (IOException e) { + + System.err.println("Error running thread."); + } } } diff --git a/src/main/java/com/university/chat/Server/ClientSession.java b/src/main/java/com/university/chat/Server/ClientSession.java index f09d64a..227bba4 100644 --- a/src/main/java/com/university/chat/Server/ClientSession.java +++ b/src/main/java/com/university/chat/Server/ClientSession.java @@ -2,62 +2,145 @@ package com.university.chat.Server; import com.university.chat.Common.ChatMessage; import com.university.chat.Common.FileMessage; +import com.university.chat.Common.MessageType; import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.net.Socket; import java.nio.file.Files; public class ClientSession implements Runnable { private String username; + private Socket socket; + private final UserManager userManager; + private ObjectOutputStream out; + private ObjectInputStream in; public ClientSession(Socket socket, UserManager userManager) { - // TODO : Create an ObjectOutputStream from socket.getOutputStream() - // and an ObjectInputStream from socket.getInputStream(). + + this.userManager = userManager; + this.socket = socket; + + try { + this.out = new ObjectOutputStream(socket.getOutputStream()); + out.flush(); + this.in = new ObjectInputStream(socket.getInputStream()); + } + catch (IOException e) { + System.out.println("Error initializing stream: " + e.getMessage()); + } } @Override public void run() { try { + System.out.println("welcome"); - // TODO: Welcome the user (login step) - // 1. Read the first object sent by the client. - // 2. Check it's a ChatMessage with type LOGIN. - // 3. Extract the username. - // 4. Try to register the user via userManager.addUser(...). - // 5. If the username is taken, send back LOGIN_FAILED and close the socket. - // 6. Otherwise, create the user's folders with FileManager.createUserFolders(...) - // and send back LOGIN_SUCCESS. + Object object = in.readObject(); - // TODO: Main message loop - // In a loop, call in.readObject(), you can separate messages by their type: - // - if it's a ChatMessage -> call handleChatMessage(msg) - // - if it's a FileMessage -> call handleFileMessage(fileMsg) - // Keep looping until the connection is closed (an exception will be thrown). + if (object instanceof ChatMessage login && login.getType() == MessageType.LOGIN) { - } catch (Exception e) { + username = login.getSender(); + + if (userManager.addUser(username, this)) { + System.out.printf("Successfully registered: %s\n", username); + + FileManager.createUserFolders(username); + + out.writeObject( + new ChatMessage(MessageType.LOGIN_SUCCESS, + "[server]", + username, + null + )); + + out.flush(); + broadcastUserList(); + } + else { + out.writeObject( + new ChatMessage(MessageType.LOGIN_FAILED, + "[server]", + username, + null + )); + out.flush(); + + socket.close(); + return; + } + + } else { + socket.close(); + return; + } + + while (true) { + + Object msg = in.readObject(); + + if (msg instanceof ChatMessage chatMsg) { + handleChatMessage(chatMsg); + } + else if (msg instanceof FileMessage fileMsg) { + handleFileMessage(fileMsg); + } + } + + } catch (IOException | ClassNotFoundException e) { System.out.println("Disconnected: " + username); - } finally { - // TODO: Remove the user from UserManager so they no longer - // receive broadcasts or appear in users list + } + finally { + if (username != null) { + userManager.removeUser(username); + broadcastUserList(); + } } } - private void handleChatMessage(ChatMessage msg) throws IOException { switch (msg.getType()) { case PUBLIC_MESSAGE -> { - // TODO: Broadcast this message to every connected client. + Iterable users = userManager.getAllSessions(); + for (ClientSession user : users) { + if (user != this) { + user.sendObject(msg); + } + } } case PRIVATE_MESSAGE -> { - // TODO: Forward this message to the receiver user. + + ClientSession receiver = userManager.getUser(msg.getReceiver()); + if (receiver != null) { + receiver.sendObject(msg); + } + else { + sendObject(new ChatMessage( + MessageType.PRIVATE_MESSAGE, + "[server]", + username, + "User not found: " + msg.getReceiver() + )); + } } case USER_LIST -> { - // TODO: Reply to the requester with the list of online users. + + sendObject(new ChatMessage(MessageType.USER_LIST, + "[server]", + username, + userManager.listUsers()) + ); } } } + private synchronized void sendObject(Object obj) throws IOException { + out.writeObject(obj); + out.flush(); + } + private void handleFileMessage(FileMessage fileMsg) throws IOException { // Storing the file var sentPath = FileManager.getSentPath(fileMsg.getSender(), fileMsg.getFilename()); @@ -66,6 +149,26 @@ public class ClientSession implements Runnable { Files.write(sentPath, fileMsg.getData()); Files.write(recvPath, fileMsg.getData()); - // TODO: Forward the received file-message to the destination user. + ClientSession receiver = userManager.getUser(fileMsg.getReceiver()); + receiver.sendObject(fileMsg); } + + private void broadcastUserList() { + + try { + String activeUsers = userManager.listUsers(); + ChatMessage updateMsg = new ChatMessage( + MessageType.USER_LIST, + "[server]", + null, + activeUsers + ); + for (ClientSession session : userManager.getAllSessions()) { + session.sendObject(updateMsg); + } + } catch (IOException e) { + System.out.println("Error broadcasting user list: " + e.getMessage()); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/university/chat/Server/UserManager.java b/src/main/java/com/university/chat/Server/UserManager.java index 07cbc2d..5ab7ec7 100644 --- a/src/main/java/com/university/chat/Server/UserManager.java +++ b/src/main/java/com/university/chat/Server/UserManager.java @@ -19,7 +19,7 @@ public class UserManager { } public String listUsers() { - return users.keySet().toString(); + return String.join(",", users.keySet()); } public Iterable getAllSessions() { diff --git a/src/main/resources/chat_style.css b/src/main/resources/chat_style.css new file mode 100644 index 0000000..0d1b889 --- /dev/null +++ b/src/main/resources/chat_style.css @@ -0,0 +1,77 @@ +.root { + -fx-font-family: "Segoe UI", "Helvetica", sans-serif; + -fx-background-color: #1e1e2f; +} + +.title-bar { + -fx-background-color: #262638; + -fx-padding: 5 10 5 10; + -fx-alignment: CENTER_LEFT; +} + +.title-text { + -fx-text-fill: #a0a0c0; + -fx-font-size: 13px; + -fx-font-weight: bold; +} + +.title-button { + -fx-background-color: transparent; + -fx-text-fill: white; + -fx-font-size: 14px; + -fx-cursor: hand; +} + +.title-button:hover { + -fx-background-color: #3b3b55; +} + +.title-button-close:hover { + -fx-background-color: #e81123; +} + + +.label { + -fx-text-fill: #d1d1e0; + } + +.text-field { + -fx-background-color: #2a2a3d; + -fx-text-fill: white; + -fx-background-radius: 6; + -fx-border-color: #3c3c55; + -fx-padding: 8; +} + +.button { + -fx-background-color: #4da3ff; + -fx-text-fill: white; + -fx-background-radius: 6; + -fx-cursor: hand; + -fx-padding: 6 15 6 15; +} + +.button:hover { + -fx-background-color: #61afff; +} + +.list-view, .text-area { + -fx-background-color: #262638; + -fx-control-inner-background: #262638; + -fx-text-fill: white; +} + + +.attach-button{ + -fx-font-size:16px; + -fx-background-color: transparent; +} + +.attach-button:hover{ + -fx-background-color:#3b3b55; +} + + +.content-wrapper { + -fx-padding: 20; +} diff --git a/src/main/resources/com/university/chat/Client/UI/chat.fxml b/src/main/resources/com/university/chat/Client/UI/chat.fxml new file mode 100644 index 0000000..853d706 --- /dev/null +++ b/src/main/resources/com/university/chat/Client/UI/chat.fxml @@ -0,0 +1,52 @@ + + + + + + + + +
+ + +