Bréviaire sur la mise en œuvre des arbres DOM
en Java standard

parcours récursif, construction, sortie sur fichier

Jean-François Perrot

révision du 6/12/2012

  1. Point de départ : l'objet Document

  2. Traitement d'un Element
    1. Accès à son nom (le nom de la balise) :
    2. Accès à ses enfants
    3. Accès à ses attributs

  3. Accès transversaux

  4. Traitement d'un sommet Text

  5. Modification d'un arbre
    1. Modifications visant les sommets-enfants
    2. Modifications visant les attributs
    3. Modifications visant les sommets-texte

  6. Création de nouveaux sommets
    1. Si on a déjà un Document : création
    2. Si on a déjà un Document : recopie
    3. Créer un Document

  7. Sortie sur fichier


Références :
  1. Les JavaDocs officielles

  2. Quelques points de détail : http://www.w3.org/DOM/faq.html

  3. Les précieux exemples commentés du site The Java Developers Almanac 1.4
    indexés par packages.

Avertissement au sujet des espaces de noms (namespaces)

  1. La plupart des méthodes d'accès aux sommets, attributs et autres qui se réfèrent à leurs noms sont dupliquées par des méthodes homonymes avec le suffixe NS, qui se réfèrent aux noms munis d'un préfixe de namespace.
    Par exemple, dans l'interface org.w3.dom.Element,



  2. Ces méthodes "NS" ne fonctionnent que si le document traité est "attentif aux espaces de noms" (en anglais namespace aware).
    Or, cette faculté est accordée au document par le parseur (DocumentBuilder) qui l'a produit,
    et le parseur lui-même ne le fera que si la DocumentBuilderFactory qui l'a créé le lui a dit,
    or par défaut elle ne le fait pas !
    La JavaDoc sur DocumentBuilderFactory::setNamespaceAware déclare :
    Specifies that the parser produced by this code will provide support for XML namespaces.
    By default the value of this is set to false.


  3. Si on veut traiter correctement les espaces de noms (par exemple, en présence de schémas XML),
    il ne faut donc pas oublier de régler l'objet DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    en lui ordonnant explicitement : dbf.setNamespaceAware(true);

  1. Point de départ : l'objet org.w3c.dom.Document

    on l'obtient, connaissant l'URI du fichier-source, d'une façon qui dépend du parseur employé ;
    à partir du parseur standard, instance de javax.xml.parsers.DocumentBuilder
    obtenu par
    DocumentBuilder parseur = DocumentBuilderFactory.newInstance().newDocumentBuilder();

    on demande
    Document doc = parseur.parse(uri);


    Sauf raffinements particuliers, on s'empresse de traiter son unique élément,
    qui est la racine de l'arbre "significatif" :
    Element rac = doc.getDocumentElement();

    Mais, en plus  d'être le point de départ du parcours, l'objet Document a des prérogatives importantes,
    notamment en matière de création de nouveaux sommets et de leur mise en place.
    Voyez ci-dessous.
  2. Traitement d'un org.w3c.dom.Element

    Avertissement : il faut prendre garde que l'Element en question sera souvent obtenu par un procédé
    qui livre en fait un Node générique (par exemple, par getFistChild(), ou à partir d'une NodeList).
    Pour pouvoir lui appliquer une méthode spécifique à Element, il faudra donc effectuer une coercion (casting) explicite,
    par exemple :
    Element elt = (Element) autre.getFistChild(); 

    Cette coercion provoquera une erreur si d'aventure le Node obtenu se trouve être un Text !
    Or, cette erreur est indétectable à la compilation, elle n'apparaîtra qu'à l'exécution, avec un diagnostic cryptique du genre
    com.sun.org.apache.xerces.internal.dom.DeferredTextImpl cannot be cast to org.w3c.dom.Element
    ou tout simplement (et ipso facto encore plus indéchiffrable)
    Erreur com.sun.org.apache.xerces.internal.dom.DeferredTextImpl

    Caveat programmator !
    Voir plus loin un remède possible.
    1. Accès à son nom (le nom de la balise) :

      String nom = elt.getNodeName; (méthode héritée de Node)
    2. Accès à ses enfants (descendants directs)

      (qui sont de type Node et peuvent être des Element ou des Text).
      Deux méthodes sont possibles :

      • Si le nombre d'enfants est connu (mais attention aux blancs parasites !) :

        • si c'est 1 : Node filsUnique = elt.getFirstChild();

        • si c'est 2 (cas fréquent des arbres binaires) :
          Node filsAine = elt.getFirstChild();
          Node filsCadet = elt.getLastChild();

        • si  c'est 3 (ça arrive !)
          Node filsAine = elt.getFirstChild();
          Node filsPuine = filsAine.getNextSibling();
          // "sibling" = frère ou sœur
          Node filsCadet = elt.getLastChild();

        • etc...

      • Sinon, il faut d'abord savoir s'il y a effectivement des enfants

        if( elt.hasChildNodes() ){ ...

        et si oui, il faut passer par la collection des enfants, qui est une instance de NodeList :

        NodeList coll = elt.getChildNodes();

        Cette collection se manipule par des boucles sur le modèle que voici :

        for( int i = 0; i < coll.getLength(); i++ ){
            l'enfant courant s'appelle coll.item(i)
            il est de type Node
        }


      Le traitement d'un enfant va probablement demander de distinguer suivant sa classe
      (sous-classe de Node)
      donc donner lieu à une conditionnelle (ou un aiguillage) du genre
      if( enfant.getNodeType() == Node.ELEMENT_NODE ){
         Element e = (Element) enfant;
         ..............
      }else{ // c'est un texte
         Text t = (Text) enfant;
         ..........
      }
      Sur le cas particulier fréquent d'un enfant qui est de type Text, voir plus loin.

      Pour une liste complète des constantes désignant les sous-classes de Node, voir la Javadoc :
      http://java.sun.com/j2se/1.4.2/docs/api/org/w3c/dom/Node.html

    3. Accès global à ses descendants (enfants, petits-enfants, etc)

      On peut bien entendu accéder à tous les descendants d'un sommet par un parcours récursif.
      DOM propose en plus des méthodes d'accès direct à l'ensemble des descendants.
      Pour un élément, seule la méthode
      NodeList getElementsByTagName(String name)
      est applicable.

      Voir plus loin "Accès transversaux".

    4. Accès à ses attributs

      Il faut distinguer soigneusement l'objet attribut lui-même, instance de Attr (sous-classe de Node)
      et sa valeur qui est une chaîne.
      L'objet Attr connaît son nom (String nom = attr.getName();)
      aussi bien que sa valeur (String val = attr.getValue();) !

      • Si les noms des attributs sont connus :
        • test de présence (pour les attributs optionnels)
          if( elt.hasAttribute(nom)  ){...
        • accès direct à la valeur par String val = elt.getAttribute(nom);
        • accès à l'objet-attribut lui-même par Attr attr = elt.getAttributeNode(nom);

      • sinon, il faut d'abord savoir s'il y a effectivement des attributs :

        if( elt.hasAttributes() ){...

        et si oui, il faut passer par la collection des attributs, qui est une NameNodeMap :
        NameNodeMap coll = elt.getAttributes();

        Cette collection se manipule par des boucles sur le modèle que voici :

        for( int i = 0; i < coll.getLength(); i++ ){
            l'attribut courant s'appelle coll.item(i)
            il est de type Node
            pour l'exploiter il convient donc d'effectuer un transtypage :
            Attr attr = (Attr)
        coll.item(i);
           
        String nom = attr.getName();
           
        String val = attr.getValue();
                etc...
        }
  3. Accès transversaux

    Le parcours récursif est bien sûr le mode d'accès de base à un arbre.
    Il permet en principe de faire "n'importe quoi", peut-être au prix d'un peu de programmation.
    Mais certaines recherches sont si fréquentes qu'elles bénéficient d'une réalisation directe dans DOM.

    1. La plus utile est la méthode getElementsByTagName commune aux interfaces Document et Element :
      extrait de JavaDoc la concernant :

      NodeList getElementsByTagName(String name)
      Returns a NodeList of all descendant Elements with a given tag name, in document order.

      Parameters:
      name - The name of the tag to match on. The special value "*" matches all tags.
      Returns:
      A list of matching Element nodes.

      Mais attention !

      • il est bien dit descendant, et non pas child. On obtient donc tous les éléments du sous-arbre ainsi nommés,
        et pas seulement les fils directs.

      • les objets NodeList sont vivants ! Toute modification à l'arbre y est immédiatement répercutée.
        Voyez ici détails et exemple.

    2. Fort utilisée en pratique est la méthode Element getElementById(String identifiant)
      qui est une prérogative de Document.
      Comme son nom l'indique (pas de 's' à Element), elle renvoie l'unique élément dans tout le document
      qui possède un attribut de nom id dont la valeur est la chaîne passée en paramètre.

      Ainsi, quand on prévoit de modifier un arbre XML "en un certain point", il suffit de marquer cet endroit
      en y insérant un élément porteur d'un id convenu à l'avance, et de demander ensuite :
      doc.replaceChild(newChild, doc.getElementById(l'id convenu));
  4. Traitement d'un sommet org.w3c.dom.Text

    1. Un sommet Text

      n'a ni enfants ni attributs, il n'a qu'un contenu, obtenu par
      String contenu = txt.getData();
       
      La plupart du temps, on souhaite se débarrasser des blancs et sauts de ligne qui encadrent ce contenu,
      d'où le recours fréquent à la méthode trim() de la classe String.

      String contenuSignificatif = txt.getData().trim();
      if(
      contenuSignificatif.length > 0 ){
       ...exploitation...
      }


    2. Traitement des feuilles

      On pense souvent aux contenus de texte qui remplissent tout un Element :
      par exemple, les notes dans le fichier ex.xml :
      <?xml version="1.0"?>
      <liste>

      <eleve nom='Pierre'> <note> 12 </note>
      </eleve>
      <eleve nom="Paul"> <note> 13 </note>
      </eleve>
      <eleve nom='Jacques'> <note>17 </note>
      </eleve>

      </liste>
      Du point de vue sémantique, de tels Elements sont des feuilles de l'arbre
      mais dans le modèle DOM ils possèdent un fils de type Text.
      L'accès à leur contenu textuel se fait donc officiellement par :
      String contenu = ((Text) elt.getFirstChild()).getData();

      L'exemple ci-dessus, supposé logé dans la variable Document doc, sera traité par

      Element liste = doc.getDocumentElement();
      NodeList les_eleves = liste.getElementsByTagName("eleve");
      for( int i=0; i<les_eleves.getLength(); i++ ){
          Element l_eleve = (Element) les_eleves.item(i);
          Element sa_note = l_eleve.getElementsByTagName("note").item(0); // feuille
              String le_nom = l_eleve.getAttribute("nom");
              String la_note = ((Text) sa_note.getFirstChild()).getData();
              etc...
      }


    3. Le problème des blancs parasites

      Mais en fait, dans ce même fichier, les eleves ont chacun deux fils Text
      et la racine liste n'en a pas moins de quatre ! Ils ne comportent que
      des blancs et des sauts de ligne, certes, mais ils existent bel et bien !
      Si on veut les éviter, la seule solution est de renoncer à toute mise en page
      et d'écrire :
      <?xml version="1.0"?>
      <liste><eleve nom='Pierre'><note> 12 </note></eleve><eleve nom="Paul"><note> 13 </note>...</liste>
      C'est une "normalisation" de ce genre qu'effectue la procédure elimTxt dans la classe PrettyMIL.

    4. Mélange de texte et de sommets

      Un Element peut fort bien avoir des enfants où se mélangent des Texts et d'autres Elements.
      Ainsi par exemple dans les fichiers du corpus Frantext, qui offrent des œuvres littéraires
      formattées en accord avec la Text Encoding Initiative.
      Voici un extrait de La Parisienne, pièce de théâtre d'Henry Becque (1885)

      <p>......*Lafont, <hi rend="I"> à sa gauche. </hi><lb/>
      votre amie, madame, est bien inconséquente.<pb n="267"/>
      *Du *Mesnil.<lb/>
      Tu vois, hein, tu vois. Je la connais, cette<lb/>
      *Mme *Simpson, il court de drôles d'histoires sur<lb/>
      son compte.<lb/>....</p>


      En ce cas, chaque fragment de texte devient un Text individuel, les autres étant des Elements à part entière.
      Dans cet exemple, l'élément <p> a ainsi 14 childNodes, dont 7 sont des Text.
      La boucle de traitement devra donc distinguer suivant les types, comme indiqué ci-dessus.

  5. Modification d'un arbre

    Nous n'avons envisagé jusqu'ici que des opérations de lecture.
    On peut aussi effectuer des écritures, qui vont modifier notre arbre (side-effects, effets de bord).
    Nous verrons plus loin les opérations de création d'arbres et de sous-arbres.
    1. Modifications visant les sommets-enfants

      • ajout d'un enfant (à la fin de la collection) :
        elt.appendChild(nouvelEnfant);
        Attention !
        Le nouvelEnfant doit avoir été créé par le Document auquel appartient l'élément elt !
        sous peine d'erreur :
        Erreur WRONG_DOCUMENT_ERR: A node is used in a different document than the one that created it
        Voir ci-après.

      • ajout d'un enfant (avant un autre) avec la même réserve que ci-dessus
        elt.insertChild(nouvelEnfant, autreEnfant);

      • suppression d'un enfant
        elt.removeChild(lenfant);

      • remplacement d'un enfant par un autre sommet, avec la même réserve que ci-dessus
        elt.replaceChild(lenouveau, lancien);

    2. Modifications visant les attributs

      • ajout d'un attribut connu par son nom, ou changement de sa valeur :
        elt.setAttribute(nom, valeur);

      • même opération en utilisant un objet-attribut, instance de Attr :
        elt.setAttributeNode(attribut);

      • suppression d'un attribut connu par son nom
        elt.removeAttribute(nom);

      • même opération sur un objet-attribut, instance de Attr :
        elt.removeAttributeNode(attribut);

    3. Modifications visant les sommets-texte

      L'accès en lecture getData() a son homologue en écriture setData(chaine),
      en outre l'interface CharacterData propose un jeu complet pour la manipulation
      des chaines-contenus.

  6. Création de nouveaux sommets

    Attention ! La création de nouveaux sommets est l'apanage de l'objet Document.
    En outre, chaque sommet connaît son document créateur (par getOwnerDocument()),
    et il ne peut être accroché (via appendChild) qu'à un sommet du même document.
    Toute tentative contraire à cette règle soulève la DOMException "WRONG_DOCUMENT_ERR".
    Pour en savoir plus sur ce point, voyez la DOM FAQ.
    1. Si on a déjà un Document : création

      Supposons d'abord que nous avons un tel objet, valeur de la variable doc.
      Soit rac son unique élément :
      rac = doc.getDocumentElement();

      • un nouvel Element de nom "sonNom" s'obtient par
        Element elt = doc.createElement("sonNom");

        Attention ! ce nouvel élément n'est fils de personne...
        Pour ne pas le perdre, il faudra faire quelquechose comme
        rac.appendChild(elt);

        D'aitre part, à sa création notre élément est vide, il ne possède ni enfants,
        ni attributs. Pour lui en donner, on procèdera par modification, cf. ci-dessus.

      • un nouveau sommet-texte contenant une chaîne "contenu" s'obtient par
        Text texte = doc.createTextNode("contenu");

      • Exemple élémentaire : pour ajouter à la racine rac un sommet de la forme
        <personne>Jean-Luc Durand</personne>
        il convient de programmer :

        Element elt = doc.createElement("personne");
        rac.appendChild(elt);
        elt.appendChild(doc.createTextNode("
        Jean-Luc Durand"));

    2. Si on a déjà un Document : recopie

      Avec les opérations de création, on peut programmer la recopie d'un élément provenant d'un autre Document
      (rappelons que l'insertion directe est prohibée).
      Comme cette situation se présente souvent, l'interface Document possède une méthode importNode
      à deux arguments :
      1. le sommet à recopier (Node)
      2. un booléen indiquant qu'on veut recopier toute l'arborescence.

      Variante de l'exemple élémentaire précédent : pour ajouter à la racine rac un sommet x de la forme
      <personne>Jean-Luc Durand</personne>
      provenant d'un autre Document on écrit simplement :

      rac.appendChild(
      doc.importNode(x, true));
    3. Créer un Document

      Reste à se munir d'un Document si on n'en a pas un sous la main,
      notamment pour construire un nouveau document XML from scratch.

      1. Il faut pour cela se procurer un objet de type org.w3c.dom.DOMImplementation,
        qu'on ne peut obtenir qu'à partir d'un parseur, instance de javax.xml.parsers.DocumentBuilder,
        qui lui-même est produit par une javax.xml.parsers.DocumentBuilderFactory.
        Cette dernière se crée d'une manière qui dépend de l'implémentation de DOM employée :

        • avec l'implémentation standard :
          DocumentBuilderFactory df = DocumentBuilderFactory.newInstance();

        Après quoi :

        DocumentBuilder parseur = df.newDocumentBuilder();
        DOMImplementation domi = parseur.getDOMImplementation();

      2. La création d'un document demande qu'on annonce
        1. l'URI de l'espace de noms employé, qui peut être "null"
        2. le nom de l'élément racine, qui est une chaîne,
        3. et l'éventuelle DTD, qui peut être "null".

        Seul l'élément-racine nous intéresse pour le moment :

        Document doc = domi.createDocument(null, "laRacine", null);

        et nous sommes ramenés au problème précédent...

      3. Pour créer un document conforme à un standard donné, il faut aller un peu plus loin,
        en matière d'espace de noms et de DTD.
        • Soit NS  l'URL de l'espace de noms.
          Sa donnée ne suffit pas, il faut aussi prévoir le préfixe qui lui sera associé, ce préfixe pouvant rester vide.
          Il entre dans la composition du nom qualifié de la racine.
          String nomQual = pref.length()==0 ? rac : pref+":"+rac;

        • La DTD est un objet de type DocumentType.
          Il est produit à partir d'un identificateur public (publId) et d'une URL (sysId), par
          DocumentType dtd = domi.createDocumentType(nomQual, publId, sysId);
          L'un des deux (publId et sysId) peut être null.
          Sur les précautions à prendre dans une sortie ur fichier, voir plus loin.

        • Il ne reste plus qu'à demander
          Document doc = domi.createDocument(NS, nomQual, dtd);

        Exemple : pour créer un document XHTML, avec préfixe vide :
        DocumentType dtd = domi.createDocumentType(
                 "html", 
                  "-//W3C//DTD XHTML 1.0 Strict//EN",
                  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
        );

        Document doc =
        domi.createDocument(
                  "http://www.w3.org/1999/xhtml",
                  "html",
                  dtd
        );

  7. Sortie sur fichier

    On peut bien entendu programmer soi-même l'impression caractère par caractère de son arbre DOM.
    Mais à moins que l'on poursuive un but particulier, pourquoi réinventer la roue ?
    S'il s'agit simplement de transférer l'arbre sur un support permanent et communicable,
    il est évidemment préférable d'utiliser les outils standard.
    N.B. La sortie ainsi produite ne contient évidemment aucun blanc ni saut de ligne
    qui ne serait pas déjà dans le document source !

    La sortie sur fichier est vue par JAXP comme un cas particulier de transformation,
    catégorie qui contient aussi les transformations XSLT.
    La notion de transformation suppose définies une notion de source et une notion de résultat.
    Notre opération de sortie sur fichier fait donc appel au package javax.xml.transform
    et à ses sous-packages javax.xml.transform.stream et javax.xml.transform.dom.


    1. Objets source.
      Ils sont instance de la classe javax.xml.transform.dom.DOMSource,
      et créés simplement à partir d'un objet DOM Document doc par :
      DOMSource ds = new DOMSource(doc);
      ou plus généralement à partir d'un sous-arbre quelconque, repéré par un Node sommet :
      DOMSource dsa = new DOMSource(sommet);

      N.B. Les mêmes objets DOMSource servent de données pour la méthode validate
      de javax.xml.validation.Validator.

    2. Objets résultat.
      Ils sont instance de la classe javax.xml.transform.stream.StreamResult.
      On peut les créer :
      • à partir d'un pointeur sur fichier obtenu comme instance de File
        (et non pas à partir d'une chaîne nom de fichier)
      • à partir d'une URL en bonne et due forme
      • à partir d'un OutputStream (octets) ou d'un Writer (caractères).

      Par exemple, pour imprimer sur la sortie standard :
      StreamResult res = new StreamResult(System.out);

    3. Objet transformateur.
      Instance de la classe abstraite javax.xml.transform.Transformer.
      Conformément à la démarche générale de JAXP, on l'obtient à partir de
      Transformer trans = TransformerFactory.newInstance().newTransformer();

      Après quoi il n'y a plus qu'à demander : trans.transform(ds, res);
      et le tour est joué !

    4. Pour une sortie sur fichier (de nom nomFich) , on aboutit à la séquence :
      DOMSource ds = new DOMSource(doc);
      StreamResult res = new StreamResult(new File(nomFich));
      Transformer trans = TransformerFactory.newInstance().newTransformer();
      trans.transform(ds, res);
      Si on veut envoyer le texte sur un flot de sortie comme celui d'une servlette (PrintWriter out),
      on prendra simplement un autre constructeur de StreamResult
      StreamResult res = new StreamResult(out);

    5. Attention ! Pour une raison obscure, la DOCTYPE n'est pas écrite dans le fichier de sortie !
      Pour l'obtenir, il faut une manœuvre supplémentaire :
      il faut avoir les deux identificateurs publicId et systemId de notre DTD,
      et procéder à deux réglages de l'objet Transformer (appelons-le trans comme ci-dessus)
      • trans.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicId);
      • trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemId);

      où on se réfère à la classe javax.xml.transform.OutputKeys
      avant
      de lancer trans.transform(ds, res);...

      Par exemple, pour la DTD de XHTML-strict on écrira :
      trans.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//W3C//DTD XHTML 1.0 Strict//EN");
      et
      trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd");

      Si on ne dispose que d'un seul des deux  publId et sysId, un seul des deux réglages suffit.