:: Enseignements :: Master :: M1 :: 2024-2025 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Sed, the stream editor
|
Programmation fonctionnelle vs. programmation orientée objet, interface, record, lambda, 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
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.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>23</release>
<compilerArgs>
<compilerArg>--enable-preview</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.0</version>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</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.
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.
Juste pour rire, nous avons demandé à ChatGPT de compléter le fichier en donnant un main() contenant
les premières lignes du fichier (celles qui contiennent l'aide).
Voilà le résultat obtenu :
public static void main(String[] args) {
if (args.length != 3) {
System.err.println(
"""
Usage: <commands> <input.txt> <output.txt>
examples of command:
u Convert the line to uppercase
l Convert the line to lowercase
*9 Replace every occurrence of one * with nine *
*4u Replace every occurrence of one * with four * and convert the result to uppercase
d Delete the entire line
""");
System.exit(1);
return;
}
// complété par ChatGPT
String commands = args[0];
String inputFile = args[1];
String outputFile = args[2];
try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));
PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
String line;
while ((line = reader.readLine()) != null) {
boolean modified = false;
for (String command : commands.split(",")) {
switch (command) {
case "u":
line = line.toUpperCase();
modified = true;
break;
case "l":
line = line.toLowerCase();
modified = true;
break;
case "*9":
line = line.replace("*", "********");
modified = true;
break;
case "*4u":
line = line.replace("*", "****").toUpperCase();
modified = true;
break;
case "d":
line = "";
modified = true;
break;
}
if (modified) {
writer.write(line);
writer.newLine();
modified = false; // Reset flag after processing a command
}
}
}
} catch (IOException e) {
System.out.println("Error reading/writing files: " + e.getMessage());
}
}
Si ce n'est pas évident pour vous, le code ne fait pas ce qui est demandé et est aussi farci de bugs,
donc nous allons plutôt développer le code à la main :)
Si cela vous intéresse de voir tous les bugs :
public static void main(String[] args) {
if (args.length != 3) {
System.err.println(
"""
...
""");
System.exit(1);
return;
}
// complété par ChatGPT
String commands = args[0]; // cela serait mieux avec des "var"
String inputFile = args[1]; // devrait être un Path
String outputFile = args[2]; // devrait être un Path
//try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));
// PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) {
// devrait être
try(BufferedReader reader = Files.newBufferedReader(inputFile);
BufferedWriter writer = Files.newBufferedWriter(outputFile)) {
// conceptuellement, il y a un problème car le re-parse les commandes pour chaque ligne
String line;
while ((line = reader.readLine()) != null) {
boolean modified = false; // pas vraiment nécessaire voir plus tard
for (String command : commands.split(",")) { // le split est n'importe quoi
switch (command) { // devrait utiliser un switch expression
case "u": // case "u" -> {
line = line.toUpperCase(); // manque un argument
modified = true; // yield le résultat
break; // }
case "l":
line = line.toLowerCase(); // manque un argument
modified = true;
break;
case "*9": // devrait détecter seulement '*' et regarder le caractère suivant
line = line.replace("*", "********"); // 8 étoiles au lieu de 9
modified = true;
break;
case "*4u":
line = line.replace("*", "****").toUpperCase(); // pas compris que l'on peut composer les commandes
modified = true;
break;
case "d":
line = ""; // sert à rien
modified = true; // bah, non justement, faire un "continue" sur le while semble plus simple
break;
} // manque une autre accolade fermante ici, car là on affiche plusieurs fois la même ligne
if (modified) {
writer.write(line);
writer.newLine(); // pas le même résultat suivant l'OS, chiant à tester (Windows est différent !)
modified = false;
}
}
}
} catch (IOException e) {
System.out.println("Error reading/writing files: " + e.getMessage()); // devrait être System.err
// manque le System.exit(2)
}
}
-
Dans un premier temps, on va prendre une commande (une chaîne de caractères comme "u" ou "*4")
et créer l'objet correspondant pour éviter de re-parser la commande à chaque ligne.
On se propose de nommer cette objet Transformer car son rôle est de transformer une ligne
du fichier en une nouvelle ligne.
En plus de la méthode createTransformer qui créé un transformer
pour une commande, on a besoin d'une autre méthode rewrite qui prend un reader,
un writer et un transformer. Elle lit chaque ligne du reader,
la transforme avec le transformer et l'écrit sur le writer.
var transformer = StreamEditor.createTransformer("u"); // exemple avec "u" comme commande
try(BufferedReader reader = ...;
BufferedWriter writer = ...) {
StreamEditor.rewrite(reader, writer, transformer);
}
Pour l'instant, on ne s'intéresse qu'aux commandes "u", "l" et "*" suivie d'un chiffre (pas un nombre !) indiquant
la répétition ("*4", "*7", etc), et on veut une classe par type de commande. Déclarer
Transformer ainsi que 3 implantations UpperCaseTransformer, LowerCaseTransformer
et StarTransformer.
Puis créer la méthode createTransformer qui prend une commande, regarde le premier caractère
et créé une instance de Transformer avec la bonne implantation.
Enfin, écrire la méthode rewrite qui boucle sur chaque ligne du reader,
envoie la ligne au transformer et écrit la ligne résultante dans le writer.
Pour le retour à la ligne on utilisera un '\n', ainsi le programme fonctionnera de la même façon
quelque soit l'OS.
Vérifier que les tests marqués "Q1" passent.
Note : il existe une méthode replace() sur la classe String. En fait, il en existe plusieurs, à vous
de prendre la bonne.
-
On veut que notre programme fonctionne de la même façon, quelle que soit la machine or la norme Unicode. On
demande que les méthodes String.toUpperCase() et String.toLowerCase()
aient un comportement spécifique en fonction de la langue (Locale) par défaut de l'OS.
Si vous ne l'avez pas déjà fait, changer votre code pour que la mise en majuscule/minuscule
soit fait de façon indépendante de l'OS. Si vous ne vous souvenez plus de comment on fait,
vous pouvez relire la javadoc de toUpperCase() ou toLowerCase()
Modifier, si nécessaire, votre code et vérifier que les tests marqués "Q2" passent.
-
Vous avez sûrement utilisé le polymorphisme pour implanter Transformer
alors que votre programme contrôle toutes les implantations possibles. C'est MAL,
vous auriez du utiliser le pattern matching.
Commenter votre code puis changer votre implantation pour utiliser le pattern matching plutôt que le polymorphisme.
Vérifier que les tests marqués "Q3" passent.
Rappel : Ne pas utiliser le polymorphisme veut dire ne pas avoir de méthode abstract dans Transformer. Et pattern matching, signifie implanter en faisant un switch sur toutes les implantations.
-
En fait, les transformations sont des actions, donc pour chaque transformation, on pourrait utiliser une lambda,
ainsi le code serait plus simple à écrire et donc à maintenir.
Comment indiquer que Transformer est implantée en utilisant des lambdas ?
Comment faire pour que l'on empêche d'autre code de fournir une autre implantation ?
Vérifier que les tests marqués "Q4" passent.
-
On veut maintenant pouvoir gérer une commande qui est elle-même composée de plusieurs commandes
comme lu ou *4u (qui respectivement mette la ligne en minuscule puis majuscule,
remplace les étoiles par quatre étoiles puis mette la ligne en majuscule).
Cela veut dire qu'il va falloir décomposer une commande en plusieurs commandes, pour cela, on se propose
d'écrire une méthode parse(command, transformer, index) qui renvoie un tuple composé
de la commande à l'index index ainsi que de l'index de la prochaine commande
(pour la mise en majuscule/miniscule la prochaine commande est à l'index suivante mais pour les étoiles
il faut sauter deux cases).
Comment représente-t-on des tuples en Java ? Dans notre cas, quel est la représentation
d'un tuple qui contient un Transformer et un index
Écrire la méthode parse(command, transformer, index) et changer le code de
la méthode createTransformer(command) en conséquence.
Vérifier que les tests marqués "Q5" passent.
-
En fait, le code précédent est la façon fonctionnelle de voir la décomposition en transformer,
on peut aussi écrire une version plus objet des choses. En POO, on va encapsuler les mutations,
ici, la mutation est l'index qui nous indique là où décoder/parser le prochain transformer
dans la commande. Encapsuler la mutation revient donc à déclarer une classe Parser
avec un champ mutable qui va être modifié à chaque fois que l'on décode une transformation.
On a de plus besoin en plus d'une méthode pour savoir si on est arrivé à la fin de la chaîne de caractères
(appelée ici, canParse())
static final class Parser {
private final String commands;
private int index;
Parser(String commands) {
this.commands = commands;
}
boolean canParse() {
return index < commands.length;
}
Transformer parse(Transformer t) {
// TODO
}
}
Écrire la classe Parser et modifier la méthode createTransformer(commande)
pour utiliser le parser (l'idée est de faire une boucle tant que canParse renvoie vrai et
envoyer le précédent transformer à parse() qui retourne le nouveau transformer.
Vérifier que les tests marqués "Q6" passent.
Note : on peut remarquer qu'il y a un modificateur static devant le nom de la classe Parser,
on verra la semaine prochaine en cours pourquoi.
-
[Revision] Pour les plus balèzes, en fait, le code précédent peut être simplifié car Parser.parse() fait
trop de choses. Au lieu d'appeler la méthode Parser.parse() avec la transformation précédente,
on peut ne rien envoyer et faire la composition entre la transformation précédente
et la nouvelle transformation en dehors de l'appel à parse().
On peut aussi remarquer l'on compose les transformers les un à la suite des autres, donc c'est
comme une réduction fonctionnelle et on peut utiliser la méthode Stream.reduce() pour cela.
Vérifier que les tests marqués "Q6" passent toujours.
Note : si vous ne voyez pas comment faire, ce n'est pas grave, vous pourrez revenir sur cette question
pendant vos révisions.
-
[Revision] Enfin pour les ultra-balèzes, il manque deux choses
-
Il faut écrire un main ainsi que la méthode rewrite(Path input, Path output, Transformer transformer)
-
Il faut implanter la commande "d" qui supprime une ligne. Le plus simple consiste à ce que la méthode dans
Transformer renvoie un Optional<String> au lieu de renvoyer juste un String
comme cela, on peut encoder le fait que la ligne peut ne plus exister.
Faite les deux modifications proposées ci-dessus.
Vérifier que les tests marqués "Q8" passent.
© Université de Marne-la-Vallée