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 :

 
Sélectionnez
<?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.

 
Sélectionnez
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 :

 
Sélectionnez
<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.

 
Sélectionnez
/*
 * 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. Cest-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.

 
Sélectionnez
/*
 * 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 :

 
Sélectionnez
<?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>
 
Sélectionnez
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.