No es un bug, es una característica no documentada

sábado, 17 de octubre de 2015

Acceso a datos. Ficheros XML

15:55 Posted by Inazio , , 7 comments
La tecnología DOM (Document Object Model) es una interfaz de programación que permite analizar y manipular dinámicamente y de manera global el contenido, el estilo y la estructura de un documento. Tiene su origen en el Consorcio World Wide Web (W3C). Esta norma está definida en tres niveles: Nivel 1, que describe la funcionalidad básica de la interfaz DOM, así como el modo de navegar por el modelo de un documento general; Nivel 2, que estudia tipos de documentos específicos (XML, HTML, CSS); y Nivel 3, que amplía las funcionalidades de la interfaz para trabajar con tipos de documentos específicos.

Para trabajar con un documento XML primero se almacena en memoria en forma de árbol con nodos padre, nodos hijo y nodos finales que son aquellos que no tienen descendientes. En este modelo todas las estructuras de datos del documento XML se transforman en algún tipo de nodo, y luego esos nodos se organizan jerárquicamente en forma de árbol para representar la estructura descrita por el XML.

Una vez creada en memoria esta estrucura, los métodos de DOM permiten recorrer los diferentes nodos del árbol y analizar a qué tipo particular pertenecen. En función del tipo de nodo, la interfaz ofrece una serie de funcionalidades u otras para poder trabajar con la información que contienen.

El siguiente documento XML muestra una serie de libros. Cada uno de ellos está definido por un atributo publicado_en, un texto que indica el año de publicación del libro y por dos elementos hijo: Título y Autor.

<?xml version="1.0" encoding="UTF-8"?>
<Libros
    xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
    xsi:noNamespaceSchemaLocation='LibrosEsquema.xsd'>

    <Libro publicado_en="1840">
        <Titulo>El Capote</Titulo>
        <Autor>Nikolai Gogol</Autor>
    </Libro>
    <Libro publicado_en="2008">
        <Titulo>El Sanador de Caballos</Titulo>
        <Autor>Gonzalo Giner</Autor>
    </Libro>
    <Libro publicado_en="1981">
        <Titulo>El Nombre de la Rosa</Titulo>
        <Autor>Umberto Eco</Autor>
    </Libro>
</Libros>


Un esquema del árbol DOM que representaría internamente este documento es mostrado en la siguiente imagen. El árbol DOM se ha creado en base al documento XML anterior. Sin embargo, para no enturbiar la claridad del gráfico solo se ha desarrollado hasta el final uno de los elementos Libro, dejando los otros dos sin detallar.


La generación del árbol DOM a partir de un documento XML se hace de la siguiente manera:
1.       Aunque el documento XML tiene asociado un esquema XML, la librería de DOM, a no ser que se le diga lo contrario, no necesita validar el documento con respecto al esquema para poder generar el árbol. Solo tiene en cuenta que el documento esté bien formado.
2.       El primer nodo que se crea es el nodo Libros que representa el elemento <Libros>
3.       De <Libros> cuelgan en el documento tres hijos <Libro> de tipo elemento, por tanto el árbol crea tres nodos Libro descendiente de Libro.
4.       Cada elemento <Libro> en el documento tiene asociado un atributo publicado_en. En DOM, los atributos son listas con el nombre del atributo y el valor. La lista contiene tantas tuplas (nombre, valor) como atributos haya en el elemento. En este caso solo hay un atributo en el elemento <Libro>.
5.       Cada <Libro> tiene dos elementos que descienden de él y que son <Titulo> y <Autor>. Al ser elementos, estos son representados en DOM como nodos descendientes de Libro, al igual que se ha hecho con Libro al descender de Libros.
6.       Cada elemento <Titulo> y <Autor> tiene un valor que es de tipo cadena de texto. Los valores de los elementos son representados en DOM como nodos #text. Sin duda esta es la parte más importante del modelo DOM. Los valores de los elementos son nodos también, a los que internamente DOM llama #text y que descienden del nodo que representa el elemento que contiene ese valor. DOM ofrece funciones para recuperar el valor de los nodos #text. Un error muy común cuando se empieza por primera vez a trabajar con árboles DOM es pensar que, por ejemplo, el valor del nodo Título es el texto que contiene el elemento <Titulo> en el documento XML. Sin embargo, eso no es así. Si se quiere recuperar el valor de un elemento, es necesario acceder al nodo #text que desciende de ese nodo y de él recuperar su valor.
7.       Hay que tener en cuenta que cuando se edita un documento XML, al ser este de tipo texto, es posible que, por legibilidad, se coloque cada uno de los elementos en líneas diferentes o incluso que se utilicen espacios en blanco para separar los elementos y ganar en claridad. Eso mismo se ha hecho con el documento XML anterior. El documento queda mucho más legible así que si se pone todo en una única línea sin espacios entre las etiquetas.

DOM no distingue cuándo un espacio en blanco o un salto de línea (\n) se hace porque es un texo asociado a un elemento XML o es algo que se hace por “estética”. Por eso, DOM trata todo lo que es texto de la misma manera, creando un nodo de tipo #text y poniéndole el valor dentro de ese nodo. Eso mismo es lo que ha ocurrido en el ejemplo de la imagen. El nodo #text que desciendo de Libro y que tiene como valor “\n” (salto de línea) es creado por DOM ya que, por estética, se ha hecho un salto de línea dentro del documento XML, para diferenciar claramente que la etiqueta <Titulo> es descendiente de <Libro>. Sin duda, en el documento XML hay muchos más “saltos de línea” que se han empleado para dar claridad al documento, sin embargo, en el ejemplo del modelo DOM no se han incluido ya que tantos nodos con “saltos de línea” dejarían el esquema ilegible.
8.       Por último, un documento XML tiene muchas más “cosas” que las mostradas en el ejemplo anterior, por ejemplo: comentarios, encabezados, espacios en blanco, entidades, etc. son algunas de ellas. Cuando se trabaja con DOM, rara vez esos elementos son necesarios para el programador, por lo que la librería DOM ofrece funciones que omiten estos elementos antes de crear el árbol, agilizando así el acceso y modificación del árbol DOM.

DOM y Java


DOM ofrece una manera de acceder a documentos XML tanto para ser leído como para ser modificado. Su único inconveniente es que en el árbol DOM se crea todo en memoria principal, por lo que si el documento XML es muy grande, la creación y manipulación de DOM será intratable.

DOM, al ser una propuesta W3C, fue muy apoyado desde el principio, y eso ocasionó que aparecieran un gran número de librerías (parsers) que permitían el acceso a DOM desde la mayoría de lenguajes de programación. Esta sección se centra en Java como lenguaje para manipular DOM, pero dejando claro que otros lenguajes también tienen sus librerías (muy similares) para gestionarlo, por ejemplo Microsoft .NET.

Java y DOM han evolucionado mucho en el último lustro. Muchas han sid las propiestas para trabajar desde Java con la gran cantidad de parsers DOM que existen. Sin embargo, para resumir, actualmente la propuesta principal se reduce al uso de JAXP (Java API for XML Processing). A través de JAXP los diversos parsers garantizan la interoperabilidad de Java. JAXP es incluido en todo JDK (superior a 1.4) diseñado para Java. La siguiente imagen muestra una estructura de bloques de una aplicación que accede a DOM. Una aplicación que desea manipular documentos XML accede a la interfaz JAXP porque le ofrece una manera transparente de utilizar los diferentes parsers que hay para manejar XML. Estos parsers son para DOM (XT, Xalan, Xerces, etc.) pero también pueden ser parsers para SAX (otra tecnología alternativa a DOM, que se verá en la siguiente sección).


En los ejemplos mostrados en las siguientes secciones se utilizará la librería Xerces para procesar representaciones en memoria de un documento XML considerado un árbol DOM. Entre los paquetes concretos que se usarán destacan:
è javax.xml.parsers.*, en concreto javax.xml.parsers.DocumentBuilder y javax.xml.parsers.DocumentBuilderFactory, para el acceso desde JAXP.
è org.w3c.dom.* que representa el modelo DOM según la W3C (objetos Node, NodeList, Document, etc. y los métodos para manejarlos). Por lo tanto, todas las especificaciones de los diferentes niveles y módulos que componen DOM están definidas en este paquete.

El siguiente esquema relaciona JAXP con el acceso a DOM. Desde la interfaz JAXP se crea un DocumentBuilderFactory. A partir de esa factoría se crea un DocumentBuilder que permitirá cargar en él la estructura del árbol DOM (Árbol de Nodos) desde un fichero XML (Entrada XML).


La clase DocumentBuilderFactory tiene métodos importantes para indicar qué interesa y qué no del fichero XML para ser incluido en el árbol DOM, o si se desea validar el XML con respecto a un esquema. Algunos de estos métodos son los siguientes:

è setIgnoringComments(boolean ignore): Sirve para ignorar los comentarios que tenga el fichero XML.
è setIgnoringElementContentWhitespace(boolean ignore): Es útil para eliminar los espacios en blanco que no tienen significado.
è setNamespaceAware(boolean aware): Usado para interpretar el documento usando el espacio de nombres.
è setValidating(boolean validate): Valida el documento XML según el esquema definido.

Con respecto a los métodos que sirven para manipular el árbol DOM, que se encuentran en el paquete org.w3c.dom, destacan los siguientes étodos asociados a la clase Node:

è Todos los nodos contienen los métodos getFirstChild() y getNextSibling() que permiten obtener uno a uno los nodos descendientes de un nodo y sus hermanos.
è El método getNodeType() devuelve una constante para poder distinguir entre los diferentes tipos de nodos: nodo de tipo Elemento (ELEMENT_NODE), nodo de tipo #text (TEXT_NODE), etc. Este método y las constantes asociadas son especialmente importantes a la hora de recorrer el árbol ya que permiten ignorar aquellos tipos de nodos que no interesan (por ejemplo, los #text que tengan saltos de línea).
è El método getAttributes() devuelve un objeto NamedNodeMap() (una lista con sus atributos) si el nodo es del tipo Elemento.
è Los métodos getNodeName() y getNodeValue() devuelven el nombre y el valor de un nodo de forma que se pueda hacer una búsqueda selectiva de un nodo concreto. El error típico es creer que el método getNodeValue() aplicado a un nodo de tipo Elemento (por ejemplo, <Titulo>) devuelve el texto que contiene. En realidad, es el nodo de tipo #text (descendiente de un nodo tipo Elemento) el que tiene el texto que representa el título del libro y es sobre él sobre el que hay que aplicar el método getNodeValue() para obtener el título.

Abrir DOM desde Java


Para abrir un documento XML desde Java y crear un árbol DOM con él se utilizan las clases DocumentBuilderFactory y DocumentBuilder, que pertenecen al paquete javax.xml.parsers, y Document, que representa un documento en DOM y pertenece al paquete org.w3c.dom. Aunque existen otras posibilidades, en el ejemplo mostrado seguidamente se usa un objeto de la clase File para indicar la localización del archivo XML.

public int abrir_XML_DOM(File fichero){

     doc = null; // doc es de tipo Document y representa al árbol DOM
    
     try{
          // Se crea un objeto DocumentBuilderFactory
          DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
         
          // Indica que el modelo DOM no debe contemplar los comentarios que tenga el XML
          factory.setIgnoringComments(true);
         
          // Ignora los espacios en blanco que tenga el documento
          factory.setIgnoringElementContentWhitespace(true);
         
          // Se crea un objeto DocumentBuilder para cargar en él la estructura del árbol DOM a partir del XML seleccionado
          DocumentBuilder builder = factory.newDocumentBuilder();
         
          // Interpreta (parsea) el documento XML (file) y genera el DOM equivalente
          doc = builder.parse(fichero);
         
          // Ahora doc apunta al árbol DOM listo para ser recorrido
         
          return 0;
     }
     catch(Exception e){
          e.printStackTrace();
          return -1;
     }

}

Como se puede entender siguiendo los comentarios del código, primeramente se crea Factory y se prepara el parser para interpretar un fichero XML en el cual ni los espacios en blanco ni los comentarios serán tenidos en cuenta a la hora de generar el DOM. El método parse() de DocumentBuilder (creado a partir de un DocumentBuilderFactory) recibe como entrada un File con la ruta del fichero XML que se desea abrir y devuelve un objeto de tipo Document. Este objeto (doc) es el árbol DOM cargado en memoria, listo para ser tratado.

Recorrer un árbol DOM


Para recorrer un árbol DOM se utilizan las clases Node y NodeList que pertenecen al paquete org.w3c.dom. En el ejemplo de esta sección se ha recorrido el árbol DOM creado del documento XML. El resultado de procesar dicho doumento origina la siguiente salida:

Publicado en: 1840
El autor es: Nikolai Gogol
El título es: El Capote
-------------------
Publicado en: 2008
El autor es: Gonzalo Giner
El título es: El Sanador de Caballos
-------------------
Publicado en: 1981
El autor es: Umberto Eco
El título es: El Nombre de la Rosa
-------------------

El siguiente código recorrer el árbol DOM para dar la salida anterior con los nombres de los elementos que contiene cada <Libro> (<Titulo>, <Autor>), sus valores y el valor y nombre del atributo de <Libro> (publicado_en).

public String recorrerDOMyMostrar(Document doc){

     String datos_nodo[] = null;
     String salida = "";
     Node node;
    
     // Obtiene el primer nodo del DOM (primer hijo)
     Node raiz = doc.getFirstChild();
    
     // Obtiene una lsita de nodos con todos los nodos hijo del raíz
     NodeList nodeList = raiz.getChildNodes();
    
     // Procesa los nodos hijo
     for (int i = 0; i < nodeList.getLength(); i++){
          node = nodeList.item(i);
          if (node.getNodeType() == Node.ELEMENT_NODE){
                // Es un nodo libro
                datos_nodo = procesarLibro(node);
                salida = salida + "\n " + "Publicado en: " + datos_nodo[0];
                salida = salida + "\n " + "El autor es: " + datos_nodo[2];
                salida = salida + "\n " + "El título es: " + datos_nodo[1];
                salida = salida + "\n --------------------";
          }
     }
     return salida;
}

El código anterior, partiendo del objeto tipo Document (doc) que contiene el árbol DOM, recorre dicho árbol para sacar los valores del atributo de <Libro> y de los elementos <Titulo> y <Autor>. Es una práctica común al trabajar con DOM comprobar el tipo del nodo que se está tratanto en cada momento ya que, como se ha comentado antes, un DOM tiene muchos tipos de nodos que no siempre tienen información que merezca la pena explorar. En el código, solo se presta atención a los nodos de tipo Elemento (ELEMENT_NODE) y de tipo #text (TEXT_NODE).

Por último queda ver el código del método ProcesarLibro.

protected String[] procesarLibro(Node n){

     String datos[] = new String[3];
     Node ntemp = null;
     int contador = 1;
    
     // Obtiene el valor del primer atributo del nodo (solo uno en este ejemplo)
     datos[0] = n.getAttributes().item(0).getNodeValue();
    
     // Obtiene los hijos del Libro (título y autor)
     NodeList nodos = n.getChildNodes();
     for (int i = 0; i <nodos.getLength(); i++){
          ntemp = nodos.item(i);
          if(ntemp.getNodeType() == Node.ELEMENT_NODE){
                /* Importante: para obtener el texto con el título y autor se accede al nodo TEXT hijo de ntemp y se saca su valor */
                datos[contador] = ntemp.getChildNodes().item(0).getNodeValue();
                contador++;
          }
     }
     return datos;
}

En el código anterior lo más destacable es:
è Que el programador sabe que el elemento <Libro> solo tiene un atributo, por lo que accede directamente a su valor (n.getAttributes().item(0).getNodeValue()).
è Una vez detectado que se está en el nodo de tipo Elemento (que puede ser el título o el autor) entonces se obtiene el hijo de este (tipo #text) y se consulta su valor (ntemp.getChildNodes().item(0).getNodeValue()).

Modificar y serializar


Ademñas de recorrer en modo “solo lectura” un árbol DOM, este también puede ser modificado y guardado en un fichero para hacerlo persistente. En esta sección se muestra un código para añadir un nuevo elemento a un árbol DOM y luego guardar todo el árbol en un documento XML. Es importante destacar lo fácil que es realizar modificaciones con la librería DOM. Si esto mismo se quisiera hacer accediendo a XML como si fuera un fichero de texto “normal” (usando FileWriter por ejemplo), el código necesario sería mucho mayor y el rendimiento en ejecución bastante más bajo.

En el siguiente código añade un nuevo libro al árbol DOM (doc) con los valores de publicado_en, titulo y autor, pasados como parámetros.

public int annadirDOM(Document doc, String titulo, String autor, String anno){

     try{
          // Se crea un nodo tipo Element con nombre 'titulo' (<Titulo>)
          Node ntitulo = doc.createElement("Titulo");
         
          // Se crea un nodo tipo texto con el títlo del libro
          Node ntitulo_text = doc.createTextNode(titulo);
         
          // Se añade el nodo de texto con el título como hijo del elemento Titulo
          ntitulo.appendChild(ntitulo_text);
         
          // Se hace lo mismo que con titulo a autor (<Autor>)
          Node nautor = doc.createElement("Autor");
          Node nautor_text = doc.createTextNode(autor);
          nautor.appendChild(nautor_text);
         
          // Se crea un nodo de tipo elemento (<libro>)
          Node nlibro = doc.createElement("Libro");
         
          //  Al nuevo nodo libro se le añade un atributo publicado_en
          ((Element)nlibro).setAttribute("publicado_en", anno);
         
          // Se añade a libro el nodo autor y titulo creados antes
          nlibro.appendChild(ntitulo);
          nlibro.appendChild(nautor);
         
          /* Finalmente, se obtiene el primer nodo del documento y a él se le
          añade como hijo el nodo libro que ya tiene colgando todos sus
          hijos y atributos creados antes. */
          Node raiz = doc.getChildNodes().item(0);
          raiz.appendChild(nlibro);
         
          return 0;
     }
     catch(Exception e){
          e.printStackTrace();
          return -1;
     }
}

Revisando el código anterior se puede apreciar que para añadir nuevos nodos a un árbol DOM hay que conocer bien cómo de diferente es el modelo DOM con respecto al documento XML original. La prueba es la necesidad de crear nodos de texto que sean descendientes de los nodos de tipo Elemento para almacenar los valores XML.

Una vez se ha modificado en memoria un árbol DOM, éste puede ser llevado a fichero para lograr la persistencia de los datos. Esto se puede hacer de muchas maneras. Una alternativa se recoge en el siguiente código.

En el ejemplo se usa las clases XMLSerializer y OutputFormat que están definidas en los paquetes com.sun.org.apache.xmo.internal.serialize.OutputFormat y com.sun.org.apache.xml.internal.serialize.XMLSerialize. Estas clases realizan la labor de serializar un documento XML.

Serializar (en inglés marshalling) es el proceso de convertir el estado de un objeto (DOM en nuestro caso) en un formato que se pueda almacenar. Serializar es, por ejemplo, llevar el estado de un objeto en Java a un fichero o, como en nuestro caso, llevar los objetos que componen un árbol DOM a un fichero XML. El proceso contrario es decir, pasar el contenido de un fichero a una estructura de objetos se conoce por unmarshalling.

La clase XMLSerializer se encarga de serializar un árbol DOM y llevarlo a fichero en formato XML bien formado. En este proceso se utiliza la clase OutputFormat porque permite asignar diferentes propiedades de formato al fichero resultado, por ejemplo que el fichero esté indentado (anglicismo, indent) es decir, que los elementos hijos de otro elemento aparezcan con más tabulación a la derecha para así mejorar la legibilidad del fichero XML.

public int guardarDOMcomoFILE(){

     try{
          // Crea un fichero llamado salida.xml
          File archivo_xml = new File("salida.xml");
         
          // Especifica el formato de salida
          OutputFormat format = new OutputFormat(doc);
         
          // Especifica que la salida esté indentada
          format.setIndenting(true);
         
          // Escribe el contenido en el FILE
          XMLSerializer serializer = new XMLSerializer(new FileOutputStream(archivo_xml), format);
          serializer.serialize(doc);
         
          return 0;
     }
     catch(Exception e){
          return -1;
     }
}

Según el código anterior, para serializar un árbol DOM se necesita:

è Un objeto File que representa al fichero resultado (en el ejemplo será salida.xml).
è Un objeto OutputFormat que permite indicar pautas de formato para la salida
è Un objeto XMLSerializer que se construye con el File de salida y el formato definido con un OutputFormat. En el ejemplo utiliza un objeto FileOutputStream para representar el flujo de salida.
è Un método serialize() de XMLSerializer que recibe como parámetro el Document (doc) que quiere llevar a fichero y lo escribe.

Crear documentos XML desde cero


Para realizar todo lo anterior, necesitaremos tener un documento XML ya creado. Pero, ¿cómo podemos generarlo en Java desde cero?

Hay dos métodos.

Método 1. Modificar el método guardarDOMcomoFILE para que acepte un argumento de tipo cadena, el nombre del fichero XML destino.

package GestionDOM;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.DOMImplementation;

public class PruebaGestionarDOM{
     public static void main(String[] args){
         
          GestionarDOM ObjetoDOM = new GestionarDOM();
          ObjetDOM.doc = null;
         
          try{
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = factory.newDocumentBuilder();
                DOMImplementation implementation = builder.getDOMImplementation();
                ObjetDOM.doc = implementation.createDocument(null, "Libros", null);
                ObjetDOM.doc.setXMLVersion("1.0");
                ObjetDOM.annadirDOM("Desarrollo de Interfaces", "Pablo Martinez", "2010");
                ObjetDOM.annadirDOM("Acceso a datos", "Alberto Carrera", "2011");
                ObjetDOM.annadirDOM("Formación y orientación laboral", "Belén Carrera", "2012");
                ObjetDOM.guardarDOMcomoFILE("D:\\profesores.XML");
          }
          catch(Exception e){
                e.printStackTrace();
          }
     }
}

Método 2. XMLSerializer es una API propietaria y puede desaparecer en un futuro, así que otro forma de generarlo es con la clase Transformer.

package GestionDOM;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.*;
import javax.xml.transform.dom.DOMSource;
import org.w3c.dom.DOMImplementation;

public class PruebaGestionarDOM_1{
     public static void main(String[] args){
         
          GestionarDOM ObjetoDOM = new GestionarDOM();
          ObjetDOM.doc = null;
         
          try{
                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
                DocumentBuilder builder = factory.newDocumentBuilder();
                DOMImplementation implementation = builder.getDOMImplementation();
                ObjetDOM.doc = implementation.createDocument(null, "Libros", null);
                ObjetDOM.doc.setXMLVersion("1.0");
                ObjetDOM.annadirDOM("Desarrollo de Interfaces", "Pablo Martinez", "2010");
                ObjetDOM.annadirDOM("Acceso a datos", "Alberto Carrera", "2011");
                ObjetDOM.annadirDOM("Formación y orientación laboral", "Belén Carrera", "2012");
               
                /* A partir de aquí cambio respecto a la versión anterior.
                Lo podría haber puesto como segundo método de guardar de ObjetoDOM y llamarlo */
                Soruce source = new DOMSource(ObjetDOM.doc);
                Result result = new StreamResult(new java.io.File("D:\profesores.XML"));
                Transformer transformer = TransformerFactory.newInstance().newTransformer();
                transformer.transform(source, result);
          }
          catch(Exception e){
                e.printStackTrace();
          }
     }
}

7 comentarios:

  1. Me ha resultado de gran ayuda leer este artículo para comprender el acceso a archivos XML utilizando las librería DOM. Muy completo y muy bien explicado. Además en español que también ayuda. Gracias por compartir esta información de forma desinteresada.

    ResponderEliminar
  2. Muy buena guía para explicar como funcionan en general las clases y el proceso 👍👍

    ResponderEliminar
  3. Genial encontrar contenido así para entender la materia cuando no te facilitan estas explicaciones en clase! Mil gracias

    ResponderEliminar