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.