3. Affichage d'une hiérarchie DOM

Pour créer une hiérarchie DOM ou la manipuler, il est utile d'avoir les idées claires sur la façon dont s'agence les noeuds dans un DOM. Dans cette partie, nous allons étudier cette structure interne du Document Object Model.

Affichage des noeuds de l'arbre DOM

Dans le premier chapitre, nous avons utilisé la méthode  write de la classe XmlDocument pour afficher les données. L'affichage produit ressemblait assez au fichier initial, mais ne nous aidait pas beaucoup dans notre compréhension de la structure interne de DOM.

Ce dont nous avons maintenant, est un moyen de voir les noeuds DOM et ce qu'ils contiennent. Pour faire cela, nous allons convertir un DOM en JTreeModel et afficher l'arbre DOM complet dans un JTree. Cela va demander un peu de travail, mais à la fin nous obtiendrons un bon outil de diagnostique (que nous pourrons réutiliser plus tard), ainsi qu'un outil nous permettant d'apprendre à utiliser la structure d'un DOM.

Convertion de notre programme DomEcho en une application graphique

Puisque le DOM est une structure arborescente, et que le composant Swing JTree s'occupe de l'affichage d'arbre, il est sensé de ranger notre DOM dans un JTree pour pouvoir le visualiser. La première étape pour cela est de modifier notre programme DomEcho pour qu'il devienne une application graphique.
Note: Le code étudié dans cette partie se trouve dans DomEcho02.java.

Ajout des instructions d'importation

Commencons par enlever l'import de la classe XmlDocument. Nous n'en n'aurons plus besoin, puisque nous n'utiliserons plus sa méthode write  :
 
import java.io.File;
import java.io.IOException;


import com.sun.xml.tree.XmlDocument;

Par contre, il nous faut importer tous les objets graphiques nécessaires pour l'interface de l'application :
 

// GUI components and layouts
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTree;

Plus tard, nous verrons comment améliorer notre afficheur de DOM en utilisant une version de JTree plus user-friendly. Lorsqu'un utilisateur sélectionnera un élément dans cet arbre, nous afficherons ses sous-éléments dans un editor pane adjacent. Donc pendant que l'on y est, importons les objets graphiques dont nous aurons besoin lors de cette deuxième phase :
 

import javax.swing.JSplitPane;
import javax.swing.JEditorPane;

Ajoutez ensuite les quelques classes nécessaires à l'agencement et la gestion de tout cela:
 

// GUI support classes
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.event.WindowEvent;
import java.awt.event.WindowAdapter; 

Finalement, importez quelques classes décoratives :
 

// For creating borders
import javax.swing.border.EmptyBorder;
import javax.swing.border.BevelBorder;
import javax.swing.border.CompoundBorder; 
(T

(Ceci est optionnel. Vous pouvez les enlever et le code qui en dépend, si vous ne voulez pas de jolies bordures :-)

Création de l'interface graphique

La prochaine étape consiste à convertir notre application en application graphique. Pour effectuer cela, la méthode main créera une instance de la classe principale, qui sera devenu un panneau (un JPanel quoi!).
Commencez donc par convertir la classe en un JPanel en étendant la classe de Swing JPanel :
 
public class DomEcho02 extends JPanel
{
    // Global value so it can be ref'd by the tree-adapter
    static Document document; 
    ...

Tant que vous y etes, définissez quelques constantes que nous utiliserons pour controler les tailles des fenetres :
 

public class DomEcho02 extends JPanel
{
    // Global value so it can be ref'd by the tree-adapter
    static Document document; 

    static final int windowHeight = 460;
    static final int leftWidth = 300;
    static final int rightWidth = 340;
    static final int windowWidth = leftWidth + rightWidth;

Maintenant, dans la méthode principale, enlevez les lignes qui produisait l'affichage du document XML, et remplacez-les par une invocation de méthode qui créera une fenetre dans laquelle se logera notre panneau :
 

 public static void main (String argv [])
 {
    ...
    DocumentBuilderFactory factory ...
    try {
       DocumentBuilder builder = factory.newDocumentBuilder();
       document = builder.parse( new File(argv[0]) );

       XmlDocument xdoc = (XmlDocument) document;
       xdoc.write (System.out);
       makeFrame();

    } catch (SAXParseException spe) {
       ...

Ensuite, nous devons définir la méthode makeFrame (bah oui, il n'y a pas de miracles :-). Elle contient le code standard de création d'une fenetre, gestion de la sortie d'application, gestion des tailles, de l'ajout de notre panneau, et finalement de l'affichage de la fenetre nouvellement créée :
 

  ...
} // main

public static void makeFrame() 
{
    // Set up a GUI framework
    JFrame frame = new JFrame("DOM Echo");
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {System.exit(0);}
    });

    // Set up the tree, the views, and display it all
    final DomEcho02 echoPanel = new DomEcho02();
    frame.getContentPane().add("Center", echoPanel );
    frame.pack();
    Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
    int w = windowWidth + 10;
    int h = windowHeight + 10;
    frame.setLocation(screenSize.width/3 - w/2, screenSize.height/2 - h/2);
    frame.setSize(w, h);
    frame.setVisible(true);
} // makeFrame

Ajout des composants gérant l'affichage

Le dernier effort à produire pour obtenir notre application graphique consiste à créer le constructeur de notre classe, et lui faire créer le contenu de notre panneau. Voici donc ce constructeur :
 
public class DomEcho02 extends JPanel
{
    ...
    static final int windowWidth = leftWidth + rightWidth; 

    public DomEcho02()
    {
    } // Constructor

Ici, on rajoute des jolies bordures (que nous avions importées plus tot), que vous pouvez enlever si vous voulez simplifier :
 

public DomEcho02()
{
   // Make a nice border
   EmptyBorder eb = new EmptyBorder(5,5,5,5);
   BevelBorder bb = new BevelBorder(BevelBorder.LOWERED);
   CompoundBorder cb = new CompoundBorder(eb,bb);
   this.setBorder(new CompoundBorder(cb,eb));

} // Constructor

Ensuite, créons un arbre vide et mettons le dans un JScrollPane, ainsi l'utilisateur pourra visualiser son contenu quelque soit sa taille :
 

public DomEcho02()
{
   ...

   // Set up the tree
   JTree tree = new JTree();

   // Build left-side view
   JScrollPane treeView = new JScrollPane(tree);
   treeView.setPreferredSize(  
      new Dimension( leftWidth, windowHeight ));

} // Constructor

Maintenant, créez un JEditPane non éditable qui se chargera de l'affichage du contenu des noeuds dans notre version futur :
 

public DomEcho02()
{
   ....

   // Build right-side view
   JEditorPane htmlPane = new JEditorPane("text/html","");
   htmlPane.setEditable(false);
   JScrollPane htmlView = new JScrollPane(htmlPane);
   htmlView.setPreferredSize( 
       new Dimension( rightWidth, windowHeight ));

}  // Constructor

Avec notre Jtree à gauche, et notre JEditorPane à droite, utilisons un JSplitPane pour les ranger (et les séparer) :
 

public DomEcho02()
{
   ....

   // Build split-pane view
   JSplitPane splitPane = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT,
                                          treeView,
                                          htmlView );
   splitPane.setContinuousLayout( true );
   splitPane.setDividerLocation( leftWidth );
   splitPane.setPreferredSize( 
       new Dimension( windowWidth + 10, windowHeight+10 ));

}  // Constructor

Avec ces instructions, on règle le JSplitPane avec une séparation verticale entre notre arbre et notre editor pane. Nous réglons aussi la localisation de la séparation pour que l'arbre puisse obtenir sa taille "préférée", et le reste de la fenetre étant allouée à notre brave editor pane. Finalement, on spécifie l'agencement du panneau et l'on rajoute le JSplitPane :
 

public DomEcho02()
{
   ...

   // Add GUI components
   this.setLayout(new BorderLayout());
   this.add("Center", splitPane );

} // Constructor

Félicitations ! Le programme est maintenant une application graphique. Vous pouvez l'exécuter pour voir de quoi il aura l'air à l'écran. Juste pour vérifier votre solution, voici le code complet du constructeur :
 

    public DomEcho02()
    {
       // Make a nice border
       EmptyBorder eb = new EmptyBorder(5,5,5,5);
       BevelBorder bb = new BevelBorder(BevelBorder.LOWERED);
       CompoundBorder cb = new CompoundBorder(eb,bb);
       this.setBorder(new CompoundBorder(cb,eb));

       // Set up the tree
       JTree tree = new JTree();

       // Build left-side view
       JScrollPane treeView = new JScrollPane(tree);
       treeView.setPreferredSize(  
           new Dimension( leftWidth, windowHeight ));

       // Build right-side view
       JEditorPane htmlPane = new JEditorPane("text/html","");
       htmlPane.setEditable(false);
       JScrollPane htmlView = new JScrollPane(htmlPane);
       htmlView.setPreferredSize( 
           new Dimension( rightWidth, windowHeight ));

       // Build split-pane view
       JSplitPane splitPane = new JSplitPane( JSplitPane.HORIZONTAL_SPLIT,
                                              treeView,
                                              htmlView );
       splitPane.setContinuousLayout( true );
       splitPane.setDividerLocation( leftWidth );
       splitPane.setPreferredSize( 
            new Dimension( windowWidth + 10, windowHeight+10 ));

       // Add GUI components
       this.setLayout(new BorderLayout());
       this.add("Center", splitPane );
    } // Constructor

Création d'Adapters pour afficher notre DOM dans un JTree

Maintenant que nous avons une jolie interface, il nous faut passer à son remplissage pour que le JTree affiche notre DOM. Malheureusement un JTree n'affiche qu'un TreeModel. Nous allons donc devoir créer une classe d'adaptation (Adapter) qui transforme notre DOM en TreeModel pour notre JTree.

Donc, lorsque le TreeModel passe des noeuds au JTree, le JTree utilise la méthode toString de ces noeuds pour récupérer le texte à afficher dans l'arbre. L'implémentation standard de toString n'est pas très lisible, nous devrons donc enrober les noeuds DOM dans des AdapterNode qui retourneront le texte que nous désirons. Ce que fournira le TreeModel au JTree, sera en fait des objets AdapterNode qui encapsuleront les noeuds DOM.

Note: Les classes qui suivent sont définies en tant que classes internes anonymes. Si vous développez en 1.1 (mettez-vous à jour !), vous devrez les définir dans des fichiers séparés.

Définition de la classe AdapterNode

Commençons par importer les classes nécessaires (tree, event ....) :
 
// For creating a TreeModel
import javax.swing.tree.*;
import javax.swing.event.*;
import java.util.*;

public class DomEcho02 extends JPanel
{

Retournons en bas du programme, et définissons un ensemble de chaines pour les différents types d'éléments noeud :
 

      ...
    } // makeFrame

    // An array of names for DOM node-types
    static final String[] typeName = {
        "none",
        "Element",
        "Attr",
        "Text",
        "CDATA",
        "EntityRef",
        "Entity",
        "ProcInstr",
        "Comment",
        "Document",
        "DocType",
        "DocFragment",
        "Notation",
    };

 } // DomEcho

Ce sont ces chaines qui seront affichées dans le JTree. Les spécifications de ces types de noeuds peuvent etre trouvées dans les commentaires de la classe  org.w3c.dom.Node. Ensuite, définissons notre classe AdapterNode qui encapsulera nos noeuds DOM :
 

    static final String[] typeName = {
        ...
    };

    public class AdapterNode 
    
      org.w3c.dom.Node domNode;

      // Construct an Adapter node from a DOM node
      public AdapterNode(org.w3c.dom.Node node) {
        domNode = node;
      }

      // Return a string that identifies this node in the tree
      // *** Refer to table at top of org.w3c.dom.Node ***
      public String toString() {
        String s = typeName[domNode.getNodeType()];
        String nodeName = domNode.getNodeName();
        if (! nodeName.startsWith("#")) {
           s += ": " + nodeName;
        }
        if (domNode.getNodeValue() != null) {
           if (s.startsWith("ProcInstr")) 
              s += ", "; 
           else 
              s += ": ";
           // Trim the value to get rid of NL's at the front
           String t = domNode.getNodeValue().trim();
           int x = t.indexOf("\n");
           if (x >= 0) t = t.substring(0, x);
           s += t;
        }
        return s;
      }

    } // AdapterNode

} // DomEcho

Cette classe possède un attribut pour gérer un noeud DOM, et nécessite que l'on le lui définisse dans son constructeur. On définit ensuite la méthode toString, qui retourne le type du noeud en fonction des constantes que nous avons défini ci-dessus, et rajoute éventuellement des informations complémentaires pour identifier le noeud. Comme vous pouvez le voir dans la table des types de noeuds dans org.w3c.dom.Node, chaque noeud posséde un type, un nom, et une valeur, qui peut etre vide ou non. Dans les cas ou le nom du noeud commence par '#', le champs duplique le type du noeud, il faut alors inclure un ':'. Cela explique les lignes suivantes :
 

if (! nodeName.startsWith("#")) {
    s += ": " + nodeName;
}

Le restant de la méthode toString s'occupe aussi de cas particuliers. Par exemple les trois lignes suivantes :
 

if (s.startsWith("ProcInstr")) 
    s += ", "; 
else 
    s += ": ";

fournissent juste du "sucre syntaxique". Le type du champs d'une instructions de traitement se termine avec un ':', cel apermet donc d'éviter le doublement de ce deux-points. Les dernières lignes intéressantes sont :
 

String t = domNode.getNodeValue().trim();
int x = t.indexOf("\n");
if (x >= 0) t = t.substring(0, x);
s += t;

Ces lignes permettent de tronquer la valeur d'un champ au premier 'newline' rencontré dans le champs. Si l'on ne faisait pas cela, on verrait apparaitre quelques caractères amusants (des carrés souvent) dans le JTree.

Note: Il est intéressant de noter ici que XML standardise la notion de fin de ligne, indépendamment du système d'ou proviennent les données. Cela rend la tache un peu plus simple.
Encapsuler un DomNode et retourner la chaine désirée sont les fonctionnalités majeures de  la classe AdapterNode. Mais comme l'Adapter de TreeModel devra répondre à des questions comme "Combien ce noeud posséde d'enfants ?" et satisfaire des requetes du type "Donne moi le Nième enfant de ce noeud, il va etre nécessaire de définir quelques méthodes supplémentaires. (L'Adapteur de TreeModel pourrait accéder directement au DomNode pour obtenir les informations, mais procéder de cette manière procure une meilleure encapsulation)

Rajoutez le code en gras ci dessous pour retourner l'index de l'enfant spécifié, le fils correspondant à un index donné, et le nombre d'enfants :
 

    public class AdapterNode 
    
      ...
      public String toString() {
        ...
      }

      public int index(AdapterNode child) {
        //System.err.println("Looking for index of " + child);
        int count = childCount();
        for (int i=0; i<count; i++) {
          AdapterNode n = this.child(i);
          if (child == n) return i;
        }
        return -1; // Should never get here.
      }

      public AdapterNode child(int searchIndex) {
        //Note: JTree index is zero-based. 
        org.w3c.dom.Node node = domNode.getChildNodes().item(searchIndex);
        return new AdapterNode(node); 
      }

      public int childCount() {
          return domNode.getChildNodes().getLength();  
      }
    } // AdapterNode

} // DomEcho
Note: Lors du développement, ce n'est qu'après avoir écrit l'Adapteur de TreeModel que j'ai réalisé la nécessité de ces méthodes, et que j'ai du les rajouter. Dans quelques instants, vous comprendrez pourquoi.

Définition de l'Adapteur de TreeModel

Nous sommes maintenant pret à écrire notre Adapteur de TreeModel. Une chose vraiment sympathique à propos du JTree est la facilité relative avec laquelle on peut convertir un arbre existant pour l'afficher. Une des raisons expliquant cela provient de la séparation claire existante entre l'affichage, qu'utilise le JTree, et les modifications possibles que l'application peut produire. Un point important de l'interface TreeModel est qu'il nous suffit de founir les méthodes (a) pour accéder aux fils (b) enregistrer le JTreeListener approprié, pour qu'il puisse se mettre à jour lorsque le modèle sous-jacent évolu.

Rajoutez le code ci-dessous pour créer l'Adapter de TreeModel et spécifier les méthodes de traitement des fils :
 

      ...

    } // AdapterNode
    // This adapter converts the current Document (a DOM) into 
    // a JTree model. 
    public class DomToTreeModelAdapter implements javax.swing.tree.TreeModel 
    {
      // Basic TreeModel operations
      public Object  getRoot() {
        //System.err.println("Returning root: " +document);
        return new AdapterNode(document);
      }
      public boolean isLeaf(Object aNode) {
        // Determines whether the icon shows up to the left.
        // Return true for any node with no children
        AdapterNode node = (AdapterNode) aNode;
        if (node.childCount() > 0) return false;
        return true;
      }
      public int     getChildCount(Object parent) {
        AdapterNode node = (AdapterNode) parent;
        return node.childCount();
      }
      public Object  getChild(Object parent, int index) {
        AdapterNode node = (AdapterNode) parent;
        return node.child(index);
      }
      public int     getIndexOfChild(Object parent, Object child) {
        AdapterNode node = (AdapterNode) parent;
        return node.index((AdapterNode) child);
      }
      public void    valueForPathChanged(TreePath path, Object newValue) {
        // Null. We won't be making changes in the GUI
        // If we did, we would ensure the new value was really new
        // and then fire a TreeNodesChanged event.
      }

    } // DomToTreeModelAdapter

} // DomEcho

Dans ces lignes, la méthode getRoot retourne le noeud racine du DOM, encapsulé dans un AdapterNode. A partir de ce point, tout le snoeuds retournés par l'Adapter seront des AdapterNodes qui encapsuleront des noeuds DOM. De la meme manière, lorsque le JTree demandera le fils d'un noeud, le nombre de fils d'un noeud, etc, le JTree se verra passer un AdapterNode. Nous en sommes sur, car nous controlons chaque noeud que "voit" le JTree, en commençant par le noeud racine.

La classe JTree utilise la méthode isLeaf pour déterminer si elle doit afficher un icone de répertoire (que l'on peut ouvrir ou fermer), cette méthode ne retourne donc vrai que lorsque le noeud posséde au moins un fils. Dans cette méthode, nous effectuons un cast sur un Object vers un AdapterNode. Nous pouvons nous permettre cela, car c'est nous qui avons fourni au JTree des AdapterNode, il nous faut cependant les "re-caster" pour pouvoir les utiliser.

Les trois méthodes suivantes retournent respectivement le nombre d'enfants d'un noeud donné, l'enfant se trouvant à l'index donné, et l'index d'un enfant donné. Tout ce ceci reste assez direct. La dernière méthode est invoquée lorsque l'utilisateur change une valeur contenues dans le JTree. Dans notre application, nous ne supporterons pas cette fonctionnalité. Mais si nous le voulions, l'application aurait à effectuer les changements sur le modèle sous-jacent et à informer tous les listeners qu'un changement s'est produit. (Le JTree n'est pas forcément le seul listener, dans la plupart des applications, ce n'est pas le cas !)

Pour informer tous les listeners qu'un changement s'est produit, il faut leur fournir la possibilité de s'enregistrer. Ce qui nous amène à écrire les deux dernières méthodes de l'interface TreeModel. Rajoutez le code en gras pour les définir :
 

public class DomToTreeModelAdapter ...
{
  ...
  public void    valueForPathChanged(TreePath path, Object newValue) {
    ...
  }
      
  private Vector listenerList = new Vector();
  public void addTreeModelListener( TreeModelListener listener ) {
    if ( listener != null && ! listenerList.contains( listener ) ) {
       listenerList.addElement( listener );
    }
  }
  public void removeTreeModelListener( TreeModelListener listener ) {
    if ( listener != null ) {
       listenerList.removeElement( listener );
    }
  }

} // DomToTreeModelAdapter

Comme cette application ne gérera pas la modification de l'arbre, ces méthodes nes seront pas utilisées pour l'instant. Cependant, elles pourront l'etre dans le futur, lorsque vous en aurez besoin.

Note:
Cette exemple utilise un Vector pour la compatibilité avec les applications 1.1.
Si vous travaillez en 1.2, pensez à utiliser le framework sur les Collections à la place :
 
private LinkedList listenerList = new LinkedList();

Les opérations sur la List sont alors add et remove. Pour itérer sur les élements de cette liste, vous pouvez utiliser :
 

Iterator it = listenerList.iterator();
while ( it.hasNext() ) {
  TreeModelListener listener = (TreeModelListener)it.next();
   ... 
}
Voici de nouveau des méthodes optionnelles qui ne sont pas nécessaires pour notre application actuelle. Cependant, au point où nous en sommes, nous avons pratiquement un joli "patron" de TreeModel. Pour etre complet sur le sujet, rajoutons donc ces dernières méthodes qui permettent de notifier les JTreeListeners lorsque des changements se produisent :
 
  public void removeTreeModelListener( TreeModelListener listener ) {
    ...
  } 
  public void fireTreeNodesChanged( TreeModelEvent e ) {
    Enumeration listeners = listenerList.elements();
    while ( listeners.hasMoreElements() ) {
      TreeModelListener listener = (TreeModelListener)listeners.nextElement();
      listener.treeNodesChanged( e );
    }
  
  public void fireTreeNodesInserted( TreeModelEvent e ) {
    Enumeration listeners = listenerList.elements();
    while ( listeners.hasMoreElements() ) {
       TreeModelListener listener = (TreeModelListener)listeners.nextElement();
       listener.treeNodesInserted( e );
    }
  }   
  public void fireTreeNodesRemoved( TreeModelEvent e ) {
    Enumeration listeners = listenerList.elements();
    while ( listeners.hasMoreElements() ) {
      TreeModelListener listener = (TreeModelListener)listeners.nextElement();
      listener.treeNodesRemoved( e );
    }
  }   
  public void fireTreeStructureChanged( TreeModelEvent e ) {
    Enumeration listeners = listenerList.elements();
    while ( listeners.hasMoreElements() ) {
      TreeModelListener listener = (TreeModelListener)listeners.nextElement();
      listener.treeStructureChanged( e );
    }
  }
} // DomToTreeModelAdapter
Note:
Ces méthodes sont reprises dans la description de la classe TreeModelSupport décrite dans Comprendre le modèle d'arbre. cette architecture à été réalisée par Tom Santos et Steve Wilson, et est beaucoup plus élégante que le rapide petit hack que nous avons ici..

Finition

Maintenant, nous avons pratiquement fini. Tout ce qu'il nous reste à faire est de retourner dans le constructeur et d'ajouter le code construisant et fixant notre Adapteur au JTree :
 
// Set up the tree
JTree tree = new JTree(new DomToTreeModelAdapter());

Vous pouvez maintenant compiler et exécuter le code sur un fichier XML.