Trabajar con XML en Groovy

1. Introducción

Groovy proporciona una cantidad sustancial de métodos dedicados a atravesar y manipular contenido XML.

En este tutorial, demostraremos cómo agregar, editar o eliminar elementos de XML en Groovy usando varios enfoques. También mostraremos cómo crear una estructura XML desde cero .

2. Definición del modelo

Definamos una estructura XML en nuestro directorio de recursos que usaremos en nuestros ejemplos:

  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  

Y léelo en una variable InputStream :

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

3. XmlParser

Comencemos a explorar esta secuencia con la clase XmlParser .

3.1. Leyendo

Leer y analizar un archivo XML es probablemente la operación XML más común que tendrá que realizar un desarrollador. El XMLParser proporciona una interfaz muy sencillo destinado a exactamente eso:

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

En este punto, podemos acceder a los atributos y valores de la estructura XML usando expresiones GPath.

Ahora implementemos una prueba simple usando Spock para verificar si nuestro objeto de artículos es correcto:

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

Para entender cómo acceder a los valores XML y cómo usar las expresiones GPath, centrémonos por un momento en la estructura interna del resultado de la operación de análisis de XmlParser # .

El objeto de artículos es una instancia de groovy.util.Node. Cada nodo consta de un nombre, mapa de atributos, valor y padre (que puede ser nulo u otro nodo) .

En nuestro caso, el valor de los artículos es una instancia groovy.util.NodeList , que es una clase contenedora para una colección de Node s. El NodeList extiende el java.util.ArrayList clase, que proporciona la extracción de elementos de índice. Para obtener un valor de cadena de un Nodo, usamos groovy.util.Node # text ().

En el ejemplo anterior, presentamos algunas expresiones GPath:

  • articles.article [0] .author.firstname - obtener primero el nombre del autor del primer artículo - articles.article [n] que acceder directamente al n º artículo
  • '*' - obtenga una lista de los hijos del artículo - es el equivalente de groovy.util.Node # children ()
  • author.'@id ' - obtiene el atributo id del elemento del autor - author.'@attributeName' accede al valor del atributo por su nombre (los equivalentes son: autor ['@ id'] y [email protected] )

3.2. Agregar un nodo

Al igual que en el ejemplo anterior, primero leamos el contenido XML en una variable. Esto nos permitirá definir un nuevo nodo y agregarlo a nuestra lista de artículos usando groovy.util.Node # append.

Implementemos ahora una prueba que demuestre nuestro 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" }

Como podemos ver en el ejemplo anterior, el proceso es bastante sencillo.

Observemos también que usamos groovy.util.NodeBuilder, que es una buena alternativa a usar el constructor de Nodo para nuestra definición de Nodo .

3.3. Modificar un nodo

También podemos modificar los valores de los nodos usando XmlParser . Para hacerlo, analicemos una vez más el contenido del archivo XML. A continuación, podemos editar el nodo de contenido cambiando el campo de valor del objeto Nodo .

Recordemos que mientras XmlParser usa las expresiones GPath, siempre recuperamos la instancia de NodeList, por lo que para modificar el primer (y único) elemento, tenemos que acceder a él usando su índice.

Revisemos nuestras suposiciones escribiendo una prueba rápida:

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

En el ejemplo anterior, también usamos la API Groovy Collections para recorrer NodeList .

3.4. Reemplazo de un nodo

A continuación, veamos cómo reemplazar todo el nodo en lugar de simplemente modificar uno de sus valores.

De manera similar a agregar un nuevo elemento, usaremos NodeBuilder para la definición de Nodo y luego reemplazaremos uno de los nodos existentes dentro de él usando 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. Eliminar un nodo

Eliminar un nodo usando XmlParser es bastante complicado. Aunque la clase Node proporciona el método remove (Node child) , en la mayoría de los casos, no lo usaríamos por sí solo.

En su lugar, mostraremos cómo eliminar un nodo cuyo valor cumple una condición determinada.

De forma predeterminada, acceder a los elementos anidados mediante una cadena de referencias Node.NodeList devuelve una copia de los nodos secundarios correspondientes. Por eso, no podemos usar el método java.util.NodeList # removeAll directamente en nuestra colección de artículos .

Para eliminar un nodo por un predicado, primero tenemos que encontrar todos los nodos que coincidan con nuestra condición, y luego recorrerlos en iteración e invocar el método java.util.Node # remove en el padre cada vez.

Implementemos una prueba que elimine todos los artículos cuyo autor tenga una identificación distinta de 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" }

Como podemos ver, como resultado de nuestra operación de eliminación, recibimos una estructura XML con un solo artículo, y su identificación es 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.

Observamos ejemplos de análisis, adición, edición, reemplazo y eliminación de nodos utilizando dos clases proporcionadas por Groovy: XmlParser y XmlSlurper . También discutimos las diferencias entre ellos y mostramos cómo podemos construir un árbol XML desde cero usando MarkupBuilder .

Como siempre, el código completo utilizado en este artículo está disponible en GitHub.