Une introduction au couple XML et Scala
Avant propos
À l'heure de YAML et
de JSON, il y a encore
des gens qui croient au typage fort des documents (non, cet
article n'est pas un troll). J'ai ramé un petit moment avant de
pouvoir manipuler du XML avec Scala sans avoir d'un côté la scaladoc
et stackoverflow
dans le brouteur, et de l'autre le code source de Scala pour
combler les manques de la scaladoc. Bien que l'API soit
sympathique, il est parfois difficile de tout comprendre et
d'avoir une vue d'ensemble du package scala.xml
, la
scaladoc n'etant pas le meilleur ami du néophite (oui tu sais « à
cause » de l'héritage très libre, des fois la scaladoc d'une
classe ça ressemble a une méga partouse entre class,
trait, et object).
Si tes yeux se mettent à pisser le sang en lisant cet article, pour une quelconque raison, je te serais extrêmement reconnaissant de bien vouloir me signaler mes erreurs.
Les exemples sont écrits dans la console Scala. J'utilise la version 2.9.1 du langage.
Écrire du XML
Mise en jambe
Avant de te présenter tout le reste, regarde cet exemple :
Oui, on peut écrire directement
du XML, y incorporer du
code scala commenté, et obtenir un objet Node
en une seule ligne. Pas de concaténation de chaînes, pas de
dépendances à une librairie de template, tout est déjà là. De plus
cela ne compile pas si c'est mal formé (marqueur ouvrant et
fermant requis, deux fois le même attribut sur un marqueur
interdit…).
Outre des balises et des attributs, on peut utiliser les autres fonctionalités d’XML :
Les processing instructions
Les commentaires
Les characters et entity refs
CDATA
On ne peut pas utiliser les sectionsCDATA
directement:
Pfiou! Disparu le CDATA
et remplacés nos chevrons! Vraiment contre-intuitif ça…
On peut aisément palier cette disparition en instanciant la
classe PCData
:
L’autre solution que j'ai trouvée là :
Le soucis, c'est qu’on passe par une String
et que
le compilateur ne nous avertira pas si c’est malformé.
Des « { » et des « } » à condition de les doubler
Passer du XML en paramètre de méthodes
Attention à bien mettre un espace avant le chevron <
,
sinon vous passerez comme moi pour un boulet (http://stackoverflow.com/questions/7553283/writing-xml-literal-as-a-parameter-in-scala):
Conservation du formatage
Quand on écrit du XML, scala conserve le formatage :
Scala crée des nœuds pour chaque chaîne de séparateurs ( espace, tabulation, retour chariot, … )
Ce qui signifie que <a>
<b> </b> </a>
n’est pas égal à <a><b></b></a>
:
Il est possible de supprimer tous
les blancs avec la méthode trim
:
Pense à utiliser trim avant de comparer deux arbres :
Afficher du XML
Cet exemple nous crache une belle représentation d'un nœud XML qui tient sur une ligne de 80 colonnes et dont l'indentation est de 10 colonnes:
Le package scala.xml
scala.xml
contient une floppée d'objets et de
classes qui représentent le « domaine métier » d'XML. Tous les
composants d'un document XML ont un pendant sous forme d’une
classe dans ce package.
Dans le SDK de Java, on manipule les objets métier definis dans
le package org.w3c.dom
. Scala définit son propre modèle métier.
Par conséquent, un Node
scala n'est pas
interopérable avec un Node
java
Un dessin avec des boîtes et des flêches
Tout d'abord un petit dessin avec du faux UML.
Pour la petite histoire:
- je représente les trait par
un stéréotype de classe baptise <<trait>>
- je représente le lien entre
un companion et une classe par une dependency avec un stereotype
baptise <<companion>>
- le companion lui-même est
représenté comme une instance de classe. Je dois avouer que j'ai
beaucoup hésité.
Si tu aurais fait autrement, je
suis preneur de ta solution bien entendu.
NodeSeq
Le papa de tout le monde, c'est NodeSeq
. NodeSeq
étendant Seq[Node]
, on peut dors et déjà affimer que
n'importe quel objet qu’on manipule est considéré comme une
collection (« je suis une bande de jeunes à moi tout seul »).
Il implémente deux opérateurs XPath \
et \\
dont je te montrerai l'utilité plus tard
Une propriété intéressante de NodeSeq
est text:String
qui renvoie la valeur de l’élément :
Cela fonctionne aussi pour d’autres types de nœuds XML, par exemple les attributs :
Node
Node est une classe abstraite qui étend NodeSeq
.
Cela signifie que n'importe quel méthode acceptant Seq[Node]
acceptera aussi un objet type Node
(en revanche
l'inverse n'est pas vrai, évidemment). Cela est valable aussi pour
les types de retour. Cette propriété est importante lorsqu'on
utilise l'API XML de scala.
Node
définit les propriétés de base d'un nœud XML:
— label : String
est abstrait. Pour une balise, cela correspond au nom de cette balise.
— attributes: Metadata
contient la liste des
attributs
— child : Node *
un certain nombre d’enfants
— prefix : String
tout le monde sait ce qu'est un prefix.
— scope : NamespaceBinding
(TODO traiter du scope plus tard)
— Une méthode intéressante est attribute(key:String):Metadata
qui renvoie l'attribut dont le nom est passé en paramètre.
L’autre indispensable est toString(): String
qui renvoit la représentation XML de l’élément. Sous le capot de cette méthode se cache un appel à Utility.toXML
que tu peux utiliser indépendamment.
Elem
Elem
est une class concrête qui représente un noœud XML. Toutes
les classes héritant de Elem
ont une méthode indispensable pour
copier un élément :
copy (prefix: String = this.prefix, label: String =
this.label, attributes: MetaData = this.attributes, scope:
NamespaceBinding = this.scope, child: Seq[Node] =
this.child.toSeq): Elem
Si on l'invoque sans argument, elle effectue une copie exacte de l'objet d'origine, exceptés pour les enfants où il s’agit d’une copie par référence.
Mais on peut aussi l'invoquer en spécifiant le nom du paramètre à
modifier. Cet exemple montre comment supprimer tous les enfants
d'un Node
:
Celui ci-permet de changer le nom de la balise :
Elem
a aussi un companion object qui définit:
— une méthode apply
pour créer une instance de
Elem
.
On peut également spécifier un nombre arbitraire d'enfants:
— une méthode unapply
(extracteur) pour pouvoir
matcher un élément par ses propriétés (j’explique les matchers plus loin).
Bref Elem
, c'est la classe incontournable de ce
package.
Nœuds spéciaux
Ensuite viennent les noeuds spéciaux regroupés sous la classe
abstraite SpecialNode
. Elle regroupe tous les types
sans enfants, sans attributs et sans namespace.
Atom
On trouve la classe Atom
, qui est une classe
paramètrée. Atom
représente des classes qui ont
juste une valeur. Elle a 3 sous classes: Text
, PCData
et Unparsed
. Ces trois classes ont en commun, entre
autre, l'attribut label qui vaut "#PCDATA"
.
Text
Text
est utilisé pour représenter du texte simple.
Par exemple:
Mais aussi comme valeur d'attibuts:
Et comme élément de formatage (voir ici)
Il n'y a aucune différence entre Atom[String]
et Text
.
Texte verbatim
Les classes PCData
et Unparsed
servent a écrire du texte verbatim (non interprété). La différence
entre les deux est que PCData
encapsule la valeur d'un <![CDATA[ ]]>
Autres valeurs
Commentaires, Entity Reference, Processing Instructions
SpecialNode
a trois autres sous-classes : Comment
sert a écrire du commentaire XML (exemple),
EntityRef
(exemple) et ProcInstr (exemple) pour les processing instruction.
Group
La classe Group
étend Node
et sert à grouper des éléments dans un conteneur sans consistance (je sais…, wtf, tout ça…).
Petite explication sur la classe Group
issue de la Scaladoc: «A hack to
group XML nodes in one node for output». Limpide!
En fait cela prend du sens quand on essaye d’afficher les enfants d’un élément sans vouloir afficher cet élément. Cela mérite bien un exemple :
Attributs
Metadata
Metadata
est une classe abstraite héritant d'Iterator[Metadata]
.
Sémantiquement, elle représente a la fois un attribut et une suite
d'attributs (tout comme NodeSeq
, elle a le syndrome
« je suis une bande de jeunes a moi tout seul ». Elle déclare 3
propriétés fondamentales: – key:String
est le nom de l'attribut.
– value:Seq[Node]
représente la valeur de
l'attribut. A première vue, cela semble étrange que la valeur d'un
attribut soit une séquence. J'aurais plutôt utiliser Atom
.
Ce lien (http://stackoverflow.com/questions/4622218/scala-xml-api-why-allow-nodeseq-as-attribute-values)
ou la spec de l'auteur (https://sites.google.com/site/burakemir/scalaxbook.docbk.html?attredirects=0) nous dit que cela sert à écrire des trucs comme ça:
Puisque Node
hérite de Seq[Node]
on
peut également utiliser une valeur atomique comme valeur d’attribut :
Tu remarques comme moi que scala a échappé l’esperluette par des Entity Refs…
Attention, scala ne sait pas relire la valeur comme une séquence
de Node
, pour lui c'est juste une instance de Text
:
– next:Metadata
permet de chaîner sur l'attribut
suivant. XML est agnostique a l'ordre des attributs. L'API l'est
aussi. La propriété next:Metadata
de chaque attribut
ne correspond pas forcément a l'attribut suivant de ton XML
source:
Les méthodes notables sont:
— append(updates: MetaData, scope: NamespaceBinding =
TopScope): MetaData
pour ajouter un attribut :
— remove(key: String): MetaData
pour en supprimer
un attribut :
— copy (next: MetaData): MetaData
qui permet de
supprimer des attributs en certaines circonstances. L'exemple
suivant garde le premier et le dernier attribut de notre
chaussette:
- get(key: String): Option[Seq[Node]]
pour
récupérer la valeur d'un attribut (n'oublie pas que la valeur d'un
attribut est un Seq[Node]
). Il existe aussi apply(key:String): Seq[Node]
, mais puisque ca peut retourner null
,
mieux vaut s'en méfier.
Pas d’attribut
La classe Null
étend Metadata
et
signifie "pas d'attributs".
Classes concrètes
— La classe Attribute
est abstraite et hérite de Metadata
.
Concrètement, cette classe ne sert a rien, sauf a partager du code
commun a ses deux sous classes UnprefixedAttribute
et PrefixedAttribute
.
— La classe UnprefixedAttribute
a un constructeur
très simple pour construire un attribut sans préfix:
next:Metadata
, ici on
a choisi Null
, on aurait pu mettre quelquechose, pour
chaîner d'autres attributs :— La classe PrefixedAttribute
sert à créer des
attributs préfixés:
Ouverture d'un flux XML
Le trait abstrait XMLLoader
est un type paramétré
(le paramètre T
doit être une sous–classe de Node
)
qui dispose de méthodes load…
permettant d'ouvrir
toute sorte de flux XML. Ces méthodes renvoient toutes un objet du
type T
.
XML
est un objet tout prêt qui implémente XMLLoader[Elem]
.
Cela permet d'ouvrir un fichier XML et d'être prêt à bosser dessus
en une simple ligne de code :
Reader
,
Source
, nom d'un fichier, String
, InputStream
,
… en une seule ligne. Customiser le parser
L'une des propriétés abstraite d'XMLLoader
est parser:SAXParser
qui définit le parser XML à utiliser pour parcourir le
flux. Par défaut l'objet XML utilise javax.xml.parsers.SAXParserFactory
pour créer le parser.
Afin d'utiliser le parser de tes rêves, il suffit d'implémenter la
seule methode abstraite d'XMLLoader
, parser
.
Voici un exemple fictif :
Désolé, je ne pouvait laisser passer ça… Plus sérieusement, cet exemple permets de créer un parser qui ne respecte pas les DTD (sic):
Cette objet s’utilise comme l’objet XML
, il suffit d’appeler les méthodes load…
pour charger un document. On pourrait aussi utiliser l'une des 10000 implémentations de
SAXParser
qui existe pour java. TagSoup par exemple.
Lecture d’un objet XML
Extraire des données avec l’API
L’extraction de données peut se faire facilement avec les
méthodes et attributs que j’ai décrit dans la
présentation du package XML. Ce document est truffé
d’exemples où je récupère le nom de la balise, les enfants, les
valeurs des attributs, …
La méthode unapply
de la classe Elem
permet aussi de récupérer des données facilement :
Les pseudos opérateurs XPATH
\
et \\
permettent de faire des
recherches dans l’arbre à la XPATH. Le plus simple est de montrer
quelques exemples :
Cet exemple renvoie la liste des enfants directs de type <c>
:
Cet exemple renvoie la liste des enfants directs de type <b>
:
Cet exemple renvoie la liste de tous les enfants de type <c>
:
Cet exemple renvoie la liste des attributs "attr"
de <c>
:
Cet exemple renvoie la liste des attributs "attr"
de la
racine :
Cet exemple renvoie la liste des attributs "attr"
de tous les
éléments :
Bon c’est sympa mais l’utilité du machin s’arrête là. Il n’est
pas possible d’appliquer des filtres sur les attributs, genre c[@attr="ac"]
par exemple. Mais puisque \
et \\
renvoient du NodeSeq
, on peut utiliser la méthode
filter
de Sequence
:
Pour plus d’exemples, va voir le site de Daniel Spiewak.
Extraire des données d’Elem
avec le matcher
Je définis une méthode qui va afficher une petite phrase selon ce qui est passé en paramètre :
On peut écrire le
pattern à matcher directement dans le case
. Entre
accolade, on définit des variables que l’on peut réutiliser après
=>
.
<sac>{dedans @ _*}</sac>
est un
Pattern Sequences (voir chapitre 8.1.9 de la Référence Scala). La variable dedans
est une séquence
qui contient tous les enfants de <sac>
.
J’applique cette méthode à un sac de nounours :
J’applique cette méthode à un sac vide :
J’applique cette méthode à un sac d’assortiments :
Si tu te demandes d’où sortent tous ces sacs vides, n’oublie pas que les espaces de formattage sont significatifs !
J’applique cette méthode à une frite :
Trucs & Astuces : l’erreur courante avec cette méthode est d’oublier que les
espaces et retours-chariots sont significatifs. Utiliser la
méthode trim
si ce
comportement n’est pas désirable.
Il est possible d’imbriquer les balises dans un case
:
Par contre on ne peut pas matcher les attributs directement, il faut
utiliser un opérateur XPATH après =>
,
dommage.
Modifier un objet XML
Afin de se conformer au standard d'immutabilité de Scala, tous
les objets du domaine XML que vous manipulez sont immutables. Donc
si on décide de modifier un élément, il faut le recopier en y
incorporant les modifications. Pour ça, il faut utiliser la
methode apply
du companion pour créer
une nouvelle instance, ou bien invoquer la methode copy
d'une instance déjà existante.
Un cas concrêt
On dispose de quelques fichiers XHTML bien formés que l'on
souhaite afficher à l'utilisateur. Le problème, c'est que les
liens href
sont erronés, il faut transformer "
http://www.gargamel.com
/pages/foo.html"
en "http://www.azrael.com/pages/foo.html"
.
L'autre soucis, c'est que ces documents ont des balises
<script>
, <link>
et <style>
dont on voudrait se
débarraser. Par exemple :
doit être transformé en :
The « Scala way »
Scala définit 3 classes sympatiques pour parcourir et modifier un arbre XML.
— BasicTransformer
est une classe marquée abstraite
qui définit 3 méthodes transform…
. Par défaut, elles
parcourent récursivement les enfants du/des nœuds passés en
paramètre en leur appliquant transform
. Elle
définit une méthode apply(n: Node): Node
qui sous
le capot appelle transform(n: Node): Seq[Node]
— RewriteRule
est aussi une classe abstraite qui
étend BasicTransformer. La différence entre RewriteRule
et BasicTransformer
est que la méthode surchargée transform(n: Node): Seq[Node]
renvoie toujours n
alors
que la méthode parente applique transform(n: Node):
Seq[Node]
récursivement sur chaque enfant. En pratique,
il faut étendre cette classe et surcharger transform(n:
Node): Seq[Node]
où on effectue les transformations sur
le nœud. Il faut également veiller à retourner une Seq[Node]
contenant un et un seul nœud si on utilise la méthode apply(n:Node): Node
.
— RuleTransformer
est aussi marquée abstraite. Le
constructeur prend en paramètre un nombre variable de RewriteRule
.
Elle surcharge transform(n: Node): Seq[Node]
et
applique sur n
chaque RewriteRule
passé en paramètre du constructeur au RewriteRule
suivant. En gros ca permet de composer les RewriteRule
et de les appliquer récursivement à tout l’arbre, y compris les
attributs et autres types de nœuds.
Pour en revenir à notre cas concrêt, voici une solution :
1) Définir un RewriteRule pour supprimer les balises enfants de script :Instanciation :
Création d’un jeu de données :
Appliquer le jeu de données au Transformer
:
2) Définir un RewriteRule
pour modifier les liens :
Instantiation :
Jeu de test :
Test :
3) Composer les deux rewriters avec un transformer
4) Passer notre XML à la moulinette
Et voilà !
Le hic du « Scala way »
Personnellement, je n'utilise JAMAIS RuleTransformer
,
parcequ'elle a un bug https://issues.scala-lang.org/browse/SI-3689.
Prenons ce simple exemple:
Tout d’abord, je crée un RewriteRule qui ajoute un attribut sur un nœud, et affiche le nœud avant et après transformation :
Ensuite je le combine avec un RuleTransformer
:
T’as pas un peu l'impression que ça mouline pour rien? Moi si. En tous cas le résultat est correct.
Quand on combine deux RewriteRule
, regarde bien c'est ignoble:
58 appels pour modifier 3
nœuds, au moins le résultat est correct…
Remède naïf
Rien ne vaut une bonne méthode manuelle, en voici une qui pousse la stack à son maximum :
« Seulement » 12 appels, mais avec par contre le gros inconvenient de bouffer plein de mémoire (À chaque appel, chaque sous-arbre est conservé dans la stack, donc en extrapolant ça prend en mémoire : taille de l'arbre d'origine * nb d'imbrication)
Conclusion
Beaucoup de gens écrivent regretter le support d’XML au sein du
langage. Moi je pense plutôt que c'est une excellente idée et que
c'est très pratique et
bien lisible. Pouvoir
composer, et lire du XML nativement et rapidement c'est plutôt
génial.
En revanche, l'API est parfois mal foutue. Je n'ai pas la
prétention de pouvoir faire mieux, mais je pense que les points
suivants sont à améliorer:
— Il est inutile que la valeur d'un attribut soit une Seq[Node]
.
Cela rajoute une boucle dans le code pour pas grand chose puisque
Scala relira la valeur de l’attribut sous la forme d’un seul
élément Text
.
— Il n’y a pas de matcher pour les attributs, récupérer la valeur
d’un attribut c’est définitivement casse-couille.
— Le package scala.xml.transform
est brouillon. Il
faudrait réécrire RuleTransformer
qui fait son
boulot vraiment n'importe comment.
— Toute les fonctionnalités d’XML ne sont pas implémentées. Par
exemple, on ne peut pas définir d’entity.
— Il n'y a pas de pointeur sur le nœud parent. Cette fontionnalité
n'est pas vraiment utile quand on parcours l'arbre du tronc vers
les branches, mais est parfois pratique pour pouvoir traiter le
resultat de requêtes XPATH.
Certains réfractaires a l'API XML de Scala ont écrit une
nouvelle API baptisée anti-xml
(ils en avaient gros sur la patate). On peut voir une chouette
présentation en vidéo
(et les diapositives)
sur le site des scaladays 2011. Il existe aussi la librairie
scalates. Mais pour être honnête, je n’ai pas encore fouiner de ce
côté.
Resources et Références
Chapitres 1.5 et 10 de la Référence Scala
http://blog.markfeeney.com/2011/03/scala-xml-gotchas.html
http://www.codecommit.com/blog/scala/working-with-scalas-xml-support
La doc d'Émir Burak, le gars qui a implémenté cette partie de l'API: https://sites.google.com/site/burakemir/scalaxbook.docbk.html?attredirects=0
Article de Michel Galbin sur l’IBM Developer Network.
Un petit dernier un peu succint a mon goût: https://wiki.scala-lang.org/display/SW/XML