1. Введение
Groovy предоставляет значительное количество методов, предназначенных для просмотра и управления содержимым XML.
В этом руководстве мы покажем, как добавлять, редактировать или удалять элементы из XML в Groovy, используя различные подходы. Мы также покажем, как создать структуру XML с нуля .
2. Определение модели
Давайте определим структуру XML в нашем каталоге ресурсов, которую мы будем использовать в наших примерах:
<articles>
<article>
<title>First steps in Java</title>
<author id="1">
<firstname>Siena</firstname>
<lastname>Kerr</lastname>
</author>
<release-date>2018-12-01</release-date>
</article>
<article>
<title>Dockerize your SpringBoot application</title>
<author id="2">
<firstname>Jonas</firstname>
<lastname>Lugo</lastname>
</author>
<release-date>2018-12-01</release-date>
</article>
<article>
<title>SpringBoot tutorial</title>
<author id="3">
<firstname>Daniele</firstname>
<lastname>Ferguson</lastname>
</author>
<release-date>2018-06-12</release-date>
</article>
<article>
<title>Java 12 insights</title>
<author id="1">
<firstname>Siena</firstname>
<lastname>Kerr</lastname>
</author>
<release-date>2018-07-22</release-date>
</article>
</articles>
И прочитайте его в переменную InputStream :
def xmlFile = getClass().getResourceAsStream("articles.xml")
3. XML-парсер
Начнем изучение этого потока с класса XmlParser
.
3.1. Чтение
Чтение и анализ XML-файла, вероятно, является наиболее распространенной XML-операцией, которую приходится выполнять разработчику. XmlParser предоставляет
очень простой интерфейс, предназначенный именно для этого:
def articles = new XmlParser().parse(xmlFile)
На этом этапе мы можем получить доступ к атрибутам и значениям структуры XML, используя выражения GPath.
Давайте теперь реализуем простой тест, используя Spock , чтобы проверить правильность нашего объекта
article:
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#parse
.
Объект article является экземпляром groovy.util.Node
.
Каждый узел
состоит из имени, карты атрибутов, значения и родителя (который может быть нулевым
или другим узлом)
.
В нашем случае значением article
является экземпляр groovy.util.NodeList
, который является классом-оболочкой для коллекции Node
s. NodeList расширяет класс
java.util.ArrayList
, который обеспечивает извлечение элементов по индексу. Чтобы получить строковое значение узла,
мы используем groovy.util.Node#text().
В приведенном выше примере мы ввели несколько выражений GPath:
article.article[0].author.firstname
— получить имя автора первой статьи — article.article[n]
будет напрямую обращаться кn
-й статье .'*'
— получить список дочерних элементовстатьи
— это аналогgroovy.util.Node#children()
author.'@id'
— получить атрибутid
элементаauthor .
author.'@attributeName'
обращается к значению атрибута по его имени (эквиваленты:author['@id']
иauthor.@id
)
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,
который является хорошей альтернативой использованию конструктора узла
для нашего определения узла
.
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()
}
В приведенном выше примере мы также использовали Groovy Collections API для обхода NodeList
.
3.4. Замена узла
Далее давайте посмотрим, как заменить весь узел, а не просто изменить одно из его значений.
Аналогично добавлению нового элемента, мы будем использовать NodeBuilder
для определения узла
, а затем заменим в нем один из существующих узлов с помощью 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)
, в большинстве случаев мы не стали бы использовать его сам по себе.
Вместо этого мы покажем, как удалить узел, значение которого удовлетворяет заданному условию.
По умолчанию доступ к вложенным элементам с помощью цепочки ссылок Node.NodeList
возвращает копию соответствующих дочерних узлов. Из-за этого мы не можем использовать метод java.util.NodeList#removeAll
непосредственно в нашей коллекции статей .
Чтобы удалить узел по предикату, мы должны сначала найти все узлы, соответствующие нашему условию, а затем пройтись по ним и каждый раз вызывать метод java.util.Node#remove для родителя
.
Давайте реализуем тест, удаляющий все статьи, id автора которых отличается от 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"
}
Как мы видим, в результате нашей операции удаления мы получили XML-структуру только с одной статьей, а ее id равен 3
.
4. XmlSlurper
Groovy также предоставляет еще один класс, предназначенный для работы с XML. В этом разделе мы покажем, как читать XML-структуру и управлять ею с помощью XmlSlurper.
4.1. Чтение
Как и в наших предыдущих примерах, начнем с разбора структуры XML из файла:
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"
}
Как мы видим, интерфейс идентичен интерфейсу XmlParser
. Однако структура вывода использует groovy.util.slurpersupport.GPathResult
, который является классом-оболочкой для Node
. GPathResult
предоставляет упрощенные определения таких методов, как equals()
и toString()
, обертывая Node#text().
В результате мы можем читать поля и параметры напрямую, используя только их имена.
4.2. Добавление узла
Добавление узла
также очень похоже на использование XmlParser
. Однако в этом случае groovy.util.slurpersupport.
GPathResult#appendNode
предоставляет метод, который принимает экземпляр java.lang.Object
в качестве аргумента. В результате мы можем упростить новые определения узлов
, следуя тому же соглашению, введенному Node
Builder
:
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"
}
Если нам нужно изменить структуру нашего XML с помощью XmlSlurper,
мы должны повторно инициализировать наш объект
article, чтобы увидеть результаты. Мы можем добиться этого, используя комбинацию методов groovy.util.XmlSlurper #parseText
и groovy.xmlXmlUtil#serialize
.
4.3. Изменение узла
Как мы упоминали ранее, GPathResult
представляет упрощенный подход к манипулированию данными. При этом, в отличие от XmlSlurper,
мы можем изменять значения напрямую, используя имя узла или имя параметра:
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()
}
Заметим, что когда мы изменяем только значения объекта XML, нам не нужно снова анализировать всю структуру.
4.4. Замена узла
Теперь приступим к замене всего узла. И снова на помощь приходит GPathResult .
Мы можем легко заменить узел, используя groovy.util.slurpersupport.NodeChild#replaceNode
, который расширяет GPathResult
и следует тому же соглашению об использовании значений Object
в качестве аргументов:
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"
}
Как и в случае добавления узла, мы модифицируем структуру XML, поэтому нам нужно снова его разобрать.
4.5. Удаление узла
Чтобы удалить узел с помощью XmlSlurper,
мы можем повторно использовать метод groovy.util.slurpersupport.NodeChild#replaceNode
, просто указав пустое определение узла
:
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"
}
Опять же, изменение структуры XML требует повторной инициализации нашего объекта article .
5. XmlParser
против XmlSlurper
Как мы показали в наших примерах, использование XmlParser
и XmlSlurper
очень похоже. Мы можем более или менее достичь одинаковых результатов с обоими. Однако некоторые различия между ними могут склонить чашу весов в сторону одного или другого.
Во-первых, XmlParser
всегда анализирует весь документ в виде DOM-структуры. Благодаря этому мы можем одновременно читать и писать в него . Мы не можем сделать то же самое с XmlSlurper
, так как он оценивает пути более лениво. В результате XmlParser
может потреблять больше памяти. ``
С другой стороны, XmlSlurper
использует более простые определения, что упрощает работу с ним. Мы также должны помнить, что любые структурные изменения, внесенные в XML с помощью XmlSlurper
, требуют повторной инициализации, что может привести к неприемлемому снижению производительности в случае внесения множества изменений друг за другом.
Решение о том, какой инструмент использовать, должно приниматься с осторожностью и полностью зависит от варианта использования.
6. Разметчик
Помимо чтения XML-дерева и управления им, Groovy также предоставляет инструменты для создания XML-документа с нуля. Давайте теперь создадим документ, состоящий из первых двух статей из нашего первого примера, используя 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)
}
В приведенном выше примере мы видим, что MarkupBuilder
использует тот же подход для определений узлов
, который мы использовали ранее с NodeBuilder
и GPathResult
.
Чтобы сравнить выходные данные MarkupBuilder
с ожидаемой структурой XML, мы использовали метод groovy.xml.XmlUtil#serialize
.
7. Заключение
В этой статье мы рассмотрели несколько способов управления XML-структурами с помощью Groovy.
Мы рассмотрели примеры разбора, добавления, редактирования, замены и удаления узлов с помощью двух классов, предоставляемых Groovy: XmlParser
и XmlSlurper
. Мы также обсудили различия между ними и показали, как можно с нуля построить дерево XML с помощью MarkupBuilder
.
Как всегда, полный код, использованный в этой статье, доступен на GitHub .