Powered by |
Développement XML en JavaMichel CASABIANCA - casa@sweetohm.netCe tutoriel est le dernier d'une série de trois (après une présentation générale de XML et une introduction à XSLT). Il traite du développement d'applications XML en Java.
IntroductionPourquoi utiliser XML dans une application ?XML est utile en de nombreuses occasions, en particulier:
Les outils pour travailler avec XMLL'outil pour travailler avec XML s'appelle un parser. Il lit le fichier XML et le rend disponible à un programme. Il peut le faire de deux manières différentes:
Il aussi parfois utile de disposer d'un processeur XSLT pour transformer des documents avec de simples feuilles de style (ce qui est souvent beaucoup plus simple que de le transformer dans un programme). Il peut être utile aussi de disposer d'un bon éditeur XML (comme Emacs et PSGML) pour éditer les documents XML dans de bonnes conditions (avec colorisation, indentation et aide à l'utilisateur par utilisation de la DTD du document). API de parsingComme nous l'avons vu, il existe principalement deux types de parsers. Chacun de ces familles correspond à une API de parsing : API SAXCette API a été définie sur la liste xml-dev hébergée par xml.org. Elle a été la première API XML largement utilisée et était à l'origine dédiée au langage Java. Il existe maintenant nombre d'implémentations pour d'autres langages de programmation (dont C, C++, Perl, Python ou Lisp). Cette API est devenue un standard de facto largement adopté. Les spécifications SAX sont dans le domaine public et l'on peut les télécharger à l'adresse http://www.saxproject.org/. Ces spécifications en sont à la version 2 qui est maintenant implémentée par les principaux parsers XML. Principes générauxCette API est essentiellement event driven. Un parser SAX ne construit pas d'arbre du document en mémoire mais appelle (callbacks) les méthodes d'un handler lorsqu'il rencontre des évènements particuliers (comme l'ouverture ou la fermeture d'un élément). On peut comparer la programmation d'un tel parser à du développement d'interfaces graphiques où l'on gère des évènements générés par l'utilisateur. L'architecture d'une application SAX est présentée dans la figure ci-dessous.
Figure 1: Architecture d'une application SAX Exemple d'application SAXSoit un document simple, comme celui du source ci-dessous : hello.xml
Lors du parsing SAX de ce document, il sera envoyé à l'application les événements suivants:
Le source d'un handler pour écrire la trace ci-dessus pourrait être le suivant (modulo quelques méthodes qui ne sont pas intéressantes dans le cadre de ce tutoriel) : SaxParse.java
Lors du parsing de ce document, le parser SAX appelle les méthodes du ContentHandler qui affiche les événements sur la sortie standard. Survol de l'API SAXAprès cette brève présentation de SAX, nous allons nous pencher en détail sur l'API SAX 2.0 : XMLReaderCette interface est implémentée par les parsers. Elle comporte en particulier une méthode pour parser un document, activer ou désactiver des features (comme la validation par exemple) ou des propriétés ainsi que des méthodes pour indiquer au parser les handlers (pour gérer le contenu du document, les erreurs ,etc.). Les principales méthodes sont résumées ci-dessous :
Le ContentHandler étant une interface, on ne peut bien sur l'instancier et l'on doit faire appel à une factory qui se trouve dans le package org.xml.sax.helper. Le nom de la classe du parser SAX utilisée doit être indiqué dans la propriété système org.xml.sax.driver. Par exemple, le code suivant instancie un parser Xerces:
Il est important de tester la propriété système avant de l'assigner de manière à ce qu'il soit possible de changer de parser en renseignant la propriété sur la ligne de commande qui lance l'application. On pourra le faire avec la ligne suivante :
Le choix d'un parser est alors une simple question de configuration (il suffit de modifier le script de lancement de l'application et de mettre le fichier jar de la bibliothèque dans le CLASSPATH). On pourra ainsi changer de parser facilement pour choisir le plus adapté à ses besoins. ContentHandlerCette interface doit être implémentée par la classe qui doit être notifiée des événements rencontrés lors du parsing. Comme nous l'avons vu ci-dessus, elle implémente des méthodes appelées lorsque le parser ouvre ou ferme le document ou un élément :
On prendra garde à épurer le texte envoyé par le parser. En effet, celui comporte souvent des blancs sans signification (de simples retours de ligne par exemple). Pour ce faire, on pourra faire le test suivant à l'entrée de la méthode characters() :
Il est souvent intéressant d'étendre DefaultHandler (du package org.xml.sax.helpers) plutôt que d'implémenter l'interface ContentHandler. En effet, cette classe abstraite implémente les interfaces EntityResolver, DTDHandler, ContentHandler et ErrorHandler avec des méthodes qui ont un comportement raisonnable. Il n'est plus alors nécessaire d'implémenter toutes les méthodes, mais seulement celles qui nous intéressent (par exemple startElement()). ErrorHandlerCette interface doit être implémentée par les handlers de gestion des erreurs de parsing. Elle définit les méthodes suivantes :
En implémentant ces méthodes, on contrôle finement la gestion des erreurs. Il est ainsi possible d'ignorer les warnings et de continuer le parsing. Si l'on souhaite continuer le parsing, il suffit de ne pas lancer d'exception, dans le cas contraire, on relancera l'exception passée en paramètre. Il est cependant peu courant d'avoir à implémenter son propre ErrorHandler (on se contentera alors du comportement par défaut). Il peut être utile d'écrire sa propre implémentation pour un validateur de document XML. Avec l'ErrorHandler suivant :
On empile les erreurs de parsing permettant ainsi de lister toutes les erreurs de syntaxe d'un document (par défaut, toute erreur interrompt le parsing). EntityResolverCe handler permet une gestion fine des entités externes. Cette interface ne définit qu'une seule méthode :
On peut alors définir la localisation d'un fichier dans un catalogue. Il existe plusieurs standards de catalogues XML, comme le standard OASIS ou le standard XML Catalog. Gestion des exceptionsLa gestion des exceptions par un parser SAX est assez particulières. L'API SAX définit deux exceptions importantes :
Pour afficher correctement les Exceptions lancée par la méthode parse() du parser, on pourra s'inspirer du source suivant :
Il est essentiel d'afficher clairement les erreurs de parsing sans quoi l'utilisateur sera incapable de corriger l'erreur dans le fichier XML (surtout si celui-ci est très long). API DOMPrincipes générauxCette API (dont le nom est l'acronyme de Document Object Model) a une approche radicalement différente de SAX : un parser DOM charge en mémoire tout le document et en fait une représentation sous forme d'un arbre d'objets. On pourrait donc schématiser le fonctionnement d'un parser DOM par la figure suivante :
Figure 2: Fonctionnement d'un parser DOM Le document étant entièrement en mémoire, il est possible de le parcourir (en parcourant l'arbre) et de le manipuler (en déplaçant, en créant ou en supprimant des noeuds). Exemple d'application DOMNous allons maintenant voir un exemple d'application DOM simple pour parcourir le document et écrire sa structure sur la sortie standard (donc l'équivalent en DOM de l'exemple SAX). Pour commencer, il nous faut récupérer une instance du parser DOM (dans le cas ci-dessous, le parser DOM de Xerces). Nous devons ensuite récupérer le document (c'est à dire une référence vers un objet org.w3.dom.Document). Nous pouvons le faire de la manière suivante :
Nous pouvons noter que :
Nous devons ensuite écrire récursivement l'arbre du document sur la sortie standard :
Cette méthode parse récursivement l'arbre du document et affiche la nature des noeuds de ce dernier. On notera au passage qu'il existe une interface Node qui est implémentée par les interfaces Element, Attr (pour Attribute), Comment ou Text (entre autres). Ces interfaces sont implémentées par des classes concrètes qui dépendent de l'implémentation. Ce design implique une gymnastique pour créer de nouveaux noeuds (en passant par des factories du document). Par exemple, pour créer un nouvel élément Foo et l'ajouter à bar, on écrira :
Il existe des méthodes (dans l'interface Node) pour ajouter, supprimer ou déplacer des noeuds d'un document. C'est une fonctionnalité intéressante des parsers DOM : ils permettent la manipulation des documents (ce qui est très difficile de faire avec SAX). D'autre part, cette API est générique (liée à aucun langage) et ne peut donc se reposer sur des APIs particulières (comme les collections du package java.util de Java) et doit fournir ses propres collections. C'est pourquoi, lorsqu'on demande la liste des attributs, ils ne sont pas renvoyés sous forme d'une collection Java (une HashMap par exemple), mais sous forme d'une NamedNodeMap. API JDOMPrincipe généralCette API repose sur les mêmes principes que DOM, mais elle se veut beaucoup plus simple que cette dernière car dédiée au langage Java. Par exemple, une liste d'éléments fils est renvoyée sous forme de List Java, ce qui est la forme la plus naturelle pour ce langage. De plus, les classes de mapping aux éléments, attributs et autres entités XML sont des classes concrètes et non des interfaces, ce qui évite d'avoir à recourir à des Factory comme c'est le cas avec DOM. Cette API n'est pas issue d'un organisme officiel comme le W3, mais est une initiative personnelle d'un développeur lassé de la complexité superflue de DOM. Cependant, JDOM a été accepté par Sun en tant que JSR 102, ce qui devrait en faire une API standard d'ici peu. Exemple d'application JDOMAvec cette API, nous récupérons le parser et chargeons le document avec le code qui suit (qui utilise le parser Xerces) :
Nous pouvons noter que le nom de la classe du parser SAX utilisé est un simple paramètre (on peut donc en changer simplement). Nous pouvons implémenter l'exemple donné dans la section DOM comme suit :
Pour ma part, je ne regrette qu'une interface Node qui serait implémentée par les noeuds et qui déclarerait les méthodes de navigation communes à tous les noeuds. API JAXPPrincipe généralL'API JAXP (pour Java API for XML Parsing) est une initiative de Sun pour uniformiser les APIs de parsing des différents parsers XML et processeurs XSLT Java. On peut alors écrire du code complètement indépendant du parser utilisé (il suffit souvent de changer le nom de la classe du parser dans un fichier de configuration). Cette API est particulièrement intéressante pour les parsers DOM et les processeurs XSLT. En effet, DOM définit l'interface des méthodes pour manipuler les documents, mais rien en ce qui concerne l'instanciation du parser ou la récupération du document. Pour ce qui est des processeurs XSLT, il n'existe pas (à part JAXP) d'API standard pour les utiliser dans du code Java. Par contre, l'API SAX définit une API indépendante du parser et l'on pourra sans regrets se passer de l'API de Sun. Cette API est donc une surcouche aux parsers et elle est très légère (elle ne comporte en tout et pour tout que 7 classes ou interfaces). Exemple de code JAXPDu fait de sa nature, il n'est pas intéressant de présenter une application JAXP (qui serait pour l'essentiel conforme aux APIs SAX ou DOM). Je me contenterai donc ici de présenter des bouts de code pour instancier les parsers et processeurs. Pour instancier un parser SAX, on pourra écrire :
À partir de ce moment, le reste du code doit se conformer à l'API SAX. On notera, comme indiqué plus haut, que l'on peut aussi écrire du code paramétrable avec l'API SAX. Pour instancier un parser DOM et récupérer un document, on pourra écrire :
On pourra ensuite manipuler le document récupéré avec l'API DOM. Pour finir, pour transformer un document XML en utilisant un processeur XSLT, on écrira :
Cette API est appelée TRAX (pour Transformation API for XML). C'est la seule API répandue pour la transformation XSLT et elle est implémentée par le processeur XSLT Apache (Xalan). Conclusion : choix d'une APIIl n'est bien sûr aucune API meilleure que les autres, tout est question de besoins. Pour résumer, on peut dire que :
Exemples d'applications XML en JavaJava est probablement le langage qui a le plus rapidement intégré XML (les premiers parsers XML et processeurs XSLT ont été implémentés en Java). On peut l'expliquer par une convergence des problématiques des deux langages : tous deux sont indépendants de la plateforme, tous deux ont été conçus avec un soucis de simplicité. Il est donc aisé de trouver de très nombreux exemples de mise en oeuvre de XML en Java. Je présenterai ici des applications qui m'ont semblé exploiter au mieux les capacités des deux langages. Lecture de fichiers de configuration structurésLes fichiers de configuration XML ont l'avantage (par rapport à des fichiers de propriétés par exemple) d'être structurés. La lecture de ces fichiers peut être réalisée avec tout parser, mais JDOM est particulièrement adapté à cette tâche. Supposons que nous ayons le fichier de configuration suivant :
Pour accéder au contenu de l'élément bar, on peut écrire le bout de code suivant (qui présuppose que le document a été chargé en mémoire dans la champ doc) :
Ce genre d'expressions simple permet de naviguer dans l'arborescence du document XML pour en extraire les informations de configuration. Il est bien sûr possible de faire le même code avec DOM ou SAX, mais c'est plus long à mettre en oeuvre. Programmation d'applications modulairesL'idée de cette approche de la programmation est de charger un arbre de Beans à partir d'un fichier de configuration XML. on peut en effet assez facilement définir une syntaxe pour "mapper" des beans sur des éléments XML:
Par exemple, le fichier XML suivant:
Lors du parsing de ce fichier, le parser va réaliser les actions suivantes:
On obtient en mémoire une structure du type:
Figure 3: Arbre de beans en mémoire Cette approche est très utile dans la mesure où ce système permet une configuration simple du programme (il suffit de modifier le fichier XML, avec un simple éditeur ou un outil graphique dédié). D'autre part, il est très extensible: pour utiliser un nouveau type de bean, il suffit de le placer dans le CLASSPATH du programme, de placer son élément associé dans le fichier de configuration et le tour est joué ! Cette approche commence à prendre une grande importance en programmation Java et il me semble que c'est maintenant un paradigme incontournable utilisé dans un nombre croissant d'applications récentes. Nous allons maintenant voir un certain nombre d'exemples de mise en oeuvre de cette approche: Système de Log de GameZillaLes logs sont envoyés (par RMI) au serveur de log qui les empile (pour rendre la main immédiatement au client). Les logs sont dépilés par un Consumer qui les envoie aux tuyaux pour filtrage et sérialisation. L'architecture d'un tuyau de log est la suivante:
Figure 4: Architecture du serveur de logs de GameZilla Le filtre permet de sélectionner les logs à sérialiser dans ce canal de log. Les logs passent ensuite par les channels qui définissent un moyen de persistance (dans des fichiers, dans une BD, par mail ou sur la console). Pour configurer ce système, nous avons mis en oeuvre la technique de mapping XML/Beans exposée ci-dessus. Voici un exemple de fichier de configuration d'un tuyau de logs:
Ce fichier de configuration indique que:
L'arbre d'objets associés à un tuyau est construit à partir du fichier de configuration puis les logs y sont envoyés lors du fonctionnement du serveur. L'avantage de cette solution est que sa mise en oeuvre est relativement simple et il n'est pas nécessaire de développer un outil graphique dans la phase de développement. Pour la configuration à chaud, les fichiers sont envoyés à l'interface d'administration par RMI. Le serveur stoppe les threads de consommation de la pile de logs, construit l'arbre des tuyaux puis relance les threads. D'autre part, le système est facilement extensible car il suffit, pour ajouter un channel (SMS par exemple), de développer le bean correspondant (en implémentant une interface propre aux channels) , de le placer dans le CLASSPATH puis de changer le fichier de configuration. Free Java InstallerCe programme est un auto-installeur Java sous licence GPL. Il permet de construire un jar qui est un programme d'installation. Examinons un exemple de fichier de configuration:
Chacun des éléments de ce fichier correspond à une étape de cette installeur (une tâche):
Figure 5: Écran d'acceuil Cet écran correspond à l'élément XML suivant:
Figure 6: Écran de licence Celui-ci correspond au suivant:
Lors de la construction de l'installeur, le fichier XML de configuration est parsé et l'arbre de beans (qui étendent la classe Task) construit. Cet arbre est ensuite sérialisé dans le fichier jar qui constitue l'installeur. Lorsque l'installeur est lancé, il désérialise l'arbre des tâches et appelle la méthode process sur le bean racine. Le fichier XML de configuration n'est plus nécessaire et n'est donc pas embarqué dans le fichier jar. Cette approche permet de rendre le programme extensible du fait que l'on peut programmer d'autres tâches. Pour les utiliser, il suffit de les placer dans le CLASSPATH et de les invoquer dans le fichier de configuration. Cette technique qui me semble extrèmement prometteuse est de plus en plus utilisée en programmation Java. On pourra en particulier étudier le fonctionnement du programme Ant qui repose sur le même principe. ResourcesLes principaux parsers Java sont les suivants :
Dernière mise à jour : 2002-05-23 |