I. Introduction▲
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 commencée avec le monteur :
Cet article fait partie de la série : | |
---|---|
monteur | construction d'une structure complexe indépendamment de son implémentation |
ioc | Inversion of control laissez votre environnement objet interagir avec vos objets |
sax | Relire un fichier XML avec l'API SAX |
implémentation | Implémentation, exemples |
II. Présentation de SAX▲
Simple API for XML ouSAX 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étail) 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 ? Eh 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étail, 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 métainformations.
III. Découverte de l'API▲
III-A. 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 nôtre) 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.
III-A-1. 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ébogage, 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 fourni 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.
III-A-2. 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.
III-A-3. 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é.
III-A-4. processingInstruction▲
Cet événement est levé pour chaque instruction de fonctionnement rencontrée. Ces instructions sont celles que vous trouvez hors de l'arbre XMLlui-même comme les instructions concernant les DTD ou plus simplement la déclaration :
<?xml version="1.0" encoding="ISO-8859-1" ?>
III-A-5. 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é.
III-A-6. endPrefixMapping▲
Son événement contraire évidemment, c'est-à-dire la fin du traitement dans un espace de nommage.
III-A-7. startElement▲
Démarrage d'un élément XML… Enfin ! Eh 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.
startElement (
String namespaceUri, String localName, String rawName, Attributs atts);
- 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▲
Évé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.
III-A-9. 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 à 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.
III-A-10. ignorableWhiteSpace▲
Permet de traiter les espaces et tabulations multiples, sachant qu'ils n'ont normalement aucune valeur en XML. Un ou deux ou dix espaces, un espace et une tabulation et trois 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. À vous de voir si vous voulez outrepasser la préconisation qui considère ces espaces comme étant inutiles.
III-A-11. skippedEntity▲
Évitez 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 dangereuse 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.
III-B. 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.
*/
public
class
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)
*/
public
void
setDocumentLocator
(
Locator value) {
locator =
value;
}
/**
* Evenement envoye au demarrage du parse du flux xml.
*
@throws
SAXException
en cas de probleme quelconque ne permettant pas de
* se lancer dans l'analyse du document.
*
@see
org.xml.sax.ContentHandler#startDocument()
*/
public
void
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 quelconque ne permettant pas de
* considerer l'analyse du document comme etant complete.
*
@see
org.xml.sax.ContentHandler#endDocument()
*/
public
void
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)
*/
public
void
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)
*/
public
void
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 non-respect d'une DTD.
*
@see
org.xml.sax.ContentHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
public
void
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)
*/
public
void
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)
*/
public
void
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 succedant 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)
*/
public
void
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)
*/
public
void
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)
*/
public
void
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;
}
IV. 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 distribution 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 telle quelle.
*
@author
smeric
*
@version
1.0
*/
public
class
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);
}
public
static
void
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 :
<?xml version="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
V. 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 œuvre à la fois cette API, d'implémenter un monteur et de se mettre dans le cadre de l'IOC.