Je vous présente dans cet article un rapide cours d'introduction à
SAX et un exemple d'implémentation d'une lecture d'un flux XML dans cette
API SAX en java.Si le prérequis XML ne fait pas encore partie de votre
bagage, n'hésitez pas à rendre visite à la section
XML de developpez.com. Il n'y aura pas de découverte fantastique
cette fois, ce tutoriel ayant pour but de présenter l'API SAX qui sert
en particulier pour l'article sur le design pattern du GOF : le monteur.
Cet article est donc la partie technique et pure java de la série du
cours débutée avec le monteur :
SimpleAPIforXMLouSAX est une API générale pour la
lecture d'un flux XML. Il existe des implémentations de cette API dans
tous les langages que vous connaissez probablement (en tout cas l'implémentation
existe pour C++, C#, Java, Pascal, perl, PHP, ...) car XML s'est largement imposé
aujourd'hui dans le monde du logiciel pour l'échange d'informations.
Il existe deux grandes API pour relire les flux XML : DOM et SAX. SAX est événementielle
(nous allons la découvrir en détails) alors que DOM transforme
l'arborescence XML en arborescence du langage cible. L'intérêt
majeur de DOM est donc la possibilité qu'il offre d'aller et venir à
votre gré dans l'arborescence, son inconvénient majeur reste la
lourdeur du traitement. En effet, SAX étant événementiel,
son traitement se fait au fil du flux entrant.
Je disais donc que SAX est événementiel, ce qui signifie qu'il
existerait des événements dans un fichier XML !? Et bien,
si l'on considère le flux XML entrant, il devient plus facile de découvrir
les événements : une balise ouvrante est un événement,
une balise fermante un autre événement... Voilà pour tout
dire l'essentiel de cette API, d'où son nom Simple API for XML,
une fois que vous savez répondre à ces deux événements,
vous pouvez déjà traiter un flux XML donc utiliser un fichier
de configuration XML. C'est étonnamment simple, et finalement extrêmement
puissant comme nous allons le découvrir.Quand vous aurez découvert
cette API, il est probable que vous choisissiez de remplacer tous vos fichiers
de propriétés de type paire clef/valeur par un fichier XML et
une relecture SAX. L'avantage d'une configuration XML est son aptitude contextuelle,
laquelle est difficile à représenter dans un fichier de propriétés.
En effet sous la forme de paires nom/valeur , la notion contextuelle
est généralement mal représentée par des clefs ayant
une forme de chemin (mon.chemin.vers.ma.propriété).
Nous étudierons, un peu plus en détails, cette API qui est un
peu plus compliquée que je ne l'ai présentée pour le moment,
en particulier parce qu'elle réagit à beaucoup plus que les simples
événements d'ouverture et de fermeture de balises. Nous trouverons
en particulier des événements pour l'ouverture et la fermeture
du document xml, des événements pour l'ouverture et la fermeture
de meta informations.
Découverte de l'API
ContentHandler
Voyons pour commencer comment nous allons pouvoir réagir à ces
fameux événements SAX générés par le parser.
Pour ce faire, il nous suffira en fait d'implémenter l'interface ContentHandler
qui se trouve dans le package org.xml.sax, puis d'instancier le
parser en lui indiquant quel gestionnaire de contenu il devra utiliser (c'est
à dire le notre) et le tour sera quasiment joué. Voici donc présenté
un aperçu de l'interface centrale de vos développements SAX :
le ContentHandler ou gestionnaire de Contenu. Ce gestionnaire est
en effet celui qui fait le véritable travail d'analyse tandis que les
autres interfaces de l'API font des travaux satellites comme gérer les
erreurs, repérer le "curseur" dans le flux pendant l'analyse.
Ces interfaces satellites ne sont pas à négliger mais ont un rôle
moins central dans votre développement.
setDocumentLocator
Un locator vous permet de localiser "le curseur" pendant le traitement
du flux vous permettant par exemple de connaître le numéro de ligne
et de colonne en cours d'analyse. Ceci est une fonctionnalité certes
très intéressante au moment du débugage mais qu'il faut
à tout prix éviter, que dis-je, vous interdire d'utiliser pour
le traitement proprement dit. Dans les Helper de l'API sax, il vous
est fournit une implémentation par défaut de toutes les interfaces.
Si l'implémentation par défaut du ContentHandler est proprement
inutile, celle du Locator devrait amplement suffire à 99% des développements,
je ne m'étendrai donc pas sur ce point.
startDocument
Cette méthode est appelée par le parser une et une seule fois
au démarrage de l'analyse de votre flux xml. Elle est appelée
avant toutes les autres méthodes de l'interface, à l'exception
unique, évidemment, de la méthode setDocumentLocator. Cet événement
devrait vous permettre d'initialiser tout ce qui doit l'être avant le
début du parcours du document.
endDocument
Et son contraire, cette méthode est donc appelée à la
fin du parcours du flux après toutes les autres méthodes. Il peut
alors être utile à ce moment de notifier d'autres objets du fait
que le travail est terminé.
processingInstruction
Cet événement est levé pour chaque instruction de fonctionnement
rencontrée. Ces instructions sont celles que vous trouvez hors de l'arbre
xml lui-même comme par exemple les instructions concernant les dtd ou
plus simplement la déclaration :
<?xml version="1.0" encoding="ISO-8859-1" ?>
startPrefixMapping
Cet événement est lancé à chaque fois qu'un mapping
préfixé, c'est à dire une balise située dans un
espace de nommage (name space) , est rencontré.
endPrefixMapping
Son événement contraire évidemment, c'est à dire
la fin du traitement dans un espace de nommage.
startElement
Démarrage d'un élément XML... Enfin ! Et oui, on peut
en effet pour démarrer avec SAX se contenter de comprendre cet événement
et son traitement ainsi que son contraire pour analyser un flux xml de manière
très puissante et efficace. Nous allons donc nous pencher un peu plus
sur cet événement.
où nameSpaceUri est la chaîne de caractères contenant
l'URI complète de l'espace de nommage du tag ou une chaîne vide
si le tag n'est pas compris dans un espace de nommage,
localName est le nom du tag sans le préfixe s'il y en avait un,
rawName est le nom du tag version xml 1.0 c'est à dire $prefix:$localname,
Enfin attributs est la liste des attributs du tag que l'on étudiera
un peu plus loin.
endElement
Evénement inverse de signature beaucoup plus simple puisque seul le
nom complet du tag a besoin d'être connu. En effet, à la fermeture
de la balise XML, aucun attribut n'est requis.
characters
Tout ce qui est dans l'arborescence mais n'est pas partie intégrante
d'un tag, déclenche la levée de cet événement. En
général, cet événement est donc levé tout
simplement par la présence de texte entre la balise d'ouverture et la
balise de fermeture comme dans l'exemple suivant:
<maBalise>un peu de texte</maBalise>
La présence de "un peu de texte" provoque la levée
de l'événement characters. Attention :
il est à noter que l'API SAX n'impose rien quant à pas l'implémentation
de cet événement. Dans le cas d'un texte épars autour de
balises filles de la balise en cours, les réactions peuvent être
diverses. Ainsi le flux xml suivant :
<maBalise>un peu
<baliseImbriquee nom="coucou"/> de texte<baliseImbriquee nom="toto"/>éparpillé
</maBalise>
Il peut soit donner lieu à trois événements contenant
respectivement le texte "un peu", " de texte", "éparpillé"
soit donner un seul événement contenant l'intégralité
du texte à savoir "un peu de texte éparpillé".
Comme l'API ne fixe rien, ce sera à vous de penser au fait que le parser
que vous avez sous la main ne sera peut être pas celui de vos clients
et d'agir en conséquence, c'est à dire en gérant les deux
types de réactions possibles de telle sorte qu'elles fournissent le même
comportement final dans les deux cas.
ignorableWhiteSpace
Permet de traiter les espaces et tabulations multiples, sachant qu'ils n'ont
normalement aucune valeur en xml. Un ou deux ou 10 espaces, 1 espace et une
tabulation et 3 retours chariot, etc sont autant d'espaces normalement ignorés
en XML. Cet événement est donc levé à chaque fois
que des espaces normalement ignorés sont rencontrés. En fait les
paramètres de la méthode contiennent la chaîne complète
de characters et les index de début et de fin de la série
d'espaces ignorables. A vous de voir si vous voulez outrepasser la préconisation
qui considère ces espaces comme étant inutiles.
skippedEntity
Evitez d'y toucher, cette méthode est levée à chaque fois
qu'une entité (une balise et toute l'arborescence descendante) est ignorée.
Elle le sera si vous avez demandé au parser de ne pas valider le document
et que la balise en question est mal formée. Bref, vous faites face à
une situation dangeureuse pour votre application, soit vous décidez alors
de partir sur des valeurs par défaut, soit, et c'est en général
le mieux, vous interrompez le traitement pour défaut dans l'environnement.
Implémentation du ContentHandler
Voyons à présent une petite implémentation rapide du ContentHandler
qui reste l'interface centrale à implémenter pour analyser un
flux XML à l'aide de SAX. Comme toujours, je vous donne une implémentation
en java qui normalement est facilement adaptable aux divers langages que vous
utilisez. Notre classe d'implémentation du ContentHandler se contentera
donc d'analyser un document et de prouver qu'il le fait bien en exprimant sur
la sortie standard (System.out.println() pour les non javayeurs) ce qu'il est
en train de faire. Je focalise votre attention sur le démarrage de la
lecture du flux, la lecture d'entités simples ou d'entités faisant
partie d'un espace de nommage. En effet, le reste devient trivial une fois que
les bases ont été comprises.
/*
* Created on 2 nov. 03
*
* To change the template for this generated file go to
* Window>Preferences>Java>Code Generation>Code and Comments
*/
package com.developpez.smeric.xml.sax;
import org.xml.sax.*;
import org.xml.sax.helpers.LocatorImpl;
/**
* @author smeric
*
* Exemple d'implementation extremement simplifiee d'un SAX XML ContentHandler. Le but de cet exemple
* est purement pedagogique.
* Very simple implementation sample for XML SAX ContentHandler.
*/
publicclass SimpleContentHandler implements ContentHandler {
/**
* Constructeur par defaut.
*/
public SimpleContentHandler() {
super();
// On definit le locator par defaut.
locator = new LocatorImpl();
}
/**
* Definition du locator qui permet a tout moment pendant l'analyse, de localiser
* le traitement dans le flux. Le locator par defaut indique, par exemple, le numero
* de ligne et le numero de caractere sur la ligne.
* @author smeric
* @param value le locator a utiliser.
* @see org.xml.sax.ContentHandler#setDocumentLocator(org.xml.sax.Locator)
*/
publicvoid setDocumentLocator(Locator value) {
locator = value;
}
/**
* Evenement envoye au demarrage du parse du flux xml.
* @throws SAXException en cas de probleme quelquonque ne permettant pas de
* se lancer dans l'analyse du document.
* @see org.xml.sax.ContentHandler#startDocument()
*/
publicvoid startDocument() throws SAXException {
System.out.println("Debut de l'analyse du document");
}
/**
* Evenement envoye a la fin de l'analyse du flux xml.
* @throws SAXException en cas de probleme quelquonque ne permettant pas de
* considerer l'analyse du document comme etant complete.
* @see org.xml.sax.ContentHandler#endDocument()
*/
publicvoid endDocument() throws SAXException {
System.out.println("Fin de l'analyse du document" );
}
/**
* Debut de traitement dans un espace de nommage.
* @param prefixe utilise pour cet espace de nommage dans cette partie de l'arborescence.
* @param URI de l'espace de nommage.
* @see org.xml.sax.ContentHandler#startPrefixMapping(java.lang.String, java.lang.String)
*/
publicvoid startPrefixMapping(String prefix, String URI) throws SAXException {
System.out.println("Traitement de l'espace de nommage : " + URI + ", prefixe choisi : " + prefix);
}
/**
* Fin de traitement de l'espace de nommage.
* @param prefixe le prefixe choisi a l'ouverture du traitement de l'espace nommage.
* @see org.xml.sax.ContentHandler#endPrefixMapping(java.lang.String)
*/
publicvoid endPrefixMapping(String prefix) throws SAXException {
System.out.println("Fin de traitement de l'espace de nommage : " + prefix);
}
/**
* Evenement recu a chaque fois que l'analyseur rencontre une balise xml ouvrante.
* @param nameSpaceURI l'url de l'espace de nommage.
* @param localName le nom local de la balise.
* @param rawName nom de la balise en version 1.0 <code>nameSpaceURI + ":" + localName</code>
* @throws SAXException si la balise ne correspond pas a ce qui est attendu,
* comme par exemple non respect d'une dtd.
* @see org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
publicvoid startElement(String nameSpaceURI, String localName, String rawName, Attributes attributs) throws SAXException {
System.out.println("Ouverture de la balise : " + localName);
if ( ! "".equals(nameSpaceURI)) { // espace de nommage particulier
System.out.println(" appartenant a l'espace de nom : " + nameSpaceURI);
}
System.out.println(" Attributs de la balise : ");
for (int index = 0; index < attributs.getLength(); index++) { // on parcourt la liste des attributs
System.out.println(" - " + attributs.getLocalName(index) + " = " + attributs.getValue(index));
}
}
/**
* Evenement recu a chaque fermeture de balise.
* @see org.xml.sax.ContentHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
*/
publicvoid endElement(String nameSpaceURI, String localName, String rawName) throws SAXException {
System.out.print("Fermeture de la balise : " + localName);
if ( ! "".equals(nameSpaceURI)) { // name space non null
System.out.print("appartenant a l'espace de nommage : " + localName);
}
System.out.println();
}
/**
* Evenement recu a chaque fois que l'analyseur rencontre des caracteres (entre
* deux balises).
* @param ch les caracteres proprement dits.
* @param start le rang du premier caractere a traiter effectivement.
* @param end le rang du dernier caractere a traiter effectivement
* @see org.xml.sax.ContentHandler#characters(char[], int, int)
*/
publicvoid characters(char[] ch, int start, int end) throws SAXException {
System.out.println("#PCDATA : " + new String(ch, start, end));
}
/**
* Recu chaque fois que des caracteres d'espacement peuvent etre ignores au sens de
* XML. C'est a dire que cet evenement est envoye pour plusieurs espaces se succedant,
* les tabulations, et les retours chariot se succedants ainsi que toute combinaison de ces
* trois types d'occurrence.
* @param ch les caracteres proprement dits.
* @param start le rang du premier caractere a traiter effectivement.
* @param end le rang du dernier caractere a traiter effectivement
* @see org.xml.sax.ContentHandler#ignorableWhitespace(char[], int, int)
*/
publicvoid ignorableWhitespace(char[] ch, int start, int end) throws SAXException {
System.out.println("espaces inutiles rencontres : ..." + new String(ch, start, end) + "...");
}
/**
* Rencontre une instruction de fonctionnement.
* @param target la cible de l'instruction de fonctionnement.
* @param data les valeurs associees a cette cible. En general, elle se presente sous la forme
* d'une serie de paires nom/valeur.
* @see org.xml.sax.ContentHandler#processingInstruction(java.lang.String, java.lang.String)
*/
publicvoid processingInstruction(String target, String data) throws SAXException {
System.out.println("Instruction de fonctionnement : " + target);
System.out.println(" dont les arguments sont : " + data);
}
/**
* Recu a chaque fois qu'une balise est evitee dans le traitement a cause d'un
* probleme non bloque par le parser. Pour ma part je ne pense pas que vous
* en ayez besoin dans vos traitements.
* @see org.xml.sax.ContentHandler#skippedEntity(java.lang.String)
*/
publicvoid skippedEntity(String arg0) throws SAXException {
// Je ne fais rien, ce qui se passe n'est pas franchement normal.
// Pour eviter cet evenement, le mieux est quand meme de specifier une dtd pour vos
// documents xml et de les faire valider par votre parser.
}
private Locator locator;
}
Lancer l'analyse d'un flux XML à l'aide de notre parser SAX
Nous avons maintenant réagi aux impulsions fournies par le parseur sax
mais nous ne savons toujours pas instancier un parser pour qu'il pointe vers
notre gestionnaire de contenu, puis lui demander de démarrer la lecture
du flux. Voyons donc comment nous allons pouvoir nous y prendre. La manipulation
est très simple, il faut obtenir une instance de XMLReader, lui fournir
une URI à analyser, lui fournir notre gestionnaire bien entendu et lancer
la lecture. Nous avons deux moyens d'arriver à initialiser notre parser
: une très simple mais aussi peu évolutive et en particulier très
liée à un éditeur ; par exemple :
XMLReader monAnalyseur = new org.apache.xerces.parsers.SAXParser();
L'autre solution sera d'utiliser une factory
et en particulier la factory proposée avec la distrtibution officielle
de SAX : org.xml.sax.helpers.XmlReadersFactory.
Voyons donc à présent une implémentation rapide d'une
classe de démarrage d'une lecture de flux.
/*
* Created on 2 nov. 03 with Eclipse for Java
*/
package com.developpez.smeric.xml.sax;
import java.io.IOException;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* Cette classe est livree tel quel.
* @author smeric
* @version 1.0
*/
publicclass SimpleSaxParser {
/**
* Contructeur.
*/
public SimpleSaxParser(String uri) throws SAXException, IOException {
XMLReader saxReader = XMLReaderFactory.createXMLReader("org.apache.xerces.parsers.SAXParser");
saxReader.setContentHandler(new SimpleContentHandler());
saxReader.parse(uri);
}
publicstaticvoid main(String[] args) {
if (0 == args.length || 2 < args.length) {
System.out.println("Usage : SimpleSaxParser uri [parserClassName]");
System.exit(1);
}
String uri = args[0];
String parserName = null;
if (2 == args.length) {
parserName = args[1];
}
try {
SimpleSaxParser parser = new SimpleSaxParser(uri);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
Ce qui donne comme résultat, lancé sur le fichier de tests suivant :
~/Sites/smeric/java/uml/monteur/test.xml.html
<?xmlversion="1.0"encoding="ISO-8859-1"?>
<tests>
<test id="1" nom="mon test"/>
<test id="2" nom="test 2" type="rien">Un peu de texte
</test>
</tests>
Debut de l'analyse du document
Ouverture de la balise : tests
Attributs de la balise :
#PCDATA :
Ouverture de la balise : test
Attributs de la balise :
- id = 1
- nom = mon test
Fermeture de la balise : test
#PCDATA :
Ouverture de la balise : test
Attributs de la balise :
- id = 2
- nom = test 2
- type = rien
#PCDATA : Un peu de texte
Fermeture de la balise : test
#PCDATA :
Fermeture de la balise : tests
Fin de l'analyse du document
Conclusion
Nous voilà armés pour continuer à étudier l'implémentation de notre monteur
à l'aide de l'API SAX en java. L'API reste utilisable dans les divers langages
et le portage de ce code ne sera pas compliqué. La prochaine étape de ce petit
cours à épisodes nous permettra donc de mettre en oeuvre à la fois cette API,
d'implémenter un monteur et de se mettre dans le cadre de l'IOC.
Copyright (c) 2003 Sébastien MERIC. Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, with the Front-Cover Texts being Cours Java - Lecture d'un flux XML via SAX, and Back-Cover Texts being Ce document à été écrit pour la communauté de développeurs francophones "www.developpez.com". A copy of the license is included in the section entitled "GNU Free Documentation License".