Перейти к основному содержимому

Работа с XML в Groovy

· 10 мин. чтения

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 .