Académique Documents
Professionnel Documents
Culture Documents
Menu
SI vous consultez ce document en ligne, vous pouvez cliquer sur n'importe que lien pour aller à la section correspondante
1. Introduction 2
2. L'API Document Object Model 3
3. L'API JDOM 39
Section 1. Introduction
Introduction
Ce tutoriel se donne pour objectif de donner un aperÇu des API DOM (Document
Object Model) et JDOM , dont la finalité est de lire et de manipuler des fichiers XML.
Bonne navigation!
Introduction au DOM
Les principales caractéristiques du modèle DOM sont les suivantes:
1. Le modèle DOM (contrairement sur ce point à une autre API fameuse: SAX),
représente une spécification qui puise ses origines dans le consortium w3C
(elle jouit donc sur ce point d'un niveau de 'respectabilité' égal à la spécification
XML elle-même).
2. Le modèle DOM est non seulement une spécification multi-plateformes, mais
aussi multi-langages: il existe des liaisons avec Java, Javascript, CORBA et
d'autres langages encore...
3. Le modèle DOM est organisé en niveaux plutôt qu'en versions.
* DOM niveau 1 représente une Recommandation acceptée dont la
spécification complète est disponible à l'adresse
http://www.w3.org/TR/REC-DOM-Level-1 (sa traduction franÇaise est
quant à elle disponible à l'adresse
http://xmlfr.org/w3c/TR/REC-DOM-Level-1/ ). Celle-ci détaille dans une
première partie un ensemble minimum d'objets et d'interfaces
fondamentales pour accéder et manipuler des objets documentaires (XML
mais aussi HTML), ainsi qu'un ensemble d'interfaces étendues spécifiques à
la manipulation de documents XML (traitant par exemple les CDATA ou les
Processing Instructions): cette première partie s'appelle le noyau DOM (core
DOM). Dans une seconde partie, la spécification DOM niveau 1 s'attache à
décrire les objets et les méthodes spécifiques aux documents HTML, qui
n'ont pas été définis dans le noyau.
* DOM niveau 2 , finalisé en novembre 2000, disponible à l'adresse
http://www.w3.org/TR/DOM-Level-2-Core/ étend le niveau 1 en proposant
un certain nombre d'interfaces supplémentaires. En ce qui concerne le
traitement de documents XML, DOM niveau 2 supporte en plus, par
exemple, les espaces de noms (on peut ainsi créer ou retrouver un élément
ou un attribut grâce non seulement à son nom local, mais aussi via son
espace de nom (ce sont par exemple et parmi de nombreuses autres
méthodes les méthodes attachées à l'interface Document
createElementNS(), createAttributeNS() et
getElementsByTagNameNS() qui permettent de faire cela).
* DOM niveau 3, finalisé le 22 octobre 2002, disponible à l'adresse
http://www.w3.org/TR/2002/WD-DOM-Level-3-Core-20021022/DOM3-Core.html ,
propose un certain nombre d'interfaces et de méthodes supplémentaires,
parmi lesquelles la possibilité de retrouver les informations relatives à la
déclaration XML (version, encodage), la comparaison de noeuds au
sein d'un document (via les méthodes isEqualNode() et
isSameNode(), la comparaison de la position entre deux noeuds au
sein d'un même document via la méthode
compareDocumentPosition(), et beaucoup d'autres choses encore...
throws DOMException;
public NodeList getElementsByTagNameNS(String namespaceURI,
String localName);
public Element getElementById(String elementId);
}
Par chance, cette API ressemble très étroitement à la structure des documents qu'elle
modélise. Par exemple, si l'on considère le document XML suivant:
<?xml version="1.0" encoding="iso-8859-1?">
<catalogue>
<livre>
<titre>La généalogie de la morale</titre>
<auteur>Friedrich Nietzsche</auteur>
<édition>folio essais</édition>
<ISBN>2-07-032327-7</ISBN>
</livre>
<livre>
<titre>Réflexions sur la poésie</titre>
<auteur>Paul Claudel</auteur>
<édition>folio essais</édition>
<ISBN>2-07-032746-9</ISBN>
</livre>
</catalogue>
Récupération de noeuds
Après cette brève présentation, utilisons DOM afin de parser et de récupérer des
données de notre fichier catalogue.xml.
La première chose à faire de créer un objet de type org.w3c.dom.Document. Tant
que l'intégralité du document n'a pas été analysée et ajoutée dans la structure
arborescente à ce niveau supérieur par rapport à l'élément racine du document XML,
les données du fichier d'entrée ne se trouvent pas dans un état utilisable. En fait,
comme le standard DOM ne spécifie pas de méthode pour obtenir l'objet Document, il
existe plusieurs méthodes à cette fin. Puisque nous nous focalisons sur le parseur
Xerces, la méthodologie à suivre est la suivante:
1. D'abord, ne pas oublier d'importer le parseur concerné, ici Xerces, via
l'instruction import org.apache.xerces.parsers.DOMParser
2. Ensuite, instancier un objet de la classe Parseur, via DOMParser parseur =
new DOMParser();
3. Enfin, utiliser la méthode getDocument() du parseur ainsi obtenu afin d'obtenir
Nous verrons plus loin dans ce tutoriel qu'il existe une autre faÇon de procéder via
l'API JAXP afin de standardiser l'accès à une arborescence DOM depuis une
implémentation quelconque d'un analyseur.
Tentons à présent de récupérer les titres des livres composant le catalogue: Pour ce
faire, nous récupérons d'abord l'élément racine du document catalogue.xml, via la
méthode getDocumentElement() appliquée au noeud de type Document document
précédemment défini. Ensuite, nous définissons un noeud de type NodeList,
équivalent à une Collection Java, qui regroupe tous les éléments dont le type est titre,
via la méthode getElementsByTagName("titre"). Enfin, nous itérons sur cette
NodeList afin de récupérer la valeur de chacun des noeuds de type Text, fils des
éléments <titre>: en effet la structure hiérarchique du DOM impose de devoir
récupérer le contenu textuel d'un élément, non à partir de l'élément lui-même
(comme cela se fait avec JDOM ainsi que nous le verrons plus tard grâce à la
méthode getText()), mais à partir du noeud Text fils de l'élément. La première
étape (récupération du noeud Text fils de l'élément <titre> se fait grâce à la
méthode getFirstChild(), tandis que la récupération de la valeur textuelle se fait
en utilisant la méthode getNodeValue() (cette denière méthode s'applique
également aux noeuds CDATA, comment et processing instructions).
Si nous voulons récupérer l'ensemble des informations afférentes à chacun des livres,
on risque de se livrer à un travail assez fastidieux. DOM est en effet un langage
bavard. Peut-être est-il plus judicieux de créer des méthodes réutilisables pour
chacune des quatre éléments caractérisant le livre (auteur, titre, edition, ISBN).
Dès lors, la récupération de toutes les informations concernant chacun de ces livres
est très simplifiée: Après avoir repris les quelques étapes du panneau précédent
(instanciation d'un parseur, création du noeud Document, récupération de l'élément
racine du document, récupération de tous les éléments <titre>), on appelle pour
chaque caractéristique du livre définie dans catalogue.xml, la méthode de classe
trouveTexte() définie dans la classe AnnexeDOM, avec comme arguments un
noeud <livre> extrait de la NodeList sur laquelle on itère, et la chaîne de caractères
correspondant au nom de l'élément enfant de ce noeud <livre>que l'on veut
récupérer. Le code est le suivant:
//DOM
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
//Importation de l'analyseur xerces
import org.apache.xerces.parsers.DOMParser;
public class TestDOM1
{
public static void main( String [] args ) throws Exception
{
DOMParser parser = new DOMParser(); //instanciation parseur
parser.parse("catalogue.xml"); //analyse du fichier catalogue.xml
Document document = parser.getDocument(); //récupération du document englobant to
données analysées
Element catalogue = document.getDocumentElement();
//récupération de l'élément racine du document, ici <catalogue>
NodeList livres = catalogue.getElementsByTagName("livre");
//récupération de tous les éléments <livre> inclus dans <catalogue>
System.out.println("Les caractéristiques des livres du catalogue sont:");
for( int i=0; i<livres.getLength(); i++ ) {
String titre = AnnexeDOM.trouveTexte( (Element)livres.item(i),"titre" );
String auteur = AnnexeDOM.trouveTexte( (Element)livres.item(i),"auteur" );
String édition = AnnexeDOM.trouveTexte(
(Element)livres.item(i),"édition" );
String ISBN = AnnexeDOM.trouveTexte( (Element)livres.item(i),"ISBN" );
System.out.println("\ntitre: "+ titre+"\nauteur:
"+auteur+"\nédition: "+ édition +"\nISBN: "+ ISBN);
}
}
}
Lorqu'on exécute ce programme (dont le fichier source est disponible ici), on obtient
cran suivant:
Bien évidemment, au niveau du livre Réfléxions sur la poésie, nous obtenons une
référence null pour son auteur. Simplement parce que le premier élément fils n'est
pas de type Text mais Element, ce pourquoi nous ne pouvons lui appliquer la
méthode getNodeValue().
Récupération d'attributs
Voyons à présent comment récupérer un genre particuliers de noeuds que
représentent les attributs. Supposons que dans notre fichier catalogue.xml, nous
rajoutions des attributs, spécifiant par exemple la langue utilisée:
<catalogue>
<livre>
<titre langue="fr">La généalogie de la morale</titre>
...
</livre>
<livre>
<titre langue="fr">Réflexions sur la poésie</titre>
...
</livre>
</catalogue>
et que nous voulions les récupérer afin d'afficher le nom et la valeur de ces attributs.
Le fonctionnement d'une telle méthode est le suivant: En premier lieu, nous réutilisons
la méthode trouvePremierElement déjà définie dans la classe afin de récupérer
chaque premier élément d'un nom donné au sein d'un élément donné (ici, comme on
l'a vu, il s'agit d'un élément <livre>) Ensuite, nous récupérons les attributs de cet
élément grâce à la méthode getAttributes (proposée par l'interface Node) ,
laquelle retourne un objet NamedNodeMap. Cet objet partage avec NodeList la
propriété d'être itérable. C'est donc ce que nous faisons, en récupérant à chaque fois
à la fois le nom de l'attribut via getNodeName() ainsi que sa valeur, via
getNodeValue()
Pour appeler cette méthode à partir de la classe TestDOM, il suffit alors d'insérer entre
deux instructions d'impression écran, l'appel à notre méthode trouveAttribut(),
comme ceci:
System.out.println("Les caractéristiques des livres du catalogue sont:");
for( int i=0; i<livres.getLength(); i++ ) {
String titre = AnnexeDOMbuf1.trouveTexte( (Element)livres.item(i),"titre" )
String auteur = AnnexeDOMbuf1.trouveTexte( (Element)livres.item(i),"auteur"
String édition = AnnexeDOMbuf1.trouveTexte(
(Element)livres.item(i),"édition" );
String ISBN = AnnexeDOMbuf1.trouveTexte( (Element)livres.item(i),"ISBN"
System.out.println("\ntitre: "+ titre);
AnnexeDOMbuf1.trouveAttribut((Element)livres.item(i),"titre");
System.out.print("\nauteur: " + auteur + "\nédition: " + édition + "\nISBN: "+ ISB
On remarque que la cible de ce formulaire est comme dit précédemment une servlet,
et que le fait d'appuyer sur le bouton de valeur Ajouter/Mettre à jour lancera la
méthode doPost() de la servlet située à l'URL
http://localhost:8080/tuto/update. Nous utilisons ici le moteur de servlet
Tomcat4.0.1 dont nous avons déjà décrit l'installation dans un précédent tutoriel, à
l'adresse http://www.planetexml.com/base_xml/base_xml-4-3.html . La seule chose à
préciser ici est la manière d'accéder à la servlet. Nous avons créé l'arborescence
webapps->tuto->WEB-INF->classes ->MiseAJourServlet.class, ainsi que le
montre la fenêtre d'Explorateur Windows suivante:
Ceci étant mis en place, il suffit juste d'éditer un fichier web.xml dans le dossier
WEB-INF et d'y insérer les quelques lignes de code suivantes:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>UpDate</servlet-name>
<servlet-class>MiseAJourServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UpDate</servlet-name>
<url-pattern>/update</url-pattern>
</servlet-mapping>
</web-app>
De plus, si l'on veut éviter de redémarrer Tomcat à chaque fois que l'on modifie
une classe dans notre répertoire de travail, il convient également de modifier le
fichier server.xml situé dans le répertoire conf de Tomcat de la faÇon suivante:
<!--Contexte Repertoire tuto -->
<Context path="/tuto" docBase="tuto" debug="0"
reloadable="true"/>
La méthode ci-dessus exposée commmence par récupérer les valeurs des quatre
paramètres no, titre, auteur et description. Une fois cela fait, elle crée un
nouvel arbre DOM via la méthode createDocument() (introduite dans DOM
Sinon, si le fichier spécifié existe déjà, il nous faut alors le modifier à l'aide des
nouveaux paramètres renseignés par l' utilisateur. Le code afin de parvenir à un tel but
pourrait être le suivant:
DOMParser parser = new DOMParser();
parser.parse(fichierXML.toURL().toString());
doc = parser.getDocument();
Element root = doc.getDocumentElement();
// Titre du livre
NodeList elementsTitre = root.getElementsByTagName("titre");
Element elementTitre = (Element)elementsTitre.item(0);
Text texteTitre = (Text)elementTitre.getFirstChild();
texteTitre.setData(titre);
// Auteur du livre
NodeList elementsAuteur = root.getElementsByTagName("auteur");
Element elementAuteur = (Element)elementsAuteur.item(0);
Text texteAuteur = (Text)elementAuteur.getFirstChild();
texteAuteur.setData(auteur);
// Description du livre
NodeList elementsDescription =
root.getElementsByTagName("description");
Element elementDescription = (Element)elementsDescription.item(0);
// Supprime et recrée la description
root.removeChild(elementDescription);
elementDescription = doc.createElement("description");
Text texteDescription = doc.createTextNode(description);
elementDescription.appendChild(texteDescription);
root.appendChild(elementDescription);
On commence par parser le document existant via la méthode parse déjà vue, puis
on récupère son élément racine, toujous avec la méthode getDocumentElement().
A partir de là, on récupère chacun des premiers noeuds de nom <auteur> ou
<titre>, puis on leur transfère un contenu contextuel correspondant au paramètre
de même nom via la méthode setData(). Pour l'élément <description>, on utilise
une approche légèrement différente car un tel élément peut contenir de nombreux
éléments imbriqués (par exemple des élément HTML) qui empêchent la récupération
du premier élément textuel à remplacer par la valeur du paramètre description. Le
plus simple ici est d'enlever directement l'élement <description> de la hiérarchie
Enfin, on sérialise l'arbre DOM (via la classe DOMSerialiseur.java que nous détaillons
au prochain chapitre), puis on écrit un message de bonne exécution de la requête via
le code suivant:
// Serialise l'arborescence DOM
DOMSerialiseur serializer = new DOMSerialiseur();
serializer.serialise(doc, fichierXML);
// Confirmation écrite du traitement
PrintWriter out = res.getWriter();
res.setContentType("text/html");
out.println("Merci pour votre requête: " +
"Celle-ci a bien été traitée.");
out.close();
Tant qu'à faire, onpeut également mettre le code HTML du formulaire au sein de la
servlet elle-même, par exemple à travers la méthode doGet(). Code source complet
de la servlet MiseAJourServlet.java Lorqu'on pointe le navigateur sur l'adresse
http://localhost:8080/tuto/update, on obtient alors l'écran suivant:
l'on obtient l'écran suivant après avoir cliqué sur le bouton de confirmation de requêtes,
si tout se passe bien:
Sérialisation
Jusqu'à présent, nous avons parsé le document catalogue.xml ou variantes, en
construisant grâce à Xerces une arborescence DOM de ce document. A partir de là,
nous avons récupéré tel ou tel élément, ou bien encore tel ou tel attribut de cet arbre
DOM. Cependant, l'une des quesiotns les plus courantes concernant l'utilisation de
DOM concerne la sérialisation des arborescences DOM, autrement dit, la faÇon dont
celles-ci peuvent être enregistrées dans un fichier. En fait, les Niveaux 1 et 2 de
DOM ne proposent aucune manière de faire cela, et cela reviendra à la charge de
DOM Niveau 3. En attendant, et avant de voir une autre faÇon de contourner le
problème via JAXP1.1 et l'API TrAX, voyons comment procéder à une telle
rialisation via des moyens ordinaires (et un peu lourds, il faut le reconnaître...). Etant
donné que nous avons plusieurs versions du fichier catalogue.xml, une bonne idée
consiste peut-être à indiquer le nom du fichier à sérialiser en paramètre, afin de ne pas
avoir à changer celui-ci dans le code et de recompiler à chaque fois. Pour cela, nous
écrivons le code suivant:
import java.io.File;
import org.w3c.dom.Document;
import org.apache.xerces.parsers.DOMParser;
public class Serialiseur {
public void serialise(String documentXML, String nomFichierSortie)
throws Exception {
File fichierSortie = new File(nomFichierSortie);
DOMParser parseur = new DOMParser();
parseur.parse(documentXML);
Document document = parseur.getDocument();
//Sérialise
}
public static void main(String[] args) {
if (args.length !=2) {
System.out.println(
"Usage: java Serialiseur [document XML à lire] "
+ "[nom du fichier de sortie]");
System.exit(0);
}
try {
Serialiseur serialiseur = new Serialiseur();
serialiseur.serialise(args[0], args[1]);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Sérialisation (suite 1)
Dans le code défini dans le panneau précédent, il nous manque bien sûr l'essentiel,
c'est-à-dire la définition d'une classe effective de sérialisation. Nous allons ici
commenter pas à pas une classe de sérialisation codée par Brett McLaughlin, un des
gourous JAVA/XML, et qu'il propose dans le chapitre consacré à DOM dans son livre
JAVA&XML publié aux éditions O'REILLY. Cette classe de sérialisation reste
imparfaite, notamment en termes d'indentations et de retours chariots, mais elle donne
un très bon aperÇu de la faÇon dont on peut travailler en DOM. Dans un premier
temps, nous configurons notre classe DOMSerialiseur de manière à la rendre la
plus générique possible. Cette configuration concerne:
1. la mise en forme du flot de sortie d'une part, via la définition de deux variables
d'instance privées indentation et sautLigne, ainsi que la définition de deux
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentType;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class DOMSerialiseur {
// Indentation à utiliser
private String indentation;
// Saut de ligne à utiliser
private String sautLigne;
// Constructeur initialisant les membres de données
public DOMSerialiseur() {
indentation = "";
sautLigne = "\n";
}
// Accesseur en modification définissant l'indentation à utiliser
public void setIndentation(String indentation) {
this.indentation = indentation;
}
// Accesseur en modification définissant le saut de ligne à utiliser
public void setSautLigne(String sautLigne) {
this.sautLigne = sautLigne;
}
//Sérialisation de l'arborescence DOM en l'OutputStream de sortie
mentionné
public void serialise(Document doc, OutputStream out)
throws IOException {
Writer writer = new OutputStreamWriter(out);
serialise(doc, writer);
}
//Sérialisation de l'arborescence DOM vers le fichier de sortie
mentionné
public void serialise(Document doc, File fichier)
throws IOException {
Writer writer = new FileWriter(fichier);
serialise(doc, writer);
}
//Sérialisation de l'arborescence DOM vers le Writer mentionné
public void serialise(Document doc, Writer writer)
throws IOException {
// Exécution de la sérialisation via la méthode
serialiseNoeud() définie dans
le panneau suivant.
//On s'assure finalement que tout le flot soit vidé
writer.flush();
}
Sérialisation (suite 2)
Venons en enfin à notre méthode de sérialisation, serialiseNoeud(). Un des
grands avantages du Modèle Objet Document consiste en ce que toutes les structures
qu'il définit étendent l'interface Node, et donc qu'une seule et même méthode, à
condition de différencier chacune de ces structures dans ce qu'elle a de spécifique (par
exemple via une commande switch) permet de définir et de gérer le processus
consistant à traverser l'arborescence DOM.
On commence ainsi par tester via la méthode getNodeType() le cas d'un noeud de
type Document: si la condition est remplie, on commence par écrire la déclaration
XML (non prise en charge par DOM Niveau 2: DOM Niveau 3 devrait remédier à
cela), puis on saute une ou plusieurs lignes suivant la faÇon dont on a défini notre
variable d'instance sautLigne, et enfin on boucle sur chacun des noeuds fils de
Document, récupérés par l'intermédiaire de la méthode getChildNodes(), en
rappelant de manière récursive la même méthode serialiseNoeud().
public void serialiseNoeud(Node noeud, Writer writer,
String niveauIndentation)
throws IOException {
// Determine l'action à accomplir en fonction du type de noeud
switch (noeud.getNodeType()) {
case Node.DOCUMENT_NODE:
writer.write("<?xml version='1.0'
encoding='iso-8859-1'?>");
writer.write(sautLigne);
// boucle sur chaque enfant
NodeList noeuds = noeud.getChildNodes();
if (noeuds != null) {
for (int i=0; i<noeuds.getLength(); i++) {
serialiseNoeud(noeuds.item(i), writer, "");
}
}
break;
...
}
}
On remarque ici que les éléments fils du document racine (dans notre cas, il s'agit de
l'unique élément <catalogue> seront écrits en sortie juste en dessous de la déclaration
XML, sans indentation par rapport à celle-ci. Si l'on veut modifier cela, il suffit
simplement de rajouter des espaces blancs au sein des guillemets dans l'instruction
serialiseNoeud(noeuds.item(i), writer, "");. On définit ainsi une
indentation par défaut à laquelle on ajoute à chaque niveau hiérarchique de
l'arborescence l'indentation définie par la variable d'instance SautLigne. Sans
surprise, l'action à accomplir relativement à un Element consiste à afficher son nom,
ses attributs, sa valeur, puis à s'occuper de ses fils. Cela se fait sans problèmes via
respectivement les méthodes getNodeName(), getAttributes() et en rappelant
de manière récursive toujours la méthode serialiseNoeud() pour le reste. Ce qui
donne le code suivant, à mettre à la suite du précédent:
case Node.ELEMENT_NODE:
String nom = noeud.getNodeName();
writer.write(niveauIndentation + "<" + nom);
NamedNodeMap attributs = noeud.getAttributes();
for (int i=0; i<attributs.getLength(); i++) {
Node courant = attributs.item(i);
writer.write(" " + courant.getNodeName() +
"=\"" + courant.getNodeValue() +
"\"");
}
writer.write(">");
// boucle sur chaque enfant
NodeList enfants = noeud.getChildNodes();
if (enfants != null) {
if ((enfants.item(0) != null) &&
(enfants.item(0).getNodeType() ==
Node.ELEMENT_NODE)) {
writer.write(sautLigne);
}
for (int i=0; i<enfants.getLength(); i++) {
serialiseNoeud(enfants.item(i), writer,
niveauIndentation + indentation);
}
if ((enfants.item(0) != null) &&
(enfants.item(enfants.getLength()-1)
.getNodeType() ==
Node.ELEMENT_NODE)) {
writer.write(niveauIndentation);
}
}
writer.write("</" + nom + ">");
writer.write(sautLigne);
break;
Il est à remarquer qu'on teste en début et en fin de boucle sur les enfants le type du
premier et du dernier fils afin de rajouter soit un saut de ligne (dans le cas du premier
fils élément), soit la même indentation que la balise ouvrante de l'élément père affectée
à la balise fermante. Si, au lieu de :
if ((enfants.item(0) != null) &&
(enfants.item(enfants.getLength()-1)
.getNodeType() ==
Node.ELEMENT_NODE)) {
writer.write(niveauIndentation);
on obtiendrait alors des espaces blancs (autant qu'en comporte l'indentation courante)
à la fin de chaque contenu textuel d'élément. En effet, comme il a déjà été dit, le
contenu textuel d'un élément est considéré comme noeud fils de cet élément. Aussi, un
élément ne contenant que du contenu textuel obéit à la condition if (enfants !=
null) et nous oblige à tester une condition supplémentaire (le fait que le dernier fils
de l'élément considéré soit de type Element) avant de définir l'indentation de la balise
de fermeture.
Il nous faut tester ensuite les noeuds de type Text, CDATA, COMMENT, PROCESSING
INSTRUCTION, , ENTITY REFERENCE, principalement via la méthode
getNodeValue().
case Node.TEXT_NODE:
writer.write(noeud.getNodeValue());
break;
case Node.CDATA_SECTION_NODE:
writer.write("<![CDATA[" +
noeud.getNodeValue() + "]]>");
break;
case Node.COMMENT_NODE:
writer.write(niveauIndentation + "<!-- " +
noeud.getNodeValue() + " -->");
writer.write(sautLigne);
break;
case Node.PROCESSING_INSTRUCTION_NODE:
writer.write("<?" + noeud.getNodeName() +
" " + noeud.getNodeValue() +
"?>");
writer.write(sautLigne);
break;
case Node.ENTITY_REFERENCE_NODE:
writer.write("&" + noeud.getNodeName() + ";");
break;
Il faut noter que le noeud de type Processing Instruction est un peu particulier,
puisqu'il requiert à la fois les méthodes getNodeName() et getNodeValue() pour
être correctement affiché en sortie. Si nous n'utilisons que getNodeName(), nous
obtenons l'écran suivant en sortie:
alors que si nous n'utilisons que getNodeValue(), nous obtenons dans ce cas le
résultat suivant:
Enfin, il nous faut considérer le cas des noeuds de type DocumentType, qui
représentent des déclarations DOCTYPE. Comme on peut y trouver des données
spécifiques, il faut veiller à transtyper l'instance de Node vers l'interface
DocumentType afin d'accéder à ces données supplémentaires. Les méthodes à
utiliser sont alors getName(), getPublicId() pour récupérer l'identifiant public (s'il
existe), et getSystemId() pour l'ID système de la DTD référencée. on obtient alors
le code suivant:
case Node.DOCUMENT_TYPE_NODE:
DocumentType docType = (DocumentType)noeud;
writer.write("<!DOCTYPE " + docType.getName());
if (docType.getPublicId() != null) {
System.out.print(" PUBLIC \"" +
docType.getPublicId() + "\" ");
} else {
writer.write(" SYSTEM ");
}
writer.write("\"" + docType.getSystemId() + "\">");
writer.write(sautLigne);
break;
Il ne reste plus dès lors qu'à compléter notre fichier DOMSerialiseur.java et d'y
placer l'instruction suivante: serialiseur.serialise(document,
fichierSortie); à la place du commentaire //Sérialise précédemment mis par
défaut (voir le panneau Sérialisation on page 19 ).
Si l'on veut d'autre part changer la valeur des variables privées indentation et
sautLigne, il suffit d'ajouter à la suite par exemple le code suivant:
serialiseur.setIndentation(" ");
serialiseur.setSautLigne("\n\n");
Les trois lignes surlignées en gras font appel aux classes spécifiques de Xerces: en
fait, comme il a déjà été dit, la spécificaton DOM ne fournit pas de standard pour
obtenir un noeud de type Document. C'est à ce niveau précis qu'intervient JAXP, à
travers l'utilisation des classes javax.xml.parsers et
javax.xml.parsers.DocumentBuilder. L'approche de base est la suivante:
* Utilisation de la méthode de construction
DocumentBuilderFactory.newInstance() afin de retourner un objet
DocumentBuilderFactory
* Utilisation de la méthode newDocumentBuilder() de cet objet
DocumentBuilderFactory afin de retourner une instance (spécifique d'un
vendeur) de la classe abstraite DocumentBuilder
* Uitlisation d'une des méthodes parse() de DocumentBuilder afin de lire le
document XML et retourner un objet org.w3c.dom.Document.
En exécuant TestDOMJAXP disponible ici, on vérifie que l'on obtient bien le même
résultat:
La validation d'un document XML par rapport à une DTD peut également être prise en
charge par le parseur produit par une factory via la méthode public boolean
isValidating() mise en oeuvre de la faÇon suivante:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(true);
Enfin, pour terminer ce petit tour d'horizon sur l'analyse de documents XML via JAXP, il
convient de revenir sur le point important suivant: comment JAXP choisit-il son
parseur?
En fait, JAXP utilise le parseur que référence la classe indiquée par la propriété
système javax.xml.parsers.DocumentBuilderFactory. Par exemple, si l'on
veut être sûr que que nous utilisons Xerces lors de l'analyse de notre document
catalogue.xml via notre classe TestDOMJAXP, le plus simple est d'exécuter cette
Si l'on veut une fois pour toutes que JAXP utilise tel parseur plutôt que tel autre sans
avoir à le spécifier à chaque fois en ligne de commande comme nous venons de le
faire, il suffit de créer dans le répertoire lib de votre installation Java un fichier
nommé jaxp.properties, qui contiendrait les informations suivantes:
javax.xml.parsers.DocumentBuilderFactory=org.apache.xerces.DocumentBuilderFactory(xerces
=org.apache.crimson.jaxp.DocumentBuilderBuilderFactory(crimson)
Si auncun renseignement n'est fourni d'une faÇon ou d'une autre, alors ce sera
l'implémentation de JAXP (par exemple Sun ou Apache) qui déterminera
l'analyseur par défaut (respectivement Crimson ou Xerces).
Du point de vue qui nous oocupe (ici celui de la sérialisation), il faut avouer que JAXP
ne dipose pas en tant que telle d'une classe de sérilaisation, mais l'astuce consiste à
utiliser une transformation à vide à partir du document XML initial pour arriver
exactement au même résultat. Regardons cela d'un peu plus près: L'API JAXP jouit
d'une grande cohérence et les instructions à fournir pour exécuter une transformation
obéissent exactement à la même dynamique que celles exécutées dans le cas de
l'analyse, à savoir :
1. Utilisation dela méthode de fabrication
TransformerFactory.newInstance() retournant un objet
javax.xml.transform.TransformerFactory
2. Utilisation de la méthode newTransformer() de cet objet
TransformerFactory afin de retourner une instance (spécifique du vendeur)
de la classe abstraite javax.xml.transform.Transformer
3. Réalisation des opérations de transformation
Illustrons cela par un exemple simple: nous parsons une des versions du fichier
catalogue.xml, le modifions par quelques manipulations DOM afin de lui rajouter un
livre dans la liste du catalogue, et finalement le sérialisons vers l'écran ou un fichier de
sortie.
Pour cela, la première chose à faire d'importer les classes de JAXP relatives aux
En plus des classes de constructions (à mettre en parallèle avec les classes d'analyse)
et d'exceptions, on doit rajouter ici les classes relatives aux entrées et aux sorties de la
transformation TrAX. Nous fournissons dans notre exemple une arborescence DOM
en entrée de transformation, ce qui explique l'importation de la classe
javax.xml.transform.dom.DOMSource. (Celle-ci constitue, ainsi que les deux
autre classes d'entrée javax.xml.transform.sax.SAXSource et
javax.xml.transform.stream.StreamSource, une implémentation concrète de
l'interface javax.xml.transform.Source). Comme nous envisageons d'effectuer
la sortie sur l'écran système ou dans un fichier, nous utilisons la classe de sortie
javax.xml.transform.stream.StreamResult qui prend comme arguments soit
un OutputStream (comme System.out), soit un Writer. Cette classe de sortie
constitue, ainsi que les classes javax.xml.transform.dom.DOMResult et
javax.xml.transform.sax.SAXResult, une implémentation concrète de
l'interface javax.xml.transform.Result
Nous passons ensuite à la sérialisation proprement dite, dont on a déjà dit qu'elle
consistait en fait via JAXP à une transformation sans feuille de style. Les étapes sont
les suivantes:
1. Obtenir une classe fabriquant des instances de la classe Transformer via la
classe TransformerFactory
2. Récupérer une instance de la classe Transfomer
3. Réaliser les opérations de transformation, en définissant les types de document
Si l'on veut effectuer la sortie vers un fichier sortie.xml, il suffit de changer la ligne
de définition de sortie: Result sortie = new StreamResult(new
FileOutputStream("sortie.xml");
Comme il a été expliqué plus haut, cela aurait normalement pour effet de créer un
élément racine du document avec un espace de nom par défaut référant à l'URI
http://www.catalogue.com. Mais si vous compilez et exécutez le code, vous vous
apercevez que votre document xml n'a subi aucun changement. C'est parce qu'il faut
ajouter à la main l'attribut xmlns à l'arboresence DOM, laquelle API ne prend pas
en charge par elle-même un tel ajout. i faut donc rajouter:
racine.setAttribute("xmlns",docNS)
Si nous voulons rajouter un autre espace de nom préfixé "livre" à certains des
éléments du document, il faudra donc également commencer par déclarer un tel
espace de nom sur l'élément racine via la ligne de code suivante:
String NS ="http://schema-livre";
racine.setAttribute("xmlns:livre", NS);
Ensuite, on place les éléments <titre> et <auteur> dans l'espace de nom préfixé
par "livre" et l'élément <description> dans l'espace de nom défini par défaut de la
manière suivante:
//Titre du livre
Element elementTitre = doc.createElementNS(NS, "livre:titre");
Text texteTitre = doc.createTextNode(titre);
elementTitre.appendChild(texteTitre);
racine.appendChild(elementTitre);
// Auteur du livre
Element elementAuteur = doc.createElementNS(NS,"livre:auteur");
Text texteAuteur = doc.createTextNode(auteur);
elementAuteur.appendChild(texteAuteur);
racine.appendChild(elementAuteur);
// Description du livre
Element elementDescription = doc.createElementNS(docNS,"description");
Text texteDescription = doc.createTextNode(description);
elementDescription.appendChild(texteDescription);
racine.appendChild(elementDescription);
if (n.getNodeType() == Node.TEXT_NODE) {
Node parent = n.getParentNode();
if (parent.getNodeName().equalsIgnoreCase("mot-clé")) {
return FILTER_ACCEPT;
}
}
//Si nous arrivons là, nous ne sommes pas intéréssés
return FILTER_SKIP;
}
}
Fichier source de NodeFilterPerso.java. Ici, nous utilisons un code DOM habituel: nous
ne nous intéressons qu'aux noeuds textuels, et souhaitons récupérer le contenu textuel
des éléments <mot-clé>, non aux éléments eux-mêmes. Pour ce faire, nous utilisons
la méthode getNodeName() appliquée au parent du noeud textuel rencontré (il est
raisonnable de présupposer que le parent d'un noeud textuel est un élément). Si le
nom de l'élément parent est mot-clé, alors le code renvoie la valeur
FILTER_ACCEPT. Sinon, il renvoie la valeur FILTER_SKIP qui évite le noeud examiné
mais continue à itérer sur ses fils. (Il existe une troisième valeur de retour,
FILTER_REJECT, qui rejette le noeud examiné ainsi que tous ses fils, et qui n'est
applicable qu'à TreeWalker que nous verrons plus loin).
N.B: Il vaut la peine de remarquer qu'il peut y avoir des "interférences" entre l'utilisation
de la constante fournie à la méthode createNodeIterator() et l'implémentation du
NodeFilter. Dans l'exemple ci-dessus, on a utilisé une constante
NodeFilter.SHOW_ALL alors que l'on retournait des éléments textuels, ce qui est
concordant. Par contre, si l'on tente de changer la constante en
NodeFilter.SHOW.ELEMENT à la méthode createNodeIterator(), on ne reÇoit
aucun mot-clé en réponse: l'explication vient du fait que l'implémentation du
NodeIterator() reÇoit des noeuds de type contenu textuel et qu'il n'est censé
afficher que des éléments. Une faÇon correcte de faire serait ici de passer la constante
NodeFilter.SHOW_TEXT
On vérifie ainsi qu'un élément ne possède jamais en tant que tel de contenu textue, ce
qui nous est indiqué par la valeur null retournée par la méthode getNodeValue().
Ensuite, il faut créer notre portée, exactement de la même faÇon que nous avions
défini un NodeIterator ou un TreeWalker, , c'est-à-dire par transtypage et
Une fois la portée créée, il faut à présent définir les points d'arrivée et de départ.
Comme on veut supprimer tout le contenu de l'élément <description>, on choisit
comme départ ce qui précède le premier fils via la méthode setStartBefore(), et
comme point d'arrivée ce qui succède au dernier fils, via setEndAfter().
//Défintion du point de départ
portee.setStartBefore(elementDescription.getFirstChild());
//Définition du point d'arrivée
portee.setEndAfter(elementDescription.getLastChild());
Dès lors, il ne reste plus qu'à créer le nouveau contenu textuel et à l'ajouter à
l'arborescence via la méthode classique appendChild()
Text texteDescription = doc.createTextNode(description);
elementDescription.appendChild(texteDescription);
ON voit l'avantage d'utiliser un module Range dans ce cas-là: si on avait en effet voulu
garder en DOM Niveau 1 l'élément description et simplement changé son contenu
textuel comme on vient de le faire, il aurait fallu crée une NodeList comprenant tous
les noeuds fils de l'élément, puis dans un second temps, il aurait fallu itérer sur chacun
d'eux pour les supprimer un à un. A l'évidence, le module Portée est beaucoup plus
pratique à utiliser. Voir le code source de RangeMiseAJourServletNS.java
Introduction
JDOM présente de grandes similitudes avec le DOM en ce sens qu'il représente un
document XML via une structure arborescente. Cependant, il s'en distingue parce
que JDOM est spécifiquement conÇu pour JAVA et que du point de vue du
développeur JAVA, il s'avère beaucoup plus pratique à utiliser (A ce propos, il
convient de noter que contrairement à ce qui est parfois écrit, le J de JDOM ne renvoie
pas à Java, et que JDOM suit la nomenclature NAA - not an abreviation - de Sun:
JDOM veut ainsi dire JDOM et rien d'autre). Nous utilisons ici JDOM1.0 beta 8
disponible à l'adresse http://www.jdom.org. La configuration est très simple: les
opérations à effectuer sont spécifiées dans le fichier README.txt de la distribution et
sont principalement au nombre de deux:
1. D'abord vérifier que la variable d'environnement JAVA_HOME est définie
correctement en spécifiant bien le dossier du JDK comprenant la JVM devant être
utilisée.
2. Ensuite, veiller à être dans le dossier dans lequel est situé le fichier build.xml
puis taper ./build.sh (unix) ou .\build.bat (windows). Cela a pour
effet de créer via l'outil Ant l'archive JDOM.jar contenant toutes les classes de
l'API. Ensuite, il faut inclure cette archive JDOM.jar, ainsi que xerces.jar,
jaxp.jar et xalan.jar fournis dans la distribution dans votre classpath.
Sérialisation JDOM
Une fois notre strcture créée, il serait bien de pouvoir la visualiser sur l'écran ou de
l'envoyer en sortie vers un fichier. L'API JDOM utilise la classe XMLOutputter pour
envoyer le code XML vers un flux encapsulant une connection réseau, un fichier ou
toute autre structure dans laquelle on souhaite placer du code XML. La classe
XMLOutputter s'utilise de la manière suivante:
XMLOutputter sortie = new XMLOutputter(" ", true);
sortie.output(document, System.out);
Si l'on veut envoyer les données XML vers un fichier, il faudrait utiliser la classe
FileOutputStream, par exemple de la faÇon suivante:
XMLOutputter outputter = new XMLOutputter(" ", true);
FileOutputStream sortie = new FileOutputStream("sortie.xml");
outputter.output(document,sortie);
sortie.flush();
sortie.close();
On commence par créer l'attribut de nom langue en lui affectant la valeur fr, puis on
l'affecte à l'élément titre1 déjà défini plus haut dans le code. Ensuite, on crée un
nouvel attribut que l'on affecte au titre du deuxième livre: celui-ci n'avait pas été
explicitement défini dans le code précédent, il faut donc d'abord le récupérer avec la
méthode getChild(). En compilant et exécutant le code ainsi obtenu, on obtient le
résultat suivant:
La méthode getChildren() renvoie une List Java (et non une NodeList
spécifique DOM), sur laquelle il est possible de naviguer via un Iterator Java
classique. Ici, deux choses sont à noter:
1. Premièrement, bien que l'on fasse la recherche uniquement sur l'espace de nom
espaceNom (espace de nom par défaut), le résultat de la requête renvoie
également l'élément associé au préfixe ctg. En effet, avec JDOM, la
comparaison entre les espaces de nom se base uniquement sur les URI.
Autrement dit, deux objets Namespace sont considérés comme égaux si leur URI
le sont aussi, indépendamment des préfixes. Cette approche est tout à fait
conforme à la spécification des espaces de noms XML, laquelle indique que deux
éléments se trouvent dans le même espace de nom si leur URI est identique, et
ce indépendamment du nom du préfixe auquel cet URI renvoie
2. Deuxièmement, on récupère le contenu textuel d'un élément directement à partir
de l'élément (et non en redescendant d'un niveau comme en DOM) via la
méthode getTextTrim(), qui est équivalente à la méthode getText(), sauf
qu'elle retourne le contenu textuel d'un élément sans les espaces blancs qui
l'entourent ni les espaces superflus entre les mots qu'elle comprimera en espace
blanc unique.
JDOM et DTD
Il se peut que vous vouliez ajouter une DTD à votre document. Cela est très facile via
JDOM. Par exemple, si on voulait ajouter une DTD catalogue.dtd à notre fichier
catalogue.xml, il nous faudrait juste écrire le code suivant:
DocType type = new DocType("catalogue", "catalogue.dtd");
Document document = new Document (racine, type);
Ici, il s'avère que notre document XML à valider est un fichier enregistré sur le disque
et que la meilleure faÇon de construire une représentation JDOM dans ce cas se fait à
partir d'un ensemble d'événements SAX. Une solution alternative peut consister à
utiliser l'autre classe de construction DOMBuilder mise à notre disposition pour
construire l'arborescence JDOM, mais ici, c'est une très mauvaise idée pour la bonne
et simple raison que DOMBuilder utilise SAX pour construire une arobrescence DOM,
avant que cette arborescence DOM ne soit convertie en JDOM. Quand on le peut, en
fait toutes les fois où le document à tranformer en JDOM ne consiste pas en une
arborescence DOM, le mieux est de se servir de la classe SAXBuilder.
Il n'y a rien de spécial à dire ici: on crée simplement une instance de DOMParseur
comme on l'a vu dans la première section, puis on lui envoie le message parse avec
pour argument un objet InputSource de SAX (de manière générale, il est conseillé
d'utiliser cette classe à la place d'une simple URI, car elle permet de fournir plus
d'informations à l'analyseur: elle permet notamment de résoudre les chemins d'accès
relatifs au sein d'un document). La conversion de DOM en JDOM s'effectue quant à
elle de la manière suivante:
//Conversion du document DOM en document JDOM
try {
DOMBuilder builder = new DOMBuilder();
org.jdom.Document documentJDOM = builder.build(documentDOM);
XMLOutputter outputter = new XMLOutputter();
outputter.output(documentJDOM, System.out);
}
catch (java.io.IOException e) {
e.printStackTrace();
}
Une fois en possession de cette sous-classe, il faut maintenant l'utiliser. Cela se fait
simplement en sous-classant l'interface org.jdom.input.DefaultJDOMFactory
qui retourne par défaut toutes les classes essentielles de JDOM. Cela se fait grâce par
exemple au code suivant, disponible ici:
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.input.DefaultJDOMFactory;
class JDOMFactoryPerso extends DefaultJDOMFactory {
public Element element(String nom) {
return new Elementctg(nom);
}
}
JDOM et XSLT
On a vu dans DOM et JAXP 1.1 (sérialisation) on page 29 qu'une bonne faÇon
d'effectuer des transformations XSLT avec des arborescences DOM en entrée ou en
sortie consistait à utiliser l'API JAXP. Eh bien, il en de même avec JDOM. Les classes
JDOM à utiliser en entrée et sortie sont respectivement
org.jdom.transform.JDOMSource(org.jdom.Document
documentJDOMEntree) et org.jdom.transform.JDOMResult(). Soit le code
suivant créant tout d'abord une arborescence JDOM à partir du fichier
catalogue.xml, opérant une transformation XSLT via JAXP à partir d'une telle
arborescence, laquelle produit une autre autre structure JDOM en sortie.
qui produit un tableau pour chacun des livres du catalogue, en indiquant l'ordre
d'apparition via la fonction position() (on remarque également que l'on a supprimé
Colophon
This tutorial was written entirely in XML, using the developerWorks Toot-O-Matic tutorial
generator. The Toot-O-Matic tool is an XSLT stylesheet and several XSLT extension
functions that convert an XML file into a number of HTML pages, a zip file, JPEG heading
graphics, and two PDF files. Our ability to generate multiple text and binary formats from a
single source file illustrates the power and flexibility of XML. (It also saves our production
team a great deal of time and effort.)