CodeGym /Cursos /Kotlin SELF /XPath sobre o DOM

XPath sobre o DOM

Kotlin SELF
Nível 50, Lição 2
Disponível

1. Mais uma forma de trabalhar com XML

Quando voce domina o DOM pela primeira vez, surge a sensacao: “Pronto, consigo chegar a qualquer no, e so caminhar pela arvore”. Isso e verdade... mais ou menos ate o primeiro XML que e um pouco mais complexo do que o do tutorial. Depois vem o classico: muito getElementsByTagName, um monte de indices item(0), casts, validacoes de vazio — e o seu codigo de leitura de dados fica maior do que o proprio XML.

XPath serve para descrever a selecao de forma declarativa: “me de o nome do usuario com id=42”, e nao “primeiro encontre todos os users, depois percorra, depois compare o id, depois extraia o name”.

Voce pode encarar XPath como uma “mini-linguagem de consultas” para a arvore XML. A sensacao e parecida com navegar por pastas (caminhos do tipo /users/user/name), mas com filtros e “busca em profundidade”. E sim — as vezes XPath parece um feitico, mas nos vamos escrever feiticos curtos e claros; caso contrario, a magia vira divida tecnica.

2. XPath funciona sobre o DOM

Antes de escrever XPath, e importante desfazer uma confusao comum. XPath nao e um “parser XML alternativo”. Na abordagem de hoje, XPath nao faz o parse do texto. Primeiro fazemos o parse do XML para DOM (obtemos um Document), e so depois executamos expressoes XPath sobre a arvore ja pronta.

Ou seja, o nosso pipeline fica assim:

flowchart LR
    A["XML (String)"] --> B["DOM parse → Document"]
    B --> C["XPath query → value/nodes"]
    C --> D["Objetos Kotlin (por exemplo, Expense)"]

O DOM e responsavel por transformar o XML em uma estrutura na memoria. O XPath e responsavel por fazer selecoes convenientes nessa estrutura.

3. Mini-dicionario de XPath: caminhos, //, atributos e text()

No comeco, XPath parece assustador por causa dos simbolos /, //, @, []. Mas, se voce separar por significado, ha uma logica bem humana ali. Nao vamos entrar em “axes” e recursos avancados — para nos, basta o conjunto do dia a dia, realmente usado em codigo de aplicacao.

A seguir, uma pequena tabela de “cola”. Da para manter por perto e nao tentar decorar de primeira (afinal, nao estamos em um exame de runas antigas).

Trecho de XPath O que significa na pratica Exemplo
/a/b/c
caminho a partir da raiz do documento
/users/user/name
//c
encontrar c em qualquer lugar do documento (busca “em profundidade”)
//item
@id
atributo id do no atual
/user/@id
[@id='42']
filtro (predicado) por atributo
//user[@id='42']
text()
no de texto (conteudo)
/user/name/text()

Duas observacoes que economizam nervos.

Primeiro, // e pratico, mas perigoso: ele pode encontrar mais nos do que voce esperava, especialmente se o documento for grande ou a estrutura tiver mudado.

Segundo, XPath frequentemente retorna strings com espacos e quebras de linha (por causa da formatacao do XML), entao trim() e seu amigo, nao seu inimigo.

4. Preparacao e modos basicos de XPath em Kotlin

Criando Document e XPath

Agora vamos montar o minimo de “infraestrutura”: como obter um Document e como criar um objeto XPath, que sabe executar consultas. Este e um Java API padrao, mas o Kotlin o chama muito bem (a esta altura, Java interoperability ja nao assusta).

O esqueleto do main em Kotlin e familiar, entao vamos comecar com um exemplo simples: parseamos XML → criamos XPath → extraimos um valor.


import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathFactory

fun main() {
    val xml = """<user id="42"><name>Ann</name></user>"""

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val xPath = XPathFactory.newInstance().newXPath()
    val name = xPath.evaluate("/user/name/text()", doc).trim()

    println(name) // Ann
}

Repare: evaluate(...) aqui retornou uma string. Este e o modo mais simples de usar XPath: “me de o resultado como texto”. E ideal para selecoes pontuais como um nome, um valor, um status.

Modo “me de uma string”: evaluate(expr, doc) e resultados vazios

Quando voce usa evaluate(...) sem indicar o tipo do resultado, normalmente voce recebe uma string. E aqui ha um detalhe pratico importante: se o no nao for encontrado, muitas vezes nao vem null, e sim uma string vazia. Nao e bug; e o contrato desse modo.

Para nao misturar “valor vazio no XML” com “nada encontrado”, e conveniente fazer uma pequena util: “retorne String?”.

import javax.xml.xpath.XPath

fun xPathTextOrNull(xPath: XPath, expr: String, doc: Any): String? {
    val raw = xPath.evaluate(expr, doc).trim()
    return raw.takeIf { it.isNotEmpty() }
}

E o uso:

import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathFactory

fun main() {
    val xml = """<user id="42"></user>"""
    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val xPath = XPathFactory.newInstance().newXPath()
    val name = xPathTextOrNull(xPath, "/user/name/text()", doc)

    println(name) // null
}

Aqui, de proposito, transformamos “string vazia” em “sem valor”. Isso deixa a logica em Kotlin muito mais honesta: null diz claramente “nao encontramos”, em vez de fazer voce lembrar que “string vazia as vezes significa ausencia”.

Modo “me de nos”: NODESET e NodeList

Quando voce precisa extrair nao um valor, mas muitos elementos do mesmo tipo (por exemplo, uma lista de compras, uma lista de transacoes, uma lista de usuarios), uma string ja nao serve. Voce precisa de um “conjunto de nos”.

No Java XPath API isso e feito via XPathConstants.NODESET, e o resultado sera um NodeList.

Importante: NodeList nao e um List. Nao da para fazer map{} e filter{} de forma decente sem converter. Mas da para iterar pelos indices: 0 until nodes.length (sim, de novo indices a partir de zero). E esse sera o estilo principal de hoje.

Mini-exemplo:

import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import org.w3c.dom.NodeList

fun main() {
    val xml = """
        <items>
            <item>Milk</item>
            <item>Bread</item>
        </items>
    """.trimIndent()

    val doc = DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

    val xPath = XPathFactory.newInstance().newXPath()
    val nodes = xPath.evaluate("//item", doc, XPathConstants.NODESET) as NodeList

    for (i in 0 until nodes.length) {
        println(nodes.item(i).textContent.trim())
        // Milk
        // Bread
    }
}

O proprio loop for em Kotlin funciona sobre um iterador, mas NodeList nao e Iterable, por isso usamos o bom e velho estilo por indice.

5. Importando despesas de XML via XPath

Agora vamos fazer o que motivou tudo isso: adicionar ao nosso app de console (digamos — um rastreador de despesas) a capacidade de extrair despesas de um XML. Nao vamos complicar com datas e moedas: que o valor seja Double, e categoria e descricao sejam strings. Vamos usar um XML simples, mas parecido com o mundo real.

Modelo de dados

Vamos criar um modelo compacto Expense. Em um projeto real, ele pode ser mais rico — hoje o foco e o import.

data class Expense(
    val amount: Double,
    val category: String,
    val note: String
)

XML que vamos importar

val xml = """
    <expenses>
        <expense category="food">
            <amount>12.50</amount>
            <note>Lunch</note>
        </expense>
        <expense category="transport">
            <amount>2.75</amount>
            <note>Bus</note>
        </expense>
    </expenses>
""".trimIndent()

Fazendo parse da string para Document

Vamos manter a funcao curta: uma responsabilidade — obter o DOM.

import javax.xml.parsers.DocumentBuilderFactory
import org.w3c.dom.Document

fun parseXml(xml: String): Document =
    DocumentBuilderFactory.newInstance()
        .newDocumentBuilder()
        .parse(xml.byteInputStream())

Selecionando os nos <expense> via XPath

Aqui, o XPath nos da uma lista de elementos <expense ...>...</expense>.

import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import org.w3c.dom.Document
import org.w3c.dom.NodeList

fun selectExpenseNodes(doc: Document): NodeList {
    val xPath = XPathFactory.newInstance().newXPath()
    return xPath.evaluate("/expenses/expense", doc, XPathConstants.NODESET) as NodeList
}

Transformando NodeList em List<Expense>

E aqui que comeca a “ponte” entre o mundo do XML e o mundo do Kotlin.

Note: lemos o atributo category e os textos amount/note. Para amount, fazemos um toDoubleOrNull() cuidadoso — porque os dados podem vir “tortos”, e nosso codigo nao deve cair por causa de um unico caractere estranho.

import org.w3c.dom.Element
import org.w3c.dom.NodeList

fun parseExpenses(nodes: NodeList): List<Expense> {
    val result = mutableListOf<Expense>()

    for (i in 0 until nodes.length) {
        val el = nodes.item(i) as Element
        val category = el.getAttribute("category").trim()
        val amount = el.getElementsByTagName("amount")
            .item(0).textContent.trim().toDoubleOrNull() ?: 0.0
        val note = el.getElementsByTagName("note")
            .item(0).textContent.trim()

        result.add(Expense(amount, category, note))
    }

    return result
}

Sim, aqui ainda existem metodos do DOM (getElementsByTagName) — e isso e normal. XPath nao e obrigado a substituir absolutamente tudo. Nosso objetivo pratico: XPath ajuda a encontrar rapidamente os “pontos de interesse” (por exemplo, todos os expense), e depois voce pode ler os campos internos da forma que fizer mais sentido.

Juntando no main e imprimindo o resultado

fun main() {
    val doc = parseXml(xml)
    val nodes = selectExpenseNodes(doc)
    val expenses = parseExpenses(nodes)

    for (e in expenses) {
        println("${e.category}: ${e.amount} — ${e.note}")
        // food: 12.5 — Lunch
        // transport: 2.75 — Bus
    }
}

Ja agora isso parece uma tarefa real: “tenho um XML vindo de algum lugar (arquivo, rede, integracao); me traga a lista de entidades”.

6. Filtros e legibilidade de XPath no projeto

Filtragem: predicados [...]

Na pratica, as pessoas gostam de XPath nao so pelo “caminho curto”, mas tambem pelos filtros. O filtro sao colchetes que refinam a selecao. O caso mais comum e filtrar por atributo.

Digamos que precisamos apenas das despesas da categoria food. A expressao XPath ficaria assim:

/expenses/expense[@category='food']

Vamos mostrar isso em codigo, bem minimalista:

import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import org.w3c.dom.NodeList

fun selectFoodExpenses(doc: Any): NodeList {
    val xPath = XPathFactory.newInstance().newXPath()
    val expr = "/expenses/expense[@category='food']"
    return xPath.evaluate(expr, doc, XPathConstants.NODESET) as NodeList
}

E agora uma ideia “adulta” sobre manutencao: e melhor manter filtros curtos. Se voce comeca a escrever um XPath que parece SQL “no maximo”, isso costuma ser um sinal de que voce esta tentando enfiar logica de negocio no XPath. XPath e excelente como linguagem de selecao, mas as “regras do produto” ficam melhor no codigo Kotlin, onde e mais facil testar e ler.

Como manter o XPath legivel

Quando XPath aparece na base de codigo, depois de um tempo ele comeca a se multiplicar. E se voce tratar isso sem disciplina, vai acabar com “strings-feitico” sem nomes e sem explicacao. E aquele caso em que o codigo funciona... mas ninguem quer mexer.

Um truque simples costuma ajudar: mover as expressoes para um val com um nome decente, em vez de deixar tudo dentro de evaluate.

const val XP_EXPENSES = "/expenses/expense"
const val XP_FOOD_EXPENSES = "/expenses/expense[@category='food']"
const val XP_FIRST_NOTE = "(/expenses/expense/note/text())[1]"

E depois:

val nodes = xPath.evaluate(XP_EXPENSES, doc, XPathConstants.NODESET) as NodeList

Mais uma dica cautelosa: use // quando voce realmente quiser “procure em todo lugar”. Se a estrutura do documento for conhecida, um caminho explicito como /expenses/expense costuma ser mais confiavel e previsivel do que //expense.

7. Erros tipicos ao usar XPath sobre o DOM

Erro №1: esperar que o XPath retorne null quando nada for encontrado.
No modo de string, evaluate(...) geralmente retorna uma string vazia, e nao null. Se depois voce fizer toInt() ou toDouble() sem validacoes, vai receber uma excecao ja no “vazio”. E mais conveniente criar logo uma util do tipo xPathTextOrNull(...): String? e distinguir honestamente “nao encontrado” de “encontrado, mas vazio”.

Erro №2: abusar de // e obter nos “demais”.
//item parece pratico enquanto o documento e pequeno. Mas se o XML crescer ou surgirem item aninhados em outro lugar, voce de repente vai comecar a importar entidades extras. E um dos erros mais desagradaveis: o codigo nao quebra; ele simplesmente faz “algo diferente”. Em geral, e melhor comecar por um caminho exato a partir da raiz.

Erro №3: esquecer de espacos e quebras de linha em textContent.
XML formatado tem recuos e quebras de linha, e isso tambem e texto. Por isso textContent e o modo de string do evaluate(...) nao raramente retornam valores com espacos “invisiveis”. Se voce nao fizer trim(), pode cair em bugs estranhos do tipo “a categoria parece food, mas nao bate”. A cura e disciplina de normalizacao de strings.

Erro №4: confundir os modos de resultado e depois cair em casts.
Se voce fez evaluate(..., NODESET) — espere um NodeList. Se voce fez apenas evaluate(expr, doc) — espere uma string. Quando isso se mistura, aparece um ClassCastException no momento mais inesperado. Ajuda bastante separar em duas utils: selectNodes(...) e selectText(...), para o contrato ficar claro pelo nome.

Erro №5: pegar item(0) sem verificar se o no existe.
NodeList pode estar vazio. Se voce fizer item(0) direto e fizer cast para Element, voce corre o risco de um NullPointerException ou erro de cast. No seu codigo de importacao, e util verificar primeiro nodes.length; e se era para existir exatamente um no, trate honestamente o caso “sem dados” (mensagem, null, excecao — dependendo do contrato da funcao).

Erro №6: deixar o XPath inteligente demais e esconder logica de negocio em uma string.
Quando a expressao dentro dos colchetes vira um “mini-programa”, a manutencao piora muito: fica dificil debugar, ler e mudar. XPath e uma excelente faca para cortar, mas uma cozinha ruim para preparar o prato inteiro. Em geral, e mais sensato selecionar os nos com XPath e depois filtrar e validar em Kotlin, onde voce controla tipos e erros.

1
Tarefa
Kotlin SELF, nível 50, lição 2
Bloqueado
Nome pelo caminho
Nome pelo caminho
1
Tarefa
Kotlin SELF, nível 50, lição 2
Bloqueado
ID de acesso
ID de acesso
1
Tarefa
Kotlin SELF, nível 50, lição 2
Bloqueado
Lista de compras
Lista de compras
1
Tarefa
Kotlin SELF, nível 50, lição 2
Bloqueado
Filtro por categoria
Filtro por categoria
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION