From 0d13bc526bd52719f254a2e2a190168178db1993 Mon Sep 17 00:00:00 2001 From: bouclyma Date: Wed, 11 Dec 2024 18:48:56 +0100 Subject: [PATCH 1/2] feat(net): ajout classes ChatServer et ClientTCP du TP9 --- .../main/java/rtgre/chat/net/ClientTCP.java | 186 ++++++++++++++++ .../main/java/rtgre/server/ChatServer.java | 210 ++++++++++++++++++ .../java/rtgre/chat/net/ClientTCPTest.java | 126 +++++++++++ .../java/rtgre/server/ChatServerTest.java | 7 + 4 files changed, 529 insertions(+) create mode 100644 chat/src/main/java/rtgre/chat/net/ClientTCP.java create mode 100644 chat/src/main/java/rtgre/server/ChatServer.java create mode 100644 chat/src/test/java/rtgre/chat/net/ClientTCPTest.java create mode 100644 chat/src/test/java/rtgre/server/ChatServerTest.java diff --git a/chat/src/main/java/rtgre/chat/net/ClientTCP.java b/chat/src/main/java/rtgre/chat/net/ClientTCP.java new file mode 100644 index 0000000..7cc9ed4 --- /dev/null +++ b/chat/src/main/java/rtgre/chat/net/ClientTCP.java @@ -0,0 +1,186 @@ +package rtgre.chat.net; + +import java.io.*; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.LogManager; +import static rtgre.chat.ChatApplication.LOGGER; + +/** + * Client TCP: envoie des chaines de caractères à un serveur et lit les chaines en retour. + *

+ * Serveur netcat à lancer en face : nc -k -l -p 2024 -v + */ +public class ClientTCP { + + public static final String RED = "\u001b[31m"; + public static final String BLUE = "\u001b[34m"; + public static final String RST = "\u001b[0m"; + public static final String END_MESSAGE = "fin"; + + static { + try { + InputStream is = ClientTCP.class.getClassLoader() + .getResource("logging.properties").openStream(); + LogManager.getLogManager().readConfiguration(is); + } catch (Exception e) { + LOGGER.log(Level.INFO, "Cannot read configuration file", e); + } + } + + public static void main(String[] args) throws Exception { + /* + ClientTCP client = new ClientTCP("localhost", 2024); + + // Essai simple d'émission / réception d'une chaine de caractères + String message = "Hello World!"; + System.out.println(BLUE + "Envoi :" + message + RST); + client.send(message); + message = client.receive(); + System.out.println(RED + "Réception: " + message + RST); + + client.close(); + */ + + ClientTCP client = new ClientTCP("localhost", 2024); + Thread envoi = new Thread(client::sendLoop); + Thread reception = new Thread(client::receiveLoop); + envoi.start(); + reception.start(); + envoi.join(); + client.close(); + } + + /** + * Socket connecté au serveur + */ + protected Socket sock; + + /** + * Flux de caractères UTF-8 en sortie + */ + protected PrintStream out; + + /** + * Flux de caractères UTF-8 en entrée + */ + protected BufferedReader in; + + /** + * Chaine de caractères "ip:port" du client + */ + protected String ipPort; + + protected boolean connected; + + /** + * Le constructeur ouvre la connexion TCP au serveur host:port + * et récupère les flux de caractères en entrée {@link #in} et sortie {@link #out} + *import static rtgre.chat.ChatApplication.LOGGER; + * @param host IP ou nom de domaine du serveur + * @param port port d'écoute du serveur + * @throws IOException + */ + public ClientTCP(String host, int port) throws IOException { + System.out.printf("Connexion à [%s:%d]%n", host, port); + sock = new Socket(host, port); + ipPort = "%s:%d".formatted(sock.getLocalAddress().getHostAddress(), sock.getLocalPort()); + LOGGER.info("[%s] Connexion établie vers [%s:%d]".formatted(ipPort, host, port)); + this.connected = true; + LOGGER.fine("[%s] Recuperation des flux d'octets en entree et sortie".formatted(ipPort)); + OutputStream os = sock.getOutputStream(); + InputStream is = sock.getInputStream(); + LOGGER.fine("[%s] Conversion flux d'octets en flux de caractères UTF-8".formatted(ipPort)); + out = new PrintStream(os, true, StandardCharsets.UTF_8); + in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8), 2048); + } + + /** + * Envoie une chaine de caractères + * + * @param message chaine de caractères à transmettre + * @throws IOException lorsqu'une erreur sur le flux de sortie est détectée + */ + public void send(String message) throws IOException { + LOGGER.finest("send: %s".formatted(message)); + out.println(message); + if (out.checkError()) { + throw new IOException("Output stream error"); + } + } + + public boolean isConnected() { + return connected; + } + + public void setConnected(boolean connected) { + this.connected = connected; + } + + /** + * Attente d'une chaine de caractères en entrée. + * + * @return chaine de caractères reçue + * @throws IOException lorsque la fin du flux est atteinte + */ + public String receive() throws IOException { + String message = in.readLine(); + LOGGER.finest("receive: %s".formatted(message)); + if (message == null) { + throw new IOException("End of the stream has been reached"); + } + return message; + } + + /** + * Fermeture de la connexion TCP + */ + public void close() { + LOGGER.info("[%s] Fermeture de la connexion".formatted(ipPort)); + try { + sock.close(); + this.connected = false; + } catch (IOException e) { + LOGGER.finest("[%s] %s".formatted(ipPort, e)); + } + } + + + public void sendLoop() { + System.out.println(BLUE + "Boucle d'envoi de messages..." + RST); + BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in)); + connected = true; + try { + while (connected) { + System.out.println(BLUE + "Votre message (\"fin\" pour terminer) : " + RST); + String message = stdIn.readLine(); + 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 receiveLoop() { + System.out.println(RED + "Boucle de réception de messages..." + RST); + connected = true; + try { + while (connected) { + String message = this.receive(); + System.out.println(RED + "Réception: " + message + RST); + } + } catch (IOException e) { + LOGGER.severe("[%s] %s".formatted(ipPort, e)); + connected = false; + } + } +} \ No newline at end of file diff --git a/chat/src/main/java/rtgre/server/ChatServer.java b/chat/src/main/java/rtgre/server/ChatServer.java new file mode 100644 index 0000000..3665663 --- /dev/null +++ b/chat/src/main/java/rtgre/server/ChatServer.java @@ -0,0 +1,210 @@ +package rtgre.server; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Vector; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; + +/** + * Programme serveur qui renvoie les chaines de caractères lues jusqu'à recevoir le message "fin" + */ +public class ChatServer { + + private static final Logger LOGGER = Logger.getLogger(ChatServer.class.getCanonicalName()); + + private Vector clientList; + + static { + try { + InputStream is = ChatServer.class.getClassLoader() + .getResource("logging.properties").openStream(); + LogManager.getLogManager().readConfiguration(is); + } catch (Exception e) { + LOGGER.log(Level.INFO, "Cannot read configuration file", e); + } + } + + + public static void main(String[] args) throws IOException { + ChatServer server = new ChatServer(2024); + server.acceptClients(); + } + + /** + * Socket passif en écoute + */ + private ServerSocket passiveSock; + + public ChatServer(int port) throws IOException { + passiveSock = new ServerSocket(port); + LOGGER.info("Serveur en écoute " + passiveSock); + clientList = new Vector<>(); + } + + public void close() throws IOException { + for (ChatClientHandler client : clientList) { + client.close(); + } + passiveSock.close(); + } + + /** + * Boucle d'attente des clients + */ + public void acceptClients() { + int clientCounter = 1; + while (true) { + try { + LOGGER.info("Attente du client n°%02d".formatted(clientCounter)); + Socket sock = passiveSock.accept(); + LOGGER.info("[%s:%d] Connexion établie (client n°%02d)" + .formatted(sock.getInetAddress().getHostAddress(), sock.getPort(), clientCounter)); + handleNewClient(sock); + } catch (IOException e) { + LOGGER.severe(e.toString()); + } + clientCounter++; + } + } + + public void removeClient(ChatClientHandler client) { + clientList.remove(client); + LOGGER.fine("Client [%s] retiré de la liste (%d clients connectés)" + .formatted(client.getIpPort(), clientList.size())); + } + + public Vector getClientList() { + return clientList; + } + + public ServerSocket getPassiveSocket() { + return passiveSock; + } + + /** + * Prise en charge d'un nouveau client + * + * @param sock socket connecté au client + * @throws IOException + */ + private void handleNewClient(Socket sock) throws IOException { + ChatClientHandler client = new ChatClientHandler(sock); + Thread clientLoop = new Thread(client::echoLoop); + clientLoop.start(); + clientList.add(client); + LOGGER.fine("Ajout du client [%s] dans la liste (%d clients connectés)" + .formatted(client.getIpPort(), clientList.size())); + //client.echoLoop(); + } + + private class ChatClientHandler { + public static final String END_MESSAGE = "fin"; + /** + * Socket connecté au client + */ + private Socket sock; + /** + * Flux de caractères en sortie + */ + private PrintStream out; + /** + * Flux de caractères en entrée + */ + private BufferedReader in; + /** + * Chaine de caractères "ip:port" du client + */ + private String ipPort; + + /** + * Initialise les attributs {@link #sock} (socket connecté au client), + * {@link #out} (flux de caractères UTF-8 en sortie) et + * {@link #in} (flux de caractères UTF-8 en entrée). + * + * @param sock socket connecté au client + * @throws IOException + */ + public ChatClientHandler(Socket sock) throws IOException { + this.sock = sock; + this.ipPort = "%s:%d".formatted(sock.getInetAddress().getHostAddress(), sock.getPort()); + OutputStream os = sock.getOutputStream(); + InputStream is = sock.getInputStream(); + this.out = new PrintStream(os, true, StandardCharsets.UTF_8); + this.in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + } + + /** + * Boucle écho : renvoie tous les messages reçus. + */ + public void echoLoop() { + 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)); + //out.println(message); + sendAllOtherClients(this, message); + + } + } catch (IOException e) { + LOGGER.severe("[%s] %s".formatted(ipPort, e)); + } + close(); + } + + public void send(String message) throws IOException { + LOGGER.finest("send: %s".formatted(message)); + out.println(message); + if (out.checkError()) { + throw new IOException("Output stream error"); + } + } + + public String getIpPort() { + return ipPort; + } + + public void sendAllOtherClients(ChatClientHandler fromClient, String message) { + for (ChatClientHandler client : clientList) { + if (!client.equals(this)) { + LOGGER.fine(clientList.toString()); + LOGGER.fine("Envoi vers [%s] : %s".formatted(client.getIpPort(), message)); + try { + client.send("[%s] %s".formatted(fromClient.getIpPort(), message)); + } catch (Exception e) { + LOGGER.severe("[%s] %s".formatted(client.getIpPort(), e)); + client.close(); + } + } + } + } + + public String receive() throws IOException { + String message = in.readLine(); + LOGGER.finest("receive: %s".formatted(message)); + if (message == null) { + throw new IOException("End of the stream has been reached"); + } + return message; + } + + public void close() { + LOGGER.info("[%s] Fermeture de la connexion".formatted(ipPort)); + try { + sock.close(); + removeClient(this); + } catch (IOException e) { + throw new RuntimeException(e); + } + + } + } +} \ No newline at end of file diff --git a/chat/src/test/java/rtgre/chat/net/ClientTCPTest.java b/chat/src/test/java/rtgre/chat/net/ClientTCPTest.java new file mode 100644 index 0000000..0b33469 --- /dev/null +++ b/chat/src/test/java/rtgre/chat/net/ClientTCPTest.java @@ -0,0 +1,126 @@ +package rtgre.chat.net; + +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 rtgre.chat.net.ClientTCP; + + +import java.io.IOException; +import java.lang.reflect.*; +import java.net.ServerSocket; +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; + +class ClientTCPTest { + + static Class classe = ClientTCP.class; + static String module = "rtgre.chat.net"; + + + @DisplayName("01-Structure de ClientTCP") + @Nested + class StructureTest { + static Method[] methodes; + static List methodesSignatures; + static List constructeursSignatures; + + @BeforeAll + static void init() { + // Les méthodes + methodes = classe.getDeclaredMethods(); + methodesSignatures = Arrays.asList(methodes).stream().map(e -> e.toString()).collect(Collectors.toList()); + Constructor[] constructeurs = classe.getConstructors(); + constructeursSignatures = Arrays.asList(constructeurs).stream().map(e -> e.toString()).collect(Collectors.toList()); + } + + + static Stream attributsProvider() { + return Stream.of( + arguments("sock", "java.net.Socket", Modifier.PROTECTED), + arguments("out", "java.io.PrintStream", Modifier.PROTECTED), + arguments("in", "java.io.BufferedReader", Modifier.PROTECTED), + arguments("ipPort", "java.lang.String", Modifier.PROTECTED), + arguments("connected", "boolean", Modifier.PROTECTED) + ); + } + @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.ClientTCP(java.lang.String,int) throws java.io.IOException") + ); + } + @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("isConnected", "public boolean %s.ClientTCP.isConnected()"), + arguments("send", "public void %s.ClientTCP.send(java.lang.String) throws java.io.IOException"), + arguments("receive", "public java.lang.String %s.ClientTCP.receive() throws java.io.IOException"), + arguments("close", "public void %s.ClientTCP.close()") + ); + } + @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-Connexion/déconnexion (port=1800)") + @Nested + class ConnexionTest { + static int port = 1800; + static ServerSocket passiveSocket; + + @BeforeAll + static void init() throws IOException { + passiveSocket = new ServerSocket(1800); + } + + @AfterAll + static void close() throws IOException { + passiveSocket.close(); + } + + @DisplayName("Connexion+deconnexion") + @Test + void testConnexion() throws IOException { + ClientTCP client = new ClientTCP("localhost", port); + Assertions.assertNotNull(client, "Connexion impossible"); + Assertions.assertTrue(client.isConnected(), "Etat de connexion erroné"); + client.close(); + Assertions.assertFalse(client.isConnected(), "Etat de connexion erroné"); + } + + } + +} \ No newline at end of file diff --git a/chat/src/test/java/rtgre/server/ChatServerTest.java b/chat/src/test/java/rtgre/server/ChatServerTest.java new file mode 100644 index 0000000..da1a615 --- /dev/null +++ b/chat/src/test/java/rtgre/server/ChatServerTest.java @@ -0,0 +1,7 @@ +package rtgre.server; + +import static org.junit.jupiter.api.Assertions.*; + +class ChatServerTest { + +} \ No newline at end of file From 8ec377bec03ad5c8571b8cd0636f97165f23a125 Mon Sep 17 00:00:00 2001 From: bouclyma Date: Sat, 28 Dec 2024 01:54:36 +0100 Subject: [PATCH 2/2] =?UTF-8?q?feat(net):=20Modification=20m=C3=A9thode=20?= =?UTF-8?q?handleConnection,=20fin=20de=20ChatClient=20et=20de=20ClientTCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/rtgre/chat/ChatController.java | 46 ++++- .../main/java/rtgre/chat/net/ChatClient.java | 53 ++++++ .../main/java/rtgre/chat/net/ClientTCP.java | 10 +- .../resources/rtgre/chat/logging.properties | 2 +- .../java/rtgre/chat/net/ChatClientTest.java | 169 ++++++++++++++++++ 5 files changed, 269 insertions(+), 11 deletions(-) create mode 100644 chat/src/main/java/rtgre/chat/net/ChatClient.java create mode 100644 chat/src/test/java/rtgre/chat/net/ChatClientTest.java diff --git a/chat/src/main/java/rtgre/chat/ChatController.java b/chat/src/main/java/rtgre/chat/ChatController.java index aac96e3..4ea8062 100644 --- a/chat/src/main/java/rtgre/chat/ChatController.java +++ b/chat/src/main/java/rtgre/chat/ChatController.java @@ -24,10 +24,8 @@ import net.synedra.validatorfx.TooltipWrapper; import net.synedra.validatorfx.Validator; import rtgre.chat.graphisme.ContactListViewCell; import rtgre.chat.graphisme.PostListViewCell; -import rtgre.modeles.Contact; -import rtgre.modeles.ContactMap; -import rtgre.modeles.Message; -import rtgre.modeles.Post; +import rtgre.chat.net.ChatClient; +import rtgre.modeles.*; import java.awt.image.BufferedImage; import java.io.File; @@ -38,6 +36,7 @@ import java.util.Date; import java.util.Objects; import java.util.ResourceBundle; import java.util.logging.Logger; +import java.util.regex.Matcher; import java.util.regex.Pattern; import static rtgre.chat.ChatApplication.LOGGER; @@ -46,10 +45,11 @@ public class ChatController implements Initializable { private static final Pattern LOGIN_PATTERN = Pattern.compile("^([a-z][a-z0-9]{2,7})$"); private static final Pattern HOST_PATTERN = Pattern.compile("/^[a-z]*((\\:?)\\d{1,5})?$/gm"); + private final Pattern hostPortPattern = Pattern.compile("^([-.a-zA-Z0-9]+)(?::([0-9]{1,5}))?$"); public MenuItem hostAddMenuItem; public MenuItem avatarMenuItem; public MenuItem aboutMenuItem; - public ComboBox hostComboBox; + public ComboBox hostComboBox; public TextField loginTextField; public ToggleButton connectionButton; public ImageView avatarImageView; @@ -66,6 +66,8 @@ public class ChatController implements Initializable { private ObservableList contactObservableList = FXCollections.observableArrayList(); private ObservableList postsObservableList = FXCollections.observableArrayList(); Validator validatorLogin = new Validator(); + private ChatClient client = null; + private PostVector postVector; @Override @@ -147,7 +149,38 @@ 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.matches(); + String host = matcher.group(1); + int port = (matcher.group(2) != null) ? Integer.parseInt(matcher.group(2)) : 2024; + try { + this.client = new ChatClient(host, port, this); + initContactListView(); + initPostListView(); + clearLists(); + contactMap.add(this.contact); + this.contact.setConnected(true); + initContactListView(); + initPostListView(); + this.statusLabel.setText("Connected to %s@%s:%s".formatted(this.contact.getLogin(), host, port)); + } 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); + statusLabel.setText("not connected to " + hostComboBox.getValue()); } + + } + + private void clearLists() { + this.contactMap = new ContactMap(); + this.postVector = new PostVector(); + contactObservableList.clear(); + postsObservableList.clear(); } private void checkLogin(Check.Context context) { @@ -185,10 +218,7 @@ public class ChatController implements Initializable { contactsListView.setCellFactory(contactListView -> new ContactListViewCell()); contactsListView.setItems(contactObservableList); File avatars = new File(getClass().getResource("avatars.png").toURI()); - Contact riri = new Contact("riri", false, avatars); Contact fifi = new Contact("fifi", true, avatars); - contactObservableList.add(riri); - contactMap.add(riri); contactObservableList.add(fifi); contactMap.add(fifi); } catch (Exception e) { diff --git a/chat/src/main/java/rtgre/chat/net/ChatClient.java b/chat/src/main/java/rtgre/chat/net/ChatClient.java new file mode 100644 index 0000000..009ffb6 --- /dev/null +++ b/chat/src/main/java/rtgre/chat/net/ChatClient.java @@ -0,0 +1,53 @@ +package rtgre.chat.net; + +import rtgre.chat.ChatController; + +import java.io.IOException; +import java.util.logging.Logger; + +import static rtgre.chat.ChatApplication.LOGGER; + +public class ChatClient extends ClientTCP { + + private final ChatController listener; + + /** + * Le constructeur ouvre la connexion TCP au serveur host:port + * et récupère les flux de caractères en entrée {@link #in} et sortie {@link #out} + * import static rtgre.chat.ChatApplication.LOGGER; + * + * @param host IP ou nom de domaine du serveur + * @param port port d'écoute du serveur + * @param listener instance de ChatController liée au client + * @throws IOException + */ + public ChatClient(String host, int port, ChatController listener) throws IOException { + super(host, port); + this.listener = listener; + } + + @Override + public void receiveLoop() { + LOGGER.info(RED + "Boucle de réception de messages..." + RST); + try { + while (connected) { + String message = this.receive(); + LOGGER.info(RED + "Réception: " + message + RST); + LOGGER.info(RED + message + RST); + } + } catch (IOException e) { + LOGGER.severe("[%s] %s".formatted(ipPort, e)); + connected = false; + } finally { + close(); + } + } + + public Logger getLogger() { + return LOGGER; + } + + public ChatController getListener() { + return listener; + } +} diff --git a/chat/src/main/java/rtgre/chat/net/ClientTCP.java b/chat/src/main/java/rtgre/chat/net/ClientTCP.java index 7cc9ed4..097a948 100644 --- a/chat/src/main/java/rtgre/chat/net/ClientTCP.java +++ b/chat/src/main/java/rtgre/chat/net/ClientTCP.java @@ -1,5 +1,7 @@ package rtgre.chat.net; +import rtgre.chat.ChatController; + import java.io.*; import java.net.Socket; import java.nio.charset.StandardCharsets; @@ -19,15 +21,16 @@ public class ClientTCP { public static final String RST = "\u001b[0m"; public static final String END_MESSAGE = "fin"; + /* static { try { InputStream is = ClientTCP.class.getClassLoader() - .getResource("logging.properties").openStream(); + .getResource("rtgre/chat/logging.properties").openStream(); LogManager.getLogManager().readConfiguration(is); } catch (Exception e) { LOGGER.log(Level.INFO, "Cannot read configuration file", e); } - } + }*/ public static void main(String[] args) throws Exception { /* @@ -94,6 +97,9 @@ public class ClientTCP { LOGGER.fine("[%s] Conversion flux d'octets en flux de caractères UTF-8".formatted(ipPort)); out = new PrintStream(os, true, StandardCharsets.UTF_8); in = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8), 2048); + Thread rcLoop = new Thread(this::receiveLoop); + rcLoop.setDaemon(true); + rcLoop.start(); } /** diff --git a/chat/src/main/resources/rtgre/chat/logging.properties b/chat/src/main/resources/rtgre/chat/logging.properties index df9bc2f..3c0fe74 100644 --- a/chat/src/main/resources/rtgre/chat/logging.properties +++ b/chat/src/main/resources/rtgre/chat/logging.properties @@ -9,7 +9,7 @@ handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter # Nom du fichier de logs -java.util.logging.FileHandler.pattern=../out/java%u.log +java.util.logging.FileHandler.pattern=../target/java%u.log # Format de logs plus compact sur 1 seule ligne java.util.logging.SimpleFormatter.format=%1$tF %1$tT.%1$tL | %4$-7s | %2$s | %5$s %6$s%n diff --git a/chat/src/test/java/rtgre/chat/net/ChatClientTest.java b/chat/src/test/java/rtgre/chat/net/ChatClientTest.java new file mode 100644 index 0000000..72bc4e6 --- /dev/null +++ b/chat/src/test/java/rtgre/chat/net/ChatClientTest.java @@ -0,0 +1,169 @@ +package rtgre.chat.net; + + +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.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import java.util.logging.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class ChatClientTest { + + static Class classe = ChatClient.class; + static String module = "rtgre.chat.net"; + + @DisplayName("01-Structure de ChatClient") + @Nested + class StructureTest { + + static List methodesSignatures; + static List constructeursSignatures; + + @BeforeAll + static void init() { + // Les méthodes + Method[] methodes = classe.getDeclaredMethods(); + methodesSignatures = Arrays.asList(methodes).stream().map(e -> e.toString()).collect(Collectors.toList()); + Constructor[] constructeurs = classe.getConstructors(); + constructeursSignatures = Arrays.asList(constructeurs).stream().map(e -> e.toString()).collect(Collectors.toList()); + } + + + static Stream attributsProvider() { + return Stream.of( + arguments("listener", "rtgre.chat.ChatController", 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.ChatClient(java.lang.String,int,rtgre.chat.ChatController) throws java.io.IOException") + ); + } + @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("receiveLoop", "public void %s.ChatClient.receiveLoop()"), + arguments("getLogger", "public java.util.logging.Logger %s.ChatClient.getLogger()") + ); + } + @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-Connexion/déconnexion (port=1810)") + @Nested + class ConnexionTest { + static int port = 1810; + static LocalServer server; + static ExecutorService executorService; + + @BeforeAll + static void init() throws IOException { + server = new LocalServer(port); + executorService = new ThreadPoolExecutor(1, 1, 2, + TimeUnit.SECONDS, new LinkedBlockingDeque()); + executorService.submit(server::acceptClients); + } + + @AfterAll + static void close() throws IOException { + server.passiveSocket.close(); + executorService.shutdown(); + } + + @DisplayName("Boucle de réception") + @Test + void testReceiveLoop() throws IOException, InterruptedException { + + ChatClient client = new ChatClient("localhost", port, null); + Logger logger = client.getLogger(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Handler handler = new StreamHandler(out, new SimpleFormatter()); + logger.addHandler(handler); + + + server.clientOut.println("bonjour"); + executorService.awaitTermination(1, TimeUnit.SECONDS); + handler.flush(); + String logMsg = out.toString(); + client.close(); + Assertions.assertTrue(logMsg.contains("bonjour"), + "Le message reçu doit être loggué"); + } + + } + +} + +class LocalServer { + public ServerSocket passiveSocket; + public Socket clientSocket; + public PrintStream clientOut; + public BufferedReader clientIn; + public boolean stop = false; + + public LocalServer(int port) throws IOException { + passiveSocket = new ServerSocket(port); + } + + public void acceptClients() { + while (!stop) { + try { + clientSocket = passiveSocket.accept(); + clientOut = new PrintStream(clientSocket.getOutputStream(), true, StandardCharsets.UTF_8); + clientIn = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8), 2048); + } catch (IOException e) { + stop = true; + } + } + } +} \ No newline at end of file