20. Aplicaciones corporativas

El poder establecer conexiones en red nos permitirá acceder a aplicaciones web corporativas desde el móvil. De esta forma, podremos hacer que estos dispositivos móviles se comporten como front-end de estas aplicaciones corporativas.

20.1. Front-ends de aplicaciones corporativas

Desde los PCs de sobremesa normalmente accedemos a las aplicaciones corporativas utilizando un navegador web. La aplicación web genera de forma dinámica la presentación en el servidor en forma de un documento HTML que será mostrado en los navegadores de los clientes. Podemos aplicar este mismo sistema al caso de los móviles, generando nuestra aplicación web la respuesta en forma de algún tipo de documento que pueda ser interpretado y mostrado en un navegador de teléfono móvil. Por ejemplo estos documentos pueden estar en formato WML, cHTML o XHTML. Esto puede ser suficiente para acceder a algunas aplicaciones desde los móviles.

Utilizar J2ME para realizar este front-end aporta una serie de ventajas sobre el paradigma anterior, como por ejemplo las siguientes:

Normalmente será preferible utilizar HTTP a sockets o datagramas porque esto nos aportará una serie de ventajas. Por un lado, HTTP está soportado por todos los dispositivos MIDP. Al utilizar HTTP tampoco tendremos problema con firewalls intermedios, cosa que puede ocurrir si conectamos por sockets mediante un puerto que esté cerrado. Además las APIs de Java incluyen facilidades para trabajar con HTTP, por lo que será sencillo realizar la comunicación tanto en el cliente como en el servidor.

La conexión de red en los móviles normalmente tiene una alta latencia, un reducido ancho de banda y es posible que se produzcan interrupciones cuando la cobertura es baja. Deberemos tener en cuenta todos estos factores cuando diseñemos nuestra aplicación. Por esta razón deberemos minimizar la cantidad de datos que se intercambian a través de la red, y permitir que la aplicación pueda continuar trabajando correctamente sin conexión.

20.1.1. Tráfico en la red

Para reducir el número de datos que se envían por la red, evitando que se hagan conexiones innecesarias, es conveniente realizar una validación de los datos introducidos por el usuario en el cliente.

Normalmente no podremos validarlos de la misma forma en que se validan en el servidor, ya que por ejemplo no tenemos acceso a las bases de datos de la aplicación, por lo que deberá volverse a validar por el servidor para realizar la validación completa. No obstante, es conveniente realizar esta validación en el cliente como una prevalidación, de forma que detecte siempre que sea posible los datos erróneos en el lado del cliente evitando así realizar una conexión innecesaria con el servidor.

Deberemos envíar y recibir sólo la información necesaria por la red. Podemos reducir este tráfico manteniendo en RMS una copia de los datos remotos que obtengamos (caché), para no tener que volver a solicitarlos si queremos volver a visualizarlos.

20.1.2. Operaciones de larga duración

Dado que la red es lenta, las operaciones que necesiten conectarse a la red serán costosas. Estas operaciones será conveniente que sean ejecutadas por hilos en segundo plano, y nunca deberemos establecer una conexión desde un callback. Otro tipo de operaciones que normalmente son de larga duración son las consultas de datos en RMS.

Cualquier operación que sea costosa temporalmente deberá ser ejecutada de esta forma, para evitar que bloquee la aplicación.

Además siempre que sea posible deberemos mostrar una barra de progreso mientras se realiza la operación, de forma que el usuario tenga constancia de que se está haciendo algo. También será conveniente permitir al usuario que interrumpa estas largas operaciones, siempre que la interrupción pueda hacerse y no cause inconsistencias en los datos.

20.1.3. Personalización

Un aspecto interesante en los clientes J2ME es la posibilidad de incorporar personalización. La personalización consiste en recordar los datos y las preferencias del usuario, de forma que la aplicación se adapte a estas preferencias y el usuario no tenga que introducir estos datos en cada sesión. Podremos pedir esta información de personalización la primera vez que ejecuta la aplicación y almacenar esta información utilizando RMS o bien registrarla en el servidor de forma remota. Si tuviésemos esta información por duplicado, en local y en remoto, deberemos proporcionar mecanismos para sincronizar ambos registros.

Normalmente los dispositivos móviles son personales, por lo que las aplicaciones instaladas en un móvil sólo va a utilizarlas una persona. Por este motivo puede ser conveniente guardar la información del usuario para que no tenga que volver a introducirla cada vez que utilice la aplicación. Podemos hacer que la aplicación recuerde el login y el password del usuario.

Podemos incluir en la ficha de datos del usuario información sobre sus preferencias, de forma que la aplicación se adapte a sus gustos y resulte más cómoda de manejar.

20.2. Integración con aplicaciones corporativas

Vamos a considerar que en el servidor tenemos una aplicación J2EE. En este caso accederemos a la aplicación corporativa a través de un Servlet. Los servlets son componentes Java en el servidor que encapsulan el mecanismo petición/respuesta. Es decir, podemos enviar una petición HTTP a un servlet, y éste la analizará y nos devolverá una respuesta.

Figura 30. Integración de J2ME y J2EE

La aplicación J2ME se comunicará con un servlet. Dentro de la aplicación J2EE en el servidor este servlet utilizará EJBs para realizar las tareas necesarias. Los EJBs son componentes reutilizables que implementan la lógica de negocio de la aplicación. Estos EJBs podrán utilizar otras APIs para realizar sus funciones, como por ejemplo JMS para enviar o recibir mensajes, JDBC para acceder a bases de datos, CORBA para acceder a objetos distribuidos, o bien acceder a Servicios Web utilizando las APIs de XML.

Normalmente tendremos servlets que se encarguen de generar una respuesta para navegadores web HTML. Para las aplicaciones móviles esta respuesta no es adecuada, por lo que deberemos crear otra versión de estos servlets que devuelvan una respuesta adaptada a las necesidades de los móviles. La arquitectura de capas de J2EE nos permitirá crear servlets para distintos tipos de clientes minimizando la cantidad de código redundante, ya que la lógica de negocio está implementada en los EJBs y estos componentes pueden ser reutilizados desde los diferentes servlets.

El proceso de comunicación entre nuestra aplicación y un servlet será el siguiente:

20.2.1. Codificación de los datos

Hemos visto que la aplicación J2ME y el servlet de la aplicación J2EE intercambian datos con una determinada codificación. En J2ME no tenemos disponibles mecanismos de alto nivel para intercambiar información con componentes remotos, como por ejemplo RMI para la invocación de métodos de objetos Java remotos, o las API de análisis de XML para intercambiar información en este formato. Por lo tanto deberemos codificar la información con formatos propios. Seremos nosotros los que decidamos qué formato y codificación deben tener estos datos.

Podemos movernos entre dos extremos: la codificación de los datos en binario y la codificación en XML.

La codificación binaria de los datos será eficiente y compacta. Será sencillo codificar información en este formato utilizando los objetos DataOutputStream y ByteArrayOutputStream. Tiene el inconveniente de que tanto el cliente como el servidor deberán conocer cómo está codificada la información dentro del array de bytes, por lo que estos componentes estarán altamente acoplados.

Si hemos definido una serialización para los objetos, podemos aprovechar esta serialización para enviarlos a través de la red. En este caso la serialización la hemos definido manualmente nosotros en un método del objeto, y no se hace automáticamente como en el caso de J2SE, por lo que deberemos tener cuidado de que en el objeto del cliente y en el del servidor se serialice y deserialice de la misma forma. Además, al transferir un objeto entre J2ME y J2EE deberemos asegurarnos de que este objeto utiliza solamente la parte común de la API de Java en ambas plataformas.

Por ejemplo, si definimos los métodos de serialización para la clase Cita de la siguiente forma:

public class Cita {

Date fecha;
String asunto;
String lugar;
String contacto;
boolean alarma;

public void serialize(DataOutputStream dos) throws IOException { dos.writeLong(fecha.getTime()); dos.writeUTF(asunto); dos.writeUTF(lugar); dos.writeUTF(contacto); dos.writeBoolean(alarma); }

public static Cita deserialize(DataInputStream dis) throws IOException { Cita cita = new Cita();

cita.setFecha(new Date(dis.readLong())); cita.setAsunto(dis.readUTF()); cita.setLugar(dis.readUTF()); cita.setContacto(dis.readUTF()); cita.setAlarma(dis.readBoolean());

return cita; } }

Podremos intercambiar objetos de este tipo mediante HTTP como se muestra a continuación:

HttpConnection con = (HttpConnection) Connector.open(URL_SERVLET);

// Envia datos al servidor DataOutputStream dos = con.openDataOutputStream(); Cita[] citasCliente = datosCliente.getCitas(); if (citasCliente == null) { dos.writeInt(0); } else { dos.writeInt(citasCliente.length); for (int i = 0; i < citasCliente.length; i++) { citasCliente[i].serialize(dos); } }

// Recibe datos del servidor DataInputStream dis = con.openDataInputStream(); int nCitasServidor = dis.readInt(); Cita[] citasServidor = new Cita[nCitasServidor]; for (int i = 0; i < nCitasServidor; i++) { citasServidor[i] = Cita.deserialize(dis); }

En el otro extremo, XML es un lenguaje complejo de analizar y la información ocupa más espacio. Como ventaja tenemos que XML es un lenguaje estándar y autodescriptivo, por lo que reduciremos el acoplamiento de cliente y servidor. Aunque en MIDP no se incluyen librerías para procesar XML, diversos fabricantes proporcionan sus propias implementaciones de las librerías de XML para J2ME. Podemos utilizar estas librerías para crear y analizar estos documentos en los clientes móviles.

Podemos encontrar incluso implementaciones de librerías de XML orientado a RPC para invocar Servicios Web SOAP directamente desde el móvil. Debemos tener cuidado al invocar Servicios Web directamente desde los móviles, ya que la construcción y el análisis de los mensajes SOAP es una tarea demasiado costosa para estos dispositivos. Para optimizar la aplicación, en lugar de invocarlos desde el mismo móvil, podemos hacer que sea la aplicación J2EE la que invoque el servicio, y que el móvil se comunique con esta aplicación a través de mensajes sencillos.

20.2.2. Mantenimiento de sesiones

El protocolo HTTP sigue un mecanismo de petición/respuesta, no mantiene información de sesión. Es decir, si realizamos varias peticiones HTTP a un servidor desde nuestro cliente, cada una de estas peticiones será tratada independientemente por el servidor, sin identificar que se trata de un mismo usuario. Para implementar sesiones sobre protocolo HTTP tendremos que recurrir a mecanismos como la reescritura de URLs o las cookies.

Normalmente cuando accedamos a una aplicación web necesitaremos mantener una sesión para que en todas las peticiones que hagamos al servidor, éste nos identifique como un mismo usuario. De esta forma por ejemplo podremos ir añadiendo con cada petición productos a un carrito de la compra en el lado del servidor, sin perder la información sobre los productos añadidos de una petición a otra.

Para mantener las sesiones lo que se hará es obtener en el cliente un identificador de la sesión, de forma que en cada petición que se haga al servidor se envíe este identificador para que el servidor sepa a qué sesión pertenece dicha petición. Este identificador puede ser obtenido mediante cookies, o bien incluirlo como parte de las URLs utilizando la técnica de reescritura de URLs.

Los navegadores web normalmente implementan las sesiones mediante cookies. Estas cookies son información que el servidor nos envía en la respuesta y que el navegador almacena de forma local en nuestra máquina. En la primera petición el servidor enviará una cookie al cliente con el identificador de la sesión, y el navegador almacenará esta cookie de forma local en el cliente. Cuando se vaya a hacer otra petición al servidor, el navegador envía esta cookie para identificarnos ante el servidor como el mismo cliente. De esta forma el servidor podrá utilizar el valor de la cookie recibida para determinar la sesión correspondiente a dicho cliente, y de esta forma poder acceder a los datos que hubiese almacenado en peticiones anteriores dentro de la misma sesión.

Sin embargo, cuando conectamos desde un cliente J2ME estamos estableciendo una conexión con la URL del servlet desde nuestra propia aplicación, no desde un navegador que gestione automáticamente estas cookies. Por lo tanto será tarea nuestra implementar los mecanismos necesarios para mantener esta sesión desde el cliente.

Vamos a ver como implementar los mecanismos para mantenimiento de sesiones en las aplicaciones J2ME. Será más fiable utilizar reescritura de URLs, ya que algunos gateways podrían filtrar las cookies y por lo tanto este mecanismo fallaría.

Estos mecanismos de cookies y reescritura de URLs se utilizan para que los navegadores mantengan las sesiones de una forma estándar para todas las aplicaciones. Pero lo que pretendemos en última instancia es tener un identificador de la sesión en el cliente que pueda ser enviado al servidor en cada petición. Si nos conectamos desde nuestra propia aplicación podremos utilizar nuestro propio identificador y enviarlo al servidor de la forma que queramos (como cabecera, parámetro, en el post, etc). Por ejemplo, podríamos hacer que en cada petición que haga nuestra aplicación J2ME envíe nuestro login al servidor, de forma que esta información le sirva al servidor para identificarnos en cada momento.

Sin embargo, será conveniente que nuestra aplicación implemente alguno de los mecanismos estándar para el mantenimiento de sesiones, ya que así podremos aprovechar las facilidades que ofrecen los componentes del servidor para mantener las sesiones. Ahora veremos como implementar las técnicas de reescritura de URLs y cookies en nuestras aplicaciones J2ME.

Reescritura de URLs

Algunos navegadores no soportan cookies. Para mantener sesiones en este caso podemos utilizar la técnica de reescritura de URLs. Esta técnica consiste en modificar las URLs a las que accederá el cliente incluyendo en ellas el identificador de sesión como parámetro.

Para utilizar esta técnica deberemos codificar la URL en el servidor y devolverla de alguna forma al cliente. Por ejemplo, podemos devolver esta URL modificada como una cabecera HTTP propia. Supongamos que devolvemos esta URL reescrita como una cabecera URL-Reescrita. Podremos obtenerla en la aplicación cliente de la siguiente forma:

String url_con_ID = con.getHeaderField("URL-Reescrita");

En la próxima petición que hagamos al servidor deberemos utilizar la URL que hemos obtenido, en lugar de la URL básica a la que conectamos inicialmente:

HttpConnection con = (HttpConnection)Connector.open(url_con_ID);

De esta forma cuando establezcamos esta segunda conexión el servlet al que conectamos sabrá que se trata de la misma sesión y podremos acceder a la información de la sesión dentro del servidor.

En el código del servlet que atiende nuestra petición en el servidor deberemos rescribir la URL y devolvérsela al cliente como la cabecera que hemos visto anteriormente. Esto podemos hacerlo de la siguiente forma:

String url = request.getRequestURL().toString();
String url_con_ID = response.encodeURL(url);
response.setHeader("URL-Reescrita", url_con_ID);

Manejo de cookies

Los servlets utilizan las cookies para mantener la sesión siempre que detecten que el cliente soporta cookies. Para el caso de estas aplicaciones J2ME el servlet detectará que el cliente no soporta cookies, por lo que utilizará únicamente reescritura de URLs. Sin embargo, si que podremos crear cookies manualmente en el servlet para permitir mantener la información del usuario en el cliente durante el tiempo que dure la sesión o incluso durante más tiempo.

Desde las aplicaciones J2ME podremos implementar las sesiones utilizando cookies. Además con las cookies podremos mantener información del usuario de forma persistente, no únicamente durante una sola sesión. Por ejemplo, podemos utilizar RMS para almacenar las cookies con información sobre el usuario, de forma que cuando se vuelve a utilizar la aplicación en otro momento sigamos teniendo esta información. Con esto podemos por ejemplo evitar que el usuario tenga que autentificarse cada vez que entra a la aplicación.

Estas cookies consisten en una pareja <nombre, valor> y una fecha de caducidad. Con esta fecha de caducidad podemos indicar si la cookie debe mantenerse sólo durante la sesión actual, o por más tiempo.

Podemos recibir las cookies que nos envía el servidor leyendo la cabecera set-cookie de la respuesta desde nuestra aplicación J2ME:

String cookie = con.getHeaderField("set-cookie");

Una vez tengamos la cookie podemos guardárnosla en memoria, o bien en RMS si queremos almacenar persistentemente esta información. En las siguientes peticiones que hagamos al servidor deberemos enviarle esta cookie en la cabecera cookie:

con.setRequestProperty("cookie", cookie);

20.2.3. Seguridad

Vamos a ver como crear aplicaciones seguras en cuando a la autentificación de los usuarios y a la confidencialidad de los datos que circulan por la red.

Confidencialidad

Para proteger los datos que circulan por la red, y evitar que alguien pueda interceptarlos, podemos enviarlos codificados mediante SSL.

Podemos establecer una conexión segura con el servidor mediante SSL (Secure Sockets Layer) simplemente indicando como protocolo en la URL de la conexión https en lugar de http.

El problema es que la mayoría de móviles con MIDP 1.0 no soportan este tipo de protocolo. Si intentamos utilizar HTTPS desde un dispositivo que no lo soporta, obtendremos una excepción de tipo ConnectionNotFoundException. Además de tener que se soportada por el móvil, este tipo de conexión deberá ser soportada por el servidor.

Autentificación

En muchas aplicaciones necesitaremos que el usuario se autentifique para que de esa forma pueda acceder a determinados datos privados alojados en el servidor, o pueda realizar determinadas acciones para las que no todos los usuarios tienen permiso.

Para autentificar a los usuarios que se conectan desde el móvil a las aplicaciones corporativas, normalmente utilizaremos seguridad a nivel de la aplicación. Es decir, enviaremos nuestros credenciales (login y password) a un servlet para que los verifique en la base de datos de usuarios que haya en el servidor.

Una vez autentificados, podremos mantener esta información de registro en la sesión del usuario, de forma que en sucesivas peticiones podamos obtener la información sin necesidad de volvernos a autentificar en cada una de ellas.

20.2.4. Transacciones

Cada petición HTTP que se haga el servidor será una transacción independiente. Todas las operaciones de la transacción se deben realizar dentro de esa conexión en el servidor, ya que las peticiones HTTP son independientes entre si y el dispositivo cliente móvil no realiza ninguna gestión de transacciones. Por lo tanto las transacciones nunca supondrán más de una petición HTTP.

En el caso en el que la inserción de unos datos en la memoria local dependa de que los datos sean aceptados por el servidor remoto, siempre realizaremos antes la petición al servidor remoto, y una vez hayamos enviados estos datos correctamente, haremos la inserción local. Esto se da por ejemplo en el caso en que queremos mantener una copia local de los datos que enviamos al servidor. Sólo insertaremos los datos en la copia local una vez hayan sido insertados en el servidor.

20.2.5. Gestión de errores

Cuando estemos accediendo al lado del servidor de nuestra aplicación mediante HTTP, si la petición que hemos hecho produce un error no podremos gestionar este error mediante excepciones ya que lo único que vamos a recibir es una respuesta HTTP.

Para gestionar los errores del servidor deberemos almacenarlos de alguna forma en el mensaje de respuesta HTTP.

Podemos utilizar el código de estado de la respuesta para indicar la presencia de algún error. Si necesitamos una mayor flexibilidad podemos establecer nuestra propia codificación para los errores dentro del contenido de la respuesta, y hacer que nuestro cliente sea capaz de interpretar dicha codificación.

20.3. Arquitectura MVC

El patrón de diseño MVC (Modelo-Vista-Controlador) suele aplicarse para diseñar las aplicaciones web J2EE. Podemos aplicar este mismo patrón a las aplicaciones cliente J2ME. En una aplicación cliente tendremos los siguientes elementos:

De esta forma con esta arquitectura estamos aislando datos, presentación y flujo de control. Es especialmente interesante el haber aislado los datos del resto de componentes. De esta forma la capa de datos podrá decidir si trabajar con datos de forma local con RMS o de forma remota a través de HTTP, sin afectar con ello al resto de la aplicación. Este diseño nos facilitará cambiar de modo remoto a modo local cuando no queramos tener que establecer una conexión de red.

20.3.1. Modelo

Normalmente en las aplicaciones para dispositivos móviles vamos a trabajar tanto con datos locales como con datos remotos. Por lo tanto, podemos ver el modelo dividido en dos subsistemas: modelo local y modelo remoto.

Modelo local

El modelo local normalmente utilizará un adaptador RMS para acceder a los datos almacenados de forma persistente en el móvil. Por ejemplo, en nuestra aplicación de agenda para la gestión de citas podemos crear la siguiente clase para acceder a los datos locales:

public class ModeloLocal {

AdaptadorRMS rms;

public ModeloLocal() throws RecordStoreException { rms = new AdaptadorRMS(); }

/* * Agrega una cita indicando si esta pendiente de ser enviada al servidor */ public void addCita(Cita cita, boolean pendiente) throws IOException, RecordStoreException { int id = rms.addCita(cita); IndiceCita indice = new IndiceCita(); indice.setId(id); indice.setFecha(cita.getFecha()); indice.setAlarma(cita.isAlarma()); indice.setPendiente(pendiente); rms.addIndice(indice);
}

/* * Obtiene todas las citas */ public Cita[] listaCitas() throws RecordStoreException, IOException { return rms.listaCitas(); }

/* * Obtiene las citas correspondientes a los indices indicados */ public Cita[] listaCitas(IndiceCita[] indices) throws RecordStoreException, IOException { Cita[] citas = new Cita[indices.length]; for (int i = 0; i < indices.length; i++) { citas[i] = rms.getCita(indices[i].getId()); }
return citas;
}
/* * Busca las citas con alarmas pendientes */ public IndiceCita[] listaAlarmasPendientes() throws RecordStoreException, IOException { IndiceCita[] indices = rms.buscaCitas(new Date(), true);
return indices; }

/* * Busca las citas pendientes de ser enviadas al servidor */ public IndiceCita[] listaCitasPendientes() throws RecordStoreException, IOException { IndiceCita[] indices = rms.listaCitasPendientes();
return indices; }

/* * Marca las citas indicada como enviadas al servidor */ public void marcaEnviados(IndiceCita[] indices) throws IOException, RecordStoreException { for (int i = 0; i < indices.length; i++) { indices[i].setPendiente(false); rms.updateIndice(indices[i]); } }

/* * Obtiene la configuracion local */ public InfoLocal getInfoLocal() throws RecordStoreException, IOException {

try { InfoLocal info = rms.getInfoLocal(); return info; } catch (Exception e) { }

InfoLocal info = new InfoLocal(); rms.setInfoLocal(info);

return info; }

/* * Modifica la configuracion local */ public void setInfoLocal(InfoLocal info) throws RecordStoreException, IOException { rms.setInfoLocal(info); }

/* * Libera recursos del modelo */ public void destroy() throws RecordStoreException { rms.cerrar(); } }

En esta clase definiremos métodos para acceder a todos los datos locales que nuestra aplicación necesite utilizar.

Modelo remoto

Además de la información local que almacenamos en el móvil, será importante tener acceso a los datos remotos de nuestra aplicación corporativa. Esta parte del modelo normalmente utilizará HTTP para acceder a estos datos.

Para implementar este subsistema del modelo podemos utilizar el patrón de diseño proxy. Este patrón se utiliza cuando accedemos a componentes remotos, encapsulando dentro de la clase proxy todo el mecanismo de comunicación necesario para acceder a las funcionalidades del servidor.

De esta forma, cuando utilizamos un proxy para acceder a componentes remotos, el que estos componentes se estén utilizando de forma remota es transparente para nosotros. El proxy implementa todo el mecanismo de comunicación necesario (por ejemplo HTTP), aislando al resto de la aplicación de él. Invocaremos los métodos del proxy directamente para utilizar funcionalidades de nuestro servidor como si se tratase de un acceso a un objeto local, sin preocuparnos de que realmente el proxy esté realizando una conexión remota.

Por ejemplo, en el caso de nuestra agenda, el servidor nos proporcionará la funcionalidad de sincronizar las citas almacenadas en el móvil con las notas almacenadas en el servidor, para hacer que en ambos lados se tenga el mismo conjunto de citas. Podemos encapsular esta operación en una clase proxy como la siguiente:

public class ProxyRemoto {

public ProxyRemoto() {
}

public SyncItem sincroniza(SyncItem datosCliente) throws IOException { HttpConnection con = (HttpConnection) Connector.open(URL_SERVLET);

// Envia datos al servidor DataOutputStream dos = con.openDataOutputStream(); Cita[] citasCliente = datosCliente.getCitas(); dos.writeLong(datosCliente.getTimeStamp()); if (citasCliente == null) { dos.writeInt(0); } else { dos.writeInt(citasCliente.length); for (int i = 0; i < citasCliente.length; i++) { citasCliente[i].serialize(dos); } }

// Recibe datos del servidor DataInputStream dis = con.openDataInputStream(); long tsServidor = dis.readLong(); int nCitasServidor = dis.readInt(); Cita[] citasServidor = new Cita[nCitasServidor]; for (int i = 0; i < nCitasServidor; i++) { citasServidor[i] = Cita.deserialize(dis); }

SyncItem datosServidor = new SyncItem(); datosServidor.setTimeStamp(tsServidor); datosServidor.setCitas(citasServidor);

return datosServidor; }
}

De esta forma desde el resto de la aplicación simplemente tendremos que invocar el método sincroniza del proxy para utilizar esta funcionalidad del servidor.

Patrón de diseño fachada

Hasta ahora hemos visto que tenemos dos subsistemas en el modelo. Esta división podría causar que el acceso al modelo desde nuestra aplicación fuese demasiado complejo.

Para evitar esto podemos utilizar el patrón de diseño fachada (facade). Este patrón consiste un implementar una interfaz única que integre varios subsistemas, proporcionando de esta forma una interfaz sencilla para el acceso al modelo.

Desde el resto de la aplicación accederemos al modelo siempre a través de la fachada, y ésta será quien se encargue de coordinar los subsistemas local y remoto. Estos dos subsistemas se mantendrán independientes, pero accederemos a ellos mediante una interfaz única.

Por ejemplo, para nuestra agenda podemos definir una fachada como la siguiente para el modelo:

public class FachadaModelo {

boolean online;
ModeloLocal mLocal;
ProxyRemoto mRemoto;

public FachadaModelo() throws RecordStoreException, IOException { mLocal = new ModeloLocal(); mRemoto = new ProxyRemoto();

InfoLocal info = getConfig(); online = info.isOnline(); }

/* * Crea una nueva cita */ public void nuevaCita(Cita cita) throws IOException, RecordStoreException { mLocal.addCita(cita, true); if (online) { sincroniza(); } }

/* * Obtiene la lista de todas las citas */ public Cita[] listaCitas() throws RecordStoreException, IOException { if (online) { sincroniza(); } return mLocal.listaCitas(); }

/* * Obtiene la lista de citas con alarmas pendientes */ public Cita[] listaAlarmasPendientes() throws RecordStoreException, IOException { if (online) { sincroniza(); } IndiceCita[] indices = mLocal.listaAlarmasPendientes(); return mLocal.listaCitas(indices); }

/* * Obtiene la configuracion local */ public InfoLocal getConfig() throws RecordStoreException, IOException { return mLocal.getInfoLocal(); }

/* * Actualiza la configuracion local */ public void updateConfig(InfoLocal config) throws RecordStoreException, IOException { InfoLocal info = mLocal.getInfoLocal(); info.setOnline(config.isOnline()); this.online = config.isOnline(); mLocal.setInfoLocal(info); }

/* * Sincroniza la lista de citas con el servidor */ public void sincroniza() throws RecordStoreException, IOException {

// Obtiene datos del cliente
InfoLocal info = mLocal.getInfoLocal();

IndiceCita[] indices = mLocal.listaCitasPendientes(); Cita[] citas = mLocal.listaCitas(indices); SyncItem datosCliente = new SyncItem(); datosCliente.setCitas(citas); datosCliente.setTimeStamp(info.getTimeStamp());

// Envia y recibe
SyncItem datosServidor = mRemoto.sincroniza(datosCliente);

mLocal.marcaEnviados(indices);

// Agrega los datos recibidos del servidor

Cita[] citasServidor = datosServidor.getCitas(); if (citasServidor != null) { for (int i = 0; i < citasServidor.length; i++) { mLocal.addCita(citasServidor[i], false); } }

info.setTimeStamp(datosServidor.getTimeStamp()); mLocal.setInfoLocal(info); }

public void destroy() throws RecordStoreException { mLocal.destroy(); }
}

Podemos ver en este ejemplo como este modelo nos permite trabajar de forma online u offline. Según el modo de conexión, se utilizarán las funciones de uno u otro subsistema. En modo online siempre que se haga una operación se accederá al servidor para leer o almacenar las novedades que haya. En modo offline sólo se leerán o almacenarán los datos de forma local, excepto cuando solicitemos de forma explícita sincronizar los datos con el servidor.

En el caso del método sincroniza, que será el método encargado de coordinar información local con información remota, podemos ver que la transacción de sincronización se realiza en una única petición HTTP. Además, los mensajes que se hayan enviado al servidor no se marcan como enviados hasta después de haber completado el envío (se llama a marcaEnviados después de llamar a sincroniza).

20.3.2. Vista

Para la vista crearemos una clase para cada pantalla de nuestra aplicación, como hemos visto en temas anteriores. La clase correspondiente a una pantalla heredará del tipo de displayable correspondiente y encapsulará la creación de la interfaz y la respuesta a los comandos.

Por ejemplo, para la edición de datos de las citas en nuestra aplicación podemos utilizar la siguiente pantalla:

public class EditaCitaUI extends Form implements CommandListener {

ControladorUI controlador;

DateField iFecha; TextField iAsunto; TextField iLugar; TextField iContacto; ChoiceGroup iAlarma; int itemAlarmaOn; int itemAlarmaOff;

Command cmdAceptar; Command cmdCancelar; int eventoAceptar = ControladorUI.EVENTO_AGREGA_CITA; int eventoCancelar = ControladorUI.EVENTO_MUESTRA_MENU;

Cita cita;
public EditaCitaUI(ControladorUI controlador) { super(controlador.getString(Recursos.STR_DATOS_TITULO));
this.controlador = controlador;

iFecha = new DateField(
controlador.getString(Recursos.STR_DATOS_ITEM_FECHA),
DateField.DATE_TIME); iAsunto = new TextField(
controlador.getString(Recursos.STR_DATOS_ITEM_ASUNTO),
"", MAX_LENGHT, TextField.ANY); iLugar = new TextField(
controlador.getString(Recursos.STR_DATOS_ITEM_LUGAR),
"", MAX_LENGHT, TextField.ANY); iContacto = new TextField(
controlador.getString(Recursos.STR_DATOS_ITEM_CONTACTO),
"", MAX_LENGHT, TextField.ANY); iAlarma = new ChoiceGroup(
controlador.getString(Recursos.STR_DATOS_ITEM_ALARMA), ChoiceGroup.EXCLUSIVE); itemAlarmaOn = iAlarma.append(
controlador.getString(Recursos.STR_DATOS_ITEM_ALARMA_ON), null); itemAlarmaOff = iAlarma.append(
controlador.getString(Recursos.STR_DATOS_ITEM_ALARMA_OFF), null);

this.append(iFecha); this.append(iAsunto); this.append(iLugar); this.append(iContacto); this.append(iAlarma);

cmdAceptar = new Command(
controlador.getString(Recursos.STR_CMD_ACEPTAR), Command.OK, 1); cmdCancelar = new Command(
controlador.getString(Recursos.STR_CMD_CANCELAR), Command.CANCEL, 1); this.addCommand(cmdAceptar); this.addCommand(cmdCancelar);

this.setCommandListener(this); }

private void setCita(Cita cita) { if (cita == null) { this.cita = new Cita(); iFecha.setDate(new Date()); iAsunto.setString(null); iLugar.setString(null); iContacto.setString(null); iAlarma.setSelectedIndex(itemAlarmaOff, true); } else { this.cita = cita; iFecha.setDate(cita.getFecha()); iAsunto.setString(cita.getAsunto()); iLugar.setString(cita.getLugar()); iContacto.setString(cita.getContacto()); if (cita.isAlarma()) { iAlarma.setSelectedIndex(itemAlarmaOn, true); } else { iAlarma.setSelectedIndex(itemAlarmaOff, true); } } }

private Cita getCita() { if(cita==null) { this.cita = new Cita(); } if(iFecha.getDate()==null) { cita.setFecha(new Date()); } else { cita.setFecha(iFecha.getDate()); } cita.setAsunto(iAsunto.getString()); cita.setLugar(iLugar.getString()); cita.setContacto(iContacto.getString()); cita.setAlarma(iAlarma.getSelectedIndex() == itemAlarmaOn);
return cita; }

public void reset(Cita cita, int eventoAceptar, int eventoCancelar) { this.setCita(cita); this.eventoAceptar = eventoAceptar; this.eventoCancelar = eventoCancelar; }

public void commandAction(Command cmd, Displayable disp) { if (cmd == cmdAceptar) { controlador.procesaEvento(eventoAceptar, this.getCita()); } else if (cmd == cmdCancelar) { controlador.procesaEvento(eventoCancelar, null); } } }

Intentaremos llevar la mayor parte de la gestión de eventos al controlador, para así aislar lo más posible la vista del controlador. En la vista implementaremos la interfaz commandAction, pero el procesamiento de los eventos lo hará el controlador mediante el método procesaEvento que veremos a continuación.

20.3.3. Controlador

El controlador será el encargado de controlar el flujo de la aplicación. Según las acciones realizadas, el controlador mostrará distintas pantalla y realizará diferentes operaciones en el modelo.

Por ejemplo, el controlador para nuestra agenda será el siguiente:

public class ControladorUI {
/* * Tipos de eventos */ public final static int EVENTO_MUESTRA_MENU = 0; public final static int EVENTO_MUESTRA_NUEVA_CITA = 1; public final static int EVENTO_MUESTRA_LISTA_CITAS = 2; public final static int EVENTO_MUESTRA_DATOS_CITA = 3; public final static int EVENTO_MUESTRA_LISTA_ALARMAS_PENDIENTES = 4; public final static int EVENTO_MUESTRA_DATOS_ALARMA_PENDIENTE = 5; public final static int EVENTO_AGREGA_CITA = 6; public final static int EVENTO_SALIR = 8; public final static int EVENTO_SINCRONIZAR = 9; public final static int EVENTO_MUESTRA_CONFIG =10; public final static int EVENTO_APLICA_CONFIG = 11;

/* * Componentes de la UI */ DatosCitaUI uiDatosCita; EditaCitaUI uiEditaCita; ListaCitasUI uiListaCitas; EditaConfigUI uiEditaConfig; MenuPrincipalUI uiMenuPrincipal; BarraProgresoUI uiBarraProgreso; /* * Gestor de recursos */ Recursos recursos; /* * Modelo */ FachadaModelo modelo; MIDletAgenda midlet; Display display; public ControladorUI(MIDletAgenda midlet) { this.midlet = midlet; display = Display.getDisplay(midlet); init(); } /* * Inicializacion de los componentes de la UI */ public void init() { // Crea gestor de recursos recursos = new Recursos();

// Crea UI uiDatosCita = new DatosCitaUI(this); uiEditaCita = new EditaCitaUI(this); uiListaCitas = new ListaCitasUI(this); uiEditaConfig = new EditaConfigUI(this); uiMenuPrincipal = new MenuPrincipalUI(this); uiBarraProgreso = new BarraProgresoUI(this); try { // Crea modelo modelo = new FachadaModelo(alarmas); } catch (Exception e) { e.printStackTrace(); } } public void destroy() throws RecordStoreException { modelo.destroy(); } public void showMenu() { display.setCurrent(uiMenuPrincipal); } public String getString(int cod) { return recursos.getString(cod); } /* * Lanza el procesamiento de un evento */ public void procesaEvento(int evento, Object param) { HiloEventos he = new HiloEventos(evento, param); he.start(); } /* * Hilo para el procesamiento de eventos */ class HiloEventos extends Thread { int evento; Object param; public HiloEventos(int evento, Object param) { this.evento = evento; this.param = param; } public void run() { Cita cita; Cita [] citas; InfoLocal info; try { switch(evento) { case EVENTO_MUESTRA_MENU: showMenu(); break;
case EVENTO_MUESTRA_NUEVA_CITA: uiEditaCita.reset(null,
ControladorUI.EVENTO_AGREGA_CITA,
ControladorUI.EVENTO_MUESTRA_MENU); display.setCurrent(uiEditaCita); break;
case EVENTO_AGREGA_CITA: cita = (Cita)param; modelo.nuevaCita(cita); showMenu(); break;
case EVENTO_MUESTRA_LISTA_CITAS: uiBarraProgreso.reset(
getString(Recursos.STR_PROGRESO_CARGA_LISTA),
10, 0, true); display.setCurrent(uiBarraProgreso); citas = modelo.listaCitas(); uiListaCitas.reset(citas,
ControladorUI.EVENTO_MUESTRA_DATOS_CITA,
ControladorUI.EVENTO_MUESTRA_MENU); display.setCurrent(uiListaCitas); break;
case EVENTO_MUESTRA_DATOS_CITA: cita = (Cita)param; uiDatosCita.reset(cita,
ControladorUI.EVENTO_MUESTRA_LISTA_CITAS); display.setCurrent(uiDatosCita); break;
case EVENTO_MUESTRA_LISTA_ALARMAS_PENDIENTES: uiBarraProgreso.reset(
getString(Recursos.STR_PROGRESO_CARGA_LISTA),
10, 0, true); display.setCurrent(uiBarraProgreso); citas = modelo.listaAlarmasPendientes(); uiListaCitas.reset(citas,
ControladorUI.EVENTO_MUESTRA_DATOS_ALARMA_PENDIENTE,
ControladorUI.EVENTO_MUESTRA_MENU); display.setCurrent(uiListaCitas); break;
case EVENTO_MUESTRA_DATOS_ALARMA_PENDIENTE: cita = (Cita)param; uiDatosCita.reset(cita,
ControladorUI.EVENTO_MUESTRA_LISTA_ALARMAS_PENDIENTES); display.setCurrent(uiDatosCita); break;
case EVENTO_SINCRONIZAR: uiBarraProgreso.reset(
getString(Recursos.STR_PROGRESO_SINCRONIZA),
10, 0, true); display.setCurrent(uiBarraProgreso); modelo.sincroniza(); display.setCurrent(uiMenuPrincipal); break;
case EVENTO_MUESTRA_CONFIG: info = modelo.getConfig(); uiEditaConfig.reset(info,
ControladorUI.EVENTO_APLICA_CONFIG,
ControladorUI.EVENTO_MUESTRA_MENU); display.setCurrent(uiEditaConfig); break;
case EVENTO_APLICA_CONFIG: info = (InfoLocal)param; modelo.updateConfig(info); display.setCurrent(uiMenuPrincipal); break;
case EVENTO_SALIR: midlet.salir(); break; } } catch(Exception e) { e.printStackTrace(); } } }
}

Vemos en este ejemplo que el controlador tiene acceso al modelo a través de la fachada, y también tiene acceso a las diferentes pantallas de la interfaz de usuario (UI). A través de estos elementos controlará la presentación de la aplicación y la lógica de negocio de la misma.

Este controlador se encargará de dar respuesta a los eventos que se produzcan en la aplicación. Vemos que para hacer esto se utiliza un método procesaEvento que a su vez utiliza un hilo para procesar el evento solicitado. Esto se hace así para no bloquear el hilo de la aplicación al realizar operaciones que normalmente serán de larga duración.

Tenemos varios tipos de eventos definidos como constantes, y para cada uno de ellos especificaremos las operaciones a realizar y los componentes de la UI que se deben mostrar. Se puede observar en el ejemplo que en las operaciones que son de más larga duración, el controlador mientras la realiza muestra en pantalla una barra de progreso.

También se puede observar que todas las cadenas de texto se encuentran centralizadas en la clase Recursos. Todos los componentes de la aplicación obtendrán el texto de esta clase. Esto nos facilitará tareas como la traducción de nuestra aplicación a otros idiomas.

Para la internacionalización de la aplicación, podríamos hacer que la clase Recursos cargase todas las cadenas de texto de un fichero externo. De esta forma, cambiando este fichero podremos cambiar el idioma de nuestra aplicación. Podríamos permitir que el usuario se descargase ficheros con otros idiomas.

20.4. Modo sin conexión

Hemos visto que en las aplicaciones MIDP es conveniente permitir trabajar sin conexión, trabajando con datos locales. También hemos visto como con el patrón MVC podemos dividir el modelo en dos subsistemas para permitir los dos modos de funcionamiento: con y sin conexión.

Vamos ahora a estudiar más a fondo el funcionamiento de este tipo de aplicaciones.

20.4.1. Tipos de aplicaciones

Podemos distinguir varios tipos de aplicaciones:

Nos centraremos en el estudio de las aplicaciones de tipo thick. Estas aplicaciones deberán trabajar de forma coordinada con el servidor, pero podremos permitir trabajar sin conexión. El permitir trabajar en este modo nos trae la ventaja de que minimiza el trafico en la red, sin embargo aumentará el coste de procesamiento, memoria y almacenamiento en el dispositivo local. Deberemos por lo tanto buscar un compromiso entre estos dos factores.

20.4.2. Replicación de datos

Para poder trabajar sin conexión es imprescindible replicar en nuestro dispositivo datos del servidor. Por lo tanto tendremos dos tipos de datos en el cliente:

Además, dentro de los datos replicados del servidor podemos distinguir:

El modelo de réplica de datos que utilicemos se caracterizará por los siguientes factores:

Según estos factores deberemos decidir el modelo de sincronización adecuado para nuestra aplicación.

Para facilitar la sincronización es conveniente reducir la granularidad de los datos en el almacenamiento, es decir, reducir la cantidad de datos que se almacenen juntos en el mismo registro.

20.4.3. Sincronización de los datos

Los datos de nuestra aplicación se pueden transferir de diferentes formas:

Descarga de datos del servidor

Este caso se puede resolver de forma sencilla. Las actualizaciones de los datos se pueden hacer de varias formas:

En muchas aplicaciones necesitaremos identificar qué datos no descargados todavía hay en el servidor. Por ejemplo, imaginemos nuestra aplicación de agenda. Podemos crear nuevas citas utilizando un navegador web en nuestro PC o desde otro dispositivo. Cuando ejecutemos la aplicación en nuestro móvil, para actualizar la agenda deberá descargar las nuevas citas que se hayan creado en el servidor.

Pero, ¿cómo podemos saber qué citas son nuevas? Podemos utilizar estampas de tiempo (timestamps) para etiquetar cada cita, de forma que cada nueva cita que añadamos al servidor tenga una estampa de tiempo superior a la de la anterior cita.

public void sincroniza() throws RecordStoreException, IOException {

// Obtiene datos del cliente InfoLocal info = mLocal.getInfoLocal();
IndiceCita[] indices = mLocal.listaCitasPendientes(); Cita[] citas = mLocal.listaCitas(indices); SyncItem datosCliente = new SyncItem(); datosCliente.setCitas(citas); datosCliente.setTimeStamp(info.getTimeStamp());

// Envia y recibe SyncItem datosServidor = mRemoto.sincroniza(datosCliente);

mLocal.marcaEnviados(indices);

// Agrega los datos recibidos del servidor

Cita[] citasServidor = datosServidor.getCitas(); if (citasServidor != null) { for (int i = 0; i < citasServidor.length; i++) { mLocal.addCita(citasServidor[i], false); } }

info.setTimeStamp(datosServidor.getTimeStamp()); mLocal.setInfoLocal(info); }

En nuestro dispositivo nos guardaremos el número de la última estampa de tiempo obtenida. Cuando solicitemos los nuevos datos al servidor, le proporcionaremos esta estampa de tiempo para que nos devuelva sólo aquellas citas que sean posteriores. Junto a estas citas nos devolverá una nueva estampa de tiempo correspondiente al último dato que hayamos recibido. El cliente tendrá la responsabilidad de almacenar esta estampa de tiempo para poder utilizarla en la próxima petición.

Envío de datos no compartidos al servidor

En este caso deberemos actualizar cada cierto período en el servidor los datos que hayamos modificado en nuestro dispositivo. La actualización podemos hacer que sea automática, cada cierto período de tiempo, o manual, a petición del usuario.

Para actualizar los datos deberemos conocer qué datos han cambiado desde la última actualización. Para ello podremos añadir a los datos almacenados en RMS un atributo que nos diga si hay cambios pendientes de actualizar en dicho dato.

Si hemos creado índices para nuestros registros de RMS, podremos añadir este atributo a los índices, para facilitar de esta forma la búsqueda de estos datos.

public class IndiceCita {
int rmsID;
int id;
Date fecha;
boolean alarma;
boolean pendiente; }

Podemos ver aquí la razón por la que preferimos una granularidad fina de los datos almacenados. Si tuviésemos almacenados muchos datos en un mismo registro, y modificamos una pequeña parte de estos datos, tendremos que marcar todo el registro como modificado, por lo que habrá que subir al servidor un mayor volumen de datos. Si estos datos hubiesen estado repartidos en varios registros, sólo hubiese hecho falta actualizar la parte que hubiese cambiado.

Envío de datos compartidos al servidor

Este caso es el más complejo. Existe una copia maestra de los datos almacenada en el servidor, y varias copias locales, conocidas como copias legales, que se descargan los usuarios a sus dispositivos.

Si varios clientes han descargado los datos en sus móviles, y modifican su copia legal, cuando actualicen los datos en la copia maestra del servidor podrían sobrescribir el trabajo de otros usuarios.

Para poder corregir estos conflictos de la mejor forma posible, deberemos tener una granularidad muy fina de los datos, de forma que los usuarios sólo modifiquen las porciones de los datos que hayan modificado. De esta forma, si nosotros hemos modificado una parte de los datos que otro usuario no ha tocado, cuando el otro usuario actualice los datos no sobrescribirá dicha parte.

Para decidir qué versión de los datos mantener en la copia maestra podemos tomar diferentes criterios:

Deberemos intentar solucionar los conflictos sin necesidad de solicitar la intervención del usuario, aunque en ciertas ocasiones la mejor solución puede ser preguntar qué copia de los datos desea que se mantenga.