Muchas veces las aplicaciones necesitan almacenar datos de forma persistente. Cuando realizamos aplicaciones para PCs de sobremesa o servidores podemos almacenar esta información en algún fichero en el disco o bien en una base de datos. Lo más sencillo será almacenarla en ficheros, pero en los dispositivos móviles no podemos contar ni tan solo con esta característica. Aunque los móviles normalmente tienen su propio sistema de ficheros, por cuestiones de seguridad MIDP no nos dejará acceder directamente a él. Es posible que en alguna implementación podamos acceder a ficheros en el dispositivo, pero esto no es requerido por la especificación, por lo que si queremos que nuestra aplicación sea portable no deberemos confiar en esta característica.
Para almacenar datos de forma persistente en el móvil utilizaremos RMS
(Record Management System). Se trata de un sistema de almacenamiento
que nos permitirá almacenar registros con información de forma
persistente en los dispositivos móviles. No se especifica ninguna forma
determinada en la que se deba almacenar esta información, cada implementación
deberá guardar estos datos de la mejor forma posible para cada dispositivo
concreto, utilizando memoria no volátil, de forma que no se pierda la
información aunque reiniciemos el dispositivo o cambiemos las baterías.
Por ejemplo, algunas implementaciones podrán utilizar el sistema de ficheros
del dispositivo para almacenar la información de RMS, o bien cualquier
otro dispositivo de memoria no volátil que contenga el móvil.
La forma de almacenamiento real de la información en el dispositivo será
transparente para los MIDlets, éstos sólo podrán acceder
a la información utilizando la API de RMS. Esta API se encuentra en el
paquete javax.microedition.rms
.
La información se almacena en almacenes de registros (Record Stores), que serán identificados con un nombre que deberemos asignar nosotros. Cada aplicación podrá crear y utilizar tantos almacenes de registros como quiera. Cada almacén de registros contendrá una serie de registros con la información que queramos almacenar en ellos.
Los almacenes de registros son propios de la suite. Es decir, los almacenes de registro creados por un MIDlet dentro de una suite, serán compartidos por todos los MIDlets de esa suite, pero no podrán acceder a ellos los MIDlets de suites distintas. Por seguridad, no se permite acceder a recursos ni a almacenes de registros de suites distintas a la nuestra.
Figura 27. Acceso a los almacenes de registros
Cada suite define su propio espacio de nombres. Es decir, los nombres de los almacenes de registros deben ser únicos para cada suite, pero pueden estar repetidos en diferentes suites. Como hemos dicho antes, nunca podremos acceder a un almacén de registros perteneciente a otra suite.
Abrir el almacén de registros
Lo primero que deberemos hacer es abrir o crear el almacén de registros. Para ello utilizaremos el siguiente método:
RecordStore rs = RecordStore.open(nombre, true);
Con el segundo parámetro a true
estamos diciendo que si
el almacén de registros con nombre nombre
no existiese en
nuestra suite lo crearía. Si por el contrario estuviese a false
,
sólo intentaría abrir un almacén de registros existente,
y si éste no existe se producirá una excepción RecordStoreNotFoundException
.
El nombre que especificamos para el almacén de registros deberá se un nombre de como mucho 32 caracteres codificado en Unicode.
Una vez hayamos terminado de trabajar con el almacén de registros, podremos cerrarlo con:
rs.close();
Listar los almacenes de registros
Si queremos ver la lista completa de almacenes de registros creados dentro de nuestra suite, podemos utilizar el siguiente método:
String [] nombres = RecordStore.listRecordStores();
Esto nos devolverá una lista con los nombres de los almacenes de registros que hayan sido creados. Teniendo estos nombres podremos abrirlos como hemos visto anteriormente para consultarlos, o bien eliminarlos.
Eliminar un almacén de registros
Podemos eliminar un almacén de registros existente proporcionando su nombre, con:
RecordStore.deleteRecordStore(nombre);
Propiedades de los almacenes de registros
Los almacenes de registros tienen una serie de propiedades que podemos obtener con información sobre ellos. Una vez hayamos abierto el almacén de registros para trabajar con él, podremos obtener los valores de las siguientes propiedades:
String nombre = rs.getName();
long timestamp = rs.getLastModified();
int version = rs.getVersion();
int tam = rs.getSize();
int libre = rs.getSizeAvailable();
El almacén de registros contendrá una serie de registros donde podemos almacenar la información. Podemos ver el almacén de registros como una tabla en la que cada fila corresponde a un registro. Los registros tienen un identificador y un array de datos.
Identificador | Datos |
1 |
array de datos ... |
2 |
array de datos ... |
3 |
array de datos ... |
... |
... |
Estos datos de cada registro se almacenan como un array de bytes. Podremos acceder a estos registros mediante su identificador o bien recorriendo todos los registros de la tabla.
Cuando añadamos un nuevo registro al almacén se le asignará
un identificador una unidad superior al identificador del último registro
que tengamos. Es decir, si añadimos dos registros y al primero se le
asigna un identificador n
, el segundo tendrá un identificador
n+1
.
Las operaciones para acceder a los datos de los registros son atómicas, por lo que no tendremos problemas cuando se acceda concurrentemente al almacén de registros.
Almacenar información
Tenemos dos formas de almacenar información en el almacén de
registros. Lo primero que deberemos hacer en ambos casos es construir un array
de bytes con la información que queramos añadir. Para
hacer esto podemos utilizar un flujo DataOutputStream
, como se
muestra en el siguiente ejemplo:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.writeUTF(nombre);
dos.writeInt(edad);
byte [] datos = baos.toByteArray();
Una vez tenemos el array de datos que queremos almacenar, podremos utilizar uno de los siguientes métodos del objeto almacén de datos:
int id = rs.addRecord(datos, 0, datos.length);
rs.setRecord(id, datos, 0, datos.length);
En el caso de addRecord
, lo que se hace es añadir un nuevo
registro al almacén con la información que hemos proporcionado,
devolviéndonos el identificador id
asignado al registro
que acabamos de añadir.
Con setRecord
lo que se hace es sobrescribir el registro correspondiente
al identificador id
indicado con los datos proporcionados. En este
caso no se añade ningún registro nuevo, sólo se almacenan
los datos en un registro ya existente.
Leer información
Si tenemos el identificador del registro que queremos leer, podemos obtener su contenido como array de bytes directamente utilizando el método:
byte [] datos = rs.getRecord(id);
Si hemos codificado la información dentro de este registro utilizando
un flujo DataOutputStream
, podemos descodificarlo realizando el
proceso inverso con un flujo DataInputStream
:
ByteArrayInputStream bais = new ByteArrayInputStream(datos);
DataInputStream dis = DataInputStream(bais);
String nombre = dis.readUTF();
String edad = dis.readInt();
dis.close();
Borrar registros
Podremos borrar un registro del almacén a partir de su identificador con el siguiente método:
rs.deleteRecord(id);
Almacenar y recuperar objetos
Si hemos definido una forma de serializar los objetos, podemos aprovechar esta serialización para almacenar los objetos de forma persistente en RMS y posteriormente poder recuperarlos.
Imaginemos que en nuestra clase MisDatos
hemos definido los siguientes
métodos para serializar y deserializar tal como vimos en el apartado
de entrada/salida:
public void serialize(OutputStream out)
public static MisDatos deserialize(InputStream in)
Podemos serializar el objeto en un array de bytes utilizando estos métodos para almacenarlo en RMS de la siguiente forma:
MisDatos md = new MisDatos();
...
ByteArrayOutputStream baos = new ByteArrayOutputStream();
md.serialize(baos);
byte [] datos = baos.toByteArray();
Una vez tenemos este array de bytes podremos almacenarlo en RMS. Cuando queramos recuperar el objeto original, leeremos el array de bytes de RMS y deserializaremos el objeto de la siguiente forma:
ByteArrayInputStream bais = new ByteArrayInputStream(datos);
MisDatos md = MisDatos.deserialize(bais);
Si no conocemos el identificador del registro al que queremos acceder, podremos
recorrer todos los registros del almacén utilizando un objeto RecordEnumeration
.
Para obtener la enumeración de registros del almacén podemos utilizar
el siguiente método:
RecordEnumeration re = rs.enumerateRecords(null, null, false);
Con los dos primeros parámetros podremos establecer la ordenación
y el filtrado de los registros que se enumeren como veremos más adelante.
Por ahora vamos a dejarlo a null
para obtener la enumeración
con todos los registros y en un orden arbitrario. Esta es la forma más
eficiente de acceder a los registros.
El tercer parámetro nos dice si la enumeración debe mantenerse
actualizada con los registros que hay realmente almacenados, o si por el contrario
los cambios que se realicen en el almacén después de haber obtenido
la enumeración no afectarán a dicha enumeración. Será
más eficiente establecer el valor a false
para evitar que
se tenga que mantener actualizado, pero esto tendrá el inconveniente
de que puede que alguno de los registros de la enumeración se haya borrado
o que se hayan añadido nuevos registros que no constan en la enumeración.
En al caso de que especifiquemos false
para que no actualice automáticamente
la enumeración, podremos forzar manualmente a que se actualice invocando
el método rebuild
de la misma, que la reconstruirá
utilizando los nuevos datos.
Recorreremos la enumeración de registros de forma similar a como recorremos
los objetos Enumeration
. Tendremos un cursor que en cada momento
estará en uno de los elementos de la enumeración. En este caso
podremos recorrer la enumeración de forma bidireccional.
Para pasar al siguiente registro de la enumeración y obtener sus datos
utilizaremos el método nextRecord
. Podremos saber si existe
un siguiente registro llamando a hasNextElement
. Nada más
crear la enumeración el cursor no se encontrará en ninguno de
los registros. Cuando llamemos a nextRecord
por primera vez se
situará en el primer registro y nos devolverá su array
de datos. De esta forma podremos seguir recorriendo la enumeración mientras
haya más registros. Un bucle típico para hacer este recorrido
es el siguiente:
while(re.hasNextElement()) {
byte [] datos = re.nextRecord();
// Procesar datos obtenidos
...
}
Hemos dicho que el recorrido puede ser bidireccional. Por lo tanto, tenemos
un método previousRecord
que moverá el cursor al
registro anterior devolviéndonos su contenido. De la misma forma, tenemos
un método hasPreviousElement
que nos dirá si existe
un registro anterior. Si invocamos previousRecord
nada más
crear la enumeración, cuando el cursor todavía no se ha posicionado
en ningún registro, moverá el cursor al último registro
de la enumeración devolviéndonos su resultado. Podemos también
volver al estado inicial de la enumeración en el que el cursor no apunta
a ningún registro llamando a su método reset
.
En lugar de obtener el contenido de los registros puede que nos interese obtener
su identificador, de forma que podamos eliminarlos o hacer otras operaciones
con ellos. Para ello tenemos los métodos nextRecordId
y
previousRecordId
, que tendrán el mismo comportamiento que
nextRecord
y previousRecord
respectivamente, salvo
porque devuelven el identificador de los registros recorridos, y no su contenido.
Ordenación de registros
Puede que nos interese que la enumeración nos ofrezca los registros
en un orden determinado. Podemos hacer que se ordenen proporcionando nosotros
el criterio de ordenación. Para ello deberemos crear un comparador de
registros que nos diga cuando un registros es mayor, menor o igual que otro
registro. Para crear este comparador deberemos crear una clase que implemente
la interfaz RecordComparator
:
public class MiComparador implements RecordComparator {
public int compare(byte [] reg1, byte [] reg2) {
if( /* reg1 es anterior a reg2 */ ) {
return RecordComparator.PRECEDES;
} else if( /* reg1 es posterior a reg2 */ ) {
return RecordComparator.FOLLOWS;
} else if( /* reg1 es igual a reg2 */ ) {
return RecordComparator.EQUIVALENT;
}
}
}
De esta manera, dentro del código de esta clase deberemos decir cuando
un registro va antes, después o es equivalente a otro registro, para
que el enumerador sepa cómo ordenarlos. Ahora, cuando creemos el enumerador
deberemos proporcionarle un objeto de la clase que hemos creado para que realice
la ordenación tal como lo hayamos especificado en el método compare
:
RecordEnumeration re =
rs.enumerateRecords(new MiComparador(), null, false);
Una vez hecho esto, podremos recorrer los registros del enumerador como hemos visto anteriormente, con la diferencia de que ahora obtendremos los registros en el orden indicado.
Filtrado de registros
Es posible que no queramos que el enumerador nos devuelva todos los registros,
sino sólo los que cumplan unas determinadas características. Es
posible realizar un filtrado para que el enumerador sólo nos devuelva
los registros que nos interesan. Para que esto sea posible deberemos definir
qué características cumplen los registros que nos interesan. Esto
lo haremos creando una clase que implemente la interfaz RecordFilter
:
public class MiFiltro implements RecordFilter {
public boolean matches(byte [] reg) {
if( /* reg nos interesa */ ) {
return true;
} else {
return false;
}
}
}
De esta forma dentro del método matches
diremos si un determinado
registro nos interesa, o si por lo contrario debe ser filtrado para que no aparezca
en la enumeración. Ahora podremos proporcionar este filtro al crear la
enumeración para que filtre los registros según el criterio que
hayamos especificado en el método matches
:
RecordEnumeration re =
rs.enumerateRecords(null, new MiFiltro(), false);
Ahora cuando recorramos la enumeración, sólo veremos los registros que cumplan los criterios impuestos en el filtro.
Es posible que queramos que en cuanto haya un cambio en el almacén de registros se nos notifique. Esto ocurrirá por ejemplo cuando estemos trabajando con la copia de los valores de un conjunto de registros en memoria, y queramos que esta información se mantenga actualizada con los últimos cambios que se hayan producido en el almacén.
Para estar al tanto de estos cambios deberemos utilizar un listener,
que escuche los cambios en el almacén de registros. Este listener
lo crearemos implementando la interfaz RecordListener
, como se
muestra a continuación:
public class MiListener implements RecordListener {
public void recordAdded(RecordStore rs, int id) {
// Se ha añadido un registro con identificador id a rs
}
public void recordChanged(RecordStore rs, int id) {
// Se ha modificador el registro con identificador id en rs
}
public void recordDeleted(RecordStore rs, int id) {
// Se ha eliminado el registro con identificador id de rs
}
De esta forma dentro de estos métodos podremos indicar qué hacer cuando se produzca uno de estos cambios en el almacén de registros. Para que cuando se produzca un cambio en el almacén de registros se le notifique a este listener, deberemos añadir el listener en el correspondiente almacén de registros de la siguiente forma:
rs.addRecordListener(new MiListener());
De esta forma cada vez que se realice alguna operación en la que se añadan, eliminen o modifiquen registros del almacén se le notificará a nuestro listener para que éste pueda realizar la operación que sea necesaria.
Por ejemplo, cuando creamos una enumeración con registros poniendo a
true
el parámetro para que mantenga en todo momento actualizados
los datos de la enumeración, lo que hará será utilizar
un listener para ser notificada de los cambios que se produzcan en
el almacén. Cada vez que se produzca un cambio, el listener
hará que los datos de la enumeración se actualicen.