Exemple d'utilisation d'un parser SAX

En pratique, cela ne vous sera pas très souvent utile d'afficher un fichier XML à l'aide d'un parser SAX. Habituellement, vous voudrez effectuer des traitements sur les données, dans le but de faire quelque chose d'utile (car pour afficher un message, la construction d'un arbre DOM et l'utilisation de ses fonctionnalités d'affichage prédéfinies est recommandé). Mais afficher un message XML est un bon moyen d'appréhender un parser SAX ern action. Nous allons étudier dans cette exercice un petit programme permettant d'imprimer les événements SAX sur la sortie standard System.out.

Considérez cet exemple comme la version XML d'"Hello World" d'un programme de traitement XML. Il vous montrera comment récupérer les données, et les afficher ensuite.

Note:
Le code utilisé dans cette partie est disponible pour vérification ici Echo01.java. Il fonctionne avec le fichier d'exemple de la section précédante slideSample01.xml.

Création du squelette

Commencez par créer un fichier nommé Echo.java et taper le squelette de notre application :
 
public class Echo extends HandlerBase
{
    public static void main (String argv[])

    {

    }

}

Cette classe étend la classe HandlerBase, qui implémente les interfaces nts EntityResolver, DTDHandler, DocumentHandler, ErrorHandler. Cela nous permettra de surcharger les méthodes nous intéressant, tout en laissant notre classe "mère" s'occuper du reste !

Comme ce programme tournera en tant qu'application, il nous faut une méthode main. De plus, nous avons besoin des arguments de la ligne de commande pour spécifier le fichier à afficher.

Importer les classes nécessaires

Ensuite, il nous faut rajouter les instructions d'importation des classes qui nous seront utiles dans l'application :
 
import java.io.*;
import org.xml.sax.*;
import javax.xml.parsers.SAXParserFactory;  
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;

public class Echo extends HandlerBase
{
  ...

Les classes dejava.io, bien sur, sont nécessaire pour l'impression. Le paquetage org.xml.sax  définit toutes les interfaces utilisées par le parser SAX. La classe SAXParserFactory permet de créer l'instance de parser que nous utiliserons. Elle nous retournera une ParserConfigurationException si elle ne peut pas créer un parser correspondant aux options de configuration. Enfin, la classe SAXParser qui est retournée par la factory qui nous servira à ... parser le fichier !

Setting up for I/O

La première chose à faire, est de récupérer de la ligne de commande le nom du fichier à tariter, et de régler le flux de sortie. Rajoutez le texte en gras ci-dessous, qui se chargera de ce travail :
 
    public static void main (String argv [])
    {
        if (argv.length != 1) {
            System.err.println ("Usage: cmd filename");
            System.exit (1);
        }
        try {
            // Set up output stream
            out = new OutputStreamWriter (System.out, "UTF8");
        } catch (Throwable t) {
            t.printStackTrace ();
        }
        System.exit (0);
    }
    static private Writer out;

Lorsque nous créons le flux d'écriture, nous choisissons l'encodage de caractère UTF-8. Nous aurions pu choisir US-ASCII, ou UTF-16, que la plateforme Java supporte aussi. Pour plus d'informations sur les différents ensembles de caractères, reportez-vous à Java's Encoding Schemes.

Mise en place du parser

Maintenant nous sommes pret à utiliser le parser. Rajoutez le texte en gras, pour le régler et le lancer :
 
    public static void main (String argv [])

    {
        if (argv.length != 1) {
            System.err.println ("Usage: cmd filename");
            System.exit (1);
        }

        // Use the default (non-validating) parser
        SAXParserFactory factory = SAXParserFactory.newInstance();
        try {
            // Set up output stream
            out = new OutputStreamWriter (System.out, "UTF8");

            // Parse the input 
            SAXParser saxParser = factory.newSAXParser();
            saxParser.parse( new File(argv [0]), new Echo() );

        } catch (Throwable t) {
            t.printStackTrace ();
        }
        System.exit (0);
    }

Avec ces lignes de code, vous avez créé une instance de SAXParserFactory, déterminée par le réglage de la propriété système javax.xml.parsers.SAXParserFactory. Vous obtenez alors un parser de la factory, et vous précisez à ce parser la classe qui traitera les événements ainsi que le fichier à traiter.

Note: La classejavax.xml.parsers.SAXParser est un wrapper qui définit un ensemble de méthodes utiles. Elle recouvre l'objet org.xml.sax.Parser (qui est un peu moins sympathique). Si nécessaire, vous pouvez toujours obtenir ce parser en utilisant la méthode getParser() de la classe SAXParser.
For now, you are simply catching any exception that the parser might throw. You'll learn more about error processing in a later section of the tutorial, Gestion des erreurs avec les parser non-validants.
Note:
La méthode parse opére sur un fichier de type File pour des raisons de facilité. Cependant, un objet org.xml.sax.InputSource sous-javent est créé pour que le parser SAX puisse travailler dessus. Pour faire cela, il utilise une méthode statique de la classe com.sun.xml.parser.Resolver  pour créer un InputSource à partir d'un objet java.io.File. Vous pourriez effectuer ce travail, mais cette méthode vous est fournie pour vous simplifier la vie (par contre elle vous lie au parser de chez Sun ...).

Implémenter l'interface DocumentHandler

L'interface qui nous intéresse le plus dans notre cas est DocumentHandler. Cette interface nécessite l'implémentation d'un certain nombre de méthodes que le parser SAX invoquera en réponse aux différents événements qui se produiront. Pour l'instant, nous ne nous occuperons que des cinq méthodes suivantes :  startDocument, endDocument, startElement, endElement, et characters. Entrez le code en gras ci-dessous, pour pouvoir gérer ces événements :
 
    ...   
    static private Writer       out;
    public void startDocument ()
    throws SAXException
    {
    }

    public void endDocument ()
    throws SAXException
    {
    }

    public void startElement (String name, AttributeList attrs)
    throws SAXException
    {
    }

    public void endElement (String name)
    throws SAXException
    {
    }

    public void characters (char buf [], int offset, int len)
    throws SAXException
    {
    }
     ...

Chacune de ces méthodes doit pouvoir lancer une SAXException. Une exception levée à ce niveau, sera répercuté sur le parser, qui la retournera au code qui a invoqué le parser. Dans notre cas, on la récupérera dans la méthode main avec le catch Throwable.

A chaque fois qu'un tag ouvrant ou qu'un tag fermant est rencontré, le nom du tag est passé en tant que chaine aux méthodes startElement ou endElement. Quand un tag ouvrant est rencontré, tous les attributs qu'il définit seront aussi passés dans un objet AttributeList. Les caractères trouvés dans l'élément sont passés comme un tableau de caractères, avec le nombre de caractères (length) et le décalage dans le tableau correspondant au premier caractère.

Ecriture sur la sortie standard

La méthode DocumentHandler peut lancer une SAXExceptions mais pas de IOExceptions, qui pourrait arriver lors de l'écriture. En fait, la SAXException peut contenir une autre exception, ainsi il y a un sens à utiliser cette méthode pour rediriger toutes les erreurs vers le meme gestionnaire d'erreurs. Rajoutez le code ci-dessous pour définir une méthode emit qui effectue l'impression :
 
public void characters (char buf [], int offset, int Len)
throws SAXException
{
}


private void emit (String s)
throws SAXException
{
    try {
        out.write (s);
        out.flush ();
    } catch (IOException e) {
        throw new SAXException ("I/O error", e);
    }
}
...

Lorsque la méthode emit est invoquée, toutes les erreurs d'I/O sont enrobées dans une SAXException avec un message l'identifiant. Cette exception est ensuite retournée au parser SAX. Rappelons nous juste que la méthode emit est une petite méthode qui gère l'affichage.

Gestion de l'indentation

Avant de procéder au parsage, nous devons rajouter une toute petite chose. Rajoutez le code de la méthode nl permettant de générer un passage à la ligne (en respectant les réglages courant du système) :
 
    private void emit (String s)

    ...

    }

  
    private void nl ()
    throws SAXException
    {
        String lineEnd =  System.getProperty("line.separator");
        try {
            out.write (lineEnd);
       
        } catch (IOException e) {
            throw new SAXException ("I/O error", e);
        }
    }
Note: Bien que cette méthode ne semble pas fondamentale, vous verrez qu'elle nous sera très utile dans la suite.

Gestion des événements de type Document

Finallement, écrivons le vode qui effectue les traitements sur les événements DocumentHandler. Rajoutez juste le code en gras ci-dessous, gérant les événements de début et de fin de document :
 
    public void startDocument ()
    throws SAXException
    {
        emit ("<?xml version='1.0' encoding='UTF-8'?>");
        nl();
    }

    public void endDocument ()
    throws SAXException
    {
        try {
            nl();
            out.flush ();
        } catch (IOException e) {
            throw new SAXException ("I/O error", e);
        }
    }

Ici, nous produisons un écho de la déclaration XLM lorsque le parser rencontrera le début du document. Comme nous avons réglé le OutputStreamWriter avec un encodage UTF-8, on inclut cette spécification dans la déclaration.

Note: Cependant les classes d'entrées-sorties ne gèrent pas les encodages avec le signe "-", on utilise donc la notation "UTF8" plutot que "UTF-8".
A la fin du document, on affiche juste le dernier tag, et on vide le flux de sortie. Plus grand chose à ajouter. Passons aux choses intéresantes. Ajouter le code gérant les début et fin d'éléments :
 
    public void startElement (String name, AttributeList attrs)
    throws SAXException
    {
        emit ("<"+name);
        if (attrs != null) {
            for (int i = 0; i < attrs.getLength (); i++) {             
                emit (" ");
                emit (attrs.getName(i)+"=\""+attrs.getValue (i)+"\"");
            }
        }
        emit (">");
    }

    public void endElement (String name)
    throws SAXException
    {
        emit ("</"+name+">");
    }
With

Avec ce code, on affiche le tag élément, en incluant tous les attributs définit dans l'élément ouvrant. Pour finaliser ce programme, ajoutez juste la dernière méthode ci-dessous, qui se chargera de l'affichage des caractères :
 

    public void characters (char buf [], int offset, int len)
    throws SAXException
    {
        String s = new String(buf, offset, len);
        emit (s);
    }

Félicitations! Vous venez juste d'écrire votre prmière applicatiion utilisant un parser SAX. La prochaine étape est la compilation et l'exécution !

Note: Pour etre plus précis, le gestionnaire de caractères devrait scanner le buffer pour chercher les caractères ('&') et  ('<') et les remplacer par les chaines de caractères "&amp;" ou "&lt;".

Compilation du programme

Pour compiler le programme que vous venez de créer, utilisez la commande approprié à votre système :
Windows:
javac -classpath %XML_HOME%\jaxp.jar;%XML_HOME%\parser.jar Echo.java
Unix:
javac -classpath ${XML_HOME}/jaxp.jar:${XML_HOME}/parser.jar Echo.java
ou:

Exécution du programme

Pour exécuter le programme, il faudra bien prendre garde à inclure les ressources nécessaires, cequi donnera selon votre système :
Windows:
java -classpath .;%XML_HOME%\jaxp.jar;%XML_HOME%\parser.jar Echo slideSample.xml
Unix:
java -classpath .:${XML_HOME}/jaxp.jar:${XML_HOME}/parser.jar Echo slideSample.xml

Scripts de commandes

Pour vous simplifier la vie, récupérez les scripts suivants qui automatiseront un peu ces taches fastidieuses.
 
  Unix Windows
Scripts build, run build.bat, run.bat
Netscape Cliquez, puis File-->Save As Clic droit, puis Save Link As.
Internet
Explorer 
-/-
Clic droit, puis Save Target As.

Vérification de l'affichage

L'affichage produit par le programme est disponible dans Echo01-01.log. Voici une partie de ce dernier, nous montrant quelques espacements ... embetants :
 
...
<slideshow title="Sample Slide Show" date="Date of publication" author="Yours Truly">


    <slide type="all">
      <title>Wake up to WonderWidgets!</title>
    </slide>
    ...

En examinant lea sortie produite, un certain nombre de questions se posent. D'abord pourquoi y-a-t-il tant de passage à la ligne ? Et pourquoi les éléments sont_ils indentés correctement alors que l'on ne fait rien de spécial dans le code ? Nous verrons ces réponses dans un court instant, mais voisi déjà un certain nombre de choses à noter :

<!-- A SAMPLE set of slides -->
n'apparait pas. Les commentaires sont par définition ignorés, sauf si vous implémentez un LexicalEventListener  au lieu d'un DocumentHandler.
Cette version du programme peut etre utilie pour afficher un fichier XML, mais ne nous apprends pas tellement ce qui sepasse réellement dans le parser. La prochaine étape consiste à modifier ce programme pour savoir d'ou proviennent nos passages à la ligne intempestifs.
Note: Le code que nous verrons dans cette section se trouve ici Echo02.java. L'affichage qu'il produit se trouve là Echo02-01.log.
Effectuez les changements suivants pour que nous identifions mieux les différents événements :
 
    public void startDocument ()
    throws SAXException
    {
        nl();
        nl(); 
        emit ("START DOCUMENT");
        nl(); 
        emit ("<?xml version='1.0' encoding='UTF-8'?>");
        nl();
    }

    public void endDocument ()
    throws SAXException
    {
        nl(); emit ("END DOCUMENT");
        try {
         ...
    }

    public void startElement (String name, AttributeList attrs)
    throws SAXException
    {
        nl(); emit ("ELEMENT: ");
        emit ("<"+name);
        if (attrs != null) {
            for (int i = 0; i < attrs.getLength (); i++) {
                emit (" ");
                emit (attrs.getName(i)+"=\""+attrs.getValue (i)+"\"");
                nl(); 
                emit("   ATTR: ");
                emit (attrs.getName (i));
                emit ("\t\"");
                emit (attrs.getValue (i));
                emit ("\"");
            }
        }
        if (attrs.getLength() > 0) nl();
        emit (">");
    }

    public void endElement (String name)
    throws SAXException
    {
        nl(); 
        emit ("END_ELM: ");
        emit ("</"+name+">");
    }

    public void characters (char buf [], int offset, int len)
    throws SAXException
    {   
        nl(); emit ("CHARS: |");     
        String s = new String(buf, offset, len);
        emit (s);
        emit ("|");
    }

Compilez et exécutez ce programme pour obtenir une trace plus détaillée. Les attributs sont maintenant affichés un par ligne, ce qui propre. Mais plus important, les lignes telles que celle-ci :

CHARS: |



    |
nous montre que la méthode characters est responsable de l'affichage des espaces qui créent l'indentation et les retours à la ligne qui séparent les attribtus.
Note: The XML specification requires all input line separators to be normalized to a single newline. The newline character is specified as \n in Java, C, and Unix systems, but goes by the alias "linefeed" in Windows systems.

"Compression" de l'affichage

Pour rendre l'affichage plus lisible, modifier le programme de telle manière qu'il n'affiche que les caractères différents de l'espace ' '.
Note: Le code de cette partie se trouve dans Echo03.java.
    public void characters (char buf [], int offset, int len)
    throws SAXException
    {
        nl(); emit ("CHARS: |");
        nl(); emit ("CHARS:   ");
        String s = new String(buf, offset, len);
        emit (s);
        emit ("|");
        if (!s.trim().equals("")) emit (s);
    }

Si vous exécuteez le programme, vous remarquerez que vous avez supprimé l'indentation aussi, car les espaces produisant l'indentation font parties des espaces qui précédent le début d'un élément. Rajouter le code en gras de ci-dessous pour gérer de nouveau l'indentation :
 

    static private Writer       out;
    
    private String indentString = "    "; // Amount to indent
    private int indentLevel = 0;

    ...

    public void startElement (String name, AttributeList attrs)
    throws SAXException
    {
        indentLevel++;
        nl(); emit ("ELEMENT: ");
        ...
    }

    public void endElement (String name)
    throws SAXException
    {
        nl(); 
        emit ("END_ELM: ");
        emit ("</"+name+">");
        indentLevel--;
    }
    ...
    private void nl ()
    throws SAXException
    {
        ...
        try {
            out.write (lineEnd);
            for (int i=0; i < indentLevel; i++) out.write(indentString);
          
        } catch (IOException e) {
        ... 
    }

Ce code permet de conserver une trace des noveaux d'indentation, et d'indiquer si il est nécessaire ou non d'invoquer la méthode nl. Si vous régler la chaine effactuant l'indantation à "", l'affichage sera non-indenté (essayez, vous comprendrez mieux l'intéret de l'indentation ;-).

Vous serez heureux d'apprendre que vous avez atteint la fin de la partie "mécanique" d'ajout de code dans notre programme Echo. A partir de maintenant, vous n'allez plus qu'examiner des choses vous montrant le fonctionnement du parser. Ce que nous avons effectué auparavant, vous a montré comment le parser traite un message XML. Nous avons aussi obtenu un outil de débugage nous permettant de "dumper" un message XML tel que le voit le parser.

Examen de l'affichage

L'affichage complet de la dernière version du programme est contenu dans Echo03-01.log. Voici un extrait de cet affichage :
    ELEMENT: <slideshow
    ...
    CHARS:   
    CHARS:   
        ELEMENT: <slide
        ...  
        END_ELM: </slide>
    CHARS:   
    CHARS:
Remarquez que la méthode characters est invoquée deux fois dans une meme ligne. En regardant le fichier source slideSample01.xml on remarque qu'il y a un comentaire juste avant le premier slide. Le premier appel à characters intervient avant ce commentaire. Le second juste après.

Remarquez également que la méthode characters est invoquée après le premier élément slide, ainsi qu'avant. Lorsque l'on y pense en termes de données structurées de manière hiérarchique, cela semble étrange. Après tout, nous nous attendions à ce que l'élément slideshow contienne un élément slide, pas du texte. Plus tard, nous verrons comment restreindre l'élément slideshow en utilisant un DTD. Lorsque l'on fait cela, la méthode characters n'est plus invoquée.

Cependant, en l'absence d'un DTD, le parser suppose que tous les éléments contiennent du texte comme l'élément <item> :

<item>Why <em>WonderWidgets</em> are great</item>
Voici à quoi ressemble la structure hiérarchique :
ELEMENT: <item>
CHARS:   Why 
    ELEMENT: <em>
    CHARS:   WonderWidgets
    END_ELM: </em>
CHARS:    are great
END_ELM: </item>

Documents et données

Dans cet exemple, il est clair qu'il ya des caractères mélangés avec la structure hiérarchique des éléments. Le fait est que du texte puisse entourer des éléments (ou ne le puisse pas avec un DTD) nous aide à comprendre pourquoi l'on entends parfois parler de "données XML" et d'autre fois de "documents XML". XML gère à la fois les textes structurés, et les documents textuels incluant des balises. La seule différence entre les deux est de savois si du texte est autorisé ou pas entre les éléments.