Trabalhando com XML no Groovy

Trabalhando com XML no Groovy

1. Introdução

Groovy fornece um número substancial de métodos dedicados a percorrer e manipular conteúdo XML.

Neste tutorial, vamos demonstrar comoadd, edit, or delete elements from XML in Groovy usando várias abordagens. Também mostraremos comocreate an XML structure from scratch.

2. Definindo o modelo

Vamos definir uma estrutura XML em nosso diretório de recursos que usaremos em nossos exemplos:


    
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

E leia em uma variávelInputStream:

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

3. XmlParser

Vamos começar a explorar este fluxo com a classeXmlParser.

3.1. Lendo

Ler e analisar um arquivo XML é provavelmente a operação XML mais comum que um desenvolvedor precisará executar. OXmlParser fornece uma interface muito simples destinada exatamente para isso:

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

Neste ponto, podemos acessar os atributos e valores da estrutura XML usando expressões GPath.

Vamos agora implementar um teste simples usandoSpock para verificar se nosso objetoarticles está correto:

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 como acessar os valores XML e como usar as expressões GPath, vamos nos concentrar por um momento na estrutura interna do resultado da operaçãoXmlParser#parse .

O objetoarticles é uma instância degroovy.util.Node. CadaNode consiste em um nome, mapa de atributos, valor e pai (que pode sernull ou outroNode) .

Em nosso caso, o valor dearticles é uma posiçãogroovy.util.NodeList , que é uma classe wrapper para uma coleção deNodes. ONodeList estende a classejava.util.ArrayList, que fornece extração de elementos por índice. Para obter um valor de string deNode, we, usegroovy.util.Node#text().

No exemplo acima, introduzimos algumas expressões GPath:

  • articles.article[0].author.firstname - obter o primeiro nome do autor para o primeiro artigo -articles.article[n] acessaria diretamente o artigonth

  • ‘*' - obtém uma lista dos filhos dearticle - é o equivalente agroovy.util.Node#children()

  • author.'@id' - obtém o atributoid do elementoauthor -author.'@attributeName' acessa o valor do atributo por seu nome (os equivalentes são:author[‘@id']e[email protected])

3.2. Adicionando um Nó

Semelhante ao exemplo anterior, vamos ler o conteúdo XML em uma variável primeiro. Isso nos permitirá definir um novo nó e adicioná-lo à nossa lista de artigos usandogroovy.util.Node#append.

Vamos agora implementar um teste que comprova nosso ponto:

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 no exemplo acima, o processo é bastante direto.

Notemos também que usamosgroovy.util.NodeBuilder,, que é uma alternativa interessante ao uso do construtorNode para nossa definiçãoNode

3.3. Modificando um Nó

Também podemos modificar os valores dos nós usandoXmlParser. Para fazer isso, vamos analisar novamente o conteúdo do arquivo XML. Em seguida, podemos editar o nó de conteúdo alterando o campovalue do objetoNode.

Vamos lembrar que, emboraXmlParser use as expressões GPath, sempre recuperamos a instância deNodeList, para modificar o primeiro (e único) elemento, temos que acessá-lo usando seu índice.

Vamos verificar nossas suposições escrevendo um teste rápido:

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

No exemplo acima, também usamosGroovy Collections API para percorrerNodeList.

3.4. Substituindo um Nó

A seguir, vamos ver como substituir o nó inteiro em vez de apenas modificar um de seus valores.

Da mesma forma que adicionar um novo elemento, usaremosNodeBuilder para a definição deNode e, em seguida, substituiremos um dos nós existentes dentro dele usandogroovy.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. Excluindo um Nó

Excluir um nó usandoXmlParser é bastante complicado. Embora a classeNode forneça o métodoremove(Node child), na maioria dos casos, não o usaríamos por si só.

Em vez disso, mostraremos como excluir um nó cujo valor atende a uma determinada condição.

Por padrão, acessar os elementos aninhados usando uma cadeia deNode.NodeList referênciasreturns a copy dos nós filhos correspondentes. Por causa disso, não podemos usar o métodojava.util.NodeList#removeAll diretamente em nossa coleçãoarticle.

Para excluir um nó por um predicado, temos que encontrar todos os nós que correspondem à nossa condição primeiro, e então iterar por eles e invocar o métodojava.util.Node#remove no pai a cada vez.

Vamos implementar um teste que remove todos os artigos cujo autor tem um id diferente de3:

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 nossa operação de remoção, recebemos uma estrutura XML com apenas um artigo, e seu id é3.

4. XmlSlurper

O Groovy também fornece outra classe dedicada ao trabalho com XML. Nesta seção, mostraremos como ler e manipular a estrutura XML usando oXmlSlurper.

4.1. Lendo

Como em nossos exemplos anteriores, vamos começar analisando a estrutura XML de um arquivo:

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

Como podemos ver, a interface é idêntica à deXmlParser. However, the output structure uses the groovy.util.slurpersupport.GPathResult, que é uma classe de wrapper paraNode. GPathResult fornece definições simplificadas de métodos, como:equals()etoString() envolvendoNode#text(). Como resultado, podemos ler campos e parâmetros diretamente usando apenas seus nomes.

4.2. Adicionando um Nó

Adicionar umNode também é muito semelhante a usarXmlParser. Nesse caso, entretanto,groovy.util.slurpersupport.GPathResult#appendNode fornece um método que usa uma instância dejava.lang.Object  como argumento. Como resultado, podemos simplificar as novas definições deNode seguindo a mesma convenção introduzida porNodeBuilder:

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

Caso precisemos modificar a estrutura do nosso XML comXmlSlurper,, temos que reinicializar nosso objetoarticles para ver os resultados. We can achieve that using the combination of the groovy.util.XmlSlurper#parseText and the groovy.xmlXmlUtil#serialize methods.

4.3. Modificando um Nó

Como mencionamos antes, oGPathResult apresenta uma abordagem simplificada para a manipulação de dados. Dito isso, em contraste comXmlSlurper,, podemos modificar os valores diretamente usando o nome do nó ou nome do parâmetro:

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

Vamos notar que quando modificamos apenas os valores do objeto XML, não precisamos analisar toda a estrutura novamente.

4.4. Substituindo um Nó

Agora vamos substituir todo o nó. Novamente, oGPathResult vem ao resgate. Podemos facilmente substituir o nó usandogroovy.util.slurpersupport.NodeChild#replaceNode, que estendeGPathResult e a areia segue a mesma convenção de usar os valoresObject como argumentos:

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

Como foi o caso ao adicionar um nó, estamos modificando a estrutura do XML, então temos que analisá-lo novamente.

4.5. Excluindo um Nó

Para remover um nó usandoXmlSlurper,, podemos reutilizar o métodogroovy.util.slurpersupport.NodeChild#replaceNode simplesmente fornecendo uma definiçãoNode vazia:

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

Novamente, a modificação da estrutura XML requer a reinicialização de nosso objetoarticles.

5. XmlParser vsXmlSlurper

Como mostramos em nossos exemplos, os usos deXmlParser andXmlSlurper são muito semelhantes. Podemos obter mais ou menos os mesmos resultados com ambos. No entanto, algumas diferenças entre eles podem inclinar a balança para uma ou outra.

Em primeiro lugar, XmlParser always parses the whole document into the DOM-ish structure. Because of that, we can simultaneously read from and write into it. Não podemos fazer o mesmo comXmlSlurper, pois ele avalia os caminhos de maneira mais preguiçosa. Como resultado,XmlParser pode consumir mais memória.

Por outro lado,XmlSlurper usa definições mais diretas, tornando-o mais simples de trabalhar. Também precisamos lembrar queany structural changes made to XML using XmlSlurper require reinitialization, which can have an unacceptable performance hit no caso de fazer muitas alterações uma após a outra.

A decisão de qual ferramenta usar deve ser tomada com cuidado e depende inteiramente do caso de uso.

6. MarkupBuilder

Além de ler e manipular a árvore XML, o Groovy também fornece ferramentas para criar um documento XML do zero. Vamos agora criar um documento que consiste nos dois primeiros artigos do nosso primeiro exemplo usandogroovy.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)
}

No exemplo acima, podemos ver queMarkupBuilder usa a mesma abordagem para as definições deNode que usamos comNodeBuildereGPathResult anteriormente.

Para comparar a saída deMarkupBuilder com a estrutura XML esperada, usamos o métodogroovy.xml.XmlUtil#serialize.

7. Conclusão

Neste artigo, exploramos várias maneiras de manipular estruturas XML usando o Groovy.

Vimos exemplos de análise, adição, edição, substituição e exclusão de nós usando duas classes fornecidas pelo Groovy:XmlParsereXmlSlurper. Também discutimos as diferenças entre eles e mostramos como poderíamos construir uma árvore XML do zero usandoMarkupBuilder.

Como sempre, o código completo usado neste artigo está disponívelover on GitHub.