En esta ocasion os traigo una entrada en la que vamos a aprender a procesar ficheros en formato XML, las API más importantes que se usan en Java y las diferencias básicas entre ellas.
Estas dos API van a ser DOM y SAX.
Recordemos, previo a las explicaciones, que los ficheros XML se usan básicamente para tratar datos, ya sea para estructurarlos, para enviar y recibir datos o como base de datos. La principal idea de los ficheros XML es que son portables, e independientes del lenguaje de programación que usemos para procesarlos, además de ser simples de editar a mano y fáciles de comprender.
Existen dos formas de procesar los ficheros XML en Java, básicamente. Por una parte tenemos el modelo DOM (Document Object Model) y por otra parte tenemos el SAX (Simple Api for XML).
DOM
A la hora de procesar un documento XML con DOM, la representación que tenemos va a ser la de un árbol jerárquico en memoria. Esto implica varias cosas:
- Podemos leer cualquier parte del árbol (todo es un nodo), de forma que podemos procesar de arriba a abajo pero también podemos volver atrás.
- Podemos modificar cualquier nodo del árbol.
- Al estar cargado en memoria, podemos tener una falta de ésta. Con ficheros XML pequeños no tendremos problemas, pero si tuvieramos un árbol muy muy grande entonces tendríamos una falta de heap space.
Comentadas las características de DOM, pasemos a explicar cómo vamos a procesar documentos con DOM. Nuestro árbol en memoria va a ser un Document. Para crear un objeto de esta clase nos valdremos de las factorías de Document, de la siguiente manera:
// Meta @ vidasconcurrentes try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.newDocument(); } catch (ParserConfigurationException e) { e.printStackTrace(); } // forma compacta try { Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); } catch (ParserConfigurationException e) { e.printStackTrace(); }
Ya tenemos un Document en memoria que representa nuestro árbol del que saldrá el fichero XML. Sin embargo este árbol no tiene ni siquiera un nodo raíz, así que el siguiente paso es crearlo:
Element root = doc.createElement("Raiz");
Damos por hecho que la variable doc ya existe del paso previo. El parámetro String que recibe la función createElement() es el texto de la etiqueta.Cada vez que queramos crear un nuevo nodo, deberemos llamar a esta función. En caso de querer añadir el texto correspondiente a un nodo, usaremos la función createTextElement(), que recibe un String que será el texto que contenga.
Element nodo = doc.createElement("NombreElemento"); Text texto = doc.createTextNode("Texto del elemento");
Existe una cosa más en los ficheros XML, y son los atributos. Un nodo (una etiqueta), puede tener una serie de atributos a los cuales nosotros asignamos nombre y valor, o puede no tener ninguno. De esta forma podríamos agregar al nodo raíz el atributo autor con valor vidasConcurrentes, de la siguiente forma:
root.setAttribute("autor", "vidasConcurrentes");
En cualquiera de los casos, en DOM todo es un nodo, de modo que la forma de agregar nodos es la misma, independientemente del tipo de nodo que sea. Sabiendo esto, solamente nos faltaría agregar cada nodo a su nodo padre:
nodo.appendChild(texto); root.appendChild(nodo); doc.appendChild(root);
Con esto hemos conseguido tener nuestro Documentcreado y cargado en memoria. Sin embargo esto no tiene formato y necesitaríamos dárselo para posteriormente escribirlo en algún lugar. En este caso vamos a escribirlo en un fichero de texto:
// Meta @ vidasconcurrentes try { // volcamos el XML al fichero TransformerFactory transFact = TransformerFactory.newInstance(); // añadimos sangrado y la cabecera de XML transFact.setAttribute("indent-number", new Integer(3)); Transformer trans = transFact.newTransformer(); trans.setOutputProperty(OutputKeys.INDENT, "yes"); trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no"); // hacemos la transformacion StringWriter sw = new StringWriter(); StreamResult sr = new StreamResult(sw); DOMSource domSource = new DOMSource(dom); trans.transform(domSource, sr); } catch(Exception ex) { ex.printStackTrace(); }
Esto nos deja en la variable swla representación XML de este documento, con su sangrado correspondiente y listo para ser tratado. Ahora podríamos escribirlo por pantalla, por fichero, mandarlo por un Socket… en este caso agregamos un poco de código para escribirlo en un fichero:
// Meta @ vidasconcurrentes try { // creamos fichero para escribir en modo texto PrintWriter writer = new PrintWriter(new FileWriter("test.xml")); // escribimos todo el arbol en el fichero writer.println(sw.toString()); // cerramos el fichero writer.close(); } catch (IOException e) { e.printStackTrace(); }
Teniendo esto habremos conseguido un fichero de texto XML a partir de un árbol cuya representación será de la siguiente forma:
<Raiz autor="vidasConcurrentes"> <NombreElemento>Texto del elemento</NombreElemento> </Raiz>
Por otra parte, podemos querer leer un fichero XML y procesarlo de alguna manera. Para conseguir esto, primero necesitamos crear un Document en memoria a partir de un fichero XML bien formado:
// Meta @ vidasconcurrentes try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); Document doc = db.parse(new File("test.xml")); doc.getDocumentElement().normalize(); } catch (ParserConfigurationException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }
Es importante darse cuenta de que podemos crear el Document a partir de una sola línea de código, con todos los constructores anidados.
Si tenemos un fichero XML mal formado recibiremos una excepción. La función normalize() elimina nodos de texto vacíos y combina los adyacentes (en caso de que los hubiera).Para poder acceder al nodo raíz del documento, vamos a utilizar la función getDocumentElement() del Document. Y a partir de aquí podemos empezar a recorrer el árbol.Cuando tenemos un nodo, por ejemplo el nodo raíz, podemos obtener todos los nodos que cuelgan de él con la función getChildNodes(). Sin embargo, esta función puede dar lugar a errores, y lo vamos a ver en un ejemplo.
Digamos que tenemos el fichero generado en la parte anterior, y lo leemos con el código que acabamos de poner. Si pidiéramos los hijos de nuestro nodo raíz, esperaríamos obtener sólo un nodo, que en este caso será el nodo con nombre NombreElemento. Pero lo obtenido es diferente:
System.out.println(doc.getDocumentElement().getChildNodes().getLength()); // output: 3
Veamos qué nodos son estos:
NodeList nodosRaiz = doc.getDocumentElement().getChildNodes(); for(int i = 0; i < nodosRaiz.getLength(); i++) { System.out.println(nodosRaiz.item(i).getNodeName()); } // output: #text // NombreElemento // #text
Además, si miramos esos nodos de tipo #text veremos que no son nada. De modo que para evitarlos podríamos filtrarlos con un if(!nodosRaiz.item(i).getNodeName().equals(“#text”)…o también podríamos seleccionar los nodos que deseamos por el nombre, puesto que sabemos el nombre de los nodos de cada nivel:
NodeList nodosRaiz = doc.getDocumentElement().getElementsByTagName("NombreElemento"); System.out.println(nodosRaiz.getLength()); System.out.println(nodosRaiz.item(0).getNodeName()); // output: 1 // NombreElemento
Es decir, la manera de ir leyendo los diferentes hijos de cada nodo es usando o bien getChildNodes() e ir filtrando por nombres (con un case por ejemplo), o bien usar varios getElementsByTagName()en caso de tener etiquetas de nombres diferentes en el mismo nivel.Recordemos que nuestro nodo raíz tenía un atributo. Supongamos que quisiéramos sacar el valor por alguna razón. El código sería el siguiente:
System.out.println(doc.getDocumentElement().getAttribute("autor")); // output: vidasConcurrentes
En caso de no ser el nodo raíz no se pondría doc.getDocumentElement() sino el Element correspondiente.
Con esto hemos cubierto el tema de tratamiento de XML con DOM en Java. Puedes ver un ejemplo completo en nuestro repositorio.
SAX
Al contrario que con DOM, al procesar en SAX no vamos a tener la representación completa del árbol en memoria, pues SAX funciona con eventos. Esto implica:
- Al no tener el árbol completo no puede volver atrás, pues va leyendo secuencialmente.
- La modificación de un nodo es mucho más compleja (y la inserción de nuevos nodos).
- Como no tiene el árbol en memoria es mucho más memory friendly, de modo que es la única opción viable para casos de ficheros muy grandes, pero demasiado complejo para ficheros pequeños.
- Al ser orientado a eventos, el procesado se vuelve bastante complejo.
De esta forma, no vamos a explicar cómo escribir o modificar ficheros con SAX debido a su complejidad y nuestra falta de espacio, sino cómo leerlos y procesarlos.
Vamos a partir de que tenemos ya el fichero XML anterior:
<Raiz autor="vidasConcurrentes"> <NombreElemento>Texto del elemento</NombreElemento> </Raiz>
Para poder procesar un fichero XML con SAX vamos a necesitar que nuestra clase lectora va a necesitar heredar de la clase DefaultHandler (recordemos que, como buena práctica de programación, los datos deben estar separados de la entrada/salida). Además vamos a necesitar un objeto de la clase XMLReader, el cual va a usar la propia clase como ContentHandler y ErrorHandler. El esqueleto de la clase entonces, sería algo así:
public class LectorXML_SAX extends DefaultHandler { private XMLReader reader; public LectorXML_SAX() { try { reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(this); reader.setErrorHandler(this); } catch (SAXException e) { e.printStackTrace(); } } }
Hemos dicho que SAX funciona por eventos. Pero ¿qué eventos son esos? Pues bien, los eventos son los siguientes:
- startDocument(): llamado cuando empieza el documento.
- endDocument(): llamado cuando acaba el documento.
- startElement(): llamado cuando empieza un nodo (por ejemplo, al llegar al < en <Raiz>).
- characters(): llamado al acabar el evento startElement(). Sirve para leer el contenido de una etiqueta (por ejemplo, el texto Texto del elemento de la etiqueta NombreElemento).
- endElement(): llamado al llegar al final de una etiqueta (por ejemplo, al llegar al </ en </NombreElemento>).
Ahora crearemos nuestra función para la lectura, de la siguiente manera:
public void leeXML(String path) { try { reader.parse(path); } catch (IOException e) { e.printStackTrace(); } catch (SAXException e) { e.printStackTrace(); } }
Al ejecutar esto nos damos cuenta de que no hemos obtenido nada. Esto es así porque las funciones explicadas más arriba para los eventos están vacías y necesitamos redefinirlas. En nuestro caso vamos a volcar el contenido del fichero por pantalla, sin sangrado:
@Override public void startElement(String uri, String localName, String name, Attributes atts) { System.out.println("<" + localName + ">"); } @Override public void characters(char[] cadena, int inicio, int length) { if(String.valueOf(cadena, inicio, length).trim().length() != 0) System.out.println(String.valueOf(cadena, inicio, length)); } @Override public void endElement(String uri, String name, String qName) { System.out.println(""); }
Si creasemos un objeto de esta clase e invocásemos a la función leerXML(), obtendríamos lo siguiente por la consola:
<Raiz><NombreElemento>Texto del elemento</NombreElemento></Raiz>
A partir de esto podríamos, en lugar de escribir por pantalla los valores, procesarlos y crear atributos de objetos de clases que hayamos definido nosotros. Un ejemplo de esto se puede encontrar aquí.
A lo largo de esta entrada, por tanto, hemos visto cómo crear ficheros XML con DOM y cómo recorrer el árbol que crea, y también hemos visto cómo leer ficheros XML con SAX.
Nuestra pequeña recomendación es que, si el fichero es pequeño o relativamente pequeño, la opción más cómoda es DOM. Si se necesita modificar el árbol jerárquico, la única opción no es DOM pero sí la más viable, ya que hacer esto con SAX es muy complejo. En caso de solo lectura y procesado, SAX es la opción más rápida y que menos memoria gasta, pero es mucho más compleja y es menos instintiva. Si el documento es demasiado extenso… entonces SAX es la única opción.
A partir de estos dos métodos surgieron diversas librerías como JDOM, que facilitan este procesado de datos.
Para acabar, sólo queda recordar que contamos con un ejemplo completo en nuestro repositorio.