From 78025bb4d9d101db401a09ac46610699bb10f71f Mon Sep 17 00:00:00 2001 From: echatzo Date: Mon, 13 Oct 2025 15:22:34 +0200 Subject: [PATCH 1/6] New exercice oop - observer pattern : chatapp --- src/main/java/oop/ChatApp.java | 256 +++++++++++++++++++++++++++++ src/test/java/oop/ChatAppTest.java | 116 +++++++++++++ 2 files changed, 372 insertions(+) create mode 100644 src/main/java/oop/ChatApp.java create mode 100644 src/test/java/oop/ChatAppTest.java diff --git a/src/main/java/oop/ChatApp.java b/src/main/java/oop/ChatApp.java new file mode 100644 index 0000000..1fe2567 --- /dev/null +++ b/src/main/java/oop/ChatApp.java @@ -0,0 +1,256 @@ +package oop; +import java.util.*; + +/** + * Pattern Observer appliqué à une Chat Room + * --------------------------------------------------------- + * Classes incluses : + * - interface User (Observer) + * - classe ChatUser (implémentation d’un utilisateur) + * - classe ChatRoom (Subject) + * - classe ChatApp (point d’entrée) + */ +public class ChatApp { + + /* ------------------ OBSERVER ------------------ */ + public interface User { + void update(String channel, String message); + String getName(); + } + + /* ------------------ CONCRETE OBSERVER ------------------ */ + public static class ChatUser implements User { + private String name; + private ArrayList mutedUsers = new ArrayList<>(); + + public ChatUser(String name) { + // TODO + // BEGIN STRIP + this.name = name; + // END STRIP + } + + /** + * Notifie l’utilisateur qu’un message a été reçu dans un canal. + * Si l’expéditeur est dans la liste des utilisateurs "mutés", + * le message est ignoré. + * + * Le message est affiché sur la console au format suivant: + * "[] reçoit sur # > : message + * @param channel nom du canal où le message est posté + * @param message message complet sous la forme "Expéditeur: contenu" + */ + @Override + public void update(String channel, String message) { + // TODO + // BEGIN STRIP + String senderName = message.split(":")[0]; + if (!mutedUsers.contains(senderName)) { + System.out.println("[" + name + "] reçoit sur #" + channel + " > " + message); + } + // END STRIP + } + + /** + * @return le nom de cet utilisateur + */ + @Override + public String getName() { + // TODO + // BEGIN STRIP + return name; + // END STRIP + } + + /** + * Envoie un message à la salle spécifiée. + * Peut être un message normal ou une commande spéciale (ex: /mute). + * + * @param room la salle de discussion où envoyer le message + * @param message le message à envoyer + */ + public void sendMessage(ChatRoom room, String message) { + // TODO + // BEGIN STRIP + room.sendMessage(this, message); + // END STRIP + } + + /** + * Active ou désactive le mute d’un utilisateur. + * Si l’utilisateur est déjà muté => on le "démute" + * Sinon => on le mute. + * @param userName nom de l’utilisateur à (dé)muter + */ + public void toggleMuteUser(String userName) { + // TODO + // BEGIN STRIP + if (isMuted(userName)) { + // Unmute + for (int i = 0; i < mutedUsers.size(); i++) { + if (mutedUsers.get(i).equals(userName)) { + mutedUsers.remove(i); + System.out.println(name + " a réactivé " + userName); + return; + } + } + } else { + // Mute + mutedUsers.add(userName); + System.out.println(name + " a muté " + userName); + } + // END STRIP + } + + /** + * Vérifie si un utilisateur donné est actuellement muté. + * + * @param userName nom de l’utilisateur à vérifier + * @return true si l’utilisateur est muté, false sinon + */ + public boolean isMuted(String userName) { + // TODO + // BEGIN STRIP + for (String muted : mutedUsers) { + if (muted.equals(userName)) { + return true; + } + } + // END STRIP + return false; + + } + } + + /* ------------------ SUBJECT ------------------ */ + /** + * Représente une salle de discussion (canal). + * C’est le "Subject" du pattern Observer : + * - Elle garde une liste d’utilisateurs abonnés. + * - Lorsqu’un message est envoyé, elle notifie tous les abonnés. + */ + public static class ChatRoom { + private String channelName; + private List users; + + /** + * Constructeur pour une nouvelle salle de discussion. + * + * @param channelName le nom du canal (ex: "general", "loisirs", "jeux vidéos", ...) + */ + public ChatRoom(String channelName) { + // TODO + // BEGIN STRIP + this.channelName = channelName; + this.users = new ArrayList<>(); + // END STRIP + } + + /** + * @return le nom de la salle + */ + public String getChannelName() { + return channelName; + } + + /** + * Ajoute un utilisateur à la liste des abonnés à la salle. + * Un message de bienvenue est affiché dans la console + * au format suivant " a rejoint le channel # + * + * Si l'utilisateur est déjà abonné au canal, le message suivant est affiché: + * " est déjà abonné au channel # + * @param user l’utilisateur qui rejoint la salle + */ + public void subscribe(User user) { + // TODO + // BEGIN STRIP + for (User channel_user : users) { + if (channel_user.equals(user)) { + System.out.println(user.getName() + " est déjà abonné au channel #" + channelName); + return; + } + } + users.add(user); + System.out.println(user.getName() + " a rejoint le channel #" + channelName); + // END STRIP + } + + /** + * Retire un utilisateur de la salle. + * Un message de départ est affiché dans la console. + * au format suivant " a quitté le channel # + * + * @param user l’utilisateur à retirer + */ + public void unsubscribe(User user) { + // TODO + // BEGIN STRIP + users.remove(user); + System.out.println(user.getName() + " a quitté le channel #" + channelName); + // END STRIP + } + + /** + * Traite un message envoyé dans la salle. + *

+ * - Si le message commence par "/mute", il est traité comme une commande + * pour activer ou désactiver le mute d’un autre utilisateur + * au format suivant : "/mute " + * + * - Sinon, le message est envoyé à tous les abonnés. + * + * @param sender l’utilisateur qui envoie le message + * @param message contenu du message ou commande + */ + public void sendMessage(User sender, String message) { + // TODO + // BEGIN STRIP + if (message.startsWith("/mute")) { + String[] parts = message.split(" "); + if (parts.length == 2 && sender instanceof ChatUser) { + ((ChatUser) sender).toggleMuteUser(parts[1]); + } else { + System.out.println("Usage: /mute "); + } + return; + } + + System.out.println(sender.getName() + " (" + channelName + "): " + message); + notifyUsers(sender, message); + // END STRIP + } + + /** + * Notifie tous les utilisateurs du canal d’un nouveau message, + * sauf l’expéditeur lui-même. + * + * @param sender l’expéditeur du message + * @param message contenu du message + */ + private void notifyUsers(User sender, String message) { + // TODO + // BEGIN STRIP + for (User user : users) { + if (!user.equals(sender)) { + user.update(channelName, sender.getName() + ": " + message); + } + } + // END STRIP + } + + /** + * @return le nombre d’utilisateurs actuellement connectés à la salle + */ + public int getUserCount() { + // TODO + // BEGIN STRIP + return users.size(); + // END STRIP + // STUDENT return false; + } + } + + /* ------------------ MAIN ------------------ */ + public static void main(String[] args) {} +} diff --git a/src/test/java/oop/ChatAppTest.java b/src/test/java/oop/ChatAppTest.java new file mode 100644 index 0000000..a79b2c0 --- /dev/null +++ b/src/test/java/oop/ChatAppTest.java @@ -0,0 +1,116 @@ +package oop; +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; +import java.io.*; + +class ChatAppTest { + + private final ByteArrayOutputStream output = new ByteArrayOutputStream(); + private PrintStream originalOut; + + @BeforeEach + void setUp() { + originalOut = System.out; + System.setOut(new PrintStream(output)); + } + + @AfterEach + void tearDown() { + System.setOut(originalOut); + } + + @Test + void testSubscriptionAndMessageNotification() { + ChatApp.ChatRoom room = new ChatApp.ChatRoom("general"); + ChatApp.ChatRoom games = new ChatApp.ChatRoom("games"); + ChatApp.ChatUser alice = new ChatApp.ChatUser("Alice"); + ChatApp.ChatUser bob = new ChatApp.ChatUser("Bob"); + ChatApp.ChatUser john = new ChatApp.ChatUser("John"); + + room.subscribe(alice); + room.subscribe(bob); + alice.sendMessage(room, "Salut tout le monde !"); + + String console = output.toString(); + + assertTrue(console.contains("Alice a rejoint le channel #general")); + assertTrue(console.contains("Bob a rejoint le channel #general")); + assertFalse(console.contains("John a rejoint le channel #general")); + + assertTrue(console.contains("Alice (general): Salut tout le monde !")); + assertTrue(console.contains("[Bob] reçoit sur #general > Alice: Salut tout le monde !")); + assertFalse(console.contains("[Bob] reçoit sur #games > Alice: Salut tout le monde !")); + + // BEGIN STRIP + assertFalse(console.contains("[John] reçoit sur #general > Alice: Salut tout le monde !")); + assertFalse(console.contains("[Alice] reçoit sur #general > Alice: Salut tout le monde !")); + // END STRIP + + output.reset(); + + room.subscribe(bob); + + console = output.toString(); + assertFalse(console.contains("Bob a rejoint le channel #general")); + assertTrue(console.contains("Bob est déjà abonné au channel #general")); + } + + @Test + void testMuteCommand() { + ChatApp.ChatRoom room = new ChatApp.ChatRoom("sport"); + ChatApp.ChatUser bob = new ChatApp.ChatUser("Bob"); + ChatApp.ChatUser charlie = new ChatApp.ChatUser("Charlie"); + + room.subscribe(bob); + room.subscribe(charlie); + + bob.sendMessage(room, "/mute Charlie"); + assertTrue(bob.isMuted("Charlie")); + + output.reset(); + charlie.sendMessage(room, "Salut !"); + String console = output.toString(); + assertFalse(console.contains("[Bob] reçoit")); + assertTrue(console.contains("Charlie (sport): Salut !")); + + // BEGIN STRIP + output.reset(); + + bob.sendMessage(room, "/mute Charlie"); + charlie.sendMessage(room, "Ca va?"); + console = output.toString(); + assertTrue(console.contains("Charlie (sport): Ca va?")); + assertTrue(console.contains("[Bob] reçoit sur #sport > Charlie: Ca va?")); + // END STRIP + + } + + @Test + void testUnsubscribe() { + ChatApp.ChatRoom room = new ChatApp.ChatRoom("TPs"); + ChatApp.ChatUser alice = new ChatApp.ChatUser("Alice"); + ChatApp.ChatUser bob = new ChatApp.ChatUser("Bob"); + + assertEquals(0, room.getUserCount()); + room.subscribe(alice); + assertEquals(1, room.getUserCount()); + room.subscribe(bob); + assertEquals(2, room.getUserCount()); + + // BEGIN STRIP + alice.sendMessage(room, "Hello"); + String console = output.toString(); + + assertTrue(console.contains("[Bob] reçoit sur #TPs > Alice: Hello")); + output.reset(); + room.unsubscribe(bob); + + + alice.sendMessage(room, "Vous avez fait l'exercice 4?"); + console = output.toString(); + assertFalse(console.contains("[Bob] reçoit")); + // END STRIP + + assertEquals(1, room.getUserCount()); + } +} \ No newline at end of file From fd5617f724a2d06066c0409ca001e5963e35d40e Mon Sep 17 00:00:00 2001 From: echatzo Date: Mon, 13 Oct 2025 15:38:27 +0200 Subject: [PATCH 2/6] oop - observer pattern : modification to unsubrscribe --- src/main/java/oop/ChatApp.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/oop/ChatApp.java b/src/main/java/oop/ChatApp.java index 1fe2567..391b0b4 100644 --- a/src/main/java/oop/ChatApp.java +++ b/src/main/java/oop/ChatApp.java @@ -181,13 +181,20 @@ public void subscribe(User user) { * Un message de départ est affiché dans la console. * au format suivant " a quitté le channel # * + * Si l'utilisateur n'est pas dans le canal, affiche le message suivant + * " n'est pas dans le channel # + * * @param user l’utilisateur à retirer */ public void unsubscribe(User user) { // TODO // BEGIN STRIP - users.remove(user); - System.out.println(user.getName() + " a quitté le channel #" + channelName); + if (users.contains(user)) { + users.remove(user); + System.out.println(user.getName() + " a quitté le channel #" + channelName); + } else { + System.out.println(user.getName() + " n'est pas dans le channel #" + channelName); + } // END STRIP } From 622ab4cad8a34bfbe7d4d161965a456e6c58ed7c Mon Sep 17 00:00:00 2001 From: echatzo Date: Mon, 13 Oct 2025 16:15:25 +0200 Subject: [PATCH 3/6] oop:chatapp : correct description for send message --- src/main/java/oop/ChatApp.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/oop/ChatApp.java b/src/main/java/oop/ChatApp.java index 391b0b4..984cc71 100644 --- a/src/main/java/oop/ChatApp.java +++ b/src/main/java/oop/ChatApp.java @@ -205,7 +205,8 @@ public void unsubscribe(User user) { * pour activer ou désactiver le mute d’un autre utilisateur * au format suivant : "/mute " * - * - Sinon, le message est envoyé à tous les abonnés. + * - Sinon, le message est envoyé à tous les abonnés et affiché sur la console sous la forme: + * " (): " * * @param sender l’utilisateur qui envoie le message * @param message contenu du message ou commande From 21d46c737d880e1eb71417035ffebff6f47cc6b4 Mon Sep 17 00:00:00 2001 From: echatzo Date: Mon, 13 Oct 2025 16:26:30 +0200 Subject: [PATCH 4/6] oop:chatapp : Add project description --- src/main/java/oop/ChatApp.java | 46 +++++++++++++++++++++++++++++- src/test/java/oop/ChatAppTest.java | 3 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/main/java/oop/ChatApp.java b/src/main/java/oop/ChatApp.java index 984cc71..6dc197e 100644 --- a/src/main/java/oop/ChatApp.java +++ b/src/main/java/oop/ChatApp.java @@ -2,7 +2,14 @@ import java.util.*; /** - * Pattern Observer appliqué à une Chat Room + * Dans ce TP, vous allez implémenter une salle de discussion en utilisant le pattern Observer. + * Lorsqu’un utilisateur envoie un message, tous les autres abonnés de la salle doivent être notifiés. + * + * Un utilisateur peut mettre en silencieux (mute) d'autres utilisateurs, dans ce cas il ne recevra pas les messages + * des utilisateurs "muted", peu importe la salle de discussion (channel) + * + * Un utilisateur peut s'abonner ou se désabonner aux différentes salles, pour recevoir les messages envoyés dessus. + * * --------------------------------------------------------- * Classes incluses : * - interface User (Observer) @@ -10,6 +17,43 @@ * - classe ChatRoom (Subject) * - classe ChatApp (point d’entrée) */ + +/** +Exemple de fonctionnement attendu avec affichage console : + +ChatRoom general = new ChatRoom("general"); +ChatUser alice = new ChatUser("Alice"); +ChatUser bob = new ChatUser("Bob"); + +general.subscribe(alice); +// Console : Alice a rejoint le channel #general +general.subscribe(bob); +// Console : Bob a rejoint le channel #general + +bob.sendMessage(general, "Salut !"); +// Console : Bob (general): Salut ! +// Console : [Alice] reçoit sur #general → Bob: Salut ! + +alice.sendMessage(general, "/mute Bob"); +// Console : Alice a muté Bob + +bob.sendMessage(general, "Encore un message"); +// Console : Bob (general): Encore un message +// Alice ne reçoit rien car Bob est muté + +alice.sendMessage(general, "/mute Bob"); +// Console : Alice a réactivé Bob + +bob.sendMessage(general, "Ça remarche ?"); +// Console : Bob (general): Ça remarche ? +// Console : [Alice] reçoit sur #general → Bob: Ça remarche ? + +general.unsubscribe(alice); +// Console : Alice a quitté le channel #general + +general.unsubscribe(alice); +// Console : Alice n'est pas dans le channel #general +*/ public class ChatApp { /* ------------------ OBSERVER ------------------ */ diff --git a/src/test/java/oop/ChatAppTest.java b/src/test/java/oop/ChatAppTest.java index a79b2c0..ad8360c 100644 --- a/src/test/java/oop/ChatAppTest.java +++ b/src/test/java/oop/ChatAppTest.java @@ -103,9 +103,10 @@ void testUnsubscribe() { assertTrue(console.contains("[Bob] reçoit sur #TPs > Alice: Hello")); output.reset(); + // END STRIP room.unsubscribe(bob); - + // BEGIN STRIP alice.sendMessage(room, "Vous avez fait l'exercice 4?"); console = output.toString(); assertFalse(console.contains("[Bob] reçoit")); From 34fbb36ed57951ce4abc1814a80deeade2e69bd2 Mon Sep 17 00:00:00 2001 From: echatzo Date: Tue, 14 Oct 2025 12:26:07 +0200 Subject: [PATCH 5/6] oop: chatapp - translation + new message object (no prints) + no instance of --- src/main/java/oop/ChatApp.java | 426 ++++++++++++++++++----------- src/test/java/oop/ChatAppTest.java | 150 +++++----- 2 files changed, 346 insertions(+), 230 deletions(-) diff --git a/src/main/java/oop/ChatApp.java b/src/main/java/oop/ChatApp.java index 6dc197e..708f527 100644 --- a/src/main/java/oop/ChatApp.java +++ b/src/main/java/oop/ChatApp.java @@ -2,304 +2,402 @@ import java.util.*; /** - * Dans ce TP, vous allez implémenter une salle de discussion en utilisant le pattern Observer. - * Lorsqu’un utilisateur envoie un message, tous les autres abonnés de la salle doivent être notifiés. + * In this practical exercise, you will implement a chat room system using the Observer pattern. * - * Un utilisateur peut mettre en silencieux (mute) d'autres utilisateurs, dans ce cas il ne recevra pas les messages - * des utilisateurs "muted", peu importe la salle de discussion (channel) + * When a user sends a message, all other subscribers of the chat room must be notified. + * ALl messages sent or received by a user are stored in its log in the form of an arraylist + * of Message objects * - * Un utilisateur peut s'abonner ou se désabonner aux différentes salles, pour recevoir les messages envoyés dessus. + * A user can mute other users. In that case, they will not receive messages from muted users, + * regardless of which chat room (channel) the messages are sent in. + * + * Users can subscribe or unsubscribe from different chat rooms to receive messages sent there. + * + * --------------------------------------------------------- + * Included classes: + * - User interface (Observer) + * - ChatUser class (concrete implementation of a user) + * - ChatRoom class (Subject) + * - ChatApp class (entry point) * * --------------------------------------------------------- - * Classes incluses : - * - interface User (Observer) - * - classe ChatUser (implémentation d’un utilisateur) - * - classe ChatRoom (Subject) - * - classe ChatApp (point d’entrée) + * + * Example of expected behavior with user logs: + * + * ChatRoom general = new ChatRoom("general"); + * ChatUser alice = new ChatUser("Alice"); + * ChatUser bob = new ChatUser("Bob"); + * + * general.subscribe(alice); + * // Alice joins channel #general + * // ChatMessage added to Alice's log: + * // new ChatMessage("join", "general", "Alice", "") + * + * general.subscribe(bob); + * // Bob joins channel #general + * // ChatMessage added to Bob's log: + * // new ChatMessage("join", "general", "Bob", "") + * + * bob.sendMessage(general, new ChatMessage("post", "general", "Bob", "Hi!")); + * // Bob posts a message in #general + * // ChatMessage added to Bob's log: + * // new ChatMessage("post", "general", "Bob", "Hi!") + * // Alice receives the message + * // ChatMessage added to Alice's log: + * // new ChatMessage("receive", "general", "Bob", "Hi!") + * + * alice.sendMessage(general, new ChatMessage("mute", "-", "Bob", "")); + * // Alice mutes Bob + * // ChatMessage added to Alice's log only: + * // new ChatMessage("mute", "-", "Bob", "") + * + * bob.sendMessage(general, new ChatMessage("post", "general", "Bob", "Another message")); + * // Bob posts another message + * // ChatMessage added to Bob's log: + * // new ChatMessage("post", "general", "Bob", "Another message") + * // Alice does NOT receive it (muted) + * + * alice.sendMessage(general, new ChatMessage("mute", "-", "Bob", "")); + * // Alice unmutes Bob + * // ChatMessage added to Alice's log only: + * // new ChatMessage("mute", "-", "Bob", "") + * + * bob.sendMessage(general, new ChatMessage("post", "general", "Bob", "It works again?")); + * // Bob posts a message + * // ChatMessage added to Bob's log: + * // new ChatMessage("post", "general", "Bob", "It works again?") + * // Alice receives it + * // ChatMessage added to Alice's log: + * // new ChatMessage("receive", "general", "Bob", "It works again?") + * + * general.unsubscribe(alice); + * // Alice leaves channel #general + * // ChatMessage added to Alice's log: + * // new ChatMessage("leave", "general", "Alice", "") + * + * general.unsubscribe(alice); + * // Alice was not in the channel + * // ChatMessage added to Alice's log: + * // new ChatMessage("info", "general", "Alice", "Alice is not in channel #general") */ -/** -Exemple de fonctionnement attendu avec affichage console : - -ChatRoom general = new ChatRoom("general"); -ChatUser alice = new ChatUser("Alice"); -ChatUser bob = new ChatUser("Bob"); - -general.subscribe(alice); -// Console : Alice a rejoint le channel #general -general.subscribe(bob); -// Console : Bob a rejoint le channel #general - -bob.sendMessage(general, "Salut !"); -// Console : Bob (general): Salut ! -// Console : [Alice] reçoit sur #general → Bob: Salut ! - -alice.sendMessage(general, "/mute Bob"); -// Console : Alice a muté Bob - -bob.sendMessage(general, "Encore un message"); -// Console : Bob (general): Encore un message -// Alice ne reçoit rien car Bob est muté +public class ChatApp { -alice.sendMessage(general, "/mute Bob"); -// Console : Alice a réactivé Bob + /** + * Represents a single entry in a user's activity log. + * + * Each ChatMessage corresponds to an event or an action in the chat application: + * + * - "join": when a user joins a channel + * - `user` = joining user + * - `channel` = channel joined + * - `content` = optional, can be empty + * + * - "leave": when a user leaves a channel + * - `user` = leaving user + * - `channel` = channel left + * - `content` = optional, can be empty + * + * - "info": informational messages (e.g., already subscribed or invalid command) + * - `user` = user affected by the info + * - `channel` = relevant channel + * - `content` = description of the info (optional, can be empty) + * - Only logged in the sender's log (not broadcast to others) + * + * + * - "post": when a user sends a message to a channel + * - `user` = sender + * - `channel` = channel where message was sent + * - `content` = message text + * + * - "receive": when a user receives a message from another user + * - `user` = sender of the message + * - `channel` = channel where message was sent + * - `content` = message text from sender + * + * - "mute": when a user mutes or unmutes another user + * - `user` = target of the mute/unmute + * - `channel` = "-" (global) + * - `content` = optional, can be empty + * - Only logged in the sender's log (not broadcast to others) + * + * These objects allow keeping a complete history of all user actions + * without relying on console output. + * + * Example: + * new ChatMessage("receive", "general", "Bob", + * "Hi!"); // Alice received a message from Bob in #general + * + * new ChatMessage("mute", "-", "Bob", ""); // Sender muted Bob (only in sender's log) + */ + public static class ChatMessage { + private final String type; + private final String channel; + private final String user; + private final String content; -bob.sendMessage(general, "Ça remarche ?"); -// Console : Bob (general): Ça remarche ? -// Console : [Alice] reçoit sur #general → Bob: Ça remarche ? + public ChatMessage(String type, String channel, String user, String content) { + this.type = type; + this.channel = channel; + this.user = user; + this.content = content; + } -general.unsubscribe(alice); -// Console : Alice a quitté le channel #general + public String getType() { return type; } + public String getChannel() { return channel; } + public String getUser() { return user; } + public String getContent() { return content; } -general.unsubscribe(alice); -// Console : Alice n'est pas dans le channel #general -*/ -public class ChatApp { + @Override + public String toString() { + return "[" + type + "] " + user + "@" + channel + ": " + content; + } + } - /* ------------------ OBSERVER ------------------ */ + /* ------------------ OBSERVER INTERFACE ------------------ */ + /** + * Defines the Observer interface. + * Each user can receive updates from a chat room, retrieve their name, + * and access their message log. + */ public interface User { - void update(String channel, String message); + void update(ChatMessage message); String getName(); + List getLog(); + void toggleMuteUser(String userName); } /* ------------------ CONCRETE OBSERVER ------------------ */ + /** + * Represents a user (Observer) who can join chat rooms, send messages, + * mute/unmute other users, and keep an internal activity log. + */ public static class ChatUser implements User { private String name; - private ArrayList mutedUsers = new ArrayList<>(); + private List mutedUsers = new ArrayList<>(); + private List log = new ArrayList<>(); public ChatUser(String name) { - // TODO // BEGIN STRIP this.name = name; // END STRIP } /** - * Notifie l’utilisateur qu’un message a été reçu dans un canal. - * Si l’expéditeur est dans la liste des utilisateurs "mutés", - * le message est ignoré. + * Called when a message is sent in a channel the user is subscribed to. + * If the sender is muted, the message is ignored. * - * Le message est affiché sur la console au format suivant: - * "[] reçoit sur # > : message - * @param channel nom du canal où le message est posté - * @param message message complet sous la forme "Expéditeur: contenu" + * @param message the complete message in the form "Sender: content" */ @Override - public void update(String channel, String message) { - // TODO + public void update(ChatMessage message) { // BEGIN STRIP - String senderName = message.split(":")[0]; - if (!mutedUsers.contains(senderName)) { - System.out.println("[" + name + "] reçoit sur #" + channel + " > " + message); + // Ignore messages from muted users + if (message.getType().equals("post") && !mutedUsers.contains(message.getUser())) { + log.add(new ChatMessage("receive", message.getChannel(), message.getUser(), message.getContent())); } // END STRIP } - /** - * @return le nom de cet utilisateur - */ + /** @return the name of this user */ @Override public String getName() { - // TODO // BEGIN STRIP return name; // END STRIP } + /** @return the list of all ChatMessages recorded for this user */ + public List getLog() { + // BEGIN STRIP + return log; + // END STRIP + } + /** - * Envoie un message à la salle spécifiée. - * Peut être un message normal ou une commande spéciale (ex: /mute). + * Sends a message to the given chat room. + * The message can be a normal text or a command (e.g. /mute). * - * @param room la salle de discussion où envoyer le message - * @param message le message à envoyer + * @param room the chat room to send the message to + * @param message the message content + * @return a list of ChatMessages generated by this action */ - public void sendMessage(ChatRoom room, String message) { - // TODO + public List sendMessage(ChatRoom room, ChatMessage message) { // BEGIN STRIP - room.sendMessage(this, message); + List messages = room.sendMessage(this, message); + log.addAll(messages); + return messages; // END STRIP } /** - * Active ou désactive le mute d’un utilisateur. - * Si l’utilisateur est déjà muté => on le "démute" - * Sinon => on le mute. - * @param userName nom de l’utilisateur à (dé)muter + * Toggles the mute state of another user. + * + * This action is local to the sender: it only affects the sender’s log. + * The `user` field of the generated ChatMessage corresponds to the user + * being muted or unmuted (the target), while the sender remains implicit. + * + * @param userName the name of the user to mute or unmute */ + @Override public void toggleMuteUser(String userName) { - // TODO // BEGIN STRIP if (isMuted(userName)) { - // Unmute - for (int i = 0; i < mutedUsers.size(); i++) { - if (mutedUsers.get(i).equals(userName)) { - mutedUsers.remove(i); - System.out.println(name + " a réactivé " + userName); - return; - } - } + mutedUsers.remove(userName); } else { - // Mute mutedUsers.add(userName); - System.out.println(name + " a muté " + userName); } + // Only logged in sender's log + log.add(new ChatMessage("mute", "-", userName, "")); // END STRIP } /** - * Vérifie si un utilisateur donné est actuellement muté. + * Checks whether a given user is currently muted. * - * @param userName nom de l’utilisateur à vérifier - * @return true si l’utilisateur est muté, false sinon + * @param userName the user to check + * @return true if muted, false otherwise */ public boolean isMuted(String userName) { - // TODO // BEGIN STRIP - for (String muted : mutedUsers) { - if (muted.equals(userName)) { - return true; - } - } + return mutedUsers.contains(userName); // END STRIP - return false; - } } /* ------------------ SUBJECT ------------------ */ /** - * Représente une salle de discussion (canal). - * C’est le "Subject" du pattern Observer : - * - Elle garde une liste d’utilisateurs abonnés. - * - Lorsqu’un message est envoyé, elle notifie tous les abonnés. + * Represents a chat room (channel). + * This class is the "Subject" in the Observer pattern: + * - It keeps a list of subscribed users. + * - When a message is sent, it notifies all subscribers except the sender. */ public static class ChatRoom { private String channelName; private List users; /** - * Constructeur pour une nouvelle salle de discussion. + * Creates a new chat room with the given name. * - * @param channelName le nom du canal (ex: "general", "loisirs", "jeux vidéos", ...) + * @param channelName the channel name (e.g. "general", "sports", "games") */ public ChatRoom(String channelName) { - // TODO // BEGIN STRIP this.channelName = channelName; this.users = new ArrayList<>(); // END STRIP } - /** - * @return le nom de la salle - */ + /** @return the name of the chat room */ public String getChannelName() { return channelName; } /** - * Ajoute un utilisateur à la liste des abonnés à la salle. - * Un message de bienvenue est affiché dans la console - * au format suivant " a rejoint le channel # + * Subscribes a user to this chat room. + * If the user is already subscribed, an informational message is added to their log. * - * Si l'utilisateur est déjà abonné au canal, le message suivant est affiché: - * " est déjà abonné au channel # - * @param user l’utilisateur qui rejoint la salle + * @param user the user joining the room + * @return the ChatMessage created for this event */ - public void subscribe(User user) { - // TODO + public ChatMessage subscribe(User user) { // BEGIN STRIP - for (User channel_user : users) { - if (channel_user.equals(user)) { - System.out.println(user.getName() + " est déjà abonné au channel #" + channelName); - return; + for (User u : users) { + if (u.equals(user)) { + ChatMessage msg = new ChatMessage("info", channelName, user.getName(), + user.getName() + " is already subscribed to channel #" + channelName); + user.getLog().add(msg); + return msg; } } users.add(user); - System.out.println(user.getName() + " a rejoint le channel #" + channelName); + ChatMessage msg = new ChatMessage("join", channelName, user.getName(), ""); + user.getLog().add(msg); + return msg; // END STRIP } /** - * Retire un utilisateur de la salle. - * Un message de départ est affiché dans la console. - * au format suivant " a quitté le channel # - * - * Si l'utilisateur n'est pas dans le canal, affiche le message suivant - * " n'est pas dans le channel # + * Unsubscribes a user from this chat room. + * If the user was not subscribed, an informational message is added instead. * - * @param user l’utilisateur à retirer + * @param user the user leaving the room + * @return the ChatMessage created for this event */ - public void unsubscribe(User user) { - // TODO + public ChatMessage unsubscribe(User user) { // BEGIN STRIP + ChatMessage msg; if (users.contains(user)) { users.remove(user); - System.out.println(user.getName() + " a quitté le channel #" + channelName); + msg = new ChatMessage("leave", channelName, user.getName(),""); } else { - System.out.println(user.getName() + " n'est pas dans le channel #" + channelName); + msg = new ChatMessage("info", channelName, user.getName(), + user.getName() + " is not in channel #" + channelName); } + user.getLog().add(msg); + return msg; // END STRIP } /** - * Traite un message envoyé dans la salle. - *

- * - Si le message commence par "/mute", il est traité comme une commande - * pour activer ou désactiver le mute d’un autre utilisateur - * au format suivant : "/mute " + * Processes a message sent to this chat room. + * + * - If the message starts with "/mute", it is treated as a command to + * mute or unmute another user in the form: "/mute " * - * - Sinon, le message est envoyé à tous les abonnés et affiché sur la console sous la forme: - * " (): " + * - Otherwise, the message is broadcast to all subscribed users, + * and a corresponding ChatMessage is returned. * - * @param sender l’utilisateur qui envoie le message - * @param message contenu du message ou commande + * @param sender the user sending the message + * @param message the message content or command + * @return a list of ChatMessages generated by this action */ - public void sendMessage(User sender, String message) { - // TODO + public List sendMessage(User sender, ChatMessage message) { // BEGIN STRIP - if (message.startsWith("/mute")) { - String[] parts = message.split(" "); - if (parts.length == 2 && sender instanceof ChatUser) { - ((ChatUser) sender).toggleMuteUser(parts[1]); - } else { - System.out.println("Usage: /mute "); + List localLogs = new ArrayList<>(); + + switch (message.getType()) { + case "mute": { + sender.toggleMuteUser(message.getUser()); + localLogs.add(message); + break; + } + case "post": { + // Sender logs the post + localLogs.add(message); + // Notify all other users + notifyUsers(sender, message); + break; } - return; + default: + localLogs.add(new ChatMessage("info", "-", sender.getName(), "Unknown message type")); } - System.out.println(sender.getName() + " (" + channelName + "): " + message); - notifyUsers(sender, message); + return localLogs; // END STRIP } /** - * Notifie tous les utilisateurs du canal d’un nouveau message, - * sauf l’expéditeur lui-même. + * Notifies all users in the channel of a new message, + * except the sender themselves. * - * @param sender l’expéditeur du message - * @param message contenu du message + * @param sender the user who sent the message */ - private void notifyUsers(User sender, String message) { - // TODO - // BEGIN STRIP + private void notifyUsers(User sender, ChatMessage message) { for (User user : users) { if (!user.equals(sender)) { - user.update(channelName, sender.getName() + ": " + message); + // Store receive messages in receiver's log, user field = sender + user.update(message); } } - // END STRIP } - /** - * @return le nombre d’utilisateurs actuellement connectés à la salle - */ + + /** @return the number of users currently subscribed to this room */ public int getUserCount() { - // TODO // BEGIN STRIP return users.size(); // END STRIP - // STUDENT return false; } } diff --git a/src/test/java/oop/ChatAppTest.java b/src/test/java/oop/ChatAppTest.java index ad8360c..4bdefa5 100644 --- a/src/test/java/oop/ChatAppTest.java +++ b/src/test/java/oop/ChatAppTest.java @@ -1,58 +1,66 @@ package oop; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -import java.io.*; +import java.util.List; -class ChatAppTest { - - private final ByteArrayOutputStream output = new ByteArrayOutputStream(); - private PrintStream originalOut; - - @BeforeEach - void setUp() { - originalOut = System.out; - System.setOut(new PrintStream(output)); - } - - @AfterEach - void tearDown() { - System.setOut(originalOut); - } +public class ChatAppTest { @Test void testSubscriptionAndMessageNotification() { ChatApp.ChatRoom room = new ChatApp.ChatRoom("general"); - ChatApp.ChatRoom games = new ChatApp.ChatRoom("games"); ChatApp.ChatUser alice = new ChatApp.ChatUser("Alice"); ChatApp.ChatUser bob = new ChatApp.ChatUser("Bob"); ChatApp.ChatUser john = new ChatApp.ChatUser("John"); + // Subscriptions room.subscribe(alice); room.subscribe(bob); - alice.sendMessage(room, "Salut tout le monde !"); - String console = output.toString(); + // Alice sends message + ChatApp.ChatMessage msg = new ChatApp.ChatMessage("post", "general", "Alice", "Salut tout le monde !"); + alice.sendMessage(room, msg); - assertTrue(console.contains("Alice a rejoint le channel #general")); - assertTrue(console.contains("Bob a rejoint le channel #general")); - assertFalse(console.contains("John a rejoint le channel #general")); + List aliceLog = alice.getLog(); + List bobLog = bob.getLog(); + List johnLog = john.getLog(); - assertTrue(console.contains("Alice (general): Salut tout le monde !")); - assertTrue(console.contains("[Bob] reçoit sur #general > Alice: Salut tout le monde !")); - assertFalse(console.contains("[Bob] reçoit sur #games > Alice: Salut tout le monde !")); + // Subscription logs: check type and user fields + assertTrue(aliceLog.stream().anyMatch(m -> m.getType().equals("join") && m.getUser().equals("Alice"))); + assertTrue(bobLog.stream().anyMatch(m -> m.getType().equals("join") && m.getUser().equals("Bob"))); - // BEGIN STRIP - assertFalse(console.contains("[John] reçoit sur #general > Alice: Salut tout le monde !")); - assertFalse(console.contains("[Alice] reçoit sur #general > Alice: Salut tout le monde !")); - // END STRIP + // Alice's post should be logged + assertTrue(aliceLog.stream().anyMatch(m -> + m.getType().equals("post") && m.getUser().equals("Alice") && + m.getChannel().equals("general") && + m.getContent().contains("Salut tout le monde !"))); - output.reset(); + // Bob should have received the message + assertTrue(bobLog.stream().anyMatch(m -> + m.getType().equals("receive") && m.getUser().equals("Alice") && + m.getChannel().equals("general") && + m.getContent().contains("Salut tout le monde !"))); - room.subscribe(bob); + // John should have empty log + assertTrue(johnLog.isEmpty()); + + // Test duplicate subscription + ChatApp.ChatMessage result = room.subscribe(bob); + + // Check that the type, user and channel are correct + assertEquals("info", result.getType()); + assertEquals("Bob", result.getUser()); + assertEquals("general", result.getChannel()); - console = output.toString(); - assertFalse(console.contains("Bob a rejoint le channel #general")); - assertTrue(console.contains("Bob est déjà abonné au channel #general")); + // Bob should still be subscribed exactly once + assertEquals(2, room.getUserCount()); // Alice + Bob + + // Unsubscribe Bob and check user count + room.unsubscribe(bob); + assertEquals(1, room.getUserCount()); // Only Alice remains + + // If we unsubscribe Bob again, he should not be present + room.unsubscribe(bob); + assertEquals(1, room.getUserCount()); // Still only Alice } @Test @@ -64,25 +72,30 @@ void testMuteCommand() { room.subscribe(bob); room.subscribe(charlie); - bob.sendMessage(room, "/mute Charlie"); + // Bob mutes Charlie + ChatApp.ChatMessage muteMsg = new ChatApp.ChatMessage("mute", "-", "Charlie", ""); + bob.sendMessage(room, muteMsg); assertTrue(bob.isMuted("Charlie")); - - output.reset(); - charlie.sendMessage(room, "Salut !"); - String console = output.toString(); - assertFalse(console.contains("[Bob] reçoit")); - assertTrue(console.contains("Charlie (sport): Salut !")); - - // BEGIN STRIP - output.reset(); - - bob.sendMessage(room, "/mute Charlie"); - charlie.sendMessage(room, "Ca va?"); - console = output.toString(); - assertTrue(console.contains("Charlie (sport): Ca va?")); - assertTrue(console.contains("[Bob] reçoit sur #sport > Charlie: Ca va?")); - // END STRIP - + assertTrue(bob.getLog().stream().anyMatch(m -> m.getType().equals("mute") && m.getUser().equals("Charlie"))); + + // Charlie sends message; Bob should not receive it + ChatApp.ChatMessage postMsg = new ChatApp.ChatMessage("post", "sport", "Charlie", "Salut !"); + charlie.sendMessage(room, postMsg); + assertFalse(bob.getLog().stream().anyMatch(m -> m.getType().equals("receive") && m.getUser().equals("Charlie"))); + assertTrue(charlie.getLog().stream().anyMatch(m -> m.getType().equals("post") && m.getContent().contains("Salut !"))); + + // Bob unmutes Charlie + bob.sendMessage(room, muteMsg); + assertFalse(bob.isMuted("Charlie")); + assertTrue(bob.getLog().stream().anyMatch(m -> m.getType().equals("mute") && m.getUser().equals("Charlie"))); + + // Now Bob should receive Charlie’s messages again + ChatApp.ChatMessage postMsg2 = new ChatApp.ChatMessage("post", "sport", "Charlie", "Ca va?"); + charlie.sendMessage(room, postMsg2); + assertTrue(bob.getLog().stream().anyMatch(m -> + m.getType().equals("receive") && m.getUser().equals("Charlie") && + m.getContent().contains("Ca va?"))); + assertTrue(charlie.getLog().stream().anyMatch(m -> m.getType().equals("post") && m.getContent().contains("Ca va?"))); } @Test @@ -97,21 +110,26 @@ void testUnsubscribe() { room.subscribe(bob); assertEquals(2, room.getUserCount()); - // BEGIN STRIP - alice.sendMessage(room, "Hello"); - String console = output.toString(); + // Alice sends message → Bob should receive + ChatApp.ChatMessage msg1 = new ChatApp.ChatMessage("post", "TPs", "Alice", "Hello"); + alice.sendMessage(room, msg1); + assertTrue(bob.getLog().stream().anyMatch(m -> + m.getType().equals("receive") && m.getUser().equals("Alice") && + m.getContent().contains("Hello"))); - assertTrue(console.contains("[Bob] reçoit sur #TPs > Alice: Hello")); - output.reset(); - // END STRIP + // Bob unsubscribes room.unsubscribe(bob); + assertEquals(1, room.getUserCount()); - // BEGIN STRIP - alice.sendMessage(room, "Vous avez fait l'exercice 4?"); - console = output.toString(); - assertFalse(console.contains("[Bob] reçoit")); - // END STRIP + // Alice sends another → Bob should NOT receive + int bobLogSizeBefore = bob.getLog().size(); + ChatApp.ChatMessage msg2 = new ChatApp.ChatMessage("post", "TPs", "Alice", "Vous avez fait l'exercice 4?"); + alice.sendMessage(room, msg2); - assertEquals(1, room.getUserCount()); + // Check that Bob did not receive any new message from Alice + assertEquals(bobLogSizeBefore, bob.getLog().size()); + assertFalse(bob.getLog().stream().anyMatch(m -> m.getType().equals("receive") && + m.getUser().equals("Alice") && + m.getContent().contains("Vous avez fait l'exercice 4?"))); } -} \ No newline at end of file +} From 3a27c132798dfd3405d5bad0ab4796b0744688df Mon Sep 17 00:00:00 2001 From: echatzo Date: Tue, 14 Oct 2025 12:53:33 +0200 Subject: [PATCH 6/6] oop: chatapp - clarification of the task for students --- src/main/java/oop/ChatApp.java | 54 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/main/java/oop/ChatApp.java b/src/main/java/oop/ChatApp.java index 708f527..5773e12 100644 --- a/src/main/java/oop/ChatApp.java +++ b/src/main/java/oop/ChatApp.java @@ -15,11 +15,13 @@ * * --------------------------------------------------------- * Included classes: + * - ChatMessage class (represents a message or event in the system) * - User interface (Observer) - * - ChatUser class (concrete implementation of a user) - * - ChatRoom class (Subject) + * - ChatUser class (concrete implementation of a user) TODO Implement its methods + * - ChatRoom class (Subject) TODO Implement its methods * - ChatApp class (entry point) * + * * --------------------------------------------------------- * * Example of expected behavior with user logs: @@ -178,19 +180,20 @@ public static class ChatUser implements User { private List log = new ArrayList<>(); public ChatUser(String name) { - // BEGIN STRIP this.name = name; - // END STRIP } /** * Called when a message is sent in a channel the user is subscribed to. - * If the sender is muted, the message is ignored. + * If the sender is muted, the message is ignored + * Otherwise, if the message is a "post" message, add the corresponding + * "receive" message to the user's log * - * @param message the complete message in the form "Sender: content" + * @param message the post message sent on the channel */ @Override public void update(ChatMessage message) { + // TODO // BEGIN STRIP // Ignore messages from muted users if (message.getType().equals("post") && !mutedUsers.contains(message.getUser())) { @@ -202,16 +205,12 @@ public void update(ChatMessage message) { /** @return the name of this user */ @Override public String getName() { - // BEGIN STRIP return name; - // END STRIP } /** @return the list of all ChatMessages recorded for this user */ public List getLog() { - // BEGIN STRIP return log; - // END STRIP } /** @@ -223,6 +222,7 @@ public List getLog() { * @return a list of ChatMessages generated by this action */ public List sendMessage(ChatRoom room, ChatMessage message) { + // TODO // BEGIN STRIP List messages = room.sendMessage(this, message); log.addAll(messages); @@ -241,6 +241,7 @@ public List sendMessage(ChatRoom room, ChatMessage message) { */ @Override public void toggleMuteUser(String userName) { + // TODO // BEGIN STRIP if (isMuted(userName)) { mutedUsers.remove(userName); @@ -253,15 +254,13 @@ public void toggleMuteUser(String userName) { } /** - * Checks whether a given user is currently muted. + * Checks whether a given user is currently muted by the current used. * * @param userName the user to check * @return true if muted, false otherwise */ public boolean isMuted(String userName) { - // BEGIN STRIP return mutedUsers.contains(userName); - // END STRIP } } @@ -282,10 +281,8 @@ public static class ChatRoom { * @param channelName the channel name (e.g. "general", "sports", "games") */ public ChatRoom(String channelName) { - // BEGIN STRIP this.channelName = channelName; this.users = new ArrayList<>(); - // END STRIP } /** @return the name of the chat room */ @@ -301,6 +298,7 @@ public String getChannelName() { * @return the ChatMessage created for this event */ public ChatMessage subscribe(User user) { + // TODO // BEGIN STRIP for (User u : users) { if (u.equals(user)) { @@ -325,6 +323,7 @@ public ChatMessage subscribe(User user) { * @return the ChatMessage created for this event */ public ChatMessage unsubscribe(User user) { + // TODO // BEGIN STRIP ChatMessage msg; if (users.contains(user)) { @@ -340,19 +339,21 @@ public ChatMessage unsubscribe(User user) { } /** - * Processes a message sent to this chat room. + * Processes a ChatMessage sent to this chat room. * - * - If the message starts with "/mute", it is treated as a command to - * mute or unmute another user in the form: "/mute " + * - If the message type is "mute", it is treated as a command to + * mute or unmute another user. The action is only logged in the sender’s log, + * and the `user` field of the message corresponds to the target user. * - * - Otherwise, the message is broadcast to all subscribed users, - * and a corresponding ChatMessage is returned. + * - If the message type is "post", it is broadcast to all other subscribed users. + * The sender logs the message in their own log, while each receiver logs it + * as a "receive" message (with `user` = sender) in their log. * - * @param sender the user sending the message - * @param message the message content or command - * @return a list of ChatMessages generated by this action + * - Any other message type is treated as unknown, generating an "info" message + * in the sender’s log. */ public List sendMessage(User sender, ChatMessage message) { + // TODO // BEGIN STRIP List localLogs = new ArrayList<>(); @@ -378,26 +379,27 @@ public List sendMessage(User sender, ChatMessage message) { } /** - * Notifies all users in the channel of a new message, + * Notifies all users in the channel of a new posted message, * except the sender themselves. * * @param sender the user who sent the message */ private void notifyUsers(User sender, ChatMessage message) { + // TODO + // BEGIN STRIP for (User user : users) { if (!user.equals(sender)) { // Store receive messages in receiver's log, user field = sender user.update(message); } } + // END STRIP } /** @return the number of users currently subscribed to this room */ public int getUserCount() { - // BEGIN STRIP return users.size(); - // END STRIP } }