Travailler avec XML dans Groovy

1. Introduction

Groovy fournit un nombre important de méthodes dédiées à la traversée et à la manipulation du contenu XML.

Dans ce didacticiel, nous allons montrer comment ajouter, modifier ou supprimer des éléments de XML dans Groovy à l' aide de différentes approches. Nous montrerons également comment créer une structure XML à partir de zéro .

2. Définition du modèle

Définissons une structure XML dans notre répertoire de ressources que nous utiliserons tout au long de nos exemples:

  First steps in Java  Siena Kerr  2018-12-01   Dockerize your SpringBoot application  Jonas Lugo  2018-12-01   SpringBoot tutorial  Daniele Ferguson  2018-06-12   Java 12 insights  Siena Kerr  2018-07-22  

Et lisez-le dans une variable InputStream :

def xmlFile = getClass().getResourceAsStream("articles.xml")

3. XmlParser

Commençons par explorer ce flux avec la classe XmlParser .

3.1. En train de lire

La lecture et l'analyse d'un fichier XML est probablement l'opération XML la plus courante qu'un développeur devra faire. Le XmlParser fournit une interface très simple destinée exactement à cela:

def articles = new XmlParser().parse(xmlFile)

À ce stade, nous pouvons accéder aux attributs et aux valeurs de la structure XML à l'aide d'expressions GPath.

Implémentons maintenant un test simple en utilisant Spock pour vérifier si notre objet articles est correct:

def "Should read XML file properly"() { given: "XML file" when: "Using XmlParser to read file" def articles = new XmlParser().parse(xmlFile) then: "Xml is loaded properly" articles.'*'.size() == 4 articles.article[0].author.firstname.text() == "Siena" articles.article[2].'release-date'.text() == "2018-06-12" articles.article[3].title.text() == "Java 12 insights" articles.article.find { it.author.'@id'.text() == "3" }.author.firstname.text() == "Daniele" }

Pour comprendre comment accéder aux valeurs XML et comment utiliser les expressions GPath, concentrons-nous un instant sur la structure interne du résultat de l' opération d' analyse XmlParser # .

L' objet articles est une instance de groovy.util.Node. Chaque nœud se compose d'un nom, d'une mappe d'attributs, d'une valeur et d'un parent (qui peut être nul ou un autre nœud) .

Dans notre cas, la valeur des articles est une instance groovy.util.NodeList , qui est une classe wrapper pour une collection de Node s. Le NodeList étend la classe java.util.ArrayList , qui fournit l'extraction des éléments par index. Pour obtenir une valeur de chaîne d'un Node, nous utilisons groovy.util.Node # text ().

Dans l'exemple ci-dessus, nous avons introduit quelques expressions GPath:

  • articles.article [0] .author.firstname - récupère le prénom de l'auteur pour le premier article - articles.article [n] accède directement au n ième article
  • '*' - obtenir une liste des enfants de l' article - c'est l'équivalent de groovy.util.Node # children ()
  • author.'@id ' - récupère l' attribut id de l'élément auteur - author.'@attributeName' accède à la valeur d'attribut par son nom (les équivalents sont: author ['@ id'] et [email protected] )

3.2. Ajouter un nœud

Similaire à l'exemple précédent, lisons d'abord le contenu XML dans une variable. Cela nous permettra de définir un nouveau nœud et de l'ajouter à notre liste d'articles en utilisant groovy.util.Node # append.

Implémentons maintenant un test qui prouve notre point:

def "Should add node to existing xml using NodeBuilder"() { given: "XML object" def articles = new XmlParser().parse(xmlFile) when: "Adding node to xml" def articleNode = new NodeBuilder().article(id: '5') { title('Traversing XML in the nutshell') author { firstname('Martin') lastname('Schmidt') } 'release-date'('2019-05-18') } articles.append(articleNode) then: "Node is added to xml properly" articles.'*'.size() == 5 articles.article[4].title.text() == "Traversing XML in the nutshell" }

Comme nous pouvons le voir dans l'exemple ci-dessus, le processus est assez simple.

Notons également que nous avons utilisé groovy.util.NodeBuilder, qui est une alternative intéressante à l'utilisation du constructeur Node pour notre définition de Node .

3.3. Modifier un nœud

Nous pouvons également modifier les valeurs des nœuds à l'aide du XmlParser . Pour ce faire, analysons à nouveau le contenu du fichier XML. Ensuite, nous pouvons modifier le nœud de contenu en modifiant le champ de valeur de l' objet Node .

Souvenons-nous que bien que XmlParser utilise les expressions GPath, nous récupérons toujours l'instance de NodeList, donc pour modifier le premier (et seul) élément, nous devons y accéder en utilisant son index.

Vérifions nos hypothèses en écrivant un test rapide:

def "Should modify node"() { given: "XML object" def articles = new XmlParser().parse(xmlFile) when: "Changing value of one of the nodes" articles.article.each { it.'release-date'[0].value = "2019-05-18" } then: "XML is updated" articles.article.findAll { it.'release-date'.text() != "2019-05-18" }.isEmpty() }

Dans l'exemple ci-dessus, nous avons également utilisé l'API Groovy Collections pour parcourir NodeList .

3.4. Remplacement d'un nœud

Ensuite, voyons comment remplacer le nœud entier au lieu de simplement modifier l'une de ses valeurs.

De la même manière que pour l'ajout d'un nouvel élément, nous utiliserons NodeBuilder pour la définition de nœud , puis remplacerons l'un des nœuds existants à l'intérieur à l'aide de groovy.util.Node # replaceNode :

def "Should replace node"() { given: "XML object" def articles = new XmlParser().parse(xmlFile) when: "Adding node to xml" def articleNode = new NodeBuilder().article(id: '5') { title('Traversing XML in the nutshell') author { firstname('Martin') lastname('Schmidt') } 'release-date'('2019-05-18') } articles.article[0].replaceNode(articleNode) then: "Node is added to xml properly" articles.'*'.size() == 4 articles.article[0].title.text() == "Traversing XML in the nutshell" }

3.5. Suppression d'un nœud

La suppression d'un nœud à l'aide de XmlParser est assez délicate. Bien que la classe Node fournisse la méthode remove (Node child) , dans la plupart des cas, nous ne l'utilisons pas seule.

Au lieu de cela, nous montrerons comment supprimer un nœud dont la valeur remplit une condition donnée.

By default, accessing the nested elements using a chain of Node.NodeList references returns a copy of the corresponding children nodes. Because of that, we can't use the java.util.NodeList#removeAll method directly on our article collection.

To delete a node by a predicate, we have to find all nodes matching our condition first, and then iterate through them and invoke java.util.Node#remove method on the parent each time .

Let's implement a test that removes all articles whose author has an id other than 3:

def "Should remove article from xml"() { given: "XML object" def articles = new XmlParser().parse(xmlFile) when: "Removing all articles but the ones with id==3" articles.article .findAll { it.author.'@id'.text() != "3" } .each { articles.remove(it) } then: "There is only one article left" articles.children().size() == 1 articles.article[0].author.'@id'.text() == "3" }

As we can see, as a result of our remove operation, we received an XML structure with only one article, and its id is 3.

4. XmlSlurper

Groovy also provides another class dedicated to working with XML. In this section, we'll show how to read and manipulate the XML structure using the XmlSlurper.

4.1. Reading

As in our previous examples, let's start with parsing the XML structure from a file:

def "Should read XML file properly"() { given: "XML file" when: "Using XmlSlurper to read file" def articles = new XmlSlurper().parse(xmlFile) then: "Xml is loaded properly" articles.'*'.size() == 4 articles.article[0].author.firstname == "Siena" articles.article[2].'release-date' == "2018-06-12" articles.article[3].title == "Java 12 insights" articles.article.find { it.author.'@id' == "3" }.author.firstname == "Daniele" }

As we can see, the interface is identical to that of XmlParser. However, the output structure uses the groovy.util.slurpersupport.GPathResult, which is a wrapper class for Node. GPathResult provides simplified definitions of methods such as: equals() and toString() by wrapping Node#text(). As a result, we can read fields and parameters directly using just their names.

4.2. Adding a Node

Adding a Node is also very similar to using XmlParser. In this case, however, groovy.util.slurpersupport.GPathResult#appendNode provides a method that takes an instance of java.lang.Object as an argument. As a result, we can simplify new Node definitions following the same convention introduced by NodeBuilder:

def "Should add node to existing xml"() { given: "XML object" def articles = new XmlSlurper().parse(xmlFile) when: "Adding node to xml" articles.appendNode { article(id: '5') { title('Traversing XML in the nutshell') author { firstname('Martin') lastname('Schmidt') } 'release-date'('2019-05-18') } } articles = new XmlSlurper().parseText(XmlUtil.serialize(articles)) then: "Node is added to xml properly" articles.'*'.size() == 5 articles.article[4].title == "Traversing XML in the nutshell" }

In case we need to modify the structure of our XML with XmlSlurper, we have to reinitialize our articles object to see the results. We can achieve that using the combination of the groovy.util.XmlSlurper#parseText and the groovy.xmlXmlUtil#serialize methods.

4.3. Modifying a Node

As we mentioned before, the GPathResult introduces a simplified approach to data manipulation. That being said, in contrast to the XmlSlurper, we can modify the values directly using the node name or parameter name:

def "Should modify node"() { given: "XML object" def articles = new XmlSlurper().parse(xmlFile) when: "Changing value of one of the nodes" articles.article.each { it.'release-date' = "2019-05-18" } then: "XML is updated" articles.article.findAll { it.'release-date' != "2019-05-18" }.isEmpty() }

Let's notice that when we only modify the values of the XML object, we don't have to parse the whole structure again.

4.4. Replacing a Node

Now let's move to replacing the whole node. Again, the GPathResult comes to the rescue. We can easily replace the node using groovy.util.slurpersupport.NodeChild#replaceNode, which extends GPathResult and follows the same convention of using the Object values as arguments:

def "Should replace node"() { given: "XML object" def articles = new XmlSlurper().parse(xmlFile) when: "Replacing node" articles.article[0].replaceNode { article(id: '5') { title('Traversing XML in the nutshell') author { firstname('Martin') lastname('Schmidt') } 'release-date'('2019-05-18') } } articles = new XmlSlurper().parseText(XmlUtil.serialize(articles)) then: "Node is replaced properly" articles.'*'.size() == 4 articles.article[0].title == "Traversing XML in the nutshell" }

As was the case when adding a node, we're modifying the structure of the XML, so we have to parse it again.

4.5. Deleting a Node

To remove a node using XmlSlurper, we can reuse the groovy.util.slurpersupport.NodeChild#replaceNode method simply by providing an empty Node definition:

def "Should remove article from xml"() { given: "XML object" def articles = new XmlSlurper().parse(xmlFile) when: "Removing all articles but the ones with id==3" articles.article .findAll { it.author.'@id' != "3" } .replaceNode {} articles = new XmlSlurper().parseText(XmlUtil.serialize(articles)) then: "There is only one article left" articles.children().size() == 1 articles.article[0].author.'@id' == "3" }

Again, modifying the XML structure requires reinitialization of our articles object.

5. XmlParser vs XmlSlurper

As we showed in our examples, the usages of XmlParser and XmlSlurper are pretty similar. We can more or less achieve the same results with both. However, some differences between them can tilt the scales towards one or the other.

First of all,XmlParser always parses the whole document into the DOM-ish structure. Because of that, we can simultaneously read from and write into it. We can't do the same with XmlSlurper as it evaluates paths more lazily. As a result, XmlParser can consume more memory.

On the other hand, XmlSlurper uses more straightforward definitions, making it simpler to work with. We also need to remember that any structural changes made to XML using XmlSlurper require reinitialization, which can have an unacceptable performance hit in case of making many changes one after another.

The decision of which tool to use should be made with care and depends entirely on the use case.

6. MarkupBuilder

Apart from reading and manipulating the XML tree, Groovy also provides tooling to create an XML document from scratch. Let's now create a document consisting of the first two articles from our first example using groovy.xml.MarkupBuilder:

def "Should create XML properly"() { given: "Node structures" when: "Using MarkupBuilderTest to create xml structure" def writer = new StringWriter() new MarkupBuilder(writer).articles { article { title('First steps in Java') author(id: '1') { firstname('Siena') lastname('Kerr') } 'release-date'('2018-12-01') } article { title('Dockerize your SpringBoot application') author(id: '2') { firstname('Jonas') lastname('Lugo') } 'release-date'('2018-12-01') } } then: "Xml is created properly" XmlUtil.serialize(writer.toString()) == XmlUtil.serialize(xmlFile.text) }

In the above example, we can see that MarkupBuilder uses the very same approach for the Node definitions we used with NodeBuilder and GPathResult previously.

To compare output from MarkupBuilder with the expected XML structure, we used the groovy.xml.XmlUtil#serialize method.

7. Conclusion

In this article, we explored multiple ways of manipulating XML structures using Groovy.

Nous avons examiné des exemples d'analyse, d'ajout, de modification, de remplacement et de suppression de nœuds à l'aide de deux classes fournies par Groovy: XmlParser et XmlSlurper . Nous avons également discuté des différences entre eux et montré comment nous pourrions créer une arborescence XML à partir de zéro en utilisant MarkupBuilder .

Comme toujours, le code complet utilisé dans cet article est disponible à l'adresse over sur GitHub.