Blog de Jérôme Prudent
Développeur ...

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 :

scala> val artiste="Didier super"
artiste: java.lang.String = Didier super
scala> val titre="on va tous crever"
titre: java.lang.String = on va tous crever
scala> val url = "http://www.didiersuper.com/diskonvatous.htm"
url: java.lang.String = http://www.didiersuper.com/diskonvatous.htm
scala> val duXML = <musique>
     | <chanson artiste={artiste.toUpperCase} titre={titre.split(" ").mkString("","_","")}>{/*à l'envers */url.reverse}</chanson>
     | </musique>
duXML: scala.xml.Elem =
<musique>
<chanson artiste="DIDIER SUPER" titre="on_va_tous_crever">mth.suotavnoksid/moc.repusreidid.www//:ptth</chanson>
</musique>
    

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

scala> <?name foo?>
res7: scala.xml.ProcInstr = <?name foo?>

Les commentaires

scala> <!-- dskf -->
res8: scala.xml.Comment = <!-- dskf -->

Les characters et entity refs

scala> <a>&nbsp;</a>
res10: scala.xml.Elem = <a>&nbsp;</a>
scala> res10.child.head.getClass
res12: java.lang.Class[_ <: scala.xml.Node] = class scala.xml.EntityRef
scala> <a>&#xaa;</a>
res30: scala.xml.Elem = <a>ª</a>
    

CDATA

On ne peut pas utiliser les sections CDATA directement:
scala> <div><![CDATA[starting tag <song> and end stag </song>]]></div>
res0: scala.xml.Elem = <div>starting tag &lt;song&gt; and end stag &lt;/song&gt;</div>
    

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:

scala> <div>{new PCData("starting tag <song> and end stag </song>")}</div>
res3: scala.xml.Elem = <div><![CDATA[starting tag <song> and end stag </song>]]></div>
    

L’autre solution que j'ai trouvée :

scala> val xml2 = ConstructingParser.fromSource(Source.fromString("<xml><test><![CDATA[a < b]]></test></xml>"), preserveWS = true).document.docElem
xml2: scala.xml.Node = <xml><test><![CDATA[a < b]]></test></xml>
    

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

scala> <accolade>ceci {{ est une accolade, ceux la }}}} sont deux accolades</accolade>
res31: scala.xml.Elem = <accolade>ceci { est une accolade, ceux la }} sont deux accolades</accolade>
    

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):

scala> Elem(null, "a", Null, TopScope, <b/>)
resN: scala.xml.Elem = <a><b></b></a>
    

Conservation du formatage

Quand on écrit du XML, scala conserve le formatage :

scala> <a> <b>     </b> </a>
res16: scala.xml.Elem = <a> <b>     </b> </a>
    

Scala crée des nœuds pour chaque chaîne de séparateurs ( espace, tabulation, retour chariot, … )

scala> res16.child.foreach(c=>println(c.getClass))
class scala.xml.Text
class scala.xml.Elem
class scala.xml.Text
    

Ce qui signifie que <a> <b>     </b> </a> n’est pas égal à <a><b></b></a> :

scala> <a> <b>     </b> </a>
res40: scala.xml.Elem = <a> <b>     </b> </a>

scala> <a><b></b></a>
res41: scala.xml.Elem = <a><b></b></a>

scala> <a> <b>     </b> </a>
res42: scala.xml.Elem = <a> <b>     </b> </a>

scala> res40 == res41
res43: Boolean = false

scala> res40 == res42
res44: Boolean = true
    

Il est possible de supprimer tous les blancs avec la méthode trim :

scala> scala.xml.Utility.trim(res42)
res45: scala.xml.Node = <a><b></b></a>
    

Pense à utiliser trim avant de comparer deux arbres :

scala> scala.xml.Utility.trim(res40) == scala.xml.Utility.trim(res41)
res45: Boolean = true
    

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:

scala> <j><k></k></j>
res31: scala.xml.Elem = <j><k></k></j>
scala> new PrettyPrinter(80,10).formatNodes(res31)
res32: String =
<j>
           <k></k>
</j>
    

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. 

<<trait>> Seq T:Node NodeSeq +text: String +\(that:String): NodeSeq +\\(that:String): NodeSeq Node +label: String +prefix: String = null +scope: NamespaceBinding = TopScope +child: Seq[Node] +attribute(key:String): Metadata Elem +copy(prefix,label,attributes,scope,child): Elem companion:Elem +apply(prefix,label,attributes,scope,child): Elem SpecialNode Atom T:Any Text PCData Unparsed Group EntityRef Comment ProcInstr <<companion>> <<trait>> Iterable T:Metadata Metadata +key: String +get(key:String): Option[Seq[Node]] +remove(key:String): Metadata +append(attr:Metadata): Metadata +copy(next:Metadata): Metadata next 1 1 Null attributes 1 Attribute UnprefixedAttribute PrefixedAttribute value *

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 :

scala> <i>isei</i>
res0: scala.xml.Elem = <i>isei</i>
scala> res0.text
res1: String = isei
    

Cela fonctionne aussi pour d’autres types de nœuds XML, par exemple les attributs :

scala> <i attr="ahah"/>
res3: scala.xml.Elem = <i attr="ahah"></i>
scala> res3.attributes("attr").text
res10: String = ahah
    

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:

scala> <ville><prison>prisonier</prison></ville>
res0: scala.xml.Elem = <ville><prison>prisonier</prison></ville>
scala> res0.copy(child=Nil)
res4: scala.xml.Elem = <ville></ville>
    

Celui ci-permet de changer le nom de la balise :

scala> <ville><prison>prisonier</prison></ville>
res0: scala.xml.Elem = <ville><prison>prisonier</prison></ville>
scala> res0.copy(label="pays")
res5: scala.xml.Elem = <pays><prison>prisonier</prison></pays>
    

Elem a aussi un companion object qui définit:

— une méthode apply pour créer une instance de Elem.

scala> Elem(null,"champignon",Null,TopScope)
res12: scala.xml.Elem = <champignon></champignon>
    

On peut également spécifier un nombre arbitraire d'enfants:

scala> <camarophyllopsisFetide/>
res0: scala.xml.Elem = <camarophyllopsisFetide></camarophyllopsisFetide>
scala> <amanitePhalloide/>
res1: scala.xml.Elem = <amanitePhalloide></amanitePhalloide>
scala> Elem(null,"a",Null,TopScope,res0,res1)
res3: scala.xml.Elem = <a><camarophyllopsisFetide></camarophyllopsisFetide><amanitePhalloide></amanitePhalloide></a>
    

— 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:

scala> <dutexte>{new Text("blabla")}</dutexte>
res6: scala.xml.Elem = <dutexte>blabla</dutexte>
scala> res6.child(0).getClass
res9: java.lang.Class[_ <: scala.xml.Node] = class scala.xml.Text
    

Mais aussi comme valeur d'attibuts:

scala> <chaussette couleur="rouge et jaune" />
res10: scala.xml.Elem = <chaussette couleur="rouge et jaune"></chaussette>
scala> res10.attribute("couleur")
res11: Option[Seq[scala.xml.Node]] = Some(rouge et jaune)
scala> res10.attribute("couleur").get(0).getClass
res12: java.lang.Class[_ <: scala.xml.Node] = class scala.xml.Text
    

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[ ]]>

scala> new Unparsed(",.<>&&dfs;")
res13: scala.xml.Unparsed = ,.<>&&dfs;

scala> new PCData(",.<>&&dfs;")
res14: scala.xml.PCData = <![CDATA[,.<>&&dfs;]]>
    
Autres valeurs
scala> new Atom(45)
res31: scala.xml.Atom[Int] = 45

scala> new Atom(3.4)
res32: scala.xml.Atom[Double] = 3.4
    

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 :

scala> <b>s<br/>alut</b>
res0: scala.xml.Elem = <b>s<br></br>alut</b>

scala> res0.toString
res5: String = <b>s<br></br>alut</b> // <- on ne veut pas afficher <b></b>

scala> res0.child.toString
res6: String = ArrayBuffer(s, <br></br>, alut) // <- pourri

scala> new Group(res0.child).toString
res4: String = s<br></br>alut // <- c’est ça !

scala> res0.child.mkString
res7: String = s<br></br>alut <- ça marche aussi

    

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:

scala> <foo name={List(Text("s"), EntityRef("uuml"), Text("ss"))}/>
res1: scala.xml.Elem = <foo name="s&uuml;ss"></foo>
scala> res1.attribute("name").get.foreach(v=>println(v.getClass))
class scala.xml.Text
class scala.xml.EntityRef
class scala.xml.Text
    

Puisque Node hérite de Seq[Node] on peut également utiliser une valeur atomique comme valeur d’attribut :

scala> <foo name={new Text("s&uuml;ss")}/>
res21: scala.xml.Elem = <foo name="s&amp;uuml;ss"></foo>
    

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:

scala> res21.attribute("name").foreach(v=>println(v.getClass))
class scala.xml.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:

scala> <chaussette couleur1="rouge" couleur2="jaune" motif="petit pois"/>
res5: scala.xml.Elem = <chaussette couleur1="rouge" motif="petit pois" couleur2="jaune"></chaussette>
    

Les méthodes notables sont:

append(updates: MetaData, scope: NamespaceBinding = TopScope): MetaData pour ajouter un attribut :

scala> res5.copy(attributes = res5.attributes.append(new UnprefixedAttribute("odeur","nc",Null)))
res18: scala.xml.Elem = <chaussette couleur2="jaune" motif="petit pois" couleur1="rouge" odeur="nc"></chaussette>
    

remove(key: String): MetaData pour en supprimer un attribut :

scala> res5.copy(attributes = res5.attributes.remove("odeur"))
res19: scala.xml.Elem = <chaussette couleur1="rouge" couleur2="jaune"></chaussette>
    

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:

scala> res5.attributes.head.copy(res5.attributes.last)
res30: scala.xml.MetaData = couleur1="rouge" couleur2="jaune"
    

- 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".

scala> <rien/>.attributes.getClass
res25: java.lang.Class[_ <: scala.xml.MetaData] = class scala.xml.Null$
    

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:

scala> new UnprefixedAttribute("constructeur","simplicime",Null)
res27: scala.xml.UnprefixedAttribute = constructeur="simplicime"
    
Le 3ème argument est next:Metadata, ici on a choisi Null, on aurait pu mettre quelquechose, pour chaîner d'autres attributs :
scala> new UnprefixedAttribute("constructeur","simplicime",res5.attributes.head)
res28: scala.xml.UnprefixedAttribute = constructeur="simplicime" couleur1="rouge" motif="petit pois" couleur2="jaune"
    

— La classe PrefixedAttribute sert à créer des attributs préfixés:

scala> new PrefixedAttribute("scala","constructeur","simplicime",Null)
res29: scala.xml.PrefixedAttribute = scala:constructeur="simplicime"
    

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 :

scala> XML.loadString("<ddd>hihi</ddd>")
res3: scala.xml.Elem = <ddd>hihi</ddd>
    
Il est possible de charger a peu près n'importe quoi: 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:SAXParserqui 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 :

object MyXML extends XMLLoader[Elem] {
  override def parser: SAXParser = {
    javax.xml.parsers.SchtroumpfParserFactory.newInstance.newSAXParser()
  }
}
    

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):

object MyXML extends XMLLoader[Elem] {
   override def parser: SAXParser = {
   val f = javax.xml.parsers.SAXParserFactory.newInstance()
     f.setFeature("http://xml.org/sax/features/validation", false)
     f.setFeature("http://apache.org/xml/features/disallow-doctype-decl", false)
     f.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false)
     f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
     f.newSAXParser()
   }
}
    

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 :

scala> val x = <a attr1="1" attr2="2">foo</a>
x: scala.xml.Elem = <a attr1="1" attr2="2">foo</a>
scala> x match {
     |     case Elem(prefix, label, attribute, _, children) => {
     |       println("prefix = "+prefix)
     |       println("label = "+label)
     |       println("attribute = "+attribute)
     |       println("children = "+children)
     |     }
     |   }
prefix = null
label = a
attribute =  attr1="1" attr2="2"
children = foo
    

Les pseudos opérateurs XPATH

\ et \\ permettent de faire des recherches dans l’arbre à la XPATH. Le plus simple est de montrer quelques exemples :

scala> <a>
     |     <b>
     |       <c attr="abc"/>
     |     </b>
     |     <c attr="ac"/>
     |   </a>
res57: scala.xml.Elem =
<a>
    <b>
      <c attr="abc"></c>
    </b>
    <c attr="ac"></c>
  </a>
    

Cet exemple renvoie la liste des enfants directs de type <c> :

scala> res57 \ "c"
res58: scala.xml.NodeSeq = NodeSeq(<c attr="ac"></c>)
    

Cet exemple renvoie la liste des enfants directs de type <b> :

scala> res57 \ "b"
res59: scala.xml.NodeSeq =
NodeSeq(<b>
      <c attr="abc"></c>
    </b>)
    

Cet exemple renvoie la liste de tous les enfants de type <c> :

scala> res57 \\ "c"
res60: scala.xml.NodeSeq = NodeSeq(<c attr="abc"></c>, <c attr="ac"></c>)
    

Cet exemple renvoie la liste des attributs "attr" de <c> :

scala> res57 \ "c" \ "@attr"
res61: scala.xml.NodeSeq = ac
    

Cet exemple renvoie la liste des attributs "attr" de la racine :

scala> res57 \ "@attr"
res62: scala.xml.NodeSeq = NodeSeq()
    

Cet exemple renvoie la liste des attributs "attr" de tous les éléments :

scala> res57 \\ "@attr"
res63: scala.xml.NodeSeq = NodeSeq(abc, ac)
    

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 :

scala> res57 \\ "@attr" filter(_.text == "ac")
res67: scala.xml.NodeSeq = NodeSeq(ac)
    

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 :

scala>   val cestQuoi:(Node)=>Unit =  {x=>
     |     x match {
     |       case <sac></sac> => println("Ce sac est vide")
     |       case <sac>{bonbon}</sac> => println("Ce sac ne contient que des " + bonbon)
     |       case <sac>{dedans @ _*}</sac> => {
     |         println("Ce sac contient ")
     |         dedans.foreach(n=>cestQuoi(n))
     |       }
     |       case _ => println("Ce n’est pas un sac")
     |     }
     |   }
cestQuoi: scala.xml.Node => Unit = <function1>
    

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 :

scala> val sacDeNounours = <sac>nounours</sac>
sacDeNounours: scala.xml.Elem = <sac>nounours</sac>

scala> cestQuoi(sacDeNounours)
Ce sac ne contient que des nounours
    

J’applique cette méthode à un sac vide :

scala> val sacVide = <sac/>
sacVide: scala.xml.Elem = <sac></sac>

scala> cestQuoi(sacVide)
Ce sac est vide
    

J’applique cette méthode à un sac d’assortiments :

scala> sacAssortiment: scala.xml.Elem =
<sac>
      <sac>fraises</sac>
      <sac>banane</sac>
    </sac>
scala>   val sacAssortiment =
     |     <sac>
     |       <sac>fraises</sac>
     |       <sac>banane</sac>
     |     </sac>

scala> cestQuoi(sacAssortiment)
Ce sac contient
Ce nest pas un sac
Ce sac ne contient que des fraises
Ce nest pas un sac
Ce sac ne contient que des banane
Ce nest pas un sac
    

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 :

scala>   val frite = <frite/>
frite: scala.xml.Elem = <frite></frite>

scala> cestQuoi(frite)
Ce nest pas un sac
    

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 :

scala>   scala.xml.Utility.trim(sacAssortiment) match {
     |     case <sac><sac>{assortiment1}</sac><sac>{assortiment2}</sac></sac> => println("Ce sac est un assortiment de " + assortiment1 + " et de " + assortiment2)
     |   }
Ce sac est un assortiment de fraises et de bananes
    

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 :

<html>
<head>
      <script>alert('nono le robot')</script>
      <title>The Hobbit</title>
      <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"/>
      <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css"/>
      <style type="text/css"></style>
</head>
<body>
      <a href="http://www.gargamel.com/pages/foo.html">Bar</a>
</body>
</html>
    

doit être transformé en :

<html>
<head>
  <title>The Hobbit</title>
  <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"/>
</head>
<body>
   <a href="http://www.azrael.com/pages/foo.html">Bar</a>
</body>
    

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 :
      scala> class RewriteNoScript extends RewriteRule {
     |   import scala.xml.Node
     |   val toDelete = List("script","link","style")
     |   override def transform(n: Node): Seq[Node] = {
     |     import xml.Elem
     |     n match {
     |       case e:Elem =>
     |           val newChildren = e.child.filter { c =>
     |             !toDelete.contains(c.label)
     |           }
     |           e.copy(child = newChildren)
     |       case o => o
     |     }
     |   }
     | }
defined class RewriteNoScript
    

Instanciation :

scala> new RewriteNoScript
res6: RewriteNoScript = <function1>
    

Création d’un jeu de données :

scala> <head>
     |       <script>alert('nono le robot')</script>
     |       <title>The Hobbit</title>
     |       <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"></meta>
     |       <link type="text/css" rel="stylesheet" href="../Styles/stylesheet.css"></link>
     |       <style type="text/css"></style>
     | </head>
res9: scala.xml.Elem =
<head>
      <script>alert('nono le robot')</script>
      <title>The Hobbit</title>
      <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"></meta>
      <link type="text/css" rel="stylesheet" href="../Styles/stylesheet.css"></link>
      <style type="text/css"></style>
</head>
    

Appliquer le jeu de données au Transformer :

scala> res6(res9)
res10: scala.xml.Node =
<head>

      <title>The Hobbit</title>
      <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"></meta>


</head>
    

2) Définir un RewriteRule pour modifier les liens :

scala> class RewriteLien extends RewriteRule {
  val oldPrefix = "http://www.gargamel.com"
  val newPrefix = "http://www.azrael.com"
  override def transform(node: Node): Seq[Node] = {
    node match {
      case e:Elem => {
          if (e.label == "a" && e.attributes.get("href").isDefined){
            val newValAttribute = e.attributes.get("href").get.map(_ match {
                case Text(value) => Text(value.replaceAll(oldPrefix,newPrefix))
                case n => n
            })
            e.copy(attributes = e.attributes.remove("href").append(new UnprefixedAttribute("href", newValAttribute, Null)))
          }
          else e
      }
      case other => other
    }
  }
}
defined class RewriteLien
    

Instantiation :

scala> new RewriteLien
res25: RewriteLien = <function1>
    

Jeu de test :

scala> <a href="http://www.gargamel.com/pages/foo.html">Bar</a>
res26: scala.xml.Elem = <a href="http://www.gargamel.com/pages/foo.html">Bar</a>
    

Test :

scala> res25(res26)
res27: scala.xml.Node = <a href="http://www.azrael.com/pages/foo.html">Bar</a>
    

3) Composer les deux rewriters avec un transformer

scala> val rwLien = new RewriteLien
rwLien: RewriteLien = <function1>
scala> val rwNS = new RewriteNoScript
rwNS: RewriteNoScript = <function1>
scala> val transformer = new RuleTransformer(rwLien,rwNS)
transformer: scala.xml.transform.RuleTransformer = <function1>
    

4) Passer notre XML à la moulinette

scala> <html>
     | <head>
     |       <script>alert('nono le robot')</script>
     |       <title>The Hobbit</title>
     |       <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"/>
     |       <link href="../Styles/stylesheet.css" rel="stylesheet" type="text/css"/>
     |       <style type="text/css"></style>
     | </head>
     | <body>
     |       <a href="http://www.gargamel.com/pages/foo.html">Bar</a>
     | </body>
     | </html>
res28: scala.xml.Elem =
<html>
<head>
      <script>alert('nono le robot')</script>
      <title>The Hobbit</title>
      <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"></meta>
      <link type="text/css" rel="stylesheet" href="../Styles/stylesheet.css"></link>
      <style type="text/css"></style>
</head>
<body>
      <a href="http://www.gargamel.com/pages/foo.html">Bar</a>
</body>
</html>

scala> transformer(res28)
res29: scala.xml.Node =
<html>
<head>

      <title>The Hobbit</title>
      <meta name="Adept.resource" value="urn:uuid:e7c9da3a-a9ce-4868-b6ae-1db58a59c870"></meta>


</head>
<body>
      <a href="http://www.azrael.com/pages/foo.html">Bar</a>
</body>
</html>
    

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 :

scala>class Rewrite(name:String) extends RewriteRule {
  import scala.xml.Node
  override def transform(n: Node): Seq[Node] = {
    println("to transform " + name + " " + n)
    import scala.xml.{Elem, Null, UnprefixedAttribute}
    val ret = n match {
      case e:Elem => e.copy(attributes = new UnprefixedAttribute(name,"yes",e.attributes))
      case o => o
    }
    println("transformed " + name + " " + ret)
    ret
 }
}
scala> val xml = <a><b><c></c></b></a>
xml: scala.xml.Elem = <a><b><c></c></b></a>
scala> val r1 = new Rewrite("r1")
r1: Rewrite = <function1>
scala> r1(xml)
to transform r1 <a><b><c></c></b></a>
transformed r1 <a r1="yes"><b><c></c></b></a>
res14: scala.xml.Node = <a r1="yes"><b><c></c></b></a>
    

Ensuite je le combine avec un RuleTransformer :

scala> val t = new RuleTransformer(r1)
t: scala.xml.transform.RuleTransformer = <function1>
scala> t(xml)
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <b><c r1="yes"></c></b>
transformed r1 <b r1="yes"><c r1="yes"></c></b>
to transform r1 <b><c r1="yes"></c></b>
transformed r1 <b r1="yes"><c r1="yes"></c></b>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <b><c r1="yes"></c></b>
transformed r1 <b r1="yes"><c r1="yes"></c></b>
to transform r1 <b><c r1="yes"></c></b>
transformed r1 <b r1="yes"><c r1="yes"></c></b>
to transform r1 <a><b r1="yes"><c r1="yes"></c></b></a>
transformed r1 <a r1="yes"><b r1="yes"><c r1="yes"></c></b></a>
to transform r1 <a><b r1="yes"><c r1="yes"></c></b></a>
transformed r1 <a r1="yes"><b r1="yes"><c r1="yes"></c></b></a>
res15: scala.xml.Node = <a r1="yes"><b r1="yes"><c r1="yes"></c></b></a>
    

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:

scala> val r2 = new Rewrite("r2")
r2: Rewrite = <function1>
scala> val t = new RuleTransformer(r1,r2)
t: scala.xml.transform.RuleTransformer = <function1>

scala> t(xml)
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r1 <b><c r2="yes" r1="yes"></c></b>
transformed r1 <b r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r1 <b><c r2="yes" r1="yes"></c></b>
transformed r1 <b r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r2 <b r1="yes"><c r2="yes" r1="yes"></c></b>
transformed r2 <b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r2 <b r1="yes"><c r2="yes" r1="yes"></c></b>
transformed r2 <b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
to transform r1 <b><c r2="yes" r1="yes"></c></b>
transformed r1 <b r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r1 <b><c r2="yes" r1="yes"></c></b>
transformed r1 <b r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r2 <b r1="yes"><c r2="yes" r1="yes"></c></b>
transformed r2 <b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r2 <b r1="yes"><c r2="yes" r1="yes"></c></b>
transformed r2 <b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b>
to transform r1 <a><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
transformed r1 <a r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
to transform r1 <a><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
transformed r1 <a r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
to transform r2 <a r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
transformed r2 <a r2="yes" r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
to transform r2 <a r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
transformed r2 <a r2="yes" r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
res18: scala.xml.Node = <a r2="yes" r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
    

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 :

scala> class MyRuleTransformer(rw:RewriteRule *) {
     | def transform(n:Node):Node = {
     |   val parent = rw.foldLeft(n){
     |     (node,rewrite) => rewrite(node)
     |   }
     |   parent match {
     |     case e:Elem => e.copy(child=e.child.map(c=>transform(c)))
     |     case o => o
     |   }
     | }
     | }
defined class MyRuleTransformer
scala> val t = new MyRuleTransformer(r1,r2)
t: MyRuleTransformer = MyRuleTransformer@9ff430

scala> t.transform(xml)
to transform r1 <a><b><c></c></b></a>
transformed r1 <a r1="yes"><b><c></c></b></a>
to transform r2 <a r1="yes"><b><c></c></b></a>
transformed r2 <a r2="yes" r1="yes"><b><c></c></b></a>
to transform r1 <b><c></c></b>
transformed r1 <b r1="yes"><c></c></b>
to transform r2 <b r1="yes"><c></c></b>
transformed r2 <b r2="yes" r1="yes"><c></c></b>
to transform r1 <c></c>
transformed r1 <c r1="yes"></c>
to transform r2 <c r1="yes"></c>
transformed r2 <c r2="yes" r1="yes"></c>
res26: scala.xml.Node = <a r2="yes" r1="yes"><b r2="yes" r1="yes"><c r2="yes" r1="yes"></c></b></a>
    

« 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 

Scala quick reference

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