:: Enseignements :: Master :: M1 :: 2022-2023 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
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.
-
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.
-
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.
-
(À 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.
-
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...
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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 ?
© Université de Marne-la-Vallée