«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 . XmlParser

Давайте начнем изучение этого потока с класса 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, который является классом-оболочкой для набора узлов. NodeList расширяет класс java.util.ArrayList, который обеспечивает извлечение элементов по индексу. Чтобы получить строковое значение узла, мы используем groovy.util.Node#text().

В приведенном выше примере мы ввели несколько выражений GPath: прямой доступ к 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, поэтому для изменения первого (и единственного) элемента мы должны получить к нему доступ, используя его индекс.

Давайте проверим наши предположения, написав быстрый тест:

В приведенном выше примере мы также использовали Groovy Collections API для обхода 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()
}

3.4. Замена узла

Далее давайте посмотрим, как заменить весь узел, а не просто изменить одно из его значений.

Аналогично добавлению нового элемента, мы будем использовать NodeBuilder для определения узла, а затем заменим в нем один из существующих узлов с помощью groovy.util.Node#replaceNode:

3.5. Удаление узла

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

Удаление узла с помощью XmlParser довольно сложно. Хотя класс Node предоставляет метод remove(Node child), в большинстве случаев мы не стали бы использовать его сам по себе.

Вместо этого мы покажем, как удалить узел, значение которого удовлетворяет заданному условию.

«По умолчанию доступ к вложенным элементам с помощью цепочки ссылок Node.NodeList возвращает копию соответствующих дочерних узлов. Из-за этого мы не можем использовать метод java.util.NodeList#removeAll непосредственно в нашей коллекции статей.

Чтобы удалить узел по предикату, мы должны сначала найти все узлы, соответствующие нашему условию, а затем пройтись по ним и каждый раз вызывать метод java.util.Node#remove для родителя.

Реализуем тест, удаляющий все статьи, id автора которых отличен от 3:

Как видим, в результате нашей операции удаления мы получили XML-структуру только с одной статьей, а его идентификатор равен 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"
}

4. XmlSlurper

Groovy также предоставляет еще один класс, предназначенный для работы с XML. В этом разделе мы покажем, как читать XML-структуру и управлять ею с помощью XmlSlurper.

4.1. Чтение

Как и в наших предыдущих примерах, начнем с разбора структуры XML из файла:

Как мы видим, интерфейс идентичен интерфейсу XmlParser. Однако структура вывода использует groovy.util.slurpersupport.GPathResult, который является классом-оболочкой для Node. GPathResult предоставляет упрощенные определения таких методов, как equals() и toString(), обертывая Node#text(). В результате мы можем читать поля и параметры напрямую, используя только их имена.

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

4.2. Добавление узла

Добавление узла также очень похоже на использование XmlParser. Однако в этом случае groovy.util.slurpersupport.GPathResult#appendNode предоставляет метод, который принимает экземпляр java.lang.Object в качестве аргумента. В результате мы можем упростить новые определения Node, следуя тому же соглашению, введенному NodeBuilder:

В случае, если нам нужно изменить структуру нашего XML с помощью XmlSlurper, мы должны повторно инициализировать наш объект article, чтобы увидеть результаты. Мы можем добиться этого, используя комбинацию методов groovy.util.XmlSlurper#parseText и groovy.xmlXmlUtil#serialize.

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

4.3. Изменение узла

Как мы упоминали ранее, GPathResult представляет упрощенный подход к манипулированию данными. При этом, в отличие от XmlSlurper, мы можем изменять значения напрямую, используя имя узла или имя параметра:

Заметим, что когда мы изменяем только значения объекта XML, нам не нужно разобрать всю структуру еще раз.

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

4.4. Замена узла

Теперь давайте перейдем к замене всего узла. И снова на помощь приходит GPathResult. Мы можем легко заменить узел, используя groovy.util.slurpersupport.NodeChild#replaceNode, который расширяет GPathResult и следует тому же соглашению об использовании значений Object в качестве аргументов:

Как и в случае добавления узла, мы повторно модифицируя структуру XML, поэтому нам нужно снова разобрать его.

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

4.5. Удаление узла

Чтобы удалить узел с помощью XmlSlurper, мы можем повторно использовать метод groovy.util.slurpersupport.NodeChild#replaceNode, просто указав пустое определение узла:

Опять же, изменение структуры XML требует повторной инициализации наши статьи возражают.

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

5. XmlParser и XmlSlurper

Как мы показали в наших примерах, использование XmlParser и XmlSlurper очень похоже. Мы можем более или менее достичь одинаковых результатов с обоими. Однако некоторые различия между ними могут склонить чашу весов в сторону одного или другого.

Во-первых, XmlParser всегда анализирует весь документ в виде DOM-структуры. Благодаря этому мы можем одновременно читать и записывать в него. Мы не можем сделать то же самое с XmlSlurper, так как он оценивает пути более лениво. В результате XmlParser может потреблять больше памяти.

С другой стороны, XmlSlurper использует более простые определения, что упрощает работу с ним. Мы также должны помнить, что любые структурные изменения, внесенные в XML с помощью XmlSlurper, требуют повторной инициализации, что может привести к неприемлемому снижению производительности в случае внесения множества изменений друг за другом.

Решение о том, какой инструмент использовать, должно приниматься с осторожностью и полностью зависит от варианта использования.

6. Построитель разметки

«Помимо чтения XML-дерева и управления им, Groovy также предоставляет инструменты для создания XML-документа с нуля. Давайте теперь создадим документ, состоящий из первых двух статей из нашего первого примера, используя groovy.xml.MarkupBuilder: NodeBuilder и GPathResult ранее.

Чтобы сравнить выходные данные MarkupBuilder с ожидаемой структурой XML, мы использовали метод groovy.xml.XmlUtil#serialize.

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

7. Заключение

В этой статье мы рассмотрели несколько способов управления структурами XML с помощью Groovy.

Мы рассмотрели примеры разбора, добавления, редактирования, замены и удаления узлов с помощью двух классов, предоставляемых Groovy: XmlParser и XmlSlurper. Мы также обсудили различия между ними и показали, как с помощью MarkupBuilder можно построить XML-дерево с нуля.

Как всегда, полный код, использованный в этой статье, доступен на GitHub.

«

As always, the complete code used in this article is available over on GitHub.