Bienvenidos sean a este post, como hemos hablado antes trabajar con SAX tiene varias particularidades que nos permiten trabajar con archivos XML en equipos con poca memoria, como un dispositivo movil, a su vez nos provee un analizador de archivos y nos da la posibilidad de poder trabajar con archivos grandes. Para poder analizar estos archivos se debe primero extender la clase DefaultHandler, para la cual debemos sobreescribir 5 metodos, los listaremos a continuacion:

  • startDocument(), comienza el documento XML
  • endDocument(), finaliza el documento XML
  • startElement(String uri, String nombreLocal, String nombreCualif, Attributes atributos), comienza una nueva etiqueta con los parametros informados.
  • endElement(String uri, String nombreLocal, String nombrecualif), termina una etiqueta
  • characters(char ch[], int comienzo, int longitud), devuelve en ch en los caracteres de una etiqueta

Antes de continuar hablaremos un poco sobre algunos de los metodos antes comentados, primero veamos los atributos de startElement:

  • uri: La uri del espacio de nombres o vacio 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

Y el metodo characters como dijimos antes nos devolvera el contenido entre la etiquetas informadas, por ejemplo:

< etiqueta > caracteres < /etiqueta >

No se preocupen por esto porque lo veremos un poco mas adelante. Hablando un poco de eso, pasemos a ver un ejemplo y para ello volveremos a nuestra app Asteroides, la abrimos con Android Studio y crearemos la clase AlmacenPuntuacionesXML_SAX, donde le ingresaremos el siguiente codigo:

package com.tinchicus.asteroides;

import android.content.Context;
import android.util.Log;

import java.io.FileNotFoundException;

public class AlmacenPuntuacionesXML_SAX implements AlmacenPuntuaciones {
    
    private static String ARCHIVO = "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(ARCHIVO));
            }
        }
        catch (FileNotFoundException e){}
        catch (Exception e){
            Log.e("Asteroides", e.getMessage(),e);
        }
        lista.nuevo(puntos,nombre,fecha);
        try{
            lista.escribirXML(contexto.openFileOutput(ARCHIVO,
                    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(ARCHIVO));
            }
        } catch (Exception e) {
            Log.e("Asteroides",e.getMessage(),e);
        }
        return lista.aVectorString();
    }
}

En este caso vamos a hacer que esta clase implementa AlmacenPuntuaciones, luego crearemos cuatro variables, la primera de tipo constante llamada ARCHIVO la cual contendra el nombre de nuestro archivo, luego una llamada contexto para nuestro entorno, lista de una clase que luego haremos llamada ListaPuntuaciones y por ultimo una llamada cargadaLista de tipo boolean, en el siguiente paso tendremos un constructor donde recibira el atributo para definir el contexto, luego crearemos el objeto lista, no se preocupen por ahora por el error ya lo corregiremos mas adelante, y por ultimo cargadaLista sera definida como false, despues definiremos nuevamente a guardarPuntuacion, donde por medio de un try/catch chequeara si cargadaLista no es verdaddero, es decir falso, y llamara por medio de leerXML y openFileInput() al archivo informado en ARCHIVO, no se preocupen por el error porque lo solucionaremos mas adelante, los bloques catch los utilizaremos por ante cualquier eventualidad que pueda suceder y lo informara en el log de la app, si no ocurre ningun error utilizaremos el metodo nuevo() y enviaremos los tres datos pertinentes (nombre, puntos y fecha), donde otra vez tendremos un bloque try/catch donde utilizaremos el metodo escribriXML para cargar lo informado en nuevo a nuestro archivo y el catch estara por cualquier eventualidad, por ultimo definimos el metodo listaPuntuaciones() el cual de forma similar al metodo anterior si cargadaLista no existe llama al metodo leerXML y por medio de openFileInput buscara a nuestro archivo informado en la variable ARCHIVO, tambien tenemos un bloque try/catch para capturar cualquier eventualidad en el proceso, por ultimo retornara el valor en lista por medio del metodo aVectorString, el cual veremos mas adelante, a continuacion agregaremos el siguiente bloque dentro de la clase, deberian agregarlo debajo del ultimo metodo que vimos pero antes de la ultima llave que cierra la clase, no se preocupen porque al final veremos como queda nuestro codigo pero el bloque seria este:

    public 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> resultado = new Vector<String>();
            for(Puntuacion puntuacion : listaPuntuaciones){
                resultado.add(puntuacion.nombre + " " + puntuacion.puntos);
            }
            return resultado;
        }

        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;
        }
    }

        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);
            }
        }

En este caso vamos a crear una clase llamada ListaPuntuaciones, la cual a su vez tendra una clase llamada Puntuacion con tres variables para almacenar el nombre, los puntos y la fecha, despues crearemos un objeto de tipo List llamado listaPuntuaciones y su referencia sera Puntuacion, despues tendremos un constructor donde definiremos que el objeto creado anteriormente sea un ArrayList, despues de esto definiremos el metodo nuevo(), citado en el bloque anterior, el cual recibira tres datos (nombre, puntos y fecha), en este metodo primero crearemos un objeto de tipo Puntuacion llamado puntuacion y le asignaremos los valores informados a las variables de la clase Puntuacion, luego tendremos el metodo aVectorString la cual por medio de un for avanzado se encargara de pasar todos los datos de listaPuntuaciones a una variable llamada resultado del tipo Vector, una vez concretado esto nos devolvera la variable resultado, nuestro siguiente metodo es leerXML(), la cual sera la encargada de leer el documento XML, para ello primero crearemos una instancia de SAXParserFactory llamada fabrica lo cual nos permitira crear el XML parser llamado parser, uno vez creado nuestro parser procederemos a crear el XMLreader derivado de nuestro parser, despues crearemos un objeto de tipo ManejadorXML el cual nos ayudara a manipular nuestro documento, no se preocupen por el error lo solucionaremos enseguida, a este objeto lo pasaremos al metodo setContentHandler, para luego poder decirle cual es el archivo a manipular y una vez realizado le diremos que la lista esta cargada por medio de cargadaLista seteandolo en true, nuestro ultimo metodo sera escribirXML, el cual se encargara de escribir nuestro archivo, en este caso primero crearemos un serializador quien va a ser el verdadero escritor de nuestros archivos, tenemos un bloque try/catch para evitar cualquier eventualidad, en este bloque primero setearemos el archivo de salida, esto gracias al archivo informado en salida al momento de declarar nuestro metodo, despues iniciaremos el documento (el tipo de archivo y un valor booleano), despues iniciaremos un elemento por medio de startTag(), en este caso llamado lista_puntuaciones, luego utilizaremos un for avanzado para obtener todos los elementos contenidos en listaPuntuaciones y en este caso utilizaremos a startTag(), attribute y text para completarlos, en este caso con startTag iniciaremos el elemento con el nombre, despues para el elemento puntuacion usaremos attribute donde le ingresaremos la fecha y obtendremos el valor por medio de valueOf, en los otros dos tags (elementos) seran para nombre y puntos y para ambos casos usaremos a text() para ingresar los valores correspondientes a los elementos, luego por medio de endTag cerraremos dichos elementos, en el ciclo for cerraremos a nombre, puntos y puntuacion y por fuera del for cerraremos a lista_puntuacion y por ultimo cerramos el archivo con endDocument. Procedamos a agregar la clase ManejadorXML y para esto debemos hacerlo como antes dentro de la clase AlmacenPuntuacionesXML_SAX pero por fuera de la clase 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 attr) throws SAXException{
                cadena.setLength(0);
                if(nombreLocal.equals("puntuacion")){
                    puntuacion = new Puntuacion();
                    puntuacion.fecha = Long.parseLong(attr.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 {}
        }

En esta clase como dijimos antes extendera a DefaultHandler para poder sobreescribir algunos metodos, primero crearemos dos variables uno de tipo StringBuilder llamado cadena y otro de tipo Puntuacion llamado puntuacion, nuestro primer metodo sera startDocument() donde iniciaremos a listaPuntuaciones y cadena nuestro siguiente metodo sera startElement() donde enviaremos varios atributos pero para nuestro caso setearemos a cadena con un tamaño de cero y luego si nombreLocal es igual a puntuacion, la iniciaremos y le agregaremos el valor determinado en fecha, nuestro siguiente metodo sera characters donde agregaremos el tramo de caracteres establecidos a cadena y por ultimo tendremos a endElement() donde chequearemos si nombreLocal es igual a puntos agregara el valor contenido en cadena a puntuacion.puntos, en caso de ser nombre agregaremos el valor de cadena a puntuacion.nombre y por ultimo si nombreLocal es igual a puntuacion agregara el valor de puntuacion a listaPuntaciones, en los dos casos anteriores utilizamos a toString() para poder agregarlo a estos elementos, por ultimo definiremos a endDocument() de forma vacia por necesidad de poder utilizar correctamente esta extension, veamos como quedo nuestro codigo final:

package com.tinchicus.asteroides;

import android.content.Context;
import android.util.Log;
import android.util.Xml;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlSerializer;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

public class AlmacenPuntuacionesXML_SAX implements AlmacenPuntuaciones {

    private static String ARCHIVO = "puntuaciones.xml";
    private Context contexto;
    private ListaPuntuaciones lista;
    public 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(ARCHIVO));
            }
        } catch (FileNotFoundException e) {
        } catch (Exception e) {
            Log.e("Asteroides", e.getMessage(), e);
        }
        lista.nuevo(puntos, nombre, fecha);
        try {
            lista.escribirXML(contexto.openFileOutput(ARCHIVO,
                    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(ARCHIVO));
            }
        } catch (Exception e) {
            Log.e("Asteroides",e.getMessage(),e);
        }
        return lista.aVectorString();
    }

    public 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> resultado = new Vector<String>();
            for(Puntuacion puntuacion : listaPuntuaciones){
                resultado.add(puntuacion.nombre + " " + puntuacion.puntos);
            }
            return resultado;
        }

        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;
        }

        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);
            }
        }

        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 attr) throws SAXException{
                cadena.setLength(0);
                if(nombreLocal.equals("puntuacion")){
                    puntuacion = new Puntuacion();
                    puntuacion.fecha = Long.parseLong(attr.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 {}
        }
    }
}

Nuestra ultima modificacion sera en MainActivity donde deberemos modificar la linea donde definimos a almacen por esta linea:

almacen = new AlmacenPuntuacionesXML_SAX(this);

Con esto si lo compilamos y probamos generaremos un archivo XML en la memoria interna de nuestro dispositivo (/data/data/com.tinchicus.asteroides/files/puntuaciones.xml) si lo buscan por Device File Explorer podran encontrarlo, aqui les muestro un ejemplo de como puede ser su estructura:

<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<lista_puntuaciones>
	<puntuacion fecha="1554564931280">
		<nombre>AAA</nombre>
		<puntos>1000</puntos>
	</puntuacion>
	<puntuacion fecha="1554564972697">
		<nombre>AAA</nombre>
		<puntos>1000</puntos>
	</puntuacion>
	<puntuacion fecha="1554564993220">
		<nombre>AAA</nombre>
		<puntos>1000</puntos>
	</puntuacion>
</lista_puntuaciones>

Vean como crearon un tag maestro llamado lista_puntuacion, y despues bloques llamados puntuacion con la fecha como atributo y dos elementos mas: nombre y puntos donde se almacenaran los mismos si lo chequeamos podriamos tener un listado de esta forma:

En resumen, hoy hemos visto como se trabaja con documentos XML por medio de la API SAX, hemos visto algunos metodos, como implementarlo, como escribir y como leer informacion en estos documentos, espero les haya sido util sigueme en Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.