Cours Java - Lecture d'un flux XML via SAX

SÃ©bastien MERIC

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.

RemerciementsÂ : Stefan Bertholon

- Titre : Cours Java - Lecture d'un flux XML via SAX

- Auteur : SÃ©bastien MERIC

- Parution : 29 octobre 2003

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 SÃ©bastien MERIC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. PrÃ©sentation de SAX

- III. DÃ©couverte de l'API

- III-A. ContentHandler

- III-A-1. setDocumentLocator

- III-A-2. startDocument

- III-A-3. endDocument

- III-A-4. processingInstruction

- III-A-5. startPrefixMapping

- III-A-6. endPrefixMapping

- III-A-7. startElement

- endElement

- III-A-9. characters

- III-A-10. ignorableWhiteSpace

- III-A-11. skippedEntity

- III-B. ImplÃ©mentation du ContentHandler

- IV. Lancer l'analyse d'un flux XML Ã  l'aide de notre parser SAX

- V. Conclusion

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="UTF-8" ?>

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.

Voir la description de l'API complÃ¨te Ã  sa source.