Работа с XML в Groovy

1. Въведение

Groovy предоставя значителен брой методи, посветени на обхождането и манипулирането на XML съдържание.

В този урок ще покажем как да добавяте, редактирате или изтривате елементи от XML в Groovy, използвайки различни подходи. Ще покажем и как да създадем XML структура от нулата .

2. Дефиниране на модела

Нека дефинираме XML структура в нашата директория с ресурси, която ще използваме в нашите примери:

  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  

И го прочетете в променлива InputStream :

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

3. XmlParser

Нека започнем да изследваме този поток с класа XmlParser .

3.1. Четене

Четенето и анализирането на XML файл е може би най-честата XML операция, която трябва да направи разработчикът. В XmlParser предоставя един много прост интерфейс, предназначен за точно това:

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

На този етап можем да осъществим достъп до атрибутите и стойностите на XML структурата, използвайки изрази GPath.

Нека сега приложим прост тест, използвайки Spock, за да проверим дали обектът ни артикули е правилен:

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

За да разберем как да осъществим достъп до XML стойности и как да използваме изразите GPath, нека се съсредоточим за момент върху вътрешната структура на резултата от операцията за разбор на XmlParser # .

Обектът артикули е екземпляр на groovy.util.Node. Всеки възел се състои от име, атрибути на карта, стойност и родител (който може да бъде нулев или друг възел) .

В нашия случай стойността на артикулите е екземпляр groovy.util.NodeList , който е клас на обвивка за колекция от Node s. В NodeList разширява java.util.ArrayList класа, която осигурява извличане на елементите от индекс. За да получим низова стойност на Node, използваме groovy.util.Node # text ().

В горния пример въведохме няколко израза GPath:

  • articles.article [0] .author.firstname - получаване на собственото име на автора за първата статия - articles.article [n] ще има директен достъп до n -та статия
  • '*' - вземете списък с деца на статията - това е еквивалентът на groovy.util.Node # children ()
  • author.'@id ' - получаване на атрибута id на авторския елемент - author.'@attributeName' осъществява достъп до стойността на атрибута по неговото име (еквивалентите са: author ['@ id'] и [email protected] )

3.2. Добавяне на възел

Подобно на предишния пример, нека първо прочетем XML съдържанието в променлива. Това ще ни позволи да дефинираме нов възел и да го добавим към нашия списък със статии, използвайки groovy.util.Node # append.

Нека сега приложим тест, който доказва нашата гледна точка:

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

Както виждаме в горния пример, процесът е доста ясен.

Нека също така забележим, че използвахме groovy.util.NodeBuilder, което е добра алтернатива на използването на конструктора Node за нашата дефиниция на Node .

3.3. Модифициране на възел

Също така можем да модифицираме стойностите на възлите, използвайки XmlParser . За да направим това, нека още веднъж да анализираме съдържанието на XML файла. След това можем да редактираме възела за съдържание, като променим полето за стойност на обекта Node .

Нека помним, че докато XmlParser използва изразите GPath, ние винаги извличаме екземпляра на NodeList, така че за да модифицираме първия (и единствен) елемент, трябва да го осъществим, използвайки неговия индекс.

Нека проверим нашите предположения, като напишем бърз тест:

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

В горния пример също използвахме API на Groovy Collections, за да преминем през NodeList .

3.4. Подмяна на възел

След това нека видим как да заменим целия възел, вместо просто да модифицираме една от неговите стойности.

Подобно на добавянето на нов елемент, ще използваме NodeBuilder за дефиницията на Node и след това ще заменим един от съществуващите възли в него, използвайки 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. Изтриване на възел

Изтриването на възел с помощта на XmlParser е доста сложно. Въпреки че класът Node осигурява метода remove (Node child) , в повечето случаи не бихме го използвали сами.

Вместо това ще покажем как да изтриете възел, чиято стойност отговаря на дадено условие.

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.

Разгледахме примери за синтактичен анализ, добавяне, редактиране, замяна и изтриване на възли, използвайки два класа, предоставени от Groovy: XmlParser и XmlSlurper . Също така обсъдихме разликите между тях и показахме как можем да изградим XML дърво от нулата, използвайки MarkupBuilder .

Както винаги, пълният код, използван в тази статия, е достъпен в GitHub.