Briques en C (17) : libxml2 et petits fichiers XML

Creative Commons License
Cette création est mise à disposition sous un contrat Creative Commons.

L'article original se trouve sur http://ymettier.free.fr/articles_lmag/.

Article publié dans le numéro 51 (juin 2003) de GNU/Linux France Magazine


Table des matières

1. Introduction
2. Sax et DOM
2.1. Sax
2.2. DOM
2.3. libxml2
3. La gestion des permissions
4. Lire un document XML avec DOM
4.1. Lire un fichier XML
4.2. Lire un document en mémoire
4.3. Libérer la mémoire
5. La structure d'un arbre DOM
5.1. La structure
5.2. Parcourir un arbre DOM
5.3. La construction d'un arbre DOM
6. Sauvegarder ou afficher un arbre DOM via un document XML
7. Modifier un arbre DOM
7.1. Ajouter un noeud
7.2. Supprimer un noeud
7.3. Modifier le contenu d'un noeud
7.4. Ajouter/supprimer des attributs
7.5. Un exemple
8. DOM et XPath
8.1. Rechercher un noeud avec XPath
8.2. Exemple: tester les permissions
9. Les fichiers de configuration au format XML
10. Conclusion
11. Références

Résumé

Briques de bases en C (17)

1. Introduction

XML est un langage à la mode, mais au vu de sa puissance, il risque, sinon de rester à la mode, d'être au moins longtemps utilisé. Il permet d'avoir un format de données qui cumule de nombreux avantages, comme son format texte lisible par ceux qui ne savent pas lire le binaire littéral, comme ses possibilités structurelles qui permet d'organiser les données dans le document, comme sa formulation qui le rendre lisible de l'informaticien non initié à XML, et j'en passe.

Et j'en passe, mais je ne passerai pas sur sa facilité d'utilisation que vous allez voir tout au long de cet article, où nous allons au travers d'exemples, lire un document XML, utiliser son contenu, le modifier, et évidemment le sauvegarder dans un fichier.

2. Sax et DOM

Pour lire un document XML, il existe deux méthodes assez répandues, Sax et DOM. Ces deux méthodes sont différentes, et si certains se plaisent à dire qu'elles sont complémentaires, je préfère dire que l'une est adaptée à certains types de problèmes et l'autre à d'autres.

2.1. Sax

Sax dispose d'un système de callbacks. Le principe est de lire balises après balises, et suivant le type de balise, le parser vous rend la main via un callback pour vous laisser agir comme bon vous semble. Cette méthode vous donne un grand contrôle sur la lecture du document XML et pour son interprétation. Ainsi, un tel parser ne prend pas beaucoup de place en mémoire contrairement à un parser basé sur DOM puisqu'à peu de choses près, la mémoire n'est allouée que par vous en fonction de vos besoins.

Un parser Sax est donc très intéressant pour lire des documents XML volumineux afin d'en extraire la partie intéressante, telle que le résultat d'une recherche. Il est aussi pratique lorsque toutes les données doivent être analysées mais n'ont pas besoin d'être en mémoire en même temps, comme pour effectuer des calculs ou réaliser des statistiques sur le contenu du document.

2.2. DOM

L'idée de DOM est très différente de celle de Sax. En effet, le parser va construire un arbre en mémoire représentant le document XML. Toutes les opérations suivantes auront donc lieu sur la représentation en mémoire. Il y a au moins deux avantages à un tel principe: d'une part l'accès aux données est rapide, bien plus rapide qu'avec la méthode Sax étant donné que les données ont été organisées en mémoire pour être facilement accédées, alors qu'avec Sax, les données restent dans le document XML, au format XML. D'autre part, les données, puisque chargées en mémoire, peuvent être relues plusieurs fois rapidement puisque le chargement est initial et n'a pas besoin d'être refait à chaque fois. DOM est donc optimal pour un document qui peut être chargé en mémoire intégralement, et sur lequel le but est d'effectuer de nombreux accès dessus.

2.3. libxml2

Il existe plusieurs bibliothèques utilisables en C pour traiter des documents XML. Les plus connues sont peut-être libxml2 et expat. Expat est utilisée dans de grands projets comme Mozilla, et vous ne prenez pas de risques à ce qu'elle soit abandonnée un jour contrairement à de petits projets qui naissent avec la mode et meurent une fois la mode passée. De même, libxml2, comme son nom l'indique, The XML C library for GNOME, est utilisée dans le projet GNOME et dans de nombreux autres projets maintenant. Contrairement à ce que son nom pourrait laisser croire, elle est par contre utilisable de manière complètement indépendante du projet GNOME: vous avez nul besoin d'avoir installé GNOME, pas même glib, pour utiliser libxml2. Par ailleurs, elle est maintenue par Daniel Veillard, grand acteur français du monde XML, que je salue bien bas pour ses réalisations.

Libxml2 implémente DOM, et comme parler sérieusement à la fois de libxml2, d'expat, de Sax et DOM, dans un seul article, est tout à fait impossible, je limiterai le sujet à l'utilisation de la méthode DOM avec libxml2, en réservant Sax et/ou expat pour d'autres briques en C futures.

3. La gestion des permissions

Si vous réalisez un programme et que vous avez besoin de donner la permission à certains utilisateurs d'effectuer une action, et pas à d'autres, vous devez implémenter un système d'autentification associé à un système de permissions. Souvent, les permissions du système de fichiers Unix suffisent amplement à cela, en interdisant par exemple l'exécution du programme sauf pour un groupe d'utilisateurs. Si vous écrivez un programme qui doit avoir un système de permissions différent de celui du système fourni par Unix via les comptes, voici une méthode simple à mettre en place, qui nous servira d'exemple. Au lieu d'avoir un fichier plat contenant la liste des utilisateurs et ce qu'ils ont le droit de faire, un fichier XML contiendra ces informations. Voici un exemple d'un tel fichier XML:

 1 <?xml version="1.0" ?>
 2 <authorizations>
 3 	<group name="users">
 4 		<user>bob</user>
 5 		<user>rebecca</user>
 6 		<user>bic</user>
 7 		<user>bac</user>
 8 	</group>
 9 	<group name="admins">
10 		<user>arkana</user>
11 		<user>spartakus</user>
12 	</group>
13 	<group name="super-admins">
14 		<user>shagshag</user>
15 	</group>
16 	<action name="conduire" object="shagshag">
17 		<authorization grantedto="admins" />
18 		<authorization grantedto="super-admin"/>
19 	</action>
20 	<action name="regarder" object="shagma">
21 		<authorization grantedto="users" />
22 		<authorization grantedto="admins" />
23 		<authorization grantedto="super-admin"/>
24 	</action>
25 </authorizations>

Ce document XML a une structure relativement simple à comprendre, et vous voyez tout de suite son principe:

  • Une action a lieu sur un objet

  • Une action n'est autorisée que pour une liste exhaustive de groupes

  • Un groupe contient une liste exhaustive d'utilisateurs

Appelez ce fichier permissions.xml par exemple.

4. Lire un document XML avec DOM

Lire un document XML avec DOM est relativement simple. Lire un document XML consiste à lire le document là où il est, à savoir en mémoire ou sur disque dans un fichier, par exemple. Cela consiste ensuite à interpréter son contenu, renvoyer d'éventuelles erreurs s'il y a des erreurs au niveau de la syntaxe XML. Attention, lire un document XML n'a rien à voir avec le fait qu'il soit valide ou non par rapport à une DTD, et n'effectue pas cette vérification. La lecture vérifie juste qu'on a bien du XML et qu'il n'y a pas de fautes d'orthographe dans le XML. Enfin, la lecture du document consiste à allouer un espace mémoire pour y construire un arbre et y mettre les données du document. Peu importe comment la lecture se fait, dans quel ordre, du moment que tout ce qui précède est fait.

4.1. Lire un fichier XML

La lecture d'un document XML contenu dans un fichier s'effectue avec l'instruction xmlParseFile() qui prend en seul et unique argument le nom du fichier. Elle renvoie une variable de type xmlDocPtr. Vous remarquerez tout au long de la lecture que libxml2 n'aime pas les étoiles. Ainsi, on préfère xmlDocPtr à xmlDocPtr* ou même à xmlDoc*. Cela donne:

1 xmlDocPtr xmldoc = NULL;
2 xmldoc = xmlParseFile ("permissions.xml");
3 if (!xmldoc)
4   {
5     fprintf (stderr, "%s:%d: y'aurait pas comme un petit probleme ?\n", 
6 	    __FILE__, __LINE__);
7     exit (EXIT_FAILURE);
8   }

xmldoc contient maintenant une structure prête à être utilisée avec les instructions DOM.

Attention: si votre programme est un programme avec des threads, vous devez initialiser le parseur avec xmlInitParser() (pas d'arguments, ne renvoie rien).

4.2. Lire un document en mémoire

Lire un document en mémoire ne semble pas présenter d'intérêt à première vue. En effet, les documents XML sont principalement des fichiers. Cependant, je connais rien que deux exemples, cas courants, qui peuvent se présenter. Le premier est lorsque vous lisez un document venant du réseau. Vous le lisez entièrement, le placez en mémoire. Ensuite, pas question de le sauvegarder dans un fichier pour le recharger avec ce que nous avons vu précédemment.

Le second concerne les fichiers de configuration. Je reviendrai sur les détails de l'utilisation d'un fichier de configuration avec libxml2 plus loin, mais voici en quoi sa lecture en mémoire peut être intéressante. Tout programme doit disposer d'une configuration par defaut. Il est très désagréable qu'un programme ne se lance pas s'il ne trouve pas le fichier de configuration de l'utilisateur. Certains programmes sont capables de se lancer malgré l'absence d'un fichier de configuration: ils contiennent la configuration par defaut via l'initialisation des variables aux bonnes valeurs. D'autres sont capables de générer un fichier de configuration par défaut avant de l'utiliser. D'autres encore ouvrent une fenêtre où l'utilisateur doit configurer son programme avant de pouvoir l'utiliser.

Lire un document XML en mémoire peut donc être intéressant pour avoir le fichier de configuration dans une variable codé en dur et, en l'absence de configuration sur le disque, le programme lit le fichier de configuration codé en dur.

Pour lire un document XML en mémoire, c'est comme pour un fichier, mais l'instruction est xmlParseDoc() et prend en argument un pointeur vers un xmlChar*. Un xmlChar peut être considéré comme un char habituel. En fait, c'est inexact, car un char est codé sur un octet. Un xmlChar est aussi codé sur un octet, mais est suscpetible de l'être sur plusieurs dans le cas d'un codage des caractères sur plusieurs octets.

Nous avons aussi l'instruction xmlParseMemory() qui fait comme xmlParseDoc(), mais qui prend en premier argument un const char*, c'est-à-dire du vrai char et non plus du xmlChar, et en second argument, la taille du bloc à lire. Cela donne par exemple:

 1 const char doc[] =
 2   "<?xml version='1.0'?><racine><texte>blabla</texte></racine>";
 3 xmlDocPtr xmldoc;
 4 xmldoc = xmlParseMemory (doc, sizeof (doc));
 5 if (!xmldoc)
 6   {
 7     fprintf (stderr, "%s:%d y'a vraiment comme un probleme ici\n", __FILE__,
 8 	     __LINE__);
 9     exit (EXIT_FAILURE);
10   }

4.3. Libérer la mémoire

Pour libérer la mémoire, comme l'on fait avec free() après un malloc(), veuillez utiliser l'instruction xmlFreeDoc(), avec en argument, le xmlDocPtr que vous avez utilisé, xmldoc dans notre cas:

1 xmlFreeDoc (xmldoc);

5. La structure d'un arbre DOM

Avant d'attaquer la structure, voici un bout de code, celui avec lequel nous allons jouer:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <libxml/tree.h>
 4 #include <libxml/xpath.h>
 5 
 6 int
 7 main (int argc, char *argv[])
 8 {
 9   xmlDocPtr xmldoc = NULL;
10   const char doc[] =
11     "<?xml version='1.0'?><racine><texte>blabla</texte></racine>";
12 
13   xmldoc = xmlParseMemory (doc, sizeof (doc));
14   if (!xmldoc)
15     {
16       fprintf (stderr, "%s:%d bah ca marche pas du tout, en fait\n", __FILE__,
17 	       __LINE__);
18       exit (EXIT_FAILURE);
19     }
20   printf ("%s\n", xmldoc->children->name);
21   xmlFreeDoc (xmldoc);
22 
23   exit (EXIT_SUCCESS);
24 }

A part la ligne 20, vous ne devriez rien découvrir hormis les en-têtes lignes 3 et 4.

Pour compiler un tel programme, rien de plus facile:

$ gcc -Wall -g `xml2-config --libs --cflags` test.c -o test

Il ne vous reste plus qu'à tester ce joli programme qui devrait vous afficher racine. Nous allons voir pourquoi ci-dessous!

5.1. La structure

La structure est une structure d'arbre, complétéede nombreuses informations propres à la bibliothèque. Ainsi, supposons que vous ayez un pointeur sur un noeud node, de type xmlNodePtr. Alors, pour obtenir le premier fils, utilisez node->children. Pour obtenir le noeud parent, c'est node->parent. Et pour obtenir les noeuds précédent et suivant, vous avez node->prev et node->next.

Un noeud a aussi un nom: c'est node->name. Vous comprenez maintenant le pourquoi du comment de la ligne 20 précédente: la racine est le premier noeud fils du document. Et nous en extrayons son nom pour l'afficher.

Voici la structure complète d'un noeud tel que spécifiée dans la documentation de libxml2:

 1 struct xmlNode {
 2     void           *_private;	/* application data */
 3     xmlElementType   type;	/* type number, must be second ! */
 4     const xmlChar   *name;      /* the name of the node, or the entity */
 5     struct _xmlNode *children;	/* parent->childs link */
 6     struct _xmlNode *last;	/* last child link */
 7     struct _xmlNode *parent;	/* child->parent link */
 8     struct _xmlNode *next;	/* next sibling link  */
 9     struct _xmlNode *prev;	/* previous sibling link  */
10     struct _xmlDoc  *doc;	/* the containing document */
11 
12     /* End of common part */
13     xmlNs           *ns;        /* pointer to the associated namespace */
14     xmlChar         *content;   /* the content */
15     struct _xmlAttr *properties;/* properties list */
16     xmlNs           *nsDef;     /* namespace definitions on this node */
17 };

Le seul type intéressant que nous avons laissé de côté pour l'instant est le type. Voyez le tableau contenant les différents types qui font l'énumération xmlElementType. Autre chose, si l'élément content peut vous intéresser, sachez que l'utilisation de content est moins simple qu'il n'y paraît. Et sachez aussi que xmlNodeGetContent() ne renvoie pas que le contenu du noeud, ce à quoi on peut s'attendre, mais en fait la concaténation de tous les contenus des noeuds fils!

typedef enum {
    XML_ELEMENT_NODE=		1,
    XML_ATTRIBUTE_NODE=		2,
    XML_TEXT_NODE=		3,
    XML_CDATA_SECTION_NODE=	4,
    XML_ENTITY_REF_NODE=	5,
    XML_ENTITY_NODE=		6,
    XML_PI_NODE=		7,
    XML_COMMENT_NODE=		8,
    XML_DOCUMENT_NODE=		9,
    XML_DOCUMENT_TYPE_NODE=	10,
    XML_DOCUMENT_FRAG_NODE=	11,
    XML_NOTATION_NODE=		12,
    XML_HTML_DOCUMENT_NODE=	13,
    XML_DTD_NODE=		14,
    XML_ELEMENT_DECL=		15,
    XML_ATTRIBUTE_DECL=		16,
    XML_ENTITY_DECL=		17,
    XML_NAMESPACE_DECL=		18,
    XML_XINCLUDE_START=		19,
    XML_XINCLUDE_END=		20
#ifdef LIBXML_DOCB_ENABLED
   ,XML_DOCB_DOCUMENT_NODE=	21
#endif
} xmlElementType;

En exercice, voyons comment afficher le blabla au lieu du nom du noeud racine dans l'exemple précédent, ligne 20:

20 printf ("%s\n", xmlNodeGetContent (xmldoc->children->children));

5.2. Parcourir un arbre DOM

Parcourir un arbre DOM est aussi simple que de parcourir n'importe quel arbre. Il suffit de parcourir tous les noeuds fils d'un noeuds, de manière récursive. Cependant, parcourir un arbre DOM ne présente pas énormément d'intérêt. En effet, un arbre DOM n'est pas un arbre avec des noeuds comme <texte> dont le contenu est blabla. Déjà, dans ce cas particulier, le noeud <texte> a un contenu vide, ou si vous vous amusez à mettre en forme le XML, il aura un contenu vide. Et il a un noeud fils, qui n'est pas visible explicitement, qui n'a pas de nom, mais dont le contenu est blabla. De plus, si on avait eu <texte><![CDATA[blabla]]></texte> au lieu de <texte>blabla</texte>, le type du noeud fils de <texte> aurait été XML_CDATA_SECTION_NODE au lieu de XML_TEXT_NODE. Ajoutez à cela qu'il faut encore prendre en compte les attributs. Plus ensuite tous les cas un peu plus rares de noeuds que reconnaît libxml2 et qui sont dans le tableau des types!

Parcourir un arbre DOM présente peu d'intérêt par rapport aux moyens à mettre en oeuvre, et par rapport aux fonctions fournies dans libxml2. Cependant, pour illustrer un peu la bibliothèque, je vous présente quand même un petit bout de code, qui affiche tous les noeuds texte (ou CDATA) et leur chemin:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <libxml/tree.h>
 5 #include <libxml/xpath.h>
 6 
 7 void
 8 show (xmlNodePtr node)
 9 {
10   if (node->type == XML_ELEMENT_NODE)
11     {
12       xmlNodePtr n;
13       for (n = node; n; n = n->next)
14 	{
15 	  if (n->children)
16 	    {
17 	      show (n->children);
18 	    }
19 	}
20     }
21   else if ((node->type == XML_CDATA_SECTION_NODE)
22 	   || (node->type == XML_TEXT_NODE))
23     {
24       xmlChar *path = xmlGetNodePath (node);
25       printf ("%s -> '%s'\n", path,
26 	      node->content ? (char *) node->content : "(null)");
27       xmlFree (path);
28     }
29 }
30 
31 int
32 main (int argc, char *argv[])
33 {
34   xmlDocPtr xmldoc = NULL;
35   const char doc[] =
36     "<?xml version='1.0'?><racine><texte>blabla</texte><t2>ca</t2><t3>va</t3><t4><![CDATA[<bien?>]]></t4></racine>";
37 
38   xmldoc = xmlParseMemory (doc, sizeof (doc));
39   if (!xmldoc)
40     {
41       fprintf (stderr, "%s:%d bah ca marche pas du tout, en fait\n", __FILE__,
42 	       __LINE__);
43       exit (EXIT_FAILURE);
44     }
45   show (xmldoc->children);
46   xmlFreeDoc (xmldoc);
47 
48   exit (EXIT_SUCCESS);
49 }

Vous devriez avoir reconnu facilement le code principal, avec une petite variation ligne 35. Ligne 45, nous appelons la fonction récursive show() qui affiche tous les noeuds texte ou CDATA et le chemin de ces noeuds.

Ligne 10, nous effectuons un indispensable test sur le type du noeud. Si c'est un élément nous parcourons tous les noeuds frères avec la boucle ligne 13. Et récursivement, nous parcourons, pour chaque noeud, ses fils, ligne 17 si le noeud en a (test ligne 15). Et nous affichons le contenu du noeud et son chemin ligne 25 si le test sur le type de noeud a conclu que nous avions un noeud texte ou CDATA.

Le chemin du noeud est récupéré avec xmlGetNodePath. La chaîne résultante, de type xmlChar*, doit être libérée avec xmlFree() (ligne 27). D'ailleurs, retenez ceci: toute chaîne de caractères allouée par libxml2 que vous devez libérer se fait avec xmlFree(). Revenons au chemin du noeud. Ce chemin est un chemin comme nous en avons l'habitude, mais dont le dernier élément, qui aurait du être vide puisqu'un noeud texte n'a pas de nom, est text(). Cela met donc bien en évidence que les noeuds que l'on croit contenir le texte, ont en fait un noeud fils, et c'est ce noeud fils qui contient réèllement le texte. Cependant, si vous voulez éviter d'afficher text(), il vous suffit de récupérer xmlGetNodePath(node->parent)au lieu de xmlGetNodePath(node) comme je l'ai fait. Voici le résultat:

/racine/texte/text() -> 'blabla'
/racine/t2/text() -> 'ca'
/racine/t3/text() -> 'va'
/racine/t4/text() -> '<bien?>'

Si vous avez compris cet exemple, essayez-le avec le fichier permissions.xml. Vous devriez avoir des surprises. En effet, ici, avec le document XML ligne 36, nous avions un cas favorable: aucun noeud texte (ou CDATA) ne contenait de noeuds fils. Avec le document XML contenu dans permissions.xml, cela n'est pas si simple. En effet, le noeud <autorizations> contient un fils de type texte, que vous n'avez peut-être pas remarqué. Vous ne l'avez probablement pas remarqué parce son contenu n'est rien d'autre que les espaces vides qui servent à l'indentation. Cependant, nous avons affaire à un véritable noeud, de type texte, et qui contient des fils! Ces fils, ce sont dans le cas de permissions.xml, tous les autres noeuds ! C'est pour cela que quasi rien ne s'affiche avec l'exemple précédent. Pour corriger cela, faites un copier/coller des lignes 13 à 18 pour les insérer avant la ligne 28. N'oubliez pas non plus la déclaration ligne 12 que vous placez par exemple après la ligne 24. Voici avant la modification:

/authorizations/text()[1] -> '
        '

Et après:

/authorizations/text()[1] -> '
        '
/authorizations/group[1]/text()[1] -> '
                '
/authorizations/group[1]/user[1]/text() -> 'bob'
/authorizations/group[1]/user[2]/text() -> 'rebecca'
/authorizations/group[1]/user[3]/text() -> 'bic'
/authorizations/group[1]/user[4]/text() -> 'bac'
/authorizations/group[2]/text()[1] -> '
                '
/authorizations/group[2]/user[1]/text() -> 'arkana'
/authorizations/group[2]/user[2]/text() -> 'spartakus'
/authorizations/group[3]/text()[1] -> '
                '
/authorizations/group[3]/user/text() -> 'shagshag'
/authorizations/action[1]/text()[1] -> '
                '
/authorizations/action[2]/text()[1] -> '
                '

Par ailleurs, j'en ai déjà un peu parlé, de ces noeuds CDATA, mais je vais vous en montrer l'utilité si vous ne l'avez pas dejè vue et que vous n'êtes pas un gourou du XML. Question: comment mettre un texte "<balise>", ou même "a<b" dans un document XML ? Les symboles < et > nous gènent. Il suffit pour cela de les mettre dans une section CDATA: tous les caractères sont autorisés dans une section CDATA, sauf la séquence ]]< qui sert à finir la section. C'est là l'intérêt d'une section CDATA, et vous devez toujours la prendre en compte quand vous prenez en compte les noeuds texte.

Enfin, pour rechercher un noeud, il y a beaucoup plus simple que le parcours de l'arbre: cela ôte encore des raisons de parcourir un arbre DOM. Nous allons voir la manière de faire plus loin.

5.3. La construction d'un arbre DOM

Construire un arbre DOM se fait en deux étapes comme pour toute structure de données. La première est de le remplir, et la seconde est d'initialiser l'arbre. Bizarrement, on commence toujours par la seconde étape, aussi, nous traiterons la première dans les paragraphes suivants.

Pour initialiser un arbre DOM, vous pouvez évidemment partir d'un arbre existant et vide ainsi:

1 const char doc[] = "<?xml version='1.0'?><racine/>";
2 xmldoc = xmlParseMemory (doc, sizeof (doc));

L'inconvénient de cette méthode est que l'on démarre en lançant le parseur, ce qui n'est peut-être pas utile.

La méthode la plus logique est d'utiliser la fonction appropriée: xmlNewDoc(). Cette fonction prend en argument le numéro de version XML que nous allons utiliser, à savoir "1.0" puisque jusqu'à nouvel ordre, il n'y a que cette version. Cela donne:

1 xmlDocPtr doc;
2 doc = xmlNewDoc ("1.0");

Contrairement à l'autre méthode, nous avons un document qui n'a pas encore de noeud, pas même la racine. Mais ajouter la racine ou ajouter des noeuds, cela revient au même, et nous voyons cela bientôt

6. Sauvegarder ou afficher un arbre DOM via un document XML

Avant de passer à la modification d'un arbre DOM, il est nécessaire de savoir afficher ou sauvegarder l'arbre pour vérifier que l'on ne fait pas de bétises, et par la suite, parce qu'il faudra bien le transférer ailleurs, dans un fichier, à l'écran, ou sur le réseau!

Le plus simple pour afficher l'arbre DOM est d'utiliser xmlDocDump() qui prend en premier argument un flux de type FILE*, donc stdout pour la sortie écran, et en second argument, notre arbre de type xmlDocPtr. Mais on peut avoir une sortie formatée avec xmlDocFormatDump() qui prend les mêmes arguments, plus un troisième supplémentaire, de type int, qui est à 1 pour dire que l'on veut une sortie formatée, et à zéro sinon. Cela donne:

1 const char doc[] =
2   "<?xml version='1.0'?><racine><texte>blabla</texte><t2>ca</t2><t3>va</t3><t4><![CDATA[<bien?>]]></t4></racine>";
3 xmlDocPtr xmldoc = xmlParseMemory (doc, sizeof (doc));
4 xmlDocFormatDump (stdout, xmldoc, 1);

Et le résultat est:

<?xml version="1.0"?>
<racine>
  <texte>blabla</texte>
  <t2>ca</t2>
  <t3>va</t3>
  <t4>
<![CDATA[<bien?>]]>
  </t4>
</racine>

Bien évidemment, vous pouvez ouvrir un fichier avec fopen() et utiliser le flux vers le fichier ainsi ouvert avec xmlDocDump() qui a été aussi fait pour cela. Cependant, s'il s'agit de sauvegarder dans un fichier, on peut se passer de fopen() et fclose() en utilisant xmlSaveFile() ou xmlSaveFormatFile(). Ces deux fonctions prennent en premier argument un bon vieux const char* et en second argument le document XML de type xmlDocPtr, ce à quoi on pouvait s'attendre. Et xmlSaveFormatFile() prend un troisième argument, un entier, 1 pour une sortie formatée et 0 sinon, ce qui était aussi prévisible.

Il existe encore deux fonctions intéressantes, qui permettent de générer une chaîne de caractère en mémoire plutôt que d'en sortir son contenu vers un flux. Ces fonctions sont xmlDocDumpMemory() et xmlDocDumpFormatMemory(). Ces deux fonctions prennent en premier argument le document XML. Les arguements 2 et 3 sont un pointeur vers une chaîne de caractères et un pointeur vers un entier, car c'est ainsi que ces deux fonctions renvoient le résultat. Vous n'avez donc juste qu'à avoir initialisé un xmlChar* et un int dont vous allez fournir l'adresse. Enfin, xmlDocDumpFormatMemory() prend un quatrième argument, 1 pour une sortie formatée... Vous devez par contre ne pas oublier de libérer la mémoire que ces deux fonctions allouent pour la chaîne de caractères! Cela donne:

 1 xmlDocPtr xmldoc = NULL;
 2 const char doc[] =
 3   "<?xml version='1.0'?><racine><texte>blabla</texte><t2>ca</t2><t3>va</t3><t4><![CDATA[<bien?>]]></t4></racine>";
 4 xmlChar *str;
 5 int size;
 6 
 7 xmldoc = xmlParseMemory (doc, sizeof (doc));
 8 xmlDocDumpFormatMemory (xmldoc, &str, &size, 1);
 9 printf ("%s\n", str);
10 xmlFree (str);
11 xmlFreeDoc (xmldoc);

7. Modifier un arbre DOM

7.1. Ajouter un noeud

Pour ajouter un noeud à un arbre DOM, il y a plusieurs techniques. La plus compliquée mais la plus puissante consiste à créer un noeud avec xmlNewNode(), qui prend en premier argument l'espace de nom, de type xmlNsPtr, et en second argument, le nom du noeud, de type xmlChar. Le résultat est un nouveau noeud de type xmlNodePtr. Puis il faut rattacher ce noeud à l'arbre, et c'est là la puissance de l'opération: il existe plusieurs fonctions pour rattacher le noeud à l'arbre: ces fonctions prennent un noeud de référence en premier argument, et le noeud que l'on veut rajouter en second argument.

  • xmlAddChild(): ajoute le noeud en tant que fils au noeud de référence

  • xmlAddSibling(): ajoute le noeud en tant que frère du noeud de référence

  • xmlAddPrevSibling(): ajoute le noeud en tant que frère précédent du noeud de référence

  • xmlAddSibling(): ajoute le noeud en tant que frère suivant du noeud de référence

  • xmlReplaceNode(): remplace le noeud de référence par le noeud que l'on veut rajouter

Dans tous les cas, si le noeud existait déjà, il est a préalable retiré de l'endroit où il était, ce qui permet de déplacer un noeud.

Si la méthode précédente est puissante, elle nécessite deux étapes, création puis rattachement à l'arbre. On peut faire cela en une étape quand il s'agit d'un fils, avec xmlNewTextChild() qui prend le noeud de référence en premier argument, l'espace de noms en second argument, le nom en troisième argument et le contenu en dernier argument. Le premier et le troisième sont obligatoires, les deux autres peuvent être NULL. Et cela ajoute un noeud fils au noeud spécifié en premier argument.

7.2. Supprimer un noeud

Pour détacher un noeud d'un arbre, on utilise xmlUnlinkNode() qui prend le noeud en premier et unique argument. Cela détache le noeud, mais ne libère pas la mémoire pour autant. Il faut pour cela utiliser xmlFreeNode() si on veut libérer la mémoire.

7.3. Modifier le contenu d'un noeud

Modifier le contenu d'un noeud pourrait se faire de manière brutale en récupérant le pointeur sur le contenu du noeud. Cependant, il est préférable de considérer l'arbre et ses noeuds comme une structure de données opaque, et utiliser les fonctions de l'API de libxml2 pour modifier le contenu d'un noeud. Nous avons xmlNodeSetContent() et xmlNodeSetContentLen() qui remplacent le contenu actuel par un nouveau contenu. En premier argument, vous indiquez le noeud de type xmlNodePtr (on commence à avoir l'habitude de ce type, ne trouvez-vous pas ? ), et en second argument la chaîne de caractères du contenu, de type xmlChar*. xmlNodeSetContentLen() porte un nom un peu bizarre car on s'attend à modifier la longueur du contenu. En fait, non. Cela vient simplement qu'elle a un argument supplémentaire par rapport à l'autre: c'est un entier spécifiant la longueur du contenu à prendre en compte.

Si vous voulez concaténer une chaîne de caractères au contenu existant plutôt que de le remplacer, vous pouvez utiliser les deux fonctions suivantes qui ont la même syntaxe que les deux précédentes: xmlNodeAddContent() et xmlNodeAddContentLen().

7.4. Ajouter/supprimer des attributs

Outre le contenu d'un noeud que vous pouvez modifier, il y a les attributs. Pour ajouter un attribut, utilisez xmlSetProp() qui prend en premier argument, comme toujours jusqu'à maintenant, le noeud auquel vous voulez rajouter un attribut. Les deuxième et troisième arguments, de type xmlChar* sont le nom de l'attribut et la valeur de l'attribut.

7.5. Un exemple

Comment ajouter un utilisateur à un groupe, dans le cas de notre exemple de permissions ? Voici un petit bout de code...

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <errno.h>
  5 #include <sys/types.h>
  6 #include <sys/stat.h>
  7 #include <unistd.h>
  8 #include <libxml/tree.h>
  9 #include <libxml/xpath.h>
 10 
 11 void
 12 show_help (const char *reason)
 13 {
 14   if (reason)
 15     fprintf (stderr, "Error: %s\n\n", reason);
 16   fprintf (stderr, "check_perms --cf=<permission config file>i\n"
 17 	   "            --user=<user name>\n"
 18 	   "            --group=<group>\n");
 19   if (reason)
 20     exit (EXIT_FAILURE);
 21   exit (EXIT_SUCCESS);
 22 }
 23 
 24 int
 25 is_file (const char *filename)
 26 {
 27   struct stat buf;
 28   if (stat (filename, &buf) == -1)
 29     return (0);
 30   if (S_ISREG (buf.st_mode))
 31     return (1);
 32   return (0);
 33 }
 34 
 35 void
 36 add_user_to_group (const char *filename, const char *user, const char *group)
 37 {
 38   xmlDocPtr xmlperms_doc = NULL;
 39   xmlNodePtr tree, node, node_user;
 40   int group_already_exists = 0;
 41 
 42   if (!is_file (filename))
 43     {
 44       fprintf (stderr, "%s:%d File not found\n", __FILE__, __LINE__);
 45       return;
 46     }
 47   xmlperms_doc = xmlParseFile (filename);
 48   if (!xmlperms_doc)
 49     {
 50       fprintf (stderr, "%s:%d Internal error\n", __FILE__, __LINE__);
 51       return;
 52     }
 53   tree = xmlperms_doc->children;
 54   tree = tree->children;
 55   for (node = tree; node; node = node->next)
 56     {
 57       if ((node->type == XML_ELEMENT_NODE) && (!strcmp (node->name, "group")))
 58 	{
 59 	  char *attr_name;
 60 	  if ((attr_name = xmlGetProp (node, "name")))
 61 	    {
 62 	      if (!strcmp (attr_name, group))
 63 		{
 64 		  int name_already_exists = 0;
 65 		  group_already_exists = 1;
 66 		  for (node_user = node->children; node_user;
 67 		       node_user = node_user->next)
 68 		    {
 69 		      if (!strcmp (node_user->name, "user"))
 70 			{
 71 			  char *text;
 72 			  text = xmlNodeGetContent (node_user);
 73 			  if (!strcmp (text, user))
 74 			    name_already_exists = 1;
 75 			  printf ("'%s'\n", text);
 76 			  xmlFree (text);
 77 			}
 78 		    }
 79 		  xmlFree (attr_name);
 80 		  if (!name_already_exists)
 81 		    xmlNewTextChild (node, NULL, "user", user);
 82 		}
 83 	    }
 84 	}
 85     }
 86   if (!group_already_exists)
 87     {
 88       node = xmlNewTextChild (tree->parent, NULL, "group", NULL);
 89       xmlSetProp (node, "name", group);
 90       xmlNewTextChild (node, NULL, "user", user);
 91     }
 92   xmlDocFormatDump (stdout, xmlperms_doc, 1);
 93   xmlFreeDoc (xmlperms_doc);
 94 }
 95 
 96 int
 97 main (int argc, char *argv[])
 98 {
 99   int i;
100   char *filename = NULL;
101   char *user = NULL;
102   char *group = NULL;
103 
104   for (i = 0; i < argc; i++)
105     {
106       if (!strncmp (argv[i], "--cf=", sizeof ("--cf")))
107 	filename = &(argv[i][sizeof ("--cf")]);
108       else if (!strncmp (argv[i], "--user=", sizeof ("--user")))
109 	user = &(argv[i][sizeof ("--user")]);
110       else if (!strncmp (argv[i], "--group=", sizeof ("--group")))
111 	group = &(argv[i][sizeof ("--group")]);
112     }
113   if (!filename)
114     show_help ("filename is missing");
115   if (!user)
116     show_help ("user is missing");
117   if (!group)
118     show_help ("group is missing");
119 
120   add_user_to_group (filename, user, group);
121   exit (EXIT_SUCCESS);
122 }

La partie intéressante de ce bout de code est manifestement la fonction add_user_to_group() dont vous devez facilement décrypter le début, lignes 35 à 53. Lignes 54, suivie de la boucle, consiste à parcourir les noeuds fils de la racine à la recherche du groupe spécifié en argument. Ligne 60, on récupère son attribut name, pour tester ligne 62 sa valeur. Si c'est bon, lignes 66 à 78 on parcours tous les noeuds fils du groupe, à la recherche de l'utilisateur, pour vérifier qu'il n'est pas déjà dans le groupe. Notez que la ligne 75 est une ligne pour le débogage et n'a rien à faire dans un code final.

Si l'on n'a pas trouvé l'utilisateur dans le groupe (ligne 80), nous l'ajoutons ligne 81. Et si en parcourant tous les groupes on n'a pas trouvé le groupe (ligne 86), alors ligne 88 nous ajoutons un nouveau noeud pour créer un groupe, puis nous lui spécifions son attribut name ligne 89, et ligne 90, nous ajoutons l'utilisateur au nouveau groupe.

Ligne 92, nous affichons le document XML correspondant au nouvel arbre DOM, ce qui peut être considéré comme une ligne pour le débogage si le but est de travailler avec l'arbre par la suite, ou comme la manière d'afficher le résultat si l'on veut rediriger la sortie vers un fichier.

Le reste du code se passe de commentaire, mais me permet de vous montrer comment on parse facilement les arguments fournis en lançant un programme.

8. DOM et XPath

8.1. Rechercher un noeud avec XPath

Un des intérêts grands intérêts de libxml2 et de son implémentation de DOM est que libxml2 implémente aussi des fonctions tirant profit de XPath. Par conséquent, plutôt que de parcourir l'arbre comme nous l'avons vu jusqu'à maintenant, il suffit de construire un chemin XPath, de lancer libxml2 dessus, et on récupère le noeud recherché.

Pour effectuer cela, il y a plusieurs étapes à respecter:

  • initialisation d'un contexte;

  • évaluation de la requète XPath;

  • récupération du résultat.

Avant d'initialiser le contexte, initialisons l'environnement XPath avec xmlXPathInit() qui ne prend pas d'arguments et ne renvoie rien. L'initialisation d'un contexte se fait facilement avec xmlXPathNewContext(), qui prend en argument l'arbre DOM (une variable de type xmlNodePtr, mais je me répète...), et qui renvoie une variable de type xmlXPathContextPtr.

Cette variable contexte va nous servir à effectuer les requêtes XPath, avec xmlXPathEval(), qui prend le chemin XPath en premier argument, de type xmlChar*, et qui prend en second argument le contexte que nous avons initialisé ci-dessus. La valeur renvoyée, de type xmlXPathObjectPtr, contient le résultat.

Pour récupérer ce que nous voulons dans le résultat, il faut taper dans la structure de données xmlXPathObject, que voici:

 1 struct xmlXPathObject
 2 {
 3   xmlXPathObjectType type;
 4   xmlNodeSetPtr nodesetval;
 5   int boolval;
 6   double floatval;
 7   xmlChar *stringval;
 8   void *user;
 9   int index;
10   void *user2;
11   int index2;
12 };

Le type conditionne ce que nous allons lire. Voici les différents types possibles:

 1 typedef enum
 2 {
 3   XPATH_UNDEFINED = 0,
 4   XPATH_NODESET = 1,
 5   XPATH_BOOLEAN = 2,
 6   XPATH_NUMBER = 3,
 7   XPATH_STRING = 4,
 8   XPATH_POINT = 5,
 9   XPATH_RANGE = 6,
10   XPATH_LOCATIONSET = 7,
11   XPATH_USERS = 8,
12   XPATH_XSLT_TREE = 9
13 } xmlXPathObjectType;

Par exemple, si nous avons un type XPathNumber, la valeur que nous allons lire sera dans l'élément floatval du résultat. Mais quand on récupère des noeuds, ce qui va nous arriver un peu plus loin, le type est XPATH_NODESET, et nous utilisons la structure nodesetval, dont le type xmlNodeSetPtr définit un pointeur sur ceci:

1 struct xmlNodeSet
2 {
3   int nodeNr;			/* nombre de noeuds dans le jeu de noeuds */
4   int nodeMax;			/* taille mémoire du tableau */
5   xmlNodePtr *nodeTab;		/* tableau de noeuds (non ordonné) */
6 };

Si le type est XPATH_NODESET, nous avons donc juste à tester que nodesetval est non nul (on ne sait jamais), qu'il contient des noeuds (nodeNr strictement positif), et si tout va bien, nous avons les noeuds les uns à la suite des autres dans le tableau nodeTab.

8.2. Exemple: tester les permissions

Dans notre exemple de permissions, voici une nouvelle fonction qui renvoie 1 si l'utilisateur est autorisé à effectuer une action donnée sur un objet donné. Voici la fonction:

 1 int
 2 is_user_authorized (const char *filename, const char *user,
 3 		    const char *action, const char *object)
 4 {
 5   xmlDocPtr xmlperms_doc = NULL;
 6   xmlXPathContextPtr xmlperms_context = NULL;
 7   xmlXPathObjectPtr xmlobject;
 8   const char path_template[] =
 9     "/authorizations/group[@name=/authorizations/action[@name='%s' and @object='%s']/authorization/@grantedto]/user/text()[.='%s']";
10   char *path;
11   int authorization = 0;
12 
13   if (!is_file (filename))
14     {
15       fprintf (stderr, "%s:%d File not found\n", __FILE__, __LINE__);
16       return (0);
17     }
18   xmlperms_doc = xmlParseFile (filename);
19   if (!xmlperms_doc)
20     {
21       fprintf (stderr, "%s:%d Could not parse the document\n", __FILE__,
22 	       __LINE__);
23       return (0);
24     }

Jusqu'ici, nous sommes en terrain connu, donc rien de nouveau sous le soleil. Lignes suivantes, nous initialisons l'environnement XPath, puis nous initialisons le contexte XPath.

25   xmlXPathInit ();
26   xmlperms_context = xmlXPathNewContext (xmlperms_doc);
27   path =
28     malloc ((sizeof (path_template) + strlen (user) + strlen (action) +
29 	     strlen (object)) * sizeof (char));
30   if (!path)
31     {
32       fprintf (stderr, "%s:%d Not enough memory\n", __FILE__, __LINE__);
33       return (0);
34     }
35   sprintf (path, path_template, action, object, user);

Lignes 27 à 29, nous avons créé la chaîne de caractères de la requête XPath, dont le modèle se trouve ligne 9. Pour mieux comprendre cette requête, veuillez vous référer à l'article sur XPath dans ce même numéro. Nous allons proposer cette requête à la fonction xmlXPathEval ci-dessous.

36   xmlobject = xmlXPathEval (path, xmlperms_context);
37   free (path);
38 

A partir de maintenant, nous récupérons le résultat. Le code suivant est incomplet: il ne teste que le cas des jeux de noeuds. D'un autre côté, d'un point de vue fonctionnel, si nous trouvons un autre type de données, cela est bizarre et ne donne pas pour autant les permissions voulues par l'utilisateur.

39   if ((xmlobject->type == XPATH_NODESET) && (xmlobject->nodesetval))
40     {
41       if (xmlobject->nodesetval->nodeNr)
42 	{

Ci-dessous, l'exemple est encore une fois un peu incomplet. En effet, si d'un point de vue toujours fonctionnel il suffit que l'on trouve un noeud pour que l'utilisateur aie la permission d'effectuer son action sur son objet, cela ne se passe pas toujours de même. Souvent, il faudra effectuer une boucle sur tous les noeuds, et ne pas prendre seulement le premier noeud comme cela est fait ligne 44.

43 	  xmlNodePtr node;
44 	  node = xmlobject->nodesetval->nodeTab[0];
45 	  if ((node->type == XML_TEXT_NODE) ||
46 	      (node->type == XML_CDATA_SECTION_NODE))
47 	    {
48 	      authorization = 1;
49 	    }
50 	}
51     }

Je n'en ai pas parlé plus haut, mais il est évident qu'après avoir joué avec XPath, il faut faire un peu de nettoyage. Cela est fait ci-dessous avec xmlXPathFreeObject() et xmlXPathFreeContext().

52   xmlXPathFreeObject (xmlobject);
53   xmlXPathFreeContext (xml_perms_context);
54   return (authorization);
55 }

Je vous laisse en exercice la réalisation d'une fonction qui rajoute un noeud <action> car cela n'a pas été traité. Mais avec tout ce qui précède, vous n'aurez aucune difficulté à faire cela.

9. Les fichiers de configuration au format XML

Un fichier de configuration est un exemple assez trivial d'utilisation du XML. Un fichier de configuration consiste quasi toujours en un jeu de paires clef/valeur. Les fichiers de configuration un peu élaborés utilisent des systèmes de blocs, chaque bloc portant un nom. Ces blocs peuvent même parfois être imbriqués. Cela définit, tout comme en XML, une structure arborescente, où la clef est constituée des différents noms de blocs suivi du nom de la clef elle-même.

Un fichier de configuration court mais générique peut donc être de ce style:

 1 <?xml version="1.0" ?>
 2 <configuration>
 3     <clef1>valeur1</clef1>
 4     <chemin1>
 5         <chemin2>
 6             <clef2>valeur2></clef2>
 7             <clef3>valeur3</clef3>
 8         </chemin2>
 9     </chemin1>
10 </configuration>

La lecture d'un tel fichier se fait exactement de la même manière que le test des permissions ci-dessus. La requête XPath pour récupérer valeur2 est "/configuration/chemin1/chemin2/clef2/text()". Reprenez le code ci-dessus, et simplement, pour récupérer la valeur, revoyez ce qui précède la ligne 36 pour la consitution du chemin XPath. Puis remplacez la ligne 48 par valeur = node->content;, ou mieux, valeur = strdup(node->content); parce que la valeur a des chances d'être déplacée dans l'arbre ou nettoyée par la suite. Renvoyez la valeur, et le tour est joué.

Pour modifier un fichier de configuration, cela se fait exactement comme les modifications du fichier des permissions. Sa sauvegarde se fait aussi exactement pareil avec xmlDocFormatDump() par exemple.

10. Conclusion

Un article sur libxml2 associé au dossier sur la création d'un site web à base de XSLT n'est pas un hasard: XML est derrière de nombreux documents. Nous avons vu dans cet article deux cas pratiques. Le cas des fichiers de configuration est un cas très courant, et je vous invite à utiliser le XML ainsi que libxml2 pour vos propres fichiers de configuration. Cela vous évitera d'écrire votre propre parseur en risquant d'inclure des bugs. Le cas du fichier de configuration est tellement simple que j'ai préféré m'attarder sur un exemple un peu plus compliqué, celui de la gestion des permissions d'accès à des objets, avec ses balises, noeuds textes, attributs... et sa requête XPath un peu plus compliquée. Mais ne vous y trompez pas: un peu de pratique et cette bibliothèque tentaculaire vous apparaîtra simple. Simple parce qu'elle est composée de tentacules, chacune indépendantes, et il n'est nul besoin de tout connaître pour utiliser une seule tentacule. Nous avons vu deux tentacules dans cet article: les arbres DOM et XPath, plus un petit bout de tentacule: le parseur. Pour votre information, la tentacule du parseur contient un certain nombre de fonctions qui permettent de mieux travailler avec les xmlChar que nous ne l'avons fait tout au long de cet article. La prochaine tentacule que nous verrons a de fortes chances d'être l'interface SAX.

11. Références

  • libxml2: http://www.xmlsoft.org

  • L'API de libxml2: http://xmlsoft.org/html/libxml-lib.html

  • La tentacule du parseur: libxml-parser.html dans la documentation de l'API

  • La tentacule de l'arbre DOM: libxml-tree.html dans la documentation de l'API

  • La tentacule de XPath: libxml-xpath.html dans la documentation de l'API

création est mise à disposition sous un contrat Creative Commons