Procesando XML con DOM

DOM (Document Object Model) es una API creada por W3C (World Wide Web Consortium) que nos permite manipular dinámicamente documentos XML y HTML. Android soporta el nivel de especificación 3, por lo que permite trabajar con definición de tipo de documento (DTD) y validación de documentos. Para no extender en exceso los ejemplos no vamos a entrar en la definición y validación de documentos.

Como ya hemos comentado, el planteamiento de DOM es muy diferente del de SAX. SAX recorre todo el documento XML secuencialmente y lo analiza, pero sin almacenarlo. Por el contrario, DOM permite cargar el documento XML en memoria RAM y manipularlo directamente en memoria. DOM representa el documento como un árbol. Podremos crear nuevos nodos, borrar o modificar los existentes. Una vez dispongamos de la nueva versión, podremos almacenarlo en un fichero o mandarlo por Internet.

Trabajar con DOM tiene sus ventajas frente a SAX: por ejemplo, nos evitamos definir a mano el proceso de parser (en el ejemplo anterior, la clase ManejadorXML) y crear una estructura para almacenar los datos (en el ejemplo anterior, la clase ListaPuntuaciones). Pero también tiene sus inconvenientes: recorrer un documento DOM puede ser algo complejo; además, al tener que cargarse todo el documento en memoria puede consumir excesivos recursos para un dispositivo como un teléfono móvil. Este inconveniente cobra especial relevancia al trabajar con documentos grandes. Para terminar, DOM procesa la información de forma más lenta.
Ejercicio: Almacenando puntuaciones en XML con DOM

Veamos cómo se implementa el ejemplo anterior mediante la API DOM.

1.Crea la clase AlmacenPuntuacionesXML_DOM y escribe el siguiente código:
public class AlmacenPuntuacionesXML_DOM implements AlmacenPuntuaciones{
   private static String FICHERO = "puntuaciones.xml";
   private Context contexto;
   private Document documento;
   private boolean cargadoDocumento;

   public AlmacenPuntuacionesXML_DOM(Context contexto) {
      this.contexto = contexto;
      cargadoDocumento = false;
   }

   @Override
   public void guardarPuntuacion(int puntos, String nombre, long fecha){
      try {
         if (!cargadoDocumento) {
            leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (FileNotFoundException e) {
         crearXML();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      nuevo(puntos, nombre, fecha);
      try {
         escribirXML(contexto.openFileOutput(FICHERO, 
                                          Context.MODE_PRIVATE));
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
   }

   @Override public Vector listaPuntuaciones(int cantidad) {
      try {
         if (!cargadoDocumento) {
            leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (FileNotFoundException e) {
         crearXML();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      return aVectorString();
   }

La clase comienza definiendo una serie de variables y constantes. Son iguales que en el ejemplo anterior, con la excepción de documento de la clase org.w3c.dom.Document.Document. Este objeto mantiene en memoria un documento XML. Tras el constructor, se definen los dos métodos de la interfaz. Su funcionamiento es similar al ejemplo anterior, aunque ahora, en lugar de definir una clase nueva, definiremos los métodos crearXML(), leerXML(), nuevo(), aVectorString() y escribirXML(). Estos métodos tienen por objeto interactuar con el objeto documento.

2. Veamos cómo cómo se implementa el ejemplo anterior mediante la API DOM.

public void crearXML() {
   try {
      DocumentBuilderFactory fabrica = 
                           DocumentBuilderFactory.newInstance();
      DocumentBuilder constructor = fabrica.newDocumentBuilder();
      documento = constructor.newDocument();
      Element raiz = documento.createElement("lista_puntuaciones");
      documento.appendChild(raiz);
      cargadoDocumento = true;
   } catch (Exception e) {
      Log.e("Asteroides", e.getMessage(), e);
   }
}
   
public void leerXML(InputStream entrada) throws Exception {
   DocumentBuilderFactory fabrica = 
                           DocumentBuilderFactory.newInstance();
   DocumentBuilder constructor = fabrica.newDocumentBuilder();
   documento = constructor.parse(entrada);
   cargadoDocumento = true;
}

En crearXML() comenzamos construyendo objetos DocumentBuilderFactory y DocumentBuilder para poder crear una nueva instancia de documento. Creamos un nuevo elemento que es añadido en la raíz de documento. Finalizamos marcando que el documento está creado.
En leerXML() el proceso es similar, aunque ahora llamamos al método parse(), que se encargará de procesar la entrada. Como puedes comprobar, el proceso de lectura del documento es mucho más sencillo en DOM que en SAX.
3. Veamos los siguientes dos métodos. Añade el siguiente código:
 
public void nuevo(int puntos, String nombre, long fecha) {
   Element puntuacion = documento.createElement("puntuacion");
   puntuacion.setAttribute("fecha", String.valueOf(fecha));
   Element e_nombre = documento.createElement("nombre");
   Text texto = documento.createTextNode(nombre);
   e_nombre.appendChild(texto);
   puntuacion.appendChild(e_nombre);
   Element e_puntos = documento.createElement("puntos");
   texto = documento.createTextNode(String.valueOf(puntos));
   e_puntos.appendChild(texto);
   puntuacion.appendChild(e_puntos);
   Element raiz = documento.getDocumentElement();
   raiz.appendChild(puntuacion);
}

public Vector aVectorString() {
   Vector result = new Vector();
   String nombre = "", puntos = "";
   Element raiz = documento.getDocumentElement();
   NodeList puntuaciones = raiz.getElementsByTagName("puntuacion");
   for (int i = 0; i < puntuaciones.getLength(); i++) {
      Node puntuacion = puntuaciones.item(i);
      NodeList propiedades = puntuacion.getChildNodes();
      for (int j = 0; j < propiedades.getLength(); j++) {
         Node propiedad = propiedades.item(j);
         String etiqueta = propiedad.getNodeName();
         if (etiqueta.equals("nombre")) {
            nombre = propiedad.getFirstChild().getNodeValue();
         } else if (etiqueta.equals("puntos")) {
            puntos = propiedad.getFirstChild().getNodeValue();
         }
      }
      result.add(nombre + " " + puntos);
   }
   return result;
}

El método nuevo() tiene por objeto insertar dentro de documento una nueva etiqueta <puntuacion> con los atributos y etiquetas interiores necesarios. Comienza creando el elemento puntuacion, al que añade el atributo fecha. Luego crea el elemento nombre y le añade el texto correspondiente. Este elemento es añadido como hijo a puntuacion. El mismo proceso se repite para el elemento puntos. Para finalizar, el elemento puntuacion es añadido a la raíz del documento. El método aVectorString() devuelve un vector de strings con las puntuaciones almacenadas. Para ello iremos recorriendo todo el documento comenzando por el elemento raiz. Obtenemos la lista de nodos puntuaciones para recorrerla en un bucle for. Para cada uno de estos nodos, obtenemos en propiedades los nodos hijos. Recorremos esta segunda lista en un nuevo bucle, donde analizamos si el nombre del nodo es nombre o puntos, y guardamos el valor asociado al nodo en la variable correspondiente. Antes de pasar al siguiente nodo puntuacion, añadimos lo obtenido al resultado.

4. Veamos el último método. Añade el siguiente código:

 
public void escribirXML(OutputStream salida) throws Exception {
   TransformerFactory fabrica = TransformerFactory.newInstance();
   Transformer transformador = fabrica.newTransformer();
   transformador.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION,"yes");
   transformador.setOutputProperty(OutputKeys.INDENT, "yes");
   DOMSource fuente = new DOMSource(documento);
   StreamResult resultado = new StreamResult(salida);
   transformador.transform(fuente, resultado);
}
}

Este método permite escribir el documento XML utilizando un objeto de la clase javax.xml.transform.Transformer. Tras configurarlo, se le indica como fuente documento y como resultado de la trasformación el OutputStream pasado como parámetro.

5. Modifica el código correspondiente para que la nueva clase pueda ser seleccionada como almacén de las puntuaciones.
6. Verifica el resultado.
Preguntas de repaso:  Trabajando con XML