From 9120d5c4ca77cfbca15efa553db688b1761ede53 Mon Sep 17 00:00:00 2001 From: bouclyma Date: Sat, 28 Dec 2024 02:16:25 +0100 Subject: [PATCH 01/10] feat(Event): Ajout classe Event --- chat/src/main/java/rtgre/modeles/Event.java | 52 ++++ .../test/java/rtgre/modeles/EventTest.java | 223 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 chat/src/main/java/rtgre/modeles/Event.java create mode 100644 chat/src/test/java/rtgre/modeles/EventTest.java diff --git a/chat/src/main/java/rtgre/modeles/Event.java b/chat/src/main/java/rtgre/modeles/Event.java new file mode 100644 index 0000000..9198f5e --- /dev/null +++ b/chat/src/main/java/rtgre/modeles/Event.java @@ -0,0 +1,52 @@ +package rtgre.modeles; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Event { + public static final String AUTH = "AUTH"; + public static final String QUIT = "QUIT"; + public static final String MESG = "MESG"; + public static final String JOIN = "JOIN"; + public static final String POST = "POST"; + public static final String CONT = "CONT"; + public static final String LIST_CONTACTS = "LSTC"; + public static final String LIST_POSTS = "LSTP"; + public static final String SYSTEM = "SYST"; + private final String type; + private final JSONObject content; + + public String getType() { + return type; + } + + public JSONObject getContent() { + return content; + } + + public Event(String type, JSONObject content) { + this.type = type; + this.content = content; + } + + @Override + public String toString() { + return "Event{type=" + type + ", content=" + content.toString() + "}"; + } + + public JSONObject toJsonObject() { + return new JSONObject().put("type", type).put("content", content); + } + + public String toJson() { + return toJsonObject().toString(); + } + + public static Event fromJson(String json) throws JSONException { + JSONObject jsonObject = new JSONObject(json); + String type = jsonObject.getString("type"); + JSONObject content = jsonObject.getJSONObject("content"); + return new Event(type, content); + } +} + diff --git a/chat/src/test/java/rtgre/modeles/EventTest.java b/chat/src/test/java/rtgre/modeles/EventTest.java new file mode 100644 index 0000000..18c9795 --- /dev/null +++ b/chat/src/test/java/rtgre/modeles/EventTest.java @@ -0,0 +1,223 @@ +package rtgre.modeles; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class EventTest { + + static Class classe = Event.class; + static String module = "rtgre.modeles"; + + // @Tag("01-mise_en_place_contact") + @DisplayName("01-Structure de Event") + @Nested + class StructureTest { + + static List constructeursSignatures; + static List methodesSignatures; + + @BeforeAll + static void init() { + Constructor[] constructeurs = classe.getConstructors(); + constructeursSignatures = Arrays.stream(constructeurs).map(Constructor::toString).collect(Collectors.toList()); + Method[] methodes = classe.getDeclaredMethods(); + methodesSignatures = Arrays.stream(methodes).map(Method::toString).collect(Collectors.toList()); + } + + static Stream constantesProvider() { + return Stream.of( + arguments("AUTH", "AUTH"), + arguments("QUIT", "QUIT"), + arguments("MESG", "MESG"), + arguments("JOIN", "JOIN"), + arguments("POST", "POST"), + arguments("CONT", "CONT"), + arguments("LIST_POSTS", "LSTP"), + arguments("LIST_CONTACTS", "LSTC"), + arguments("SYSTEM", "SYST") + ); + } + + @DisplayName("Déclaration des constantes") + @ParameterizedTest + @MethodSource("constantesProvider") + void testDeclarationConstantes(String constante, String value) throws NoSuchFieldException, IllegalAccessException { + Field field = classe.getField(constante); + Assertions.assertEquals(Modifier.PUBLIC | Modifier.FINAL | Modifier.STATIC, field.getModifiers(), + "Visibilité " + constante + " erronée"); + Assertions.assertEquals("java.lang.String", field.getType().getName(), + "Type " + constante + " erroné"); + Assertions.assertEquals(value, field.get(null), "Valeur " + constante + " erronée"); + } + + + static Stream attributsProvider() { + return Stream.of( + arguments("type", "java.lang.String", Modifier.PRIVATE | Modifier.FINAL), + arguments("content", "org.json.JSONObject", Modifier.PRIVATE | Modifier.FINAL) + ); + } + + @DisplayName("Déclaration des attributs") + @ParameterizedTest + @MethodSource("attributsProvider") + void testDeclarationAttributs(String nom, String type, int modifier) throws NoSuchFieldException { + Field field = classe.getDeclaredField(nom); + Assertions.assertEquals(type, field.getType().getName(), + "Type " + nom + " erroné : doit être " + type); + Assertions.assertEquals(modifier, field.getModifiers(), + "Visibilité " + nom + " erronée : doit être " + modifier); + + } + + static Stream constructeursProvider() { + return Stream.of( + arguments("public %s.Event(java.lang.String,org.json.JSONObject)") + ); + } + + @DisplayName("Déclaration des constructeurs") + @ParameterizedTest + @MethodSource("constructeursProvider") + void testConstructeurs1(String signature) { + Assertions.assertTrue(constructeursSignatures.contains(String.format(signature, module)), + String.format("Constructeur non déclaré : doit être %s\nalors que sont déclarés %s", + signature, constructeursSignatures)); + + } + + static Stream methodesProvider() { + return Stream.of( + arguments("getType", "public java.lang.String %s.Event.getType()"), + arguments("getContent", "public org.json.JSONObject %s.Event.getContent()"), + arguments("toString", "public java.lang.String %s.Event.toString()"), + arguments("toJsonObject", "public org.json.JSONObject %s.Event.toJsonObject()"), + arguments("toJson", "public java.lang.String %s.Event.toJson()"), + arguments("fromJson", "public static %s.Event %s.Event.fromJson(java.lang.String) throws org.json.JSONException") + ); + } + + @DisplayName("Déclaration des méthodes") + @ParameterizedTest + @MethodSource("methodesProvider") + void testDeclarationMethodes1(String nom, String signature) { + Assertions.assertTrue(methodesSignatures.contains(String.format(signature, module, module)), + String.format("Méthode non déclarée : doit être %s\nalors que sont déclarés %s", + signature, methodesSignatures)); + } + } + + @DisplayName("02-Instanciation d'un évènement") + @Nested + class InstanciationTest { + + @Test + @DisplayName("Constructeur par défaut") + void TestContructeurParDefaut() { + JSONObject json = new JSONObject("{\"login\":\"riri\"}"); + Event event = new Event("AUTH", json); + Assertions.assertEquals("AUTH", event.getType(), "Type erroné"); + Assertions.assertEquals(json, event.getContent(), "Content erroné"); + } + + + + } + + @DisplayName("03-Instanciation fromJSON") + @Nested + class fromJSONTest { + + static Stream contentProvides() { + return Stream.of( + arguments("AUTH", "{\"type\":\"AUTH\",\"content\":{\"login\":\"riri\"}}"), + arguments("QUIT", "{\"type\": \"QUIT\",\"content\": {}}"), + arguments("MESG", "{\"type\": \"MESG\",\"content\": {\"to\":\"DEST\",\"body\":\"BODY\"}}") + ); + } + @DisplayName("Récupération du type") + @ParameterizedTest + @MethodSource("contentProvides") + void TestContructeurType(String type, String json) { + Event event = Event.fromJson(json); + Assertions.assertEquals(type, event.getType(), "Type erroné"); + } + + static Stream keyProvides() { + return Stream.of( + arguments("login", "riri", "{\"type\":\"AUTH\",\"content\":{\"login\":\"riri\"}}"), + arguments("to", "DEST", "{\"type\": \"MESG\",\"content\": {\"to\":\"DEST\",\"body\":\"BODY\"}}"), + arguments("body", "BODY", "{\"type\": \"MESG\",\"content\": {\"to\":\"DEST\",\"body\":\"BODY\"}}") + ); + } + + @DisplayName("Récupération du contenu") + @ParameterizedTest + @MethodSource("keyProvides") + void TestContructeurContenu(String key, String value, String json) { + Event event = Event.fromJson(json); + Assertions.assertTrue(event.getContent().has(key), "Contenu erroné"); + Assertions.assertEquals(value, event.getContent().get(key), "Contenu erroné"); + } + + @DisplayName("Levée d'exception") + @Test + void TestFromJsonException() { + assertThrows(JSONException.class, () -> Event.fromJson("bonjour")); + } + } + + @Nested + @DisplayName("04-Méthodes") + + class TestMethodes { + @Test + @DisplayName("Méthode toString") + void TestToString() { + Event event = Event.fromJson("{\"type\":\"AUTH\",\"content\":{\"login\":\"riri\"}}"); + String repr = event.toString(); + Assertions.assertEquals("Event{type=AUTH, content={\"login\":\"riri\"}}", repr, "Représentation textuelle erronée"); + } + + @Test + @DisplayName("Méthode toJsonObject") + void TestToJsonObject() { + Event event = Event.fromJson("{\"type\":\"AUTH\",\"content\":{\"login\":\"riri\"}}"); + JSONObject json = event.toJsonObject(); + Assertions.assertTrue(json.has("type"), "Représentation JSON erronée"); + Assertions.assertEquals("AUTH", json.get("type"), "Représentation JSON erronée"); + Assertions.assertTrue(json.has("content"), "Représentation JSON erronée"); + Assertions.assertTrue(json.getJSONObject("content").has("login"), "Représentation JSON erronée"); + Assertions.assertEquals("riri", json.getJSONObject("content").get("login"), "Représentation JSON erronée"); + } + + @Test + @DisplayName("Méthode toJson") + void TestToJson() { + Event event = Event.fromJson("{\"type\":\"AUTH\",\"content\":{\"login\":\"riri\"}}"); + String json = event.toJson(); + Assertions.assertEquals("{\"type\":\"AUTH\",\"content\":{\"login\":\"riri\"}}", + json, "Sérialisation JSON erronée"); + } + + + } + + +} \ No newline at end of file From d2ad689b48e3871609563e8518700103bf0812a3 Mon Sep 17 00:00:00 2001 From: bouclyma Date: Sat, 28 Dec 2024 02:42:16 +0100 Subject: [PATCH 02/10] =?UTF-8?q?feat(Event):=202.6=20-=20Traitement=20de?= =?UTF-8?q?=20l=E2=80=99=C3=A9v=C3=A8nement=20"AUTH"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/rtgre/modeles/ContactMap.java | 12 ++++ .../main/java/rtgre/server/ChatServer.java | 58 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/chat/src/main/java/rtgre/modeles/ContactMap.java b/chat/src/main/java/rtgre/modeles/ContactMap.java index f44bcc0..5924158 100644 --- a/chat/src/main/java/rtgre/modeles/ContactMap.java +++ b/chat/src/main/java/rtgre/modeles/ContactMap.java @@ -9,4 +9,16 @@ public class ContactMap extends TreeMap { public Contact getContact(String login) { return this.get(login); } + + public void loadDefaultContacts() { + this.put("mickey", new Contact("mickey", null)); + this.put("minnie", new Contact("minnie", null)); + this.put("dingo", new Contact("dingo", null)); + this.put("riri", new Contact("riri", null)); + this.put("fifi", new Contact("fifi", null)); + this.put("loulou", new Contact("loulou", null)); + this.put("donald", new Contact("donald", null)); + this.put("daisy", new Contact("daisy", null)); + this.put("picsou", new Contact("picsou", null)); + } } diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index 3665663..957765e 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -1,5 +1,10 @@ package rtgre.server; +import org.json.JSONException; +import org.json.JSONObject; +import rtgre.modeles.ContactMap; +import rtgre.modeles.Event; + import java.io.*; import java.net.ServerSocket; import java.net.Socket; @@ -17,6 +22,7 @@ public class ChatServer { private static final Logger LOGGER = Logger.getLogger(ChatServer.class.getCanonicalName()); private Vector clientList; + private ContactMap contactMap; static { try { @@ -43,6 +49,8 @@ public class ChatServer { passiveSock = new ServerSocket(port); LOGGER.info("Serveur en écoute " + passiveSock); clientList = new Vector<>(); + contactMap = new ContactMap(); + contactMap.loadDefaultContacts(); } public void close() throws IOException { @@ -101,6 +109,10 @@ public class ChatServer { //client.echoLoop(); } + public ContactMap getContactMap() { + return contactMap; + } + private class ChatClientHandler { public static final String END_MESSAGE = "fin"; /** @@ -160,6 +172,52 @@ public class ChatServer { close(); } + public void eventReceiveLoop() { + try { + String message = null; + while (!END_MESSAGE.equals(message)) { + message = in.readLine(); + if (message == null) { + break; + } + LOGGER.info("[%s] Réception de : %s".formatted(ipPort, message)); + LOGGER.info("[%s] Envoi de : %s".formatted(ipPort, message)); + try { + if (handleEvent(message)) { + break; + } + } catch (Exception e) { + break; + } + } + } catch (IOException e) { + LOGGER.severe("[%s] %s".formatted(ipPort, e)); + } + close(); + } + + private boolean handleEvent(String message) throws JSONException, IllegalStateException { + Event event = Event.fromJson(message); + switch (event.getType()) { + case Event.AUTH: + doLogin(event.getContent()); + return false; + default: + return true; + } + } + + private void doLogin(JSONObject content) { + String login = content.getString("login"); + if (login.equals("")) { + throw new JSONException("Aucun login fourni"); + } else if (!contactMap.containsKey(login)) { + throw new IllegalStateException("Login non-authorisé"); + } else { + contactMap.getContact(login).setConnected(true); + } + } + public void send(String message) throws IOException { LOGGER.finest("send: %s".formatted(message)); out.println(message); From df7442f7a636d1ea318d6734e40aa28891aeddb6 Mon Sep 17 00:00:00 2001 From: bouclyma Date: Sat, 4 Jan 2025 18:35:58 +0100 Subject: [PATCH 03/10] feat(Event): 2.6.3 - findClient => mappage inexistant ?? --- .../main/java/rtgre/chat/ChatController.java | 1 + .../main/java/rtgre/chat/net/ChatClient.java | 31 +++++++++++++++++++ .../main/java/rtgre/server/ChatServer.java | 24 +++++++++----- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index 4ea8062..d14a9e9 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -163,6 +163,7 @@ public class ChatController implements Initializable { initContactListView(); initPostListView(); this.statusLabel.setText("Connected to %s@%s:%s".formatted(this.contact.getLogin(), host, port)); + client.sendAuthEvent(contact); } catch (IOException e) { new Alert(Alert.AlertType.ERROR, "Erreur de connexion").showAndWait(); connectionButton.setSelected(false); diff --git a/chat/src/main/java/rtgre/chat/net/ChatClient.java b/chat/src/main/java/rtgre/chat/net/ChatClient.java index 009ffb6..b7b1571 100644 --- a/chat/src/main/java/rtgre/chat/net/ChatClient.java +++ b/chat/src/main/java/rtgre/chat/net/ChatClient.java @@ -1,8 +1,13 @@ package rtgre.chat.net; +import org.json.JSONObject; import rtgre.chat.ChatController; +import rtgre.modeles.Contact; +import rtgre.modeles.Event; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStreamReader; import java.util.logging.Logger; import static rtgre.chat.ChatApplication.LOGGER; @@ -26,6 +31,32 @@ public class ChatClient extends ClientTCP { this.listener = listener; } + + + public void sendEvent(Event event) { + connected = true; + try { + String message = event.toJson(); + if (message == null) { // fin du flux stdIn + message = END_MESSAGE; + } + System.out.println(BLUE + "Envoi: " + message + RST); + this.send(message); + if (END_MESSAGE.equals(message)) { + connected = false; + } + } catch (IOException e) { + LOGGER.severe(e.toString()); + connected = false; + } + } + + public void sendAuthEvent(Contact contact) { + Event authEvent = new Event(Event.AUTH, new JSONObject().put("login", contact.getLogin())); + sendEvent(authEvent); + } + + @Override public void receiveLoop() { LOGGER.info(RED + "Boucle de réception de messages..." + RST); diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index 957765e..a342590 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -2,6 +2,7 @@ package rtgre.server; import org.json.JSONException; import org.json.JSONObject; +import rtgre.modeles.Contact; import rtgre.modeles.ContactMap; import rtgre.modeles.Event; @@ -101,7 +102,7 @@ public class ChatServer { */ private void handleNewClient(Socket sock) throws IOException { ChatClientHandler client = new ChatClientHandler(sock); - Thread clientLoop = new Thread(client::echoLoop); + Thread clientLoop = new Thread(client::eventReceiveLoop); clientLoop.start(); clientList.add(client); LOGGER.fine("Ajout du client [%s] dans la liste (%d clients connectés)" @@ -181,9 +182,8 @@ public class ChatServer { break; } LOGGER.info("[%s] Réception de : %s".formatted(ipPort, message)); - LOGGER.info("[%s] Envoi de : %s".formatted(ipPort, message)); try { - if (handleEvent(message)) { + if (!handleEvent(message)) { break; } } catch (Exception e) { @@ -201,23 +201,33 @@ public class ChatServer { switch (event.getType()) { case Event.AUTH: doLogin(event.getContent()); - return false; - default: + LOGGER.finest("Login successful"); return true; + default: + LOGGER.warning("Unhandled event type: " + event.getType()); + return false; } } private void doLogin(JSONObject content) { String login = content.getString("login"); - if (login.equals("")) { + if (login.isEmpty()) { + LOGGER.warning("Aucun login fourni"); throw new JSONException("Aucun login fourni"); } else if (!contactMap.containsKey(login)) { + LOGGER.warning("Login non-authorisé"); throw new IllegalStateException("Login non-authorisé"); } else { + LOGGER.info("Connexion de " + login); contactMap.getContact(login).setConnected(true); } } + public ChatClientHandler findClient(Contact contact) { + String login = contact.getLogin(); + Contact contactRes = contactMap.get() + } + public void send(String message) throws IOException { LOGGER.finest("send: %s".formatted(message)); out.println(message); @@ -247,7 +257,7 @@ public class ChatServer { public String receive() throws IOException { String message = in.readLine(); - LOGGER.finest("receive: %s".formatted(message)); + LOGGER.info("receive: %s".formatted(message)); if (message == null) { throw new IOException("End of the stream has been reached"); } From 8b3a0e5b2113c991fc3108e897fa9e5d7d129e3b Mon Sep 17 00:00:00 2001 From: bouclyma Date: Tue, 7 Jan 2025 08:41:51 +0100 Subject: [PATCH 04/10] =?UTF-8?q?feat(event):=20ContactWithEventTest3.java?= =?UTF-8?q?:=20test=20pass=C3=A9=20(2.6.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/java/rtgre/modeles/Contact.java | 16 +++ .../main/java/rtgre/server/ChatServer.java | 37 +++++- .../resources/rtgre/chat/banque_avatars.png | Bin 0 -> 53818 bytes .../rtgre/modeles/ContactWithEventTest3.java | 110 ++++++++++++++++++ 4 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 chat/src/main/resources/rtgre/chat/banque_avatars.png create mode 100644 chat/src/test/java/rtgre/modeles/ContactWithEventTest3.java diff --git a/chat/src/main/java/rtgre/modeles/Contact.java b/chat/src/main/java/rtgre/modeles/Contact.java index 549fefa..bb9ff5a 100644 --- a/chat/src/main/java/rtgre/modeles/Contact.java +++ b/chat/src/main/java/rtgre/modeles/Contact.java @@ -1,6 +1,8 @@ package rtgre.modeles; +import org.json.JSONObject; + import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; @@ -101,6 +103,20 @@ public class Contact { return 0; } + public JSONObject toJsonObject() { + return new JSONObject() + .put("login", this.login) + .put("connected", this.connected); + } + + public String toJson() { + return toJsonObject().toString(); + } + + public static Contact fromJSON(JSONObject jsonObject, File banque_avatars) { + return new Contact(jsonObject.getString("login"), jsonObject.getBoolean("connected"), banque_avatars); + } + public static BufferedImage avatarFromLogin(File fichier, String login) throws IOException { /** * Renvoie une sous-image en fonction d'une banque d'image et d'un login. diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index a342590..cd620dd 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -110,6 +110,35 @@ public class ChatServer { //client.echoLoop(); } + public ChatClientHandler findClient(Contact contact) { + for (ChatClientHandler user: clientList) { + if (user.user.equals(contact)) { + return user; + } + } + return null; + } + + public void sendEventToContact(Contact contact, Event event) { + ChatClientHandler user = findClient(contact); + if (!(user == null)) { + try { + user.send(event.toString()); + } catch (Exception e) { + LOGGER.warning("!!Erreur de l'envoi d'Event à %s, fermeture de la connexion".formatted(user.user.getLogin())); + user.close(); + } + } + } + + public void sendEventToAllContacts(Event event) { + for (Contact contact: contactMap.values()) { + if (contact.isConnected()) { + sendEventToContact(contact, event); + } + } + } + public ContactMap getContactMap() { return contactMap; } @@ -133,6 +162,8 @@ public class ChatServer { */ private String ipPort; + private Contact user; + /** * Initialise les attributs {@link #sock} (socket connecté au client), * {@link #out} (flux de caractères UTF-8 en sortie) et @@ -220,14 +251,10 @@ public class ChatServer { } else { LOGGER.info("Connexion de " + login); contactMap.getContact(login).setConnected(true); + this.user = contactMap.getContact(login); } } - public ChatClientHandler findClient(Contact contact) { - String login = contact.getLogin(); - Contact contactRes = contactMap.get() - } - public void send(String message) throws IOException { LOGGER.finest("send: %s".formatted(message)); out.println(message); diff --git a/chat/src/main/resources/rtgre/chat/banque_avatars.png b/chat/src/main/resources/rtgre/chat/banque_avatars.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e2c99fa10650f1f6493392f51ed1d3e44f51f5 GIT binary patch literal 53818 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 literal 0 HcmV?d00001 diff --git a/chat/src/test/java/rtgre/modeles/ContactWithEventTest3.java b/chat/src/test/java/rtgre/modeles/ContactWithEventTest3.java new file mode 100644 index 0000000..a45d891 --- /dev/null +++ b/chat/src/test/java/rtgre/modeles/ContactWithEventTest3.java @@ -0,0 +1,110 @@ +package rtgre.modeles; + +import org.json.JSONObject; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.params.provider.Arguments.arguments; + +/** Tests unitaires du modèle de base de Contact (étape 1) */ + +class ContactWithEventTest3 { + + static Class classe = Contact.class; + static String module = "rtgre.modeles"; + + @DisplayName("01-Structure de la classe Contact") + @Nested + class StructureTest { + + static List constructeursSignatures; + static List methodesSignatures; + + @BeforeAll + static void init() { + Constructor[] constructeurs = classe.getConstructors(); + constructeursSignatures = Arrays.stream(constructeurs).map(Constructor::toString).collect(Collectors.toList()); + Method[] methodes = classe.getDeclaredMethods(); + methodesSignatures = Arrays.stream(methodes).map(Method::toString).collect(Collectors.toList()); + } + + static Stream methodesProvider3() { + return Stream.of( + arguments("toJsonObject", "public org.json.JSONObject %s.Contact.toJsonObject()"), + arguments("toJson", "public java.lang.String %s.Contact.toJson()"), + arguments("fromJSON", "public static %s.Contact %s.Contact.fromJSON(org.json.JSONObject,java.io.File)") + ); + } + // @Disabled("Jusqu'à ce que soit codé les events") + @DisplayName("Déclaration des méthodes (avec event et JSON)") + @ParameterizedTest + @MethodSource("methodesProvider3") + void testDeclarationMethodes3(String nom, String signature) { + Assertions.assertTrue(methodesSignatures.contains(String.format(signature, module, module)), + String.format("Méthode non déclarée : doit être %s\nalors que sont déclarés %s", + signature, methodesSignatures)); + } + + + + } + + + @DisplayName("07-Représentation JSON") + @Nested + class JSONTest { + + @Test + @DisplayName("JSONObject") + void TestToJSONObject() { + String erreur = "Représentation JSON erronée"; + Contact fifi = new Contact("fifi", true, (Image) null); + JSONObject json = fifi.toJsonObject(); + Assertions.assertTrue(json.has("login"), erreur); + Assertions.assertEquals("fifi", json.get("login"), erreur); + Assertions.assertTrue(json.has("connected"), erreur); + Assertions.assertEquals(true, json.get("connected"), erreur); + } + + @Test + @DisplayName("Sérialisation JSON") + void TestToJSON() { + String erreur = "Sérialisation de la représentation JSON erronée"; + Contact riri = new Contact("riri", true, (Image) null); + String json = riri.toJson(); + Assertions.assertTrue(json.contains("\"login\":\"riri\""), erreur); + Assertions.assertTrue(json.contains("\"connected\":true"), erreur); + } + + @Test + @DisplayName("Contact à partir d'un JSON") + void TestConstructeur() throws IOException { + String work_dir = System.getProperty("user.dir"); + Assertions.assertTrue(work_dir.endsWith("chat"), + "Le working dir doit être /chat/ et non : " + work_dir); + File f = new File("src/main/resources/rtgre/chat/banque_avatars.png"); + String json = "{\"login\":\"riri\",\"connected\":true}"; + Contact toto = Contact.fromJSON(new JSONObject(json), f); + Assertions.assertEquals("riri", toto.getLogin(), "Login erroné"); + Assertions.assertEquals(true, toto.isConnected(), "Connected erroné"); + Assertions.assertNotNull(toto.getAvatar(), "Avatar non chargé"); + } + + } + +} \ No newline at end of file From b145e8097828962848c3763e8fbc0b4b276e7def Mon Sep 17 00:00:00 2001 From: bouclyma Date: Tue, 7 Jan 2025 10:44:53 +0100 Subject: [PATCH 05/10] =?UTF-8?q?feat(event):=202.6.4:=20Listing=20des=20c?= =?UTF-8?q?ontacts=20connus=20du=20serveur=20envoy=C3=A9=20au=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/rtgre/chat/ChatController.java | 54 ++++++++++++++++- .../main/java/rtgre/chat/net/ChatClient.java | 4 ++ .../main/java/rtgre/server/ChatServer.java | 58 ++++++++++++++++--- 3 files changed, 104 insertions(+), 12 deletions(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index d14a9e9..79b474d 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -22,11 +22,13 @@ import javafx.stage.Stage; import net.synedra.validatorfx.Check; import net.synedra.validatorfx.TooltipWrapper; import net.synedra.validatorfx.Validator; +import org.json.JSONObject; import rtgre.chat.graphisme.ContactListViewCell; import rtgre.chat.graphisme.PostListViewCell; import rtgre.chat.net.ChatClient; import rtgre.modeles.*; +import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; @@ -154,24 +156,30 @@ public class ChatController implements Initializable { String host = matcher.group(1); int port = (matcher.group(2) != null) ? Integer.parseInt(matcher.group(2)) : 2024; try { + LOGGER.info(host + ":" + port); this.client = new ChatClient(host, port, this); initContactListView(); initPostListView(); clearLists(); contactMap.add(this.contact); this.contact.setConnected(true); + + client.sendAuthEvent(contact); + client.sendEvent(new rtgre.modeles.Event(rtgre.modeles.Event.LIST_CONTACTS, new JSONObject())); + initContactListView(); initPostListView(); this.statusLabel.setText("Connected to %s@%s:%s".formatted(this.contact.getLogin(), host, port)); - client.sendAuthEvent(contact); } catch (IOException e) { new Alert(Alert.AlertType.ERROR, "Erreur de connexion").showAndWait(); connectionButton.setSelected(false); } } else if (!connectionButton.isSelected()) { clearLists(); - this.client.close(); - this.contact.setConnected(false); + if (this.client.isConnected()) { + this.client.close(); + this.contact.setConnected(false); + } statusLabel.setText("not connected to " + hostComboBox.getValue()); } @@ -265,4 +273,44 @@ public class ChatController implements Initializable { postsObservableList.add(postSys); postListView.refresh(); } + + public void handleEvent(rtgre.modeles.Event event) { + LOGGER.info("Received new event! : " + event); + LOGGER.info(event.getType()); + +// switch (event.getType()) { +// case "CONT": +// handleContEvent(event.getContent()); +// default: +// LOGGER.warning("Unhandled event type: " + event.getType()); +// this.client.close(); +// } + + if (event.getType().equals("CONT")) { + handleContEvent(event.getContent()); + } else { + LOGGER.warning("Unhandled event type: " + event.getType()); + this.client.close(); + } + } + + private void handleContEvent(JSONObject content) { + Contact contact = contactMap.getContact(content.getString("login")); + if (contact != null) { + LOGGER.info(contactMap.toString()); + contactMap.getContact(content.getString("login")).setConnected(content.getBoolean("connected")); + contactsListView.refresh(); + LOGGER.info(contactMap.toString()); + } else { + LOGGER.info(contactMap.toString()); + Contact user = Contact.fromJSON( + content, + new File("src/main/resources/rtgre/chat/avatars.png") + ); + contactMap.add(user); + contactObservableList.add(user); + LOGGER.info(contactMap.toString()); + } + } + } \ No newline at end of file diff --git a/chat/src/main/java/rtgre/chat/net/ChatClient.java b/chat/src/main/java/rtgre/chat/net/ChatClient.java index b7b1571..4c60c58 100644 --- a/chat/src/main/java/rtgre/chat/net/ChatClient.java +++ b/chat/src/main/java/rtgre/chat/net/ChatClient.java @@ -1,5 +1,6 @@ package rtgre.chat.net; +import javafx.application.Platform; import org.json.JSONObject; import rtgre.chat.ChatController; import rtgre.modeles.Contact; @@ -65,6 +66,9 @@ public class ChatClient extends ClientTCP { String message = this.receive(); LOGGER.info(RED + "Réception: " + message + RST); LOGGER.info(RED + message + RST); + if (listener != null) { + Platform.runLater(() -> listener.handleEvent(Event.fromJson(message))); + } } } catch (IOException e) { LOGGER.severe("[%s] %s".formatted(ipPort, e)); diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index cd620dd..ec234f1 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -2,6 +2,7 @@ package rtgre.server; import org.json.JSONException; import org.json.JSONObject; +import rtgre.chat.net.ChatClient; import rtgre.modeles.Contact; import rtgre.modeles.ContactMap; import rtgre.modeles.Event; @@ -38,6 +39,7 @@ public class ChatServer { public static void main(String[] args) throws IOException { ChatServer server = new ChatServer(2024); + daisyConnect(); server.acceptClients(); } @@ -123,7 +125,7 @@ public class ChatServer { ChatClientHandler user = findClient(contact); if (!(user == null)) { try { - user.send(event.toString()); + user.send(event.toJson()); } catch (Exception e) { LOGGER.warning("!!Erreur de l'envoi d'Event à %s, fermeture de la connexion".formatted(user.user.getLogin())); user.close(); @@ -143,6 +145,12 @@ public class ChatServer { return contactMap; } + /** Temporaire : connecte daisy pour test */ + public static void daisyConnect() throws IOException { + ChatClient client = new ChatClient("localhost", 2024, null); + client.sendAuthEvent(new Contact("daisy", null)); + } + private class ChatClientHandler { public static final String END_MESSAGE = "fin"; /** @@ -229,14 +237,42 @@ public class ChatServer { private boolean handleEvent(String message) throws JSONException, IllegalStateException { Event event = Event.fromJson(message); - switch (event.getType()) { - case Event.AUTH: - doLogin(event.getContent()); - LOGGER.finest("Login successful"); - return true; - default: - LOGGER.warning("Unhandled event type: " + event.getType()); - return false; +// switch (event.getType()) { +// case Event.AUTH: +// doLogin(event.getContent()); +// LOGGER.finest("Login successful"); +// return true; +// case Event.LIST_CONTACTS: +// doListContact(event.getContent()); +// LOGGER.finest("Sending contacts"); +// default: +// LOGGER.warning("Unhandled event type: " + event.getType()); +// return false; +// } + if (event.getType().equals(Event.AUTH)) { + doLogin(event.getContent()); + LOGGER.finest("Login successful"); + return true; + } else if (event.getType().equals(Event.LIST_CONTACTS)) { + doListContact(event.getContent()); + LOGGER.finest("Sending contacts"); + } else { + LOGGER.warning("Unhandled event type: " + event.getType()); + return false; + + } + return false; + } + + private void doListContact(JSONObject content) throws JSONException, IllegalStateException { + for (Contact contact: contactMap.values()) { + if (contactMap.getContact(user.getLogin()).isConnected()) { + try { + send(new Event(Event.CONT, contact.toJsonObject()).toJson()); + } catch (IOException e) { + throw new IllegalStateException(); + } + } } } @@ -252,6 +288,10 @@ public class ChatServer { LOGGER.info("Connexion de " + login); contactMap.getContact(login).setConnected(true); this.user = contactMap.getContact(login); + sendAllOtherClients( + findClient(contactMap.getContact(login)), + new Event("CONT", user.toJsonObject()).toJson() + ); } } From 2f47614779c9ea81246ddd598b451a3f98fe081d Mon Sep 17 00:00:00 2001 From: bouclyma Date: Tue, 7 Jan 2025 11:51:33 +0100 Subject: [PATCH 06/10] =?UTF-8?q?feat(event):=202.6.5:=20R=C3=A9ception=20?= =?UTF-8?q?du=20post=20par=20le=20client=20(to=20debug)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/rtgre/chat/ChatController.java | 32 ++++++++++----- .../main/java/rtgre/chat/net/ChatClient.java | 5 +++ .../main/java/rtgre/server/ChatServer.java | 40 +++++++++++-------- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index 79b474d..5b2cd19 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -3,6 +3,7 @@ package rtgre.chat; import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.embed.swing.SwingFXUtils; @@ -110,6 +111,10 @@ public class ChatController implements Initializable { .decorates(loginTextField) .immediate(); + ObservableValue canSendCondition = connectionButton.selectedProperty().not() + .or(contactsListView.getSelectionModel().selectedItemProperty().isNull()); + sendButton.disableProperty().bind(canSendCondition); + messageTextField.disableProperty().bind(canSendCondition); /* /!\ Set-up d'environnement de test /!\ */ /* -------------------------------------- */ @@ -122,7 +127,8 @@ public class ChatController implements Initializable { String login = getSelectedContactLogin(); if (login != null) { Message message = new Message(login, messageTextField.getText()); - LOGGER.info(message.toString()); + LOGGER.info("Sending " + message); + client.sendMessageEvent(message); } } @@ -269,7 +275,8 @@ public class ChatController implements Initializable { if (contactSelected != null) { LOGGER.info("Clic sur " + contactSelected); } - Post postSys = new Post("system", contactSelected.getLogin(), "Bienvenue dans la discussion avec " + contactSelected.getLogin()); + Post postSys = new Post("system", loginTextField.getText(), "Bienvenue dans la discussion avec " + contactSelected.getLogin()); + postsObservableList.clear(); postsObservableList.add(postSys); postListView.refresh(); } @@ -277,23 +284,26 @@ public class ChatController implements Initializable { public void handleEvent(rtgre.modeles.Event event) { LOGGER.info("Received new event! : " + event); LOGGER.info(event.getType()); - -// switch (event.getType()) { -// case "CONT": -// handleContEvent(event.getContent()); -// default: -// LOGGER.warning("Unhandled event type: " + event.getType()); -// this.client.close(); -// } - if (event.getType().equals("CONT")) { handleContEvent(event.getContent()); + } else if (event.getType().equals("POST")) { + handlePostEvent(event.getContent()); } else { LOGGER.warning("Unhandled event type: " + event.getType()); this.client.close(); } } + private void handlePostEvent(JSONObject content) { + if (content.getString("from").equals(contactsListView.getSelectionModel().getSelectedItem()) || + content.getString("to").equals(loginTextField.getText())) { + postVector.add(Post.fromJson(content)); + postsObservableList.add(Post.fromJson(content)); + postListView.refresh(); + + } + } + private void handleContEvent(JSONObject content) { Contact contact = contactMap.getContact(content.getString("login")); if (contact != null) { diff --git a/chat/src/main/java/rtgre/chat/net/ChatClient.java b/chat/src/main/java/rtgre/chat/net/ChatClient.java index 4c60c58..f311385 100644 --- a/chat/src/main/java/rtgre/chat/net/ChatClient.java +++ b/chat/src/main/java/rtgre/chat/net/ChatClient.java @@ -5,6 +5,7 @@ import org.json.JSONObject; import rtgre.chat.ChatController; import rtgre.modeles.Contact; import rtgre.modeles.Event; +import rtgre.modeles.Message; import java.io.BufferedReader; import java.io.IOException; @@ -85,4 +86,8 @@ public class ChatClient extends ClientTCP { public ChatController getListener() { return listener; } + + public void sendMessageEvent(Message msg) { + sendEvent(new Event("MESG", msg.toJsonObject())); + } } diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index ec234f1..adbc87e 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -3,9 +3,7 @@ package rtgre.server; import org.json.JSONException; import org.json.JSONObject; import rtgre.chat.net.ChatClient; -import rtgre.modeles.Contact; -import rtgre.modeles.ContactMap; -import rtgre.modeles.Event; +import rtgre.modeles.*; import java.io.*; import java.net.ServerSocket; @@ -24,6 +22,7 @@ public class ChatServer { private static final Logger LOGGER = Logger.getLogger(ChatServer.class.getCanonicalName()); private Vector clientList; + private PostVector postVector; private ContactMap contactMap; static { @@ -237,18 +236,6 @@ public class ChatServer { private boolean handleEvent(String message) throws JSONException, IllegalStateException { Event event = Event.fromJson(message); -// switch (event.getType()) { -// case Event.AUTH: -// doLogin(event.getContent()); -// LOGGER.finest("Login successful"); -// return true; -// case Event.LIST_CONTACTS: -// doListContact(event.getContent()); -// LOGGER.finest("Sending contacts"); -// default: -// LOGGER.warning("Unhandled event type: " + event.getType()); -// return false; -// } if (event.getType().equals(Event.AUTH)) { doLogin(event.getContent()); LOGGER.finest("Login successful"); @@ -256,12 +243,31 @@ public class ChatServer { } else if (event.getType().equals(Event.LIST_CONTACTS)) { doListContact(event.getContent()); LOGGER.finest("Sending contacts"); + return true; + } else if (event.getType().equals(Event.MESG)) { + doMessage(event.getContent()); + return true; } else { LOGGER.warning("Unhandled event type: " + event.getType()); return false; - } - return false; + } + + private void doMessage(JSONObject content) throws JSONException, IllegalStateException { + if (content.getString("to").equals(user.getLogin()) || !contactMap.containsKey(content.getString("to"))) { + throw new IllegalStateException(); + } else { + Post post = new Post( + user.getLogin(), + Message.fromJson(content) + ); + Event postEvent = new Event("POST", post.toJsonObject()); + + sendEventToContact(contactMap.getContact(post.getFrom()), postEvent); + sendEventToContact(contactMap.getContact(post.getTo()), postEvent); + + postVector.add(post); + } } private void doListContact(JSONObject content) throws JSONException, IllegalStateException { From 48da92717baec0d8e86449a317423376e841533d Mon Sep 17 00:00:00 2001 From: Emi Boucly Date: Tue, 7 Jan 2025 12:55:10 +0100 Subject: [PATCH 07/10] =?UTF-8?q?feat(Event):=202.6.5:=20R=C3=A9ception=20?= =?UTF-8?q?des=20posts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/java/rtgre/chat/ChatController.java | 10 +++++++--- chat/src/main/java/rtgre/server/ChatServer.java | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index 5b2cd19..3f72050 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -286,7 +286,7 @@ public class ChatController implements Initializable { LOGGER.info(event.getType()); if (event.getType().equals("CONT")) { handleContEvent(event.getContent()); - } else if (event.getType().equals("POST")) { + } else if (event.getType().equals(rtgre.modeles.Event.POST)) { handlePostEvent(event.getContent()); } else { LOGGER.warning("Unhandled event type: " + event.getType()); @@ -295,11 +295,15 @@ public class ChatController implements Initializable { } private void handlePostEvent(JSONObject content) { - if (content.getString("from").equals(contactsListView.getSelectionModel().getSelectedItem()) || - content.getString("to").equals(loginTextField.getText())) { + System.out.println(content.getString("to").equals(((Contact) contactsListView.getSelectionModel().getSelectedItem()).getLogin())); + if (content.getString("to").equals(((Contact) contactsListView.getSelectionModel().getSelectedItem()).getLogin()) || + content.getString("from").equals(loginTextField.getText())) { postVector.add(Post.fromJson(content)); + System.out.println(postVector); postsObservableList.add(Post.fromJson(content)); postListView.refresh(); + System.out.println(postsObservableList); + System.out.println(postListView); } } diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index adbc87e..a8c4cc2 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -52,6 +52,7 @@ public class ChatServer { LOGGER.info("Serveur en écoute " + passiveSock); clientList = new Vector<>(); contactMap = new ContactMap(); + postVector = new PostVector(); contactMap.loadDefaultContacts(); } @@ -225,6 +226,7 @@ public class ChatServer { break; } } catch (Exception e) { + LOGGER.severe(e.getMessage()); break; } } @@ -246,6 +248,7 @@ public class ChatServer { return true; } else if (event.getType().equals(Event.MESG)) { doMessage(event.getContent()); + LOGGER.info("Receiving message"); return true; } else { LOGGER.warning("Unhandled event type: " + event.getType()); @@ -267,6 +270,7 @@ public class ChatServer { sendEventToContact(contactMap.getContact(post.getTo()), postEvent); postVector.add(post); + LOGGER.info("Fin de doMessage"); } } From 1732a63a4a5b65e13b7cb30097df50826b6b2bbe Mon Sep 17 00:00:00 2001 From: Emi Boucly Date: Tue, 7 Jan 2025 16:04:40 +0100 Subject: [PATCH 08/10] =?UTF-8?q?feat(Event):=202.6.5:=20R=C3=A9cup=C3=A9r?= =?UTF-8?q?ation=20des=20posts=20apr=C3=A8s=20changement=20de=20discussion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/rtgre/chat/ChatController.java | 1 + .../main/java/rtgre/chat/net/ChatClient.java | 10 +++++ .../main/java/rtgre/server/ChatServer.java | 45 ++++++++++++++----- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index 3f72050..514c994 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -278,6 +278,7 @@ public class ChatController implements Initializable { Post postSys = new Post("system", loginTextField.getText(), "Bienvenue dans la discussion avec " + contactSelected.getLogin()); postsObservableList.clear(); postsObservableList.add(postSys); + client.sendListPostEvent(0, contactSelected.getLogin()); postListView.refresh(); } diff --git a/chat/src/main/java/rtgre/chat/net/ChatClient.java b/chat/src/main/java/rtgre/chat/net/ChatClient.java index f311385..b9bb8b5 100644 --- a/chat/src/main/java/rtgre/chat/net/ChatClient.java +++ b/chat/src/main/java/rtgre/chat/net/ChatClient.java @@ -58,6 +58,16 @@ public class ChatClient extends ClientTCP { sendEvent(authEvent); } + public void sendListPostEvent(long since, String select) { + Event listPostEvent = new Event( + Event.LIST_POSTS, + new JSONObject() + .put("since", since) + .put("select", select) + ); + sendEvent(listPostEvent); + } + @Override public void receiveLoop() { diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index a8c4cc2..4cfb012 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -250,27 +250,48 @@ public class ChatServer { doMessage(event.getContent()); LOGGER.info("Receiving message"); return true; + } else if (event.getType().equals(Event.LIST_POSTS)) { + doListPost(event.getContent()); + LOGGER.info("Sending Posts"); + return true; } else { LOGGER.warning("Unhandled event type: " + event.getType()); return false; } } + private void doListPost(JSONObject content) throws JSONException, IllegalStateException { + + if (contactMap.getContact(user.getLogin()).isConnected()) { + if (!contactMap.containsKey(content.getString("select"))) { + throw new IllegalStateException(); + } + for (Post post: postVector.getPostsSince(content.getLong("since"))) { + if (post.getTo().equals(content.getString("select")) || + post.getFrom().equals(content.getString("select"))) { + sendEventToContact(contactMap.getContact(user.getLogin()), new Event(Event.POST, post.toJsonObject())); + } + } + } + } + private void doMessage(JSONObject content) throws JSONException, IllegalStateException { - if (content.getString("to").equals(user.getLogin()) || !contactMap.containsKey(content.getString("to"))) { - throw new IllegalStateException(); - } else { - Post post = new Post( - user.getLogin(), - Message.fromJson(content) - ); - Event postEvent = new Event("POST", post.toJsonObject()); + if (contactMap.getContact(user.getLogin()).isConnected()) { + if (content.getString("to").equals(user.getLogin()) || !contactMap.containsKey(content.getString("to"))) { + throw new IllegalStateException(); + } else { + Post post = new Post( + user.getLogin(), + Message.fromJson(content) + ); + Event postEvent = new Event("POST", post.toJsonObject()); - sendEventToContact(contactMap.getContact(post.getFrom()), postEvent); - sendEventToContact(contactMap.getContact(post.getTo()), postEvent); + sendEventToContact(contactMap.getContact(post.getFrom()), postEvent); + sendEventToContact(contactMap.getContact(post.getTo()), postEvent); - postVector.add(post); - LOGGER.info("Fin de doMessage"); + postVector.add(post); + LOGGER.info("Fin de doMessage"); + } } } From 0b513c19bcc51d6ac917ee11b7e6e17cc1f9b965 Mon Sep 17 00:00:00 2001 From: Emi Boucly Date: Tue, 7 Jan 2025 21:15:38 +0100 Subject: [PATCH 09/10] =?UTF-8?q?feat(Event):=202.6.6:=20Gestion=20des=20d?= =?UTF-8?q?=C3=A9connexions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat/src/main/java/rtgre/chat/ChatController.java | 9 ++++----- chat/src/main/java/rtgre/chat/net/ChatClient.java | 5 +++++ chat/src/main/java/rtgre/server/ChatServer.java | 5 +++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index 514c994..cf828ea 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -99,6 +99,7 @@ public class ChatController implements Initializable { avatarMenuItem.setOnAction(this::handleAvatarChange); avatarImageView.setOnMouseClicked(this::handleAvatarChange); sendButton.setOnAction(this::onActionSend); + messageTextField.setOnAction(this::onActionSend); initContactListView(); initPostListView(); @@ -157,7 +158,7 @@ public class ChatController implements Initializable { contactMap.put(this.contact.getLogin(), this.contact); LOGGER.info("Nouveau contact : " + contact); LOGGER.info(contactMap.toString()); - Matcher matcher = hostPortPattern.matcher("localhost:2024"); + Matcher matcher = hostPortPattern.matcher(hostComboBox.getValue()); matcher.matches(); String host = matcher.group(1); int port = (matcher.group(2) != null) ? Integer.parseInt(matcher.group(2)) : 2024; @@ -181,9 +182,9 @@ public class ChatController implements Initializable { connectionButton.setSelected(false); } } else if (!connectionButton.isSelected()) { + this.client.sendQuitEvent(); clearLists(); if (this.client.isConnected()) { - this.client.close(); this.contact.setConnected(false); } statusLabel.setText("not connected to " + hostComboBox.getValue()); @@ -300,11 +301,8 @@ public class ChatController implements Initializable { if (content.getString("to").equals(((Contact) contactsListView.getSelectionModel().getSelectedItem()).getLogin()) || content.getString("from").equals(loginTextField.getText())) { postVector.add(Post.fromJson(content)); - System.out.println(postVector); postsObservableList.add(Post.fromJson(content)); postListView.refresh(); - System.out.println(postsObservableList); - System.out.println(postListView); } } @@ -322,6 +320,7 @@ public class ChatController implements Initializable { content, new File("src/main/resources/rtgre/chat/avatars.png") ); + System.out.println(user.getAvatar()); contactMap.add(user); contactObservableList.add(user); LOGGER.info(contactMap.toString()); diff --git a/chat/src/main/java/rtgre/chat/net/ChatClient.java b/chat/src/main/java/rtgre/chat/net/ChatClient.java index b9bb8b5..aed444e 100644 --- a/chat/src/main/java/rtgre/chat/net/ChatClient.java +++ b/chat/src/main/java/rtgre/chat/net/ChatClient.java @@ -68,6 +68,11 @@ public class ChatClient extends ClientTCP { sendEvent(listPostEvent); } + public void sendQuitEvent() { + Event quitEvent = new Event(Event.QUIT, new JSONObject()); + sendEvent(quitEvent); + } + @Override public void receiveLoop() { diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java index 4cfb012..8a85ebd 100644 --- a/chat/src/main/java/rtgre/server/ChatServer.java +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -254,6 +254,9 @@ public class ChatServer { doListPost(event.getContent()); LOGGER.info("Sending Posts"); return true; + } else if (event.getType().equals(Event.QUIT)) { + LOGGER.info("Déconnexion"); + return false; } else { LOGGER.warning("Unhandled event type: " + event.getType()); return false; @@ -367,6 +370,8 @@ public class ChatServer { try { sock.close(); removeClient(this); + user.setConnected(false); + sendEventToAllContacts(new Event(Event.CONT, user.toJsonObject())); } catch (IOException e) { throw new RuntimeException(e); } From 19cbc815ab62c7b71b42f85604797f07178b3404 Mon Sep 17 00:00:00 2001 From: Emi Boucly Date: Wed, 8 Jan 2025 10:47:08 +0100 Subject: [PATCH 10/10] fix(ChatController): changement du chemin du fichier d'avatars --- chat/src/main/java/rtgre/chat/ChatController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index cf828ea..5e8e0cc 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -130,6 +130,7 @@ public class ChatController implements Initializable { Message message = new Message(login, messageTextField.getText()); LOGGER.info("Sending " + message); client.sendMessageEvent(message); + this.messageTextField.setText(""); } } @@ -318,7 +319,7 @@ public class ChatController implements Initializable { LOGGER.info(contactMap.toString()); Contact user = Contact.fromJSON( content, - new File("src/main/resources/rtgre/chat/avatars.png") + new File("chat/src/main/resources/rtgre/chat/avatars.png") ); System.out.println(user.getAvatar()); contactMap.add(user);