:: Enseignements :: ESIPE :: E4INFO :: 2025-2026 :: Java Avancé ::
[LOGO]

Sed, the stream editor


Programmation Orientée Donnée (DOP), Programmation Orientée Objet (OOP) vs Lambda et entrées/sorties.
Le but de ce TP est d'implanter une petite partie des commandes d'un outil comme sed.

Pour les machines des salles de TP, si java --version n'affiche pas la version 25, vous pouvez utiliser les versions que Rémi a installées sur son compte enseignant.
  • Java 25 est là /home/ens/edu-forax/java/jdk-25
    Pour exécuter la commande java : /home/ens/edu-forax/java/jdk-25/bin/java

  • Pour démarrer Eclipse: /home/ens/edu-forax/java/eclipse-light/eclipse

Exercice 1 - Maven

Nous allons utiliser Maven avec la configuration, le pom.xml, suivante
<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.13.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.14.0</version>
                <configuration>
                    <release>25</release>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.5.3</version>
            </plugin>
        </plugins>
    </build>
</project>
   
Créer un projet Maven (pas un projet Java) puis cocher 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.
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.

L'application prend en premier paramètre une chaîne de caractères qui contient les différentes commandes à exécuter. Les commandes peuvent être composées en concaténant les lettres. Par exemple "rl" remplace les tabs par des espaces (r) puis met la chaîne de caractères en minuscules (l).
Voilà le main de l'application que nous voulons implanter.
static void main(String[] args) {
   if (args.length!= 3) {
     System.err.println(
       """
         Usage: <commands> <input.txt> <output.txt>

         commands:
           l          lower case
           r          replace each tab by a space
           *9         replace one star by 9 stars
           *4l        replace one star by 4 stars and to lower case
       """);
     System.exit(1);
     return;
   }
   var transformer = parseTransformer(args[0]);
   var inputPath = Path.of(args[1]);
   var outputPath = Path.of(args[2]);

   try {
     rewrite(inputPath, outputPath, transformer);
   } catch (IOException e) {
     System.err.println("error " + e.getMessage());
     System.exit(1);
   }
}
   

La méthode parseTransformer créer un objet Transformer (pas ce genre de Transformer là...) qui sert à représenter la transformation décrite par la commande, qui est elle-même spécifiée sous forme de chaîne de caractères. Par exemple, on pourra créer un LowerCaseTransformer pour la commande "l".
La méthode rewrite parcourt chaque ligne du fichier inputPath, transforme celle-ci en utilisant le transformer et écrit la ligne résultante dans le fichier outputPath.

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

  1. Dans un premier temps, on va se limiter à des chaînes de caractères contenant une seule commande ; on verra comment généraliser plus tard.
    On a besoin d'écrire deux méthodes :
    • La méthode parseTransformer(command) prend en paramètre une commande (* suivie d'un chiffre, l ou r) et créée un objet Transformer correspondant, qui permettra d'appliquer sur une ligne de fichier la transformation spécifiée par la commande .
    • La méthode rewrite(bufferedReader, writer, transformer) prend chaque ligne du reader (en fait, on utilise un BufferedReader plutôt qu'un Reader, car BufferedReader a une méthode BufferedReader.readLine() bien pratique) et écrit sa version transformée dans le writer.
      On s'occupera plus tard d'écrire la méthode rewrite qui prend des Path en paramètre.

    • Note : attention, readLine() supprime le caractère '\n' de fin de ligne, donc quand on écrit dans le Writer, il faut l'ajouter !

    Écrire une classe StreamEditor.
    Juste par ce que c'est pratique pour le TP, nous déclarerons tous le code (les méthodes, mais aussi les classes) dans la classe StreamEditor (oui, en Java, on peut mettre des classes dans les classes, on verra les détails dans un prochain cours).
    Écrire la méthode publique parseTransformer(command). Puis écrire la méthode publique rewrite(bufferedReader, writer, transformer). Ici, vous devez utiliser le pattern matching pour associer à un Transformer la transformation à effectuer.
    Vérifier que les tests marqués "Q1" passent.
    Note : il existe deux méthodes replace() sur la classe String, String.replace(char, char) et String.replace(String, String).

  2. On veut que notre programme fonctionne de la même façon, quelle que soit la machine sur laquelle il tourne, hors, il y a de grandes chances que votre code mette en minuscules en fonction de la Locale courante.
    Lisez la doc de String.toLowerCase() et corrigez votre code.
    Vérifier que les tests marqués "Q2" passent.

  3. En fait, on veut écrire 3 versions du même code (pour comparer), vous venez d'écrire la version utilisant le pattern matching, on va maintenant écrire les versions utilisant le polymorphisme puis dans la question suivante, une version utilisant les lambdas.
    Rappeler ce qu'est le polymorphisme.
    Comment le mettre en place ?
    Commenter le code précédent, et écrire une nouvelle version utilisant le polymorphisme.
    Vérifier que les tests marqués "Q3" passent.

  4. On souhaite maintenant écrire une version avec des lambdas. En effet, une lambda est une instance qui implante une interface, donc il est possible d'implanter les Transformer avec des lambdas.
    Vérifier que les tests marqués "Q4" passent.

  5. Selon vous, dans quel cas doit-on utiliser chaque technique ?

  6. On souhaite maintenant implanter le support de plusieurs commandes (par exemple "2*l2*"), il faut donc falloir modifier le fonctionnement de la méthode parseTransformer.
    Si on réfléchit un petit peu, il faut
    • découper la chaîne de caractères en commandes, sachant que les commandes font 1 ou 2 lettres
    • pour chaque commande, créer le Transformer correspondant, donc on obtient une nouvelle liste de commandes
    • transformer une liste de commandes en une seule commande, pour éviter de changer la méthode rewrite

    Nous allons implanter ces étapes dans les questions suivantes.
    Premièrement, pour découper une chaîne de caractères en commandes, on va transformer la chaîne de caractères contenant les commandes en itérateur de caractères, ainsi, il sera facile de lire un ou plusieurs caractères.
    Implanter la méthode characterIterator(commands) qui prend une chaîne de caractères en paramètre et renvoie un itérateur des caractères.
    Vérifier que les tests marqués "Q6" passent.
    Note : si vous ne voyez pas comment faire, vous pouvez dans un premier temps créer une liste puis demander son itérateur.
    Note 2 : quand vous serez un peu plus grands, on verra comment écrire les itérateurs directement.

  7. Maintenant, on va écrire la méthode parseOneTransformer(iterator) qui utilise l'itérateur pris en paramètre pour renvoyer le Transformer correspondant au(x) un ou deux prochain(s) caractère(s) de l'itérateur.
    L'idée est que l'on pourra appeler cette méthode en boucle tant qu'il restera des caractères accessibles par l'itérateur pour obtenir tous les Transformers.
    Écrire la méthode parseOneTransformer(iterator).
    Vérifier que les tests marqués "Q7" passent.

  8. Écrire la méthode parseAllTransformers(iterator) qui renvoie la liste de Transformer dans l'ordre de l'itérateur.
    Vérifier que les tests marqués "Q8" passent.

  9. Enfin, écrire la méthode createOneTransformer(transformers) qui prend en paramètre une liste de Transformer et renvoie le Transformer qui applique les différents Transformer dans l'ordre.
    Une fois la méthode createOneTransformer(transformers) écrite, changer le code de parseTransformer(commands) pour gérer plusieurs commandes.
    Vérifier que les tests marqués "Q9" passent.

  10. Enfin, pour que le main fonctionne, on souhaite écrire la méthode rewrite(input, output, transformer), qui prend deux Path, le premier est le fichier contenant les lignes à réécrire, le second est le fichier où l'on écrit le résultat.
    Écrire la méthode rewrite(input, output, transformer).
    Vérifier que les tests marqués "Q10" passent.
    Note : quand on ouvre un fichier, il ne faut pas oublier de le fermer, proprement (sinon, le fichier peut être vide, car l'OS, pour optimiser, ne l'écrira que lorsqu'il sera fermé !).

  11. Pour les plus balèzes, il existe une façon fonctionnelle d'écrire parseAllTransformers et createOneTransformer en un seul Stream pour éviter d'utiliser une liste intermédiaire. L'idée est de créer un Stream à partir d'un itérateur (il y a plusieurs façons de faire) et d'utiliser reduce (le bon) pour fusionner les Transformer 2 à 2.
    Réécrire parseTransformer de façon fonctionnelle, en utilisant juste les méthodes characterIterator et parseOneTransformer.
    Les testent doivent continuer à passer.
    Note : c'est moins de lignes à écrire, mais malheureusement, c'est aussi moins efficace... mais c'est sympa comme exercice.
    Note 2 : pour ceux qui ont du mal avec le reduce sur les Stream, c'est la même idée que le fold en Haskell.