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

Sed, the stream editor


Interface, lambda, Optional, méthode statique et par défaut, gestion des entrées/sorties.
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.10.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</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, et ce n'est toujours pas une solution viable maintenant que l'on peut avoir des fichiers de plusieurs centaines de giga-octets.
On parle de traitement en flux, en stream en Anglais, d'où le nom de Stream EDitor, sed.

Le stream editor que nous allons créer prend un ensemble de règles (rules) et transforme chaque ligne du fichier suivant les règles.
Voici le main du programme avec l'aide qui explique les différentes règles et comment on peut les composer.
  public static void main(String[] args) {
    if (args.length != 3) {
      System.err.println("""
        stream-editor rules input.txt output.txt

          rules:
            s          strip whitespaces
            u          upper case
            l          lower case
            d          delete
            su         strip and upper case
            i=;d       if is empty delete
            i=foo;d    if equals "foo" delete
            iu=FOO;d   if upper case equals "FOO" delete
            isu=FOO;d  if strip upper case equals "FOO" delete
            i=h.*;d    if starts with "h" delete
            i=a|b;d    if "a" or "b" delete
            is=;d      if strip is empty delete
        """);
      System.exit(1);
      return;
    }
    var rule = StreamEditor.createRules(args[0]);
    var inputPath = Path.of(args[1]);
    var outputPath = Path.of(args[2]);

    var editor = new StreamEditor(rule);
    try {
      editor.rewrite(inputPath, outputPath);
    } catch (IOException e) {
      System.err.println("error " + e.getMessage());
      System.exit(2);
    }
  }
   
La classe StreamEditor possède
  • Une méthode createRules qui prend les règles sous forme d'une chaine de caractères et renvoie un objet de type Rule. Rule peut représenter à la fois une règle ou une composition de règles.
  • Un constructeur qui prend une règle (Rule).
  • Une méthode rewrite qui prend en paramètre deux fichiers (input et output) et pour chaque ligne du fichier input, transforme celle-ci en utilisant les règles et écrit le résultat (s'il y en a un) dans le fichier output.

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

  1. On va dans un premier temps définir une interface Rule qui va représenter une règle. Une règle prend en entrée une ligne (une String) et renvoie soit une nouvelle ligne soit rien (on peut supprimer une ligne).
    Rappeler comment on indique, en Java, qu'une méthode peut renvoyer quelque chose ou rien ?
    À l'intérieur de la classe StreamEditor, créer l'interface Rule avec sa méthode rewrite.
    Vérifier que les tests marqués "Q1" passent.

  2. Avant de créer, dans StreamEditor, la méthode rewrite qui prend deux fichiers, on va créer une méthode rewrite intermédiaire qui travaille sur des flux de caractères. On souhaite écrire une méthode rewrite(reader, writer) qui prend en paramètre un BufferedReader (qui possède une méthode readLine()) ainsi qu'un Writer qui possède la méthode write(String).
    Comment doit-on gérer l'IOException ?
    Écrire la classe StreamEditor avec son constructeur qui se contente de stocker la règle prise en paramètre. Ajouter la méthode rewrite(reader, writer) qui, pour chaque ligne du reader, applique la règle puis écrit le résultat, s'il existe, dans le writer.
    Vérifier que les tests marqués "Q2" passent.

    Note : on va utiliser "\n" comme séparateur de lignes, ainsi on aura le même comportement quelque soit l'OS sur lequel l'application s'exécute.

  3. On souhaite créer la méthode rewrite(input, output) qui prend deux fichiers (pour être exact, deux chemins vers les fichiers) en paramètre et applique la règle sur les lignes du fichier input et écrit le résultat dans le fichier output.
    Comment faire en sorte que les fichiers ouverts soit correctement fermés ?
    Comment doit-on gérer l'IOException ?
    Écrire la méthode rewrite(input, output).
    Vérifier que les tests marqués "Q3" passent.

  4. On va écrire la méthode createRules qui prend en paramètre une chaîne de caractères et qui construit la règle correspondante.
    Pour l'instant, on va considérer qu'une règle est spécifiée par un seul caractère :
    • "s" veut dire strip (supprimer les espaces),
    • "u" veut dire uppercase (mettre en majuscules),
    • "l" veut dire lowercase (mettre en minuscules) et
    • "d" veut dire delete (supprimer).
    Écrire la méthode createRules(description).
    Vérifier que les tests marqués "Q4" passent.

    Note : on souhaite que la mise en majuscules/minuscules fonctionne de la même façon, quelle que soit la configuration de l'OS sur lequel tourne l'application.

  5. On veut pouvoir composer les règles, par exemple, on veut que "sl" strip les espaces puis mette le résultat en minuscules. Pour cela, dans un premier temps, on va écrire une méthode statique andThen dans Rule, qui prend en paramètre deux règles et renvoie une nouvelle règle qui applique la première règle puis applique la seconde règle sur le résultat de la première.
    Écrire la méthode statique andThen et vérifier que les deux premiers tests correspondant à "Q5" passent.

    Note : pensez à regarder la javadoc de la méthode Optional.flatMap...
    Puis modifier le code de createRules pour que les règles soient appliquées les une après les autres.
    Vérifier que tous les tests marqués "Q5" passent.

    Note : on peut remarquer que la chaîne vide "" correspond à une règle qui recopie toute la ligne.

  6. En fait, déclarer andThen en tant que méthode statique n'est pas très "objet" ... En orienté objet, on préfèrerait écrire rule1.andThen(rule2) plutôt que Rule.andThen(rule1, rule2). On va donc implanter une nouvelle méthode andThen dans Rule, cette fois-ci comme une méthode d'instance.
    Écrire la méthode d'instance andThen dans Rule et modifier createRules pour utiliser cette nouvelle méthode.
    Vérifier que les tests marqués "Q6" passent.

    Note : on va garder l'ancienne méthode pour que les tests continuent de fonctionner.

  7. On souhaite implanter la règle qui correspond au if, par exemple, "i=foo;u", qui veut dire si la ligne courante est égal à foo (le texte entre le '=' et le ';') alors, on met en majuscules sinon on recopie la ligne.
    Avant de modifier createRules(), on va créer, dans Rule, une méthode statique guard(function, rule) qui prend en paramètre une fonction et une règle et crée une règle qui est appliquée à la ligne courante si la fonction renvoie vrai pour cette ligne. Autrement dit, on veut pouvoir créer une règle qui s'applique uniquement aux lignes pour lesquelles la fonction renvoie vrai.
    Quelle interface fonctionnelle correspond à une fonction qui prend une String et renvoie un boolean ?
    Écrire la méthode statique guard(function, rule) et vérifier que les 4 premiers tests correspondant à "Q7" passent.

    Modifier la méthode createRules pour reconnaître un if, extraire le texte correspondant et utiliser la méthode guard(text, rule) pour créer la règle correspondant au if.
    On rappelle qu'en Java, on peut fabriquer l’automate qui reconnaît une expression régulière comme un objet de type Pattern que l'on créé avec la méthode Pattern.compile(regex), et que pour vérifier si une ligne correspond à cette expression régulière, on va créer un Matcher avec pattern.matcher(line) suivi d'un appel à matcher.matches().
    Vérifier que tous les tests marqués "Q7" passent.

    Note 1 : attention à ne pas créer le même automate pour reconnaitre l'expression régulière plusieurs fois...
    Note 2 : la notion de group pourra vous être utile.
    Note 3 : on peut se demander pourquoi passer une fonction et pas le texte sous forme de String en premier paramètre de guard... Il suffit de lire la question suivante pour avoir la réponse.

  8. On souhaite que le test du if puisse être non seulement une String mais aussi une expression régulière.
    Modifier la méthode createRules() et vérifier que les tests marqués "Q8" passent.

    Note : là encore, attention à ne pas créer le même automate plusieurs fois...

  9. Enfin, pour les plus balèzes, on souhaite pouvoir effectuer une transformation sur la condition du if, par exemple, "is=foo;u" veut dire si la ligne, une fois "strippée", est égale à foo, alors on met la ligne (attention pas la ligne 'strippée') en majuscules.
    Pour cela, on va introduire dans l'interface Rule une méthode withAsFilter(function) qui prend en paramètre une fonction permettant de tester une ligne (c'est-à-dire une fonction qui prend une ligne en paramètre et renvoie un booléen). La méthode withAsFilter renvoie une nouvelle fonction de test de ligne qui correspond au test pris en paramètre pour une ligne à laquelle on a appliqué la règle courante.
    Écrire la méthode withAsFilter et vérifier que les 2 premiers tests correspondant à "Q9" passent.
    Une fois la méthode withAsFilter écrite, modifier la méthode createRules pour qu'il soit possible d'appliquer une règle sur la condition d'un if.
    Vérifier que les tests marqués "Q9" passent.