Hace un tiempo publicamos una entrada sobre el Tratamiento de XML en Java usando DOM y SAX. Las aplicaciones de Android, en su mayor parte, están programadas en Java y esto significa que podríamos usar las mismas estrategias tratadas en la entrada mencionada para realizar nuestras lecturas y escrituras de ficheros XML.
Sin embargo Android no es sólo Java, sino que es un Sistema Operativo para dispositivos móviles. Esto significa que, tal y como comentamos en la entrada mencionada, no deberíamos usar un DOM parser para leer un fichero XML pues en ciertos casos podemos quedarnos sin memoria (ficheros muy grandes). Para estas tareas Android va a usar unas interfaces similar al SAX parser, llamadas XmlSerializer y XmlPullParser.
En esta entrada vamos a, primero, escribir un fichero XML y, posteriormente, leerlo. ¡Comencemos!
El primer paso, como siempre, será abrir nuestro Eclipse configurado con el Android SDK. Crearemos un nuevo proyecto de Android al que, por ejemplo, llamaremos TratamientoXML. Para hacerlo sencillo vamos a usar la propia Activity que se nos ha creado automáticamente para hacer nuestras pruebas. Por supuesto, en un escenario de uso real, estas funcionalidades deberían estar encapsuladas en una clase aparte a la que podamos instanciar o usar.
Escritura
Creamos un nuevo método en la clase al que vamos a llamar desde el constructor:
public class TratamientoXML extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); escribirXML(); } private void escribirXML() { } }
Para el caso de la escritura vamos a usar el XmlSerializer. Lo importante del funcionamiento de esta interfaz es que nosotros seremos responsables de decir que empezamos una etiqueta, que creamos atributos, que ponemos texto y que cerramos la etiqueta. Su filosofía es más cercana al SAX parser que al DOM parser: mientras que DOM crea todo el árbol en la memoria y podemos acceder a cada nodo (cada etiqueta) para manipularla, en este caso sólo podemos hacer cosas con la última etiqueta que hemos dejado abierta. Dicho de otra forma, no hay navegación.
Lo primero que debemos hacer es crear un fichero en el cual vayamos a escribir. Debemos recordar Android es básicamente un sistema operativo Linux y, en este caso, esto significa que cada .apk que instalamos recibe su propio User-ID. Además, cuando instalamos una aplicación, se crea un directorio propio del paquete en el directorio /data/data de la memoria interna al que sólo puede acceder este User-ID y el superusuario root. Podemos, por tanto, crear tres tipos de fichero:
- MODE_PRIVATE: sólo la propia aplicación (y root) puede acceder para leer y escribir.
- MODE_WORLD_READABLE: cualquier User-ID puede leer el fichero.
- MODE_WORLD_WRITEABLE: cualquier User-ID puede escribir en el fichero.
Para nuestro ejemplo vamos a suponer que lo que queremos generar es un fichero XML que contenga información del estado de la aplicación (podría ser una configuración de las opciones, podrían ser las stats de un jugador si nuestra aplicación fuera un juego…), de modo que usaremos MODE_PRIVATE.
Para ello, editamos nuestro método:
private void escribirXML() { FileOutputStream fout = null; try { fout = openFileOutput("test.xml", MODE_PRIVATE); } catch (FileNotFoundException e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); } }
El método openFileOutput() pertenece al Context de la aplicación, por lo que si movemos este código por nuestra aplicación deberemos pasarle el Context. Recibe el nombre del fichero que va a escribir y el modo de éste (de entre los tres mencionados). La ruta absoluta a nuestro fichero, dado nuestro ejemplo, será /data/data/com.vidasconcurrentes.tratamientoxml/files/test.xml. Automáticamente se crea el directorio files si no existe al crear un fichero. Lo siguiente es crear el serializador de XML, el cual recibe un FileOutputStream. Además vamos a ponerle que la codificación sea UTF-8 y que se indente correctamente. Para ello agregamos el código que viene a continuación antes del final del código superior:
XmlSerializer serializer = Xml.newSerializer(); try { serializer.setOutput(fout, "UTF-8"); serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); } catch (Exception e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); }
Con setOutput() elegimos el fichero donde escribiremos y la codificación de caracteres. Con startDocument() decimos que comience a crear el árbol del XML en memoria, el atributo booleano indica si el fichero es standalone o no. Y la característica que ponemos con setFeature() en este caso es que se indente para poder visualizarlo más fácilmente.
Lo siguiente será crear nuestro fichero. Para este ejemplo vamos a crear un fichero muy tonto. Este código irá antes de la sentencia catch del código anterior y tras la sentencia setFeature():
serializer.startTag(null, "vidasConcurrentes"); serializer.attribute(null, "numero_autores", "2"); serializer.startTag(null, "autor"); serializer.attribute(null, "nombre", "meta"); serializer.text("Programación en diferentes lenguajes"); serializer.endTag(null, "autor"); serializer.startTag(null, "autor"); serializer.attribute(null, "nombre", "sshMan"); serializer.text("Seguridad Informática y Pentesting"); serializer.endTag(null, "autor"); serializer.endTag(null, "vidasConcurrentes"); serializer.endDocument(); serializer.flush(); fout.close(); Toast.makeText(getApplicationContext(), "Escrito correctamente", Toast.LENGTH_LONG).show();
Importante: es buena práctica que, cada vez que abramos una etiqueta la cerremos inmediatamente y luego agreguemos código entre medias. Así evitamos ficheros mal formados.
Aquí vemos que tenemos cuatro funciones:
- startTag(): recibe el Namespace (en este caso null porque no hay Namespace) y el nombre de la etiqueta que va a crear.
- endTag(): recibe los mismos parámetros que el anterior y sirve para cerrar una etiqueta abierta previamente con ese nombre. En realidad el serializador sólo genera una etiqueta de cierre con dicho nombre, se haya abierto antes algo del mismo nombre o no. Hay que tener cuidado al usarlo, por tanto. ¡Pero que no falte!
- attribute(): recibe el Namespace, el nombre del atributo y el valor del atributo. Todos los componentes son Strings, y hay que tenerlo en cuenta si sacamos dichos valores de variables (como enteros, por ejemplo).
- text(): recibe el texto que se va a poner entre el comienzo y el fin de una etiqueta.
Si ejecutamos ahora nuestra aplicación y abrimos un explorador de ficheros con permisos de superusuario (para poder navegar hasta el directorio) como RootExplorer, tras navegar al directorio /data/data/com.vidasconcurrentes.tratamientoxml/files/test.xml, podemos abrirlo y veremos el contenido:
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?> <vidasConcurrentes numero_autores="2"> <autor nombre="meta">Programación en diferentes lenguajes</autor> <autor nombre="sshMan">Seguridad Informática y Pentesting</autor> </vidasConcurrentes>
Hasta aquí la parte de escritura, el código del método de escribir quedaría:
private void escribirXML() { FileOutputStream fout = null; try { fout = openFileOutput("test.xml", MODE_PRIVATE); } catch (FileNotFoundException e) { Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); } XmlSerializer serializer = Xml.newSerializer(); try { serializer.setOutput(fout, "UTF-8"); serializer.startDocument(null, true); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "vidasConcurrentes"); serializer.attribute(null, "numero_autores", "2"); serializer.startTag(null, "autor"); serializer.attribute(null, "nombre", "meta"); serializer.text("Programación en diferentes lenguajes"); serializer.endTag(null, "autor"); serializer.startTag(null, "autor"); serializer.attribute(null, "nombre", "sshMan"); serializer.text("Seguridad Informática y Pentesting"); serializer.endTag(null, "autor"); serializer.endTag(null, "vidasConcurrentes"); serializer.endDocument(); serializer.flush(); fout.close(); Toast.makeText(getApplicationContext(), "Escrito correctamente", Toast.LENGTH_LONG).show(); } catch (Exception e) { Toast.makeText(getApplicationContext(), e.getMessage(), Toast.LENGTH_LONG).show(); } }
Lectura
Una vez que hemos ejecutado nuestra aplicación con la escritura del fichero tendremos un ficherito test.xml en el directorio privado de la aplicación. El siguiente paso va a ser leerlo y volcarlo tal cual. Para no complicarnos las cosas, y como esta aplicación es sólo para fines didácticos (para uso con Eclipse diréctamente), todo lo que leamos del fichero lo vamos a mandar a LogCat por medio de los Log. Lo primero será mostrar la vista de LogCat si no la tenemos activa ya.
Para ello vamos a Window > Show view > Other… y bajo Android elegimos LogCat. Esto nos abrirá la vista de LogCat que usaremos para mostrar nuestro fichero. Abrimos dicha vista pulsando en la pestaña creada y en la parte derecha pulsamos en el símbolo + verde. Escribimos TratamientoXML en Filter name y en by Log Tag para crear un nuevo filtro que sólo nos muestre lo que tenga la etiqueta TratamientoXML. Lo siguiente, por comodidad, va a ser añadir este atributo a la clase que estamos usando:
public static final String TAG = "TratamientoXML";
Lo siguiente será crear un método en el que vayamos a leer. El contenido va a ser análogo al método escribirXML(). Los pasos serán: abrir el fichero del que vamos a leer, usar un XmlPullParser para ir recorriendo el XML e ir mostrando por el Log cada elemento. La primera parte es la siguiente:
private void leerXML() { FileInputStream fin = null; try { fin = openFileInput("test.xml"); } catch (Exception e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); } }
En este caso usamos FileInputStream para leer y openFileInput() para abrir el fichero de nuestro directorio privado. El siguiente caso será enchufar un XmlPullParser a dicho fichero:
XmlPullParser parser = Xml.newPullParser(); try { parser.setInput(fin, "UTF-8"); } catch (Exception e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); }
Lo siguiente será procesar el fichero. Para ello entran en juego un par de conceptos. En el caso del XmlPullParser tenemos que ir avanzando usando el método next() cada vez que queramos el siguiente elemento. Dicho elemento viene dado por un evento, que puede ser: comienzo de documento, apertura de etiqueta, cierre de etiqueta, comienzo de texto y fin de documento. Para leer el fichero vamos a ir evento por evento hasta encontrar el fin del documento:
int event = parser.next(); while(event != XmlPullParser.END_DOCUMENT) { event = parser.parser.next(); } fin.close();
Este código iría añadido después del setInput() del código anterior. Imaginemos que esto fuera una cinta transportadora que va trayendo elementos. La hemos cargado en nuestra mesa de trabajo (que es la memoria). Lo primero que tenemos que hacer es avanzar para coger el primer evento (que debe ser que empezamos el documento). El código del bucle sería, entonces:
int event = parser.next(); while(event != XmlPullParser.END_DOCUMENT) { if(event == XmlPullParser.START_TAG) { Log.d(TAG, "<" + parser.getName() + ">"); for(int i = 0; i < parser.getAttributeCount(); i++) { Log.d(TAG, "\t" + parser.getAttributeName(i) + " = " + parser.getAttributeValue(i)); } } if(event == XmlPullParser.TEXT && parser.getText().trim().length() != 0) Log.d(TAG, "\t\t" + parser.getText()); if(event == XmlPullParser.END_TAG) Log.d(TAG, "</" + parser.getName() + ">"); event = parser.next(); } fin.close(); Toast.makeText(this, "Leido correctamente", Toast.LENGTH_LONG).show();
Cuando leemos una etiqueta podemos consultar cuántos atributos tiene usando el método getAttributeCount() y podemos acceder a su nombre y valor con getAttributeName() y diversas modalidades de getAttributeValue(), ambas reciben el índice del atributo al que nos referimos dentro de dicha etiqueta.
¿Por qué el caso del evento TEXT es tan raro? Siempre existe un texto en cada elemento de un XML, aunque no lo hayamos puesto. Y si no lo hemos puesto será una cadena de espacios. Por tanto, si usamos la función trim() de los String que quita todos los espacios al inicio y al fin de la cadena (como solo hay espacios nos quitará todo) y luego comprobamos que su longitud es 0, es un evento TEXT “no válido”. Sin embargo, si la longitud no fuera 0 entonces lo mostramos por el Log.
Si ahora cambiamos la llamada de nuestra actividad para que sea leerXML(), podemos ejecutar y veremos cómo nuestro fichero se ha leido y escrito en el Log:
<vidasConcurrentes> numero_autores = 2 <autor> nombre = meta Programación en diferentes lenguajes </autor> <autor> nombre = sshMan Seguridad Informática y Pentesting </autor> </vidasConcurrentes>
Hasta aquí la parte de la lectura, el código completo de dicho método es:
private void leerXML() { FileInputStream fin = null; try { fin = openFileInput("test.xml"); } catch (Exception e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); } XmlPullParser parser = Xml.newPullParser(); try { parser.setInput(fin, "UTF-8"); int event = parser.next(); while(event != XmlPullParser.END_DOCUMENT) { if(event == XmlPullParser.START_TAG) { Log.d(TAG, "<" + parser.getName() + ">"); for(int i = 0; i < parser.getAttributeCount(); i++) { Log.d(TAG, "\t" + parser.getAttributeName(i) + " = " + parser.getAttributeValue(i)); } } if(event == XmlPullParser.TEXT && parser.getText().trim().length() != 0) Log.d(TAG, "\t\t" + parser.getText()); if(event == XmlPullParser.END_TAG) Log.d(TAG, "</" + parser.getName() + ">"); event = parser.next(); } fin.close(); Toast.makeText(this, "Leido correctamente", Toast.LENGTH_LONG).show(); } catch (Exception e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); } }
Más allá
Existen dos situaciones dignas de mención a la hora de tratar con ficheros y con ficheros XML en este caso.
La primera es que podemos crear ficheros (XML o no) diréctamente en la tarjeta SD. Para hacer esto lo que necesitaríamos hacer (abstraído de lo demás) sería usar lo siguiente:
File file = Environment.getExternalStorageDirectory() + "nombre_fichero.extension"; file.createNewFile();
A la hora de la lectura de dicho fichero sería lo mismo sólo que no habría que usar createNewFile(). Este fichero puede pasarse como parámetro al constructor de un FileInputStream o FileOutputStream. ¡Debemos recordar cerrarlo!
La segunda es que podemos agregar ficheros XML a nuestras aplicaciones como recurso propio de la aplicación. Esto puede ser útil para ficheros de configuración de la aplicación (si queremos almacenar datos de niveles de un juego, de rutas de otros ficheros, opciones por defecto de un menu…). Para ello primero creamos una carpeta en el directorio res de nuestra aplicación llamado xml, y dentro pegamos nuestro fichero. Para acceder a ello usaremos:
mContext.getResources().getXML(R.xml.el_fichero);
Siendo mContext el Context de la aplicación. Recordemos que R se autogenera, no debe ser modificado manualmente y nos da acceso a los recursos de esta forma.
Hasta aquí la entrada de hoy. En ella hemos aprendido sobre los ficheros de las aplicaciones en Android tratando la lectura y escritura en general y parándonos en el tratamiento de ficheros XML en particular. Además hemos aprendido un poco sobre el sistema de ficheros de Android y cómo asigna un User-ID a cada aplicación para que ninguna pueda acceder a los datos de otra.
¡Un saludo y muchas gracias por vuestro tiempo!