:: Enseignements :: Master :: M1 :: 2024-2025 :: Java Avancé ::
[LOGO]

Implantation de l'API DOM


Concepts : programmation orientée objet, pattern matching, liste et map, mutation, copie défensive, encapsulation et vue
There are 2 hard problems in computer science: cache invalidation, naming things, and off-by-1 errors.
-- Leon Bambrick

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.

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.

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

  1. 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.

  2. Ê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).

  3. 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.

  4. 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.

  5. 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.

  6. 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 ?

  7. 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.

  8. [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.