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