:: Enseignements :: Master :: M1 :: 2024-2025 :: Java Avancé ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Implantation de l'API DOM
|
Concepts : programmation orientée objet, pattern matching, liste et map, mutation, copie défensive, encapsulation et vue
Pour l'ensemble des TDs de cette année, nous allons utiliser la nouvelle version de Java, la version 23
et l'IDE Eclipse (4.33 2024-09)
Eclipse s'exécute en tapant dans un terminal :
eclipse-light &
Après démarrage, indiquer le JDK 23 : dans Window > Preferences > Java > Installed JREs,
Le JDK 23 est présent dans le répertoire /usr/local/apps/java23.
Puis vérifier que le compilateur a bien été configuré en mode 23 :
dans Window > Preferences > Java > Compiler, le Compiler compliance level doit être à 23.
Chaque exercice vient avec des tests unitaires
JUnit
qui vous permettent de vérifier que votre implantation passe les tests.
Exercice 1 - Maven
Pour ce TP et les TPs suivant, nous allons utiliser Maven comme outil de build (et de gestion de dépendances).
Maven est un programme que l'on configure en écrivant un fichier pom.xml (Project Object Model)
qui décrit des propriétés, les dépendances et le build (la séquence d'actions pour créer le programme).
Le plugin Maven (m2eclipse) doit être installé (c'est le cas sur les machines de TP de la fac),
sinon il faut aller dans Help > Install New Software... Dans la fenêtre d'installation, sélectionner
2024-09 - https://download.eclipse.org/releases/2024-09 (la version courante de votre Eclipse),
puis dans la barre de filtre (filter), taper Maven et sélectionner M2E puis
appuyer sur Finish. Une fois Eclipse redémarré, le plugin pour Maven sera installé.
Comme nous utilisons Eclipse comme IDE, il faut aussi indiquer à Eclipse qu'au lieu d'utiliser ses fichiers de
configuration habituels (le .classpath et le .projet), la configuration est dans le pom.xml.
De plus, par défaut, Eclipse ne met pas à jour sa configuration en utilisant la configuration du pom.xml,
il faut aller dans les Preferences > Maven et cocher l'option
Automatically update Maven projects configuration.
-
Dans un premier temps, créer un projet Maven (attention pas un projet Java, un projet Maven).
Au niveau du premier écran, cocher create simple project car on va tout faire à la main
plutôt que partir d'un modèle existant puis passer à l'écran suivant en indiquant Next.
Eclipse vous demande d'indiquer un groupId fr.uge.dom , un artefactId dom et
une version 0.0.1-SNAPSHOT. Le groupId est plus ou moins le nom du package, l'artefactId le nom du projet.
Une version correspond le plus souvent à 3 nombres séparés par des points
(la norme s'appelle semver), le -SNAPSHOT indique que ce n'est pas une
release, une version disponible pour n'importe qui, mais une version de développement.
Puis cliquer sur Finish.
-
Si tout se passe bien, vous devriez avoir un projet avec le pom.xml suivant qui reprend les informations
(on parle de coordonnées) du projet.
<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.dom</groupId>
<artifactId>dom</artifactId>
<version>0.0.1-SNAPSHOT</version>
</project>
De plus, Eclipse a créé deux répertoires importants, src/main/java qui va contenir le code source
(les fichiers .java) et src/test/java qui va contenir le code des tests (aussi des fichiers .java)
-
À partir de maintenant, on va configurer le projet en ajoutant des informations de configuration
au pom.xml, votre IDE se mettra à jour automatiquement.
Voilà le fichier POM que nous allons utiliser
<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.dom</groupId>
<artifactId>dom</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>
-
Pour exécuter Maven, on utilise soit la ligne de commande mvn package soit avec un clic bouton droit
sur le pom.xml suivi d'un run build avec le goal package.
Rappel : si vous voulez créer une classe, il faut la mettre dans un package (ici fr.uge.dom)
dans le répertoire src/main/java. Pour les tests, il faut les mettre dans le même package
(donc aussi fr.uge.dom) mais dans le répertoire src/test/java.
Exercice 2 - Document Object Model
Le but de cet exercice est de réimplanter les 3 méthodes principales de l'API DOM.
Un document (
DOMDocument) est un objet qui permet de créer des nœuds (
DOMNodes) et
de rechercher un nœud par son
id.
Un nœud correspondant à un tag HTML comme P ou UL avec un nom (
name comme "p", "ul", etc...),
une
Map (nommée
attributes) qui associe à un nom d'attribut la valeur d'attribut correspondante
(par exemple, pour le tag HTML <a href="foo.txt">, le nom de l'attribut est "href" et sa valeur est "foo.txt").
De plus, un nœud peut posséder lui-même des sous-noeuds enfants (
children).
On peut ajouter un nœud en tant que fils d'un nœud existant, en appelant la méthode
element.appendChild(child)
sur un nœud existant.
Deux nœuds différents ne sont jamais égaux, car ils représentent différents endroits dans l'arbre DOM.
Enfin, on ne veut pas montrer l'implantation exacte d'un nœud, pour cela un nœud va être représenté
par l'interface
DOMNode visible et une classe non-visible implantant l'interface
DOMNode.
Toutes les classes,
record, etc..., doivent être définis dans le package
fr.uge.dom.
-
Dans un premier temps, créer l'interface DOMNode avec une méthode name qui renvoie le nom
du nœud de l'arbre DOM et une méthode attributes qui renvoie les attributs ainsi
que la classe correspondante implantant l'interface.
La valeur d'un attribut ne peut être qu'une chaîne de caractères, un booléen, un entier, un entier long,
un flottant ou un flottant long.
Créer ensuite la classe DOMDocument, et sa méthode createElement(name, attributes)
qui renvoie une nouvelle instance de DOMNode telles que le code suivant fonctionne
DOMDocument document = new DOMDocument();
DOMNode node = document.createElement("div", Map.of("color", "red"));
System.out.println(node.name()); // div
System.out.println(node.attributes()); // {color=red}
Vérifier que les tests JUnit marqués "Q1" passent.
-
Êtes-vous sûr d'avoir d'avoir bien respecté l'ensemble des consignes ci-dessus et écrit vos classes correctement ?
Vérifier que les tests JUnit marqués "Q2" passent.
Note : lorsqu'un test ne passe pas (pas vert), il faut regarder la stack trace (la fenêtre en bas à gauche quand
on clique sur le test) puis rechercher du haut vers le bas la méthode qui est dans la classe de test
(dans notre cas, la classe DOMNodeTest). Cliquer sur cette méthode vous emportera à la ligne
où il y a un problème.
Note 2 : pour corriger le problème, lisez le code du test pour essayer de comprendre pourquoi le test ne passe pas.
Vous pouvez vous aider de la javadoc (si on laisse le curseur sur une méthode la javadoc apparaît) pour comprendre
les méthodes que vous ne connaissez pas. Une fois que vous avez compris votre erreur, modifiez votre code
pour que le test pas (bien sûr, on ne modifie jamais le code du test).
Note 3 : Dans le cas d'un assertAll, cette méthode peut reporter plusieurs erreurs, dans ce cas, la
stack trace est composée de l'erreur principale qui dit juste qu'il y a plusieurs erreurs et qu'il faut regarder
les stack traces des exceptions suppressed (en dessous de l'exception principale).
-
On souhaite ajouter une méthode getElementById à la classe DOMDocument
qui renvoie un nœud par son id.
En HTML, un id doit être une chaine de caractère (non vide) et
si pour un même document, il y a plusieurs nœuds avec le même id seul
le premier nœud est enregistré avec cet id.
var document = new DOMDocument();
var node = document.createElement("div", Map.of("id", "foo42"));
var node2 = document.getElementById("foo42");
System.out.println(node == node2); // true
Quelle structure de données doit-on utiliser pour permettre de trouver un nœud par son id ?
Ajouter la méthode getElementById dans la classe DOMDocument telle que le code
ci-dessus fonctionne.
Vérifier que les tests JUnit marqués "Q3" passent.
-
On souhaite pouvoir ajouter des fils à un nœud (DOMNode) existant en utilisant la méthode
appendChild(child) et accéder à ses enfants en utilisant la méthode children().
Il ne doit être possible d'ajouter un nœud que si le nœud parent et le nœud enfant
sont issus du même document.
var document = new DOMDocument();
var parent = document.createElement("foo", Map.of());
var child = document.createElement("bar", Map.of());
parent.appendChild(child);
System.out.println(parent.children().getFirst() == child); // true
Expliquer pourquoi on souhaite que la liste renvoyée par children() soit non modifiable ?
Sachant cela, implanter les méthodes children() et appendChild(child).
Vérifier que les tests JUnit marqués "Q4" passent.
Note : pour l'implantation de la méthode children, on pourra utiliser une vue pour éviter
de dupliquer trop d'objets.
-
L'implantation de appendChild a un bug, un nœud peut appartenir à plusieurs parents,
par exemple en écrivant ceci.
var document = new DOMDocument();
var child = document.createElement("bar", Map.of());
var parent1 = document.createElement("parent1", Map.of());
var parent2 = document.createElement("parent2", Map.of());
parent1.appendChild(child);
parent2.appendChild(child);
La spécification de appendChild indique que lorsque l'on ajoute un nœud à un parent,
si celui-ci a déjà un parent, alors il est d'abord retiré de l'ancien parent avant d'être ajouté
en tant qu'enfant du nouveau parent.
Implanter ce comportement.
Vérifier que les tests JUnit marqués "Q5" passent.
-
On veut maintenant afficher les nœuds (le nom, les attributs et les enfants) au format HTML.
var document = new DOMDocument();
var parent = document.createElement("foo", Map.of());
var child = document.createElement("bar", Map.of("enable", true));
System.out.println(child); // <bar enable="true"></bar>
parent.appendChild(child);
System.out.println(parent); // <foo><bar enable="true"></bar></foo>
Implanter l'affichage pour que le code ci-dessus fonctionne.
Vérifier que les tests JUnit marqués "Q6" passent.
Note : comment éviter d'allouer plein de chaînes de caractères intermédiaires ?
-
En fait, si on demande plusieurs fois l'affichage, on va à chaque fois recalculer celui-ci,
on souhaite améliorer cela en ajoutant un cache pour que le calcul pour un nœud
ne soit pas fait plus souvent que nécessaire.
Pour cela, on ajoute un champ cache qui va contenir la chaîne de caractères correspondant à l'affichage,
donc si on demande de faire l'affichage plusieurs fois, la même chaîne de caractères va être renvoyée.
Modifier votre code pour implanter cette amélioration.
Vérifier que les tests JUnit marqués "Q7" passent.
-
[Revision] Enfin, pour les plus balèzes, il reste deux problèmes à corriger
-
Avec createElement, on peut créer un cycle, avec un nœud étant son propre parent, il faut détecter
ces cas (il peut aussi être son grand parent, etc) et planter.
-
Lorsque l'on ajoute/retire un nœud, le cache n'est plus valide, il faut donc invalider les caches impactés
(pas tous !).
Modifier votre code pour corriger ses deux problèmes.
Vérifier que les tests JUnit marqués "Q8" passent.
Note : si vous ne voyez pas comment faire, ce n'est pas grave, vous pourrez revenir sur cette question
pendant vos révisions.
© Université de Marne-la-Vallée