Lavorare con XML in Groovy

1. Introduzione

Groovy fornisce un numero considerevole di metodi dedicati all'attraversamento e alla manipolazione del contenuto XML.

In questo tutorial, dimostreremo come aggiungere, modificare o eliminare elementi da XML in Groovy utilizzando vari approcci. Mostreremo anche come creare una struttura XML da zero .

2. Definizione del modello

Definiamo una struttura XML nella nostra directory delle risorse che useremo nei nostri esempi:

  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  

E leggilo in una variabile InputStream :

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

3. XmlParser

Iniziamo a esplorare questo flusso con la classe XmlParser .

3.1. Lettura

La lettura e l'analisi di un file XML è probabilmente l'operazione XML più comune che uno sviluppatore dovrà eseguire. Il XmlParser fornisce un'interfaccia molto semplice significato per esattamente questo:

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

A questo punto, possiamo accedere agli attributi e ai valori della struttura XML utilizzando espressioni GPath.

Implementiamo ora un semplice test usando Spock per verificare se il nostro oggetto articoli è corretto:

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

Per capire come accedere ai valori XML e come utilizzare le espressioni GPath, concentriamoci per un momento sulla struttura interna del risultato dell'operazione di analisi XmlParser # .

L' oggetto articoli è un'istanza di groovy.util.Node. Ogni nodo è costituito da un nome, una mappa di attributi, un valore e un genitore (che può essere nullo o un altro nodo) .

Nel nostro caso, il valore di articoli è un'istanza groovy.util.NodeList , che è una classe wrapper per una raccolta di Node . Il NodeList estende la java.util.ArrayList classe, che prevede l'estrazione di elementi di indice. Per ottenere un valore stringa di un nodo, usiamo groovy.util.Node # text ().

Nell'esempio precedente, abbiamo introdotto alcune espressioni GPath:

  • articles.article [0] .author.firstname - ottenere prima il nome dell'autore per il primo articolo - articles.article [n] avrebbe accedere direttamente al n ° articolo
  • '*' - ottieni un elenco dei figli di un articolo - è l'equivalente di groovy.util.Node # children ()
  • author.'@id ' - ottiene l' attributo id dell'elemento autore - author.'@attributeName' accede al valore dell'attributo tramite il suo nome (gli equivalenti sono: author ['@ id'] e [email protected] )

3.2. Aggiunta di un nodo

In modo simile all'esempio precedente, leggiamo prima il contenuto XML in una variabile. Questo ci permetterà di definire un nuovo nodo e aggiungerlo alla nostra lista di articoli usando groovy.util.Node # append.

Implementiamo ora un test che dimostri il nostro punto:

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

Come possiamo vedere nell'esempio sopra, il processo è piuttosto semplice.

Notiamo anche che abbiamo usato groovy.util.NodeBuilder, che è una valida alternativa all'utilizzo del costruttore Node per la nostra definizione di Node .

3.3. Modifica di un nodo

Possiamo anche modificare i valori dei nodi usando XmlParser . Per fare ciò, analizziamo ancora una volta il contenuto del file XML. Successivamente, possiamo modificare il nodo del contenuto cambiando il campo del valore dell'oggetto Nodo .

Ricordiamoci che mentre XmlParser utilizza le espressioni GPath, noi recuperiamo sempre l'istanza del NodeList, quindi per modificare il primo (e unico) elemento dobbiamo accedervi utilizzando il suo indice.

Controlliamo le nostre ipotesi scrivendo un breve test:

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() }

Nell'esempio sopra, abbiamo anche utilizzato l'API Groovy Collections per attraversare NodeList .

3.4. Sostituzione di un nodo

Successivamente, vediamo come sostituire l'intero nodo invece di modificare solo uno dei suoi valori.

Analogamente all'aggiunta di un nuovo elemento, utilizzeremo NodeBuilder per la definizione del nodo e quindi sostituiremo uno dei nodi esistenti al suo interno utilizzando 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. Eliminazione di un nodo

L'eliminazione di un nodo utilizzando XmlParser è piuttosto complicata. Sebbene la classe Node fornisca il metodo remove (Node child) , nella maggior parte dei casi, non lo useremo da solo.

Invece, mostreremo come eliminare un nodo il cui valore soddisfa una determinata condizione.

Per impostazione predefinita, l'accesso agli elementi nidificati utilizzando una catena di riferimenti Node.NodeList restituisce una copia dei nodi figli corrispondenti. Per questo motivo , non possiamo utilizzare il metodo java.util.NodeList # removeAll direttamente nella nostra raccolta di articoli .

Per eliminare un nodo con un predicato, dobbiamo prima trovare tutti i nodi che corrispondono alla nostra condizione, quindi iterarli e invocare ogni volta il metodo java.util.Node # remove sul genitore.

Implementiamo un test che rimuova tutti gli articoli il cui autore ha un ID diverso da 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" }

Come possiamo vedere, come risultato della nostra operazione di rimozione, abbiamo ricevuto una struttura XML con un solo articolo e il suo ID è 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.

Abbiamo esaminato esempi di analisi, aggiunta, modifica, sostituzione ed eliminazione di nodi utilizzando due classi fornite da Groovy: XmlParser e XmlSlurper . Abbiamo anche discusso le differenze tra di loro e mostrato come si potrebbe costruire un albero XML da zero usando MarkupBuilder .

Come sempre, il codice completo utilizzato in questo articolo è disponibile su GitHub.