:: Enseignements :: Master :: M1 :: 2022-2023 :: Java Avancé ::
[LOGO]

Sed, the stream editor


Interface, named type (class, record) et unnamed type (lambda)
Le but de ce TP est d'implanter une petite partie des commandes de l'outil sed.

Exercice 1 - Maven

Comme pour le TP précédent, nous allons utiliser Maven avec une configuration (le pom.xml) très similaire.
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>fr.uge.sed</groupId>
    <artifactId>sed</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.9.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <release>19</release>
                    <compilerArgs>
                        <compilerArg>--enable-preview</compilerArg>
                    </compilerArgs>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
                <configuration>
                    <argLine>--enable-preview</argLine>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
   
Créer un projet Maven en cochant create simple project au niveau du premier écran, puis passer à l'écran suivant en indiquant Next.
Pour ce TP, le groupId est fr.uge.sed , l'artefactId est sed et la version est 0.0.1-SNAPSHOT. Pour finir, cliquer sur Finish.

Exercice 2 - Astra inclinant, sed non obligant

Le but de cet exercice est de créer un petit éditeur comme sed.
Pour ceux qui ne connaîtraient pas sed, c'est un utilitaire en ligne de commande qui prend en entrée un fichier et génère en sortie un nouveau fichier en effectuant des transformations ligne à ligne. sed permet facilement de supprimer une ligne soit spécifiée par son numéro, soit si elle contient une expression régulière ou de remplacer un mot (en fait une regex) par un mot.
L'utilitaire sed traite le fichier ligne à ligne, il ne stocke pas tout le fichier en mémoire (ce n'était pas une solution viable à la création de sed en 1974). On parle de traitement en flux, en stream en Anglais, d'où le nom de Stream EDitor, sed.

Les tests JUnit 5 de cet exercice sont StreamEditorTest.java.

  1. Dans un premier temps, on va créer une classe StreamEditor dans le package fr.uge.sed avec une méthode d'instance transform qui prend en paramètre un LineNumberReader et un Writer et écrit, ligne à ligne, le contenu du LineNumberReader dans le Writer.
    Rappel, un BufferedReader possède une méthode readLine() et un Writer une méthode append().
    Comme on veut que le programme fonctionne de la même façon, quelle que soit la plate-forme, le retour à la ligne écrit dans le Writer est toujours '\n'.
    Vérifier que les tests JUnit marqués "Q1" passent.

  2. On veut maintenant pouvoir spécifier une commande à la création du StreamEditor pour transformer les lignes du fichier en entrée. Ici, lineDelete renvoie un record LineDeleteCommand qui indique la ligne à supprimer (la première ligne d'un fichier est 1, pas 0).
    L'exemple ci-dessous montre comment supprimer la ligne 2 d'un fichier.
            var command = StreamEditor.lineDelete(2);
            var editor = new StreamEditor(command);
            editor.transform(reader, writer);
        
    L'idée est la suivante : on parcourt le fichier ligne à ligne comme précédemment, mais si le numéro de la ligne courante est égal à celui de la ligne à supprimer, alors on ne l'écrit pas dans le Writer.
    Vérifier que les tests JUnit marqués "Q2" passent.
    Note : LineNumberReader compte le nombre de lignes.
    Note 2 : Il faut que le code utilisant le constructeur sans paramètre continue de fonctionner.

  3. (À la maison) On souhaite maintenant écrire un main qui prend en paramètre sur la ligne de commande un nom de fichier, supprime la ligne 2 de ce fichier et écrit le résultat sur la sortie standard.
    Vérifier que les tests JUnit marqués "Q3" passent.
    Note : on présupposera que le fichier et la sortie standard utilisent l'encodage UTF-8 (StandardCharsets.UTF_8)
    Note 2 : pour transformer un OutputStream (un PrintStream est une sorte d'OutputStream) en Writer, on utilise un OutputStreamWriter et comme on veut spécifier l'encodage, on va utiliser le constructeur qui prend aussi un Charset en paramètre.
    Rappel : vous devez utiliser un try-with-resources pour fermer correctement les ressources ouvertes.

  4. On souhaite introduire une nouvelle commande qui permet de supprimer une ligne si elle contient une expression régulière. Avant de le coder, on va faire un peu de re-factoring pour préparer le fait que l'on puisse choisir entre des commandes différentes.
    L'idée est que chaque commande doit posséder une méthode que l'on pourra appeler avec la ligne courante (une String) et son numéro et qui renvoie une action à effectuer. L'action peut être soit DELETE pour indiquer que la ligne ne doit pas être affichée, soit PRINT pour indiquer que la ligne doit être affichée.
    Pour cela, on va utiliser l'enum suivant
           enum Action {
             DELETE, PRINT
           }
         
    On pourrait utiliser un booléen comme type de retour, mais DELETE/PRINT c'est plus parlant que true/false.
    Changer le code pour que le record LineDeleteCommand possède la méthode décrite plus haut et changer le code de transform pour qu'elle appelle cette méthode.
    Vérifier que les tests JUnit marqués "Q4" passent.
    Note : au lieu de mettre l'enum dans un fichier .java dans le même package, on va le ranger dans StreamEditor. En Java, mettre un enum, un record ou une interface dans une classe ne pose pas de problème, par contre mettre une classe dans une classe est un peu plus compliqué, on verra ça plus tard.
    Note 2 : si vous ne comprenez pas pourquoi la méthode doit aussi prendre en paramètre la ligne elle-même, lisez la question suivante...

  5. Maintenant que l'on a bien préparé le terrain, on peut ajouter une nouvelle commande renvoyée par la méthode findAndDelete qui prend en paramètre un java.util.regex.Pattern telle que le code suivant fonctionne
            var command = StreamEditor.findAndDelete(Pattern.compile("foo|bar"));
            var editor = new StreamEditor(command);
            editor.transform(reader, writer);
         

    Faite les changements qui s'imposent puis vérifier que les tests JUnit marqués "Q5" passent.
    Rappel : pour voir si un texte contient un motif pattern, on instancie un Matcher du motif sur le texte et on utilise la méthode find sur ce Matcher.

  6. En fait, cette implantation n'est pas satisfaisante, car les records LineDeleteCommand et FindAndDeleteCommand ont beaucoup de code qui ne sert à rien. Il serait plus simple de les transformer en lambdas, car la véritable information intéressante est comment effectuer la transformation d'une ligne.
    Modifier votre code pour que les implantations des commandes renvoyées par les méthodes lineDelete et findAndDelete soit des lambdas.
    Vérifier que les tests JUnit marqués "Q6" passent.

  7. On souhaite maintenant introduire une commande substitute(pattern, replacement) qui dans une ligne remplace toutes les occurrences du motif par une chaîne de caractère de remplacement. Malheureusement, notre enum Action n'est pas à même de gérer ce cas, car il faut que la commande puisse renvoyer PRINT mais avec une nouvelle ligne.
    On se propose pour cela de remplacer l'enum Action par une interface et DELETE et PRINT par des records implantant cette interface comme ceci
           private interface Action {
             record DeleteAction() implements Action {}
             record PrintAction(String text) implements Action {}
           }
         

    Modifier votre code sans introduire pour l'instant la commande substitute pour utiliser l'interface Action au lieu de l'enum.
    Vérifier que les tests JUnit marqués "Q7" passent.
    Rappel : on peut faire un switch sur des objets (des Actions) en Java.

  8. On peut enfin ajouter la commande substitute(pattern, replacement) telle que le code suivant fonctionne
           var command = StreamEditor.substitute(Pattern.compile("foo|bar"), "hello");
           var editor = new StreamEditor(command);
           editor.transform(reader, writer);
         
    Écrire le code de la méthode substitute et vérifier que les tests JUnit marqués "Q8" passent.

  9. Optionnellement, créer une DeleteAction avec un new semble bizarre car une DeleteAction est un record qui n'a pas de composant donc on pourrait toujours utiliser la même instance.
    Comment faire tout en gardant le record DeleteAction pour éviter de faire un new à chaque fois que l'on veut avoir une instance de DeleteAction ?
    En fait, il y a une façon plus élégante de faire la même chose que la précédente en transformant DeleteAction en enum (chercher "enum singleton java" sur internet).
    Vérifier que les tests JUnit marqués "Q9" passent.

  10. Enfin, on peut vouloir combiner plusieurs commandes en ajoutant une méthode andThen à Command tel que le code suivant fonctionne.
           var command1 = StreamEditor.substitute(Pattern.compile("foo"), "hello");
           var command2 = StreamEditor.findAndDelete(Pattern.compile("baz"));
           var editor = new StreamEditor(command1.andThen(command2));
           editor.transform(reader), writer);
         
    andThen doit appliquer la première commande puis la seconde (dans cet ordre).
    Modifier Command pour introduire la méthode d'instance andThen.
    Vérifier que les tests JUnit marqués "Q10" passent.

  11. En conclusion, dans quel cas, à votre avis, va-t-on utiliser des records pour implanter de différentes façons une interface et dans quel cas va-t-on utiliser des lambdas ?