Java XML DOM Parser
Aprende a analizar, navegar, modificar y serializar XML en Java con el parser DOM integrado, con un ejemplo completo de lectura-modificación-escritura.
El parser DOM (Document Object Model) lee un documento XML completo en memoria y te entrega un árbol de nodos que puedes navegar, consultar y modificar. Viene incluido en el JDK en javax.xml.parsers y org.w3c.dom, por lo que no necesitas añadir nada al classpath. DOM es la herramienta adecuada cuando los documentos son lo suficientemente pequeños para caber en memoria y necesitas acceso aleatorio a cualquier parte del árbol: leer un archivo de configuración, transformar una carga útil o construir XML de forma programática.
Este capítulo recorre el ciclo de vida completo: cómo DOM modela un documento, cómo analizar una fuente en un árbol, cómo leer y modificar nodos, y cómo escribir de nuevo el árbol como XML. Si eres nuevo en XML en Java, comienza con la introducción a XML; para documentos grandes donde la memoria importa, compara DOM con el parser SAX de streaming.
Cómo DOM modela un documento
DOM convierte el marcado en un árbol de objetos Node. Cada elemento, atributo, fragmento de texto y comentario es un nodo, y todo el documento cuelga de una única raíz Document. Lees el árbol preguntando a los nodos por sus hijos, y lo modificas creando, moviendo o eliminando nodos.
| Concepto | Interfaz | Qué representa |
|---|---|---|
| Documento | Document | El archivo analizado completo; punto de entrada al árbol |
| Elemento | Element | Una etiqueta como <book>, con atributos e hijos |
| Atributo | Attr | Un par nombre/valor en un elemento |
| Texto | Text | Datos de caracteres dentro de un elemento |
| Lista de nodos | NodeList | Una colección de nodos ordenada e indexable |
La compensación clave: DOM es conveniente porque todo el árbol es accesible, pero carga todo en memoria a la vez. Para feeds de varios gigabytes, recurrirías a SAX o StAX, que hacen streaming del documento sin construir un árbol. Y si estás mapeando XML hacia y desde objetos Java en lugar de recorrer nodos crudos, JAXB suele requerir menos código que DOM escrito a mano.
Analizar un documento
Nunca construyes un parser directamente. Le pides a un DocumentBuilderFactory un DocumentBuilder, luego llamas a parse sobre un stream, archivo o URI. Configura la factory antes de construir: la conciencia de namespaces y la validación son interruptores a nivel de factory.
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new File("library.xml"));
// Collapse adjacent text nodes and drop empty ones so getTextContent is clean.
doc.getDocumentElement().normalize();parse lanza SAXException para XML malformado e IOException si la fuente no puede leerse, por lo que ambas son excepciones comprobadas que debes manejar. Llamar a normalize() una vez tras el análisis fusiona nodos de texto divididos, una fuente común de sorpresas al leer texto de elementos.
Navegar el árbol
Dos métodos cubren la mayor parte de la lectura: getElementsByTagName encuentra todos los descendientes con una etiqueta dada, y getChildNodes devuelve los hijos directos de un nodo. Recuerda que getChildNodes incluye nodos de texto con espacios en blanco, así que filtra por tipo de nodo cuando solo quieras elementos.
Element root = doc.getDocumentElement(); // <library>
NodeList books = doc.getElementsByTagName("book"); // every <book> in the tree
for (int i = 0; i < books.getLength(); i++) {
Element book = (Element) books.item(i);
String id = book.getAttribute("id"); // attribute by name
String title = book.getElementsByTagName("title")
.item(0).getTextContent(); // first child <title> text
System.out.println(id + " -> " + title);
}NodeList está basado en índices, no es iterable, por lo que haces el bucle con getLength() e item(i). getAttribute devuelve una cadena vacía (nunca null) cuando el atributo está ausente, algo que conviene saber antes de escribir una comprobación de null que nunca se dispara.
Modificar y crear nodos
El árbol DOM es mutable. Cambias texto con setTextContent, cambias atributos con setAttribute, y haces crecer el árbol creando nodos mediante los métodos factory de Document y añadiéndolos. Los nodos deben ser creados por el mismo documento en el que se insertarán.
// Update existing content.
Element price = (Element) book.getElementsByTagName("price").item(0);
price.setTextContent("49.50");
price.setAttribute("currency", "USD");
// Build a new subtree and attach it.
Element added = doc.createElement("book");
added.setAttribute("id", "b3");
Element title = doc.createElement("title");
title.setTextContent("The Pragmatic Programmer");
added.appendChild(title);
doc.getDocumentElement().appendChild(added);createElement crea un nodo desconectado; nada aparece en el documento hasta que lo añadas con appendChild en algún lugar. Para eliminar un nodo, llama a parent.removeChild(child).
Escribir el árbol de vuelta
DOM no tiene un toString() que produzca XML. Para serializar, pasa el documento a un Transformer con un DOMSource y un StreamResult. El mismo paquete javax.xml.transform permite escribir en un archivo, una cadena o cualquier stream, y establecer opciones de formateo.
Transformer tr = TransformerFactory.newInstance().newTransformer();
tr.setOutputProperty(OutputKeys.INDENT, "yes");
tr.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
tr.transform(new DOMSource(doc), new StreamResult(new File("out.xml")));Para entradas no confiables, refuerza la factory antes de analizar: deshabilita las declaraciones DOCTYPE con factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) para bloquear ataques XXE (XML External Entity).
Un ejemplo completo
Este programa analiza un documento de biblioteca en memoria, lee cada libro, eleva cada precio un 10%, inserta un nuevo libro y serializa la primera línea de precio actualizada de vuelta a XML, ejercitando el ciclo completo de lectura-modificación-escritura sobre un único árbol.
Qué extraer de la ejecución:
- El elemento raíz se imprime como
libraryporquegetDocumentElement()devuelve el único nodo superior del que cuelga todo lo demás. getElementsByTagName("book")reporta un recuento de 2 antes de la inserción, confirmando que recogió ambos descendientes<book>de la raíz.- Los precios se leen con
getTextContent()y se analizan conDouble.parseDouble, de modo que45.00y38.50suman el total impreso de83.50. - Tras
appendChild, la misma consultagetElementsByTagName("book")devuelve 3, mostrando que el árbol en vivo recogió el nodo creado condoc.createElement. - La primera línea de precio serializada muestra
49.50, lo que prueba quesetTextContentmutó el nodo en memoria y elTransformerescribió el valor actualizado (45.00 elevado un 10%) de vuelta a XML.