Procesando XML con SAX

 

Procesando XML con SAX

El uso de la API SAX (Simple API for XML) se recomienda cuando se desea un programa de análisis rápido y se quiere reducir al mínimo el consumo de memoria de la aplicación. Eso hace que sea muy apropiado para un dispositivo móvil con Android. También resulta ventajoso para procesar ficheros de gran tamaño.

SAX nos facilita realizar un parser (analizador) sobre un documento XML para así poder analizar su contenido. Ha de quedar claro que SAX no almacena los datos. Por lo tanto necesitaremos una estructura de datos donde guardar la información contenida en el XML. Para realizar este parser se van a ir generando una serie de eventos a medida que se vaya leyendo el documento secuencialmente. Por ejemplo, al analizar el documento XML anterior, SAX generará los siguientes eventos:

Comienza elemento: lista_puntuaciones

Comienza elemento: puntuacion, con atributo fecha="1288122023410"

Comienza elemento: nombre

Texto de nodo: Mi nombre

Finaliza elemento: nombre

Comienza elemento: puntos

Texto de nodo: 45000

Finaliza elemento: puntos

Finaliza elemento: puntuacion

Comienza elemento: puntuacion, con atributo fecha="1288122428132"

Comienza elemento: nombre

Texto de nodo: Otro nombre

Finaliza elemento: nombre

Comienza elemento: puntos

Texto de nodo: 31000

Finaliza elemento: puntos

Finaliza elemento: puntuacion

Finaliza elemento: lista_puntuaciones

Para analizar un documento mediante SAX, vamos a escribir métodos asociados a cada tipo de evento. Este proceso se realiza extendiendo la clase DefaultHandler que nos permite reescribir 5 métodos. Los métodos listados a continuación serán llamados a medida que ocurran los eventos listados anteriormente.

startDocument():Comienza el Documento XML.

endDocument()Finaliza documento XML.

startElement(String uri, String nombreLocal, String nombreCualif, Attributes atributos): Comienza una nueva etiqueta; se indican los parámetros

uri: La uri del espacio de nombres o vacío, si no se ha definido.

nombreLocal: Nombre local de la etiqueta sin prefijo.

nombreCualif: Nombre cualificado de la etiqueta con prefijo.

atributos: Lista de atributos de la etiqueta.

endElement(String uri, String nombreLocal, String nombreCualif): Termina una etiqueta.

characters(char ch[], int comienzo, int longitud): Devuelve en ch los caracteres dentro de una etiqueta. Es decir, en <etiqueta> caracteres </etiqueta> devolvería caracteres. Para obtener un String con estos caracteres: String s = new String(ch,comienzo,longitud). Más adelante veremos un ejemplo de cómo utilizar este método.

Ejercicio paso a paso: Almacenando  puntuaciones en XML con SAX

Una vez descritos los principios de trabajo con SAX, pasemos a implementar la interfaz AlmacenPuntuaciones mediante esta API.

1.     Crea la clase AlmacenPuntuacionesXML_SAX en la aplicación Asteroides y escribe el siguiente código:

  public class AlmacenPuntuacionesXML_SAX implements AlmacenPuntuaciones {
   private static String FICHERO = "puntuaciones.xml";
   private Context contexto;
   private ListaPuntuaciones lista;
   private boolean cargadaLista;
   
   public AlmacenPuntuacionesXML_SAX(Context contexto) {
      this.contexto = contexto;
      lista = new ListaPuntuaciones();
      cargadaLista = false;
   }

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

   @Override
   public Vector<String> listaPuntuaciones(int cantidad) {
      try {
         if (!cargadaLista){
            lista.leerXML(contexto.openFileInput(FICHERO));
         }
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
      return lista.aVectorString();
   }

La nueva clase comienza definiendo una serie de variables y constantes. En primer lugar, el nombre del fichero donde se guardarán los datos. Con el valor indicado, el fichero se almacenará en /data/data/org.example.asteroides/ files/puntuaciones.xml. Pero puedes almacenarlos en otro lugar, como por ejemplo en la memoria SD. La variable más importante es lista de la clase ListaPuntuaciones. En ella guardaremos la información contenida en el fichero XML. Esta clase se define a continuación. La variable cargadaLista nos indica si lista ya ha sido leída desde el fichero.

El código continúa sobrescribiendo los dos métodos de la interfaz. En guardarPuntuacion() comenzamos verificando si lista ya ha sido cargada, para hacerlo en caso necesario. Es posible que el programa se esté ejecutando por primera vez, en cuyo caso el fichero no existirá. En este caso se producirá una excepción de tipo FileNotFoundException al tratar de abrir el fichero. Esta excepción es capturada por nuestro código, pero no realizamos ninguna acción dado que no se trata de un verdadero error. A continuación se añade un nuevo elemento a lista y se escribe de nuevo el fichero XML. El siguiente método, listaPuntuacion(), resulta sencillo de entender, al limitarse a métodos definidos en la clase ListaPuntuaciones

2.     Pasemos a mostrar el comienzo de la clase ListaPuntuaciones No es necesario almacenarla en un fichero aparte, puedes definirla dentro de la clase anterior. Para ello copia el siguiente código justo antes del último } de la clase AlmacenPuntuacionesXML_SAX:

 private class ListaPuntuaciones {

   private class Puntuacion {
      int puntos;
      String nombre;
      long fecha;
   }

   private List<Puntuacion> listaPuntuaciones;

   public ListaPuntuaciones() {
      listaPuntuaciones = new ArrayList<Puntuacion>();
   }

   public void nuevo(int puntos, String nombre, long fecha) {
      Puntuacion puntuacion = new Puntuacion();
      puntuacion.puntos = puntos;
      puntuacion.nombre = nombre;
      puntuacion.fecha = fecha;
      listaPuntuaciones.add(puntuacion);
   }

   public Vector<String> aVectorString() {
      Vector<String> result = new Vector<String>();
      for (Puntuacion puntuacion : listaPuntuaciones) {
         result.add(puntuacion.nombre+" "+puntuacion.puntos);
      }
      return result;
   }

 

El objetivo de esta clase es mantener una lista de objetoPuntuacion. Dispone de métodos para insertar un nuevo elemento (nuevo()) y devolver un listado con todas las puntuaciones almacenadas (aVectorString()).

 

3.     Lo verdaderamente interesante de esta clase es que permite la lectura y escritura de los datos desde un documento XML (leerXML() y escribirXML()). Veamos primero como leer un documento XML usando SAX.  Escribe el siguiente código a continuación del anterior:

public void leerXML(InputStream entrada) throws Exception {
   SAXParserFactory fabrica = SAXParserFactory.newInstance();
   SAXParser parser = fabrica.newSAXParser();
   XMLReader lector = parser.getXMLReader();
   ManejadorXML manejadorXML = new ManejadorXML();
   lector.setContentHandler(manejadorXML);
   lector.parse(new InputSource(entrada));
   cargadaLista = true;
}

Para leer un documento XML comenzamos creando una instancia de la clase SAXParserFactory, lo que nos permite crear un nuevo parser XML de tipo SAXParser. Luego creamos un lector, de la clase XMLReader, asociado a este parser. Creamos ManejadorXML de la clase XMLHadler y asociamos este manejador al XMLReader. Para finalizar le indicamos al XMLReader que entrada tiene para que realice el proceso de parser. Una vez finalizado el proceso, marcamos que el fichero está cargado.

Como ves el proceso es algo largo, pero siempre se realiza igual. Donde sí que tendremos que trabajar algo más es en la creación de la clase ManejadorXML,  dado que va a depender del formato del fichero que queramos leer. Esta clase se lista en el siguiente punto.

 

4.     Escribir este código a continuación del anterior:

class ManejadorXML extends DefaultHandler {
   private StringBuilder cadena;
   private Puntuacion puntuacion;

   @Override
   public void startDocument() throws SAXException {
      listaPuntuaciones = new ArrayList<Puntuacion>();
      cadena = new StringBuilder();
   }

   @Override
   public void startElement(String uri, String nombreLocal, String
               nombreCualif, Attributes atr) throws SAXException {
      cadena.setLength(0);
      if (nombreLocal.equals("puntuacion")) {
         puntuacion = new Puntuacion();
         puntuacion.fecha = Long.parseLong(atr.getValue("fecha"));
         }
   }

   @Override
   public void characters(char ch[], int comienzo, int lon) {
      cadena.append(ch, comienzo, lon);
   }

   @Override
   public void endElement(String uri, String nombreLocal,
                  String nombreCualif) throws SAXException {
      if (nombreLocal.equals("puntos")) {
         puntuacion.puntos = Integer.parseInt(cadena.toString());
      } else if (nombreLocal.equals("nombre")) {
         puntuacion.nombre = cadena.toString();
      } else if (nombreLocal.equals("puntuacion")) {
         listaPuntuaciones.add(puntuacion);
      }
   }
   
   @Override
   public void endDocument() throws SAXException {}
}

Esta clase define un manejador que captura los cinco eventos generados en el proceso de parsing en SAX. En startDocument() nos limitamos a inicializar variables. En startElement() verificamos que hemos llegado a una etiqueta <puntuación>. En tal caso, creamos un nuevo objeto de la clase Puntuacion e inicializamos el campo fecha con el valor indicado en uno de los atributos.

El método characters() se llama cuando aparece texto dentro de una etiqueta (<etiqueta> caracteres </etiqueta>). Nos limitamos a almacenar este texto en la variable cadena para utilizarlo en el siguiente método. SAX no nos garantiza que nos pasará todo el texto en un solo evento: si el texto es muy extenso, se realizarán varias llamadas a este método. Por esta razón, el texto se va acumulando en cadena.

El método endElement() resulta más complejo, dado que en función de que etiqueta esté acabando realizaremos una tarea diferente. Si se trata de </puntos> o de </nombre> utilizaremos el valor de la variable cadena para actualizar el valor correspondiente. Si se trata de </puntuacion> añadimos el objeto puntuacion a la lista.

5.      Introduce a continuación el último método de la claseListaPuntuaciones, que nos permite escribir el documento XML:

   public void escribirXML(OutputStream salida) {
      XmlSerializer serializador = Xml.newSerializer();
      try {
         serializador.setOutput(salida, "UTF-8");
         serializador.startDocument("UTF-8", true);
         serializador.startTag("", "lista_puntuaciones");
         for (Puntuacion puntuacion : listaPuntuaciones) {
            serializador.startTag("", "puntuacion");
            serializador.attribute("", "fecha",
                              String.valueOf(puntuacion.fecha));
            serializador.startTag("", "nombre");
            serializador.text(puntuacion.nombre);
            serializador.endTag("", "nombre");
            serializador.startTag("", "puntos");
            serializador.text(String.valueOf(puntuacion.puntos));
            serializador.endTag("", "puntos");
            serializador.endTag("", "puntuacion");
         }
         serializador.endTag("", "lista_puntuaciones");
         serializador.endDocument();
      } catch (Exception e) {
         Log.e("Asteroides", e.getMessage(), e);
      }
   }
} //Cerramos ListaPuntuaciones
} //Cerramos AlmacenPuntuacionesXML_SAX

Como puedes ver, todo el trabajo se realiza por medio de un objeto de la clase XmlSerializer, que escribe el código XML en el OutputStream que hemos pasado como parámetros.

6.     La variable almacen ha de inicializarse de forma adecuada.

7.     Modifica el código correspondiente para que este método pueda ser seleccionado para almacenar las puntuaciones.

8.     Verifica el resultado.