martes, agosto 30, 2005

DWR y Spring

DWR (Direct Web Remoting) o "Easy AJAX for Java" es una biblioteca para facilitar la utilización de AJAX. Y con AJAX (Asynchronous JavaScript And XML for Web GUI processing) podemos actualizar una página web sin necesidad de gets y posts, eliminando el correspondiente "roundtrip" al servidor y refrescos de páginas, logrando una interfaz similar a cualquier aplicación desktop.
DWR expone métodos de una clase Java utilizando Javascript, permitiendo asi su acceso desde cualquier página web (HTML/JSP/etc):

interaction diagram

¿Pero cómo se configura DWR y cómo logramos que esa clase Java sea un bean de Spring?

Luego de copiar dwr.jar al servidor, configuraremos DWR en el WEB-INF/web.xml de la aplicación:

<servlet>
<servlet-name>dwr-invoker</servlet-name>
<display-name>DWR Servlet</display-name>
<description>Direct Web Remoter Servlet</description>
<servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>

Luego crearemos un fichero WEB-INF/dwr.xml donde especificaremos qué métodos de qué beans queremos "exponer" mediante dwr, y que beans van a servir de transporte de datos y por lo tanto dwr debe convertir.
Por ejemplo, si queremos llenar una lista desplegable con categorías (paises, por ejemplo) y otra dependiente de esta (ciudades), necesitamos dos métodos de un bean Java (ciudadesManager), y un DTO para transferencia de datos (ListBean).

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 0.4//EN"
"http://www.getahead.ltd.uk/dwr/dwr.dtd">

<dwr>
<allow>
<create creator="spring" javascript="ciudadesManager" beanName="ciudadesManager">
<include method="getPaises"/>
<include method="getCiudades"/>
</create>
<convert converter="bean" match="com.package.ListBean" />
</allow>
</dwr>

Consecuentemente, en nuestro beans.xml estará el bean ciudadesManager:

<bean id="ciudadesManager" class="com.package.ciudadesManager"> 
<property name="paisesDAO" ref="paisesDAO"/>
<property name="ciudadesDAO" ref="ciudadesDAO"/>
</bean>

Llegados a este punto, lo que queda es desarrollar la página de nuestra aplicación, y para ello deberemos incluir el javascript necesario para acceder al ciudadesManager (recordando actualizar el [appfolder]:

<script type='text/javascript' src='/[appfolder]/dwr/interface/ciudadesManager.js'></script>
<script type='text/javascript' src='/[appfolder]/dwr/engine.js'></script>
<script type='text/javascript' src='/[appfolder]/dwr/util.js'></script>

El primer include corresponde al javascript que DWR generará automaticamente para nuestro bean ciudadesManager, el segundo es el core de DWR, y el tercero una serie de utilidades para manipular controles html, como gets y sets de propiedades.

Las funciones javascript necesarias para acceder remotamente a los métodos del bean se dividen en dos: uno que hace la llamada y otro de callback. Asi, para llenar la lista de paises crearemos las funciones execPaises y fillPaises, y para llenar la lista de ciudades execCiudades y fillCiudades:

<script>
function execPaises() {
ciudadesManager.getPaises(fillPaises);
}

function fillPaises(data) {
DWRUtil.fillList('paises', data, 'key', 'value');
}

function execCiudades() {
DWRUtil.removeAllOptions('ciudades');
ciudadesManager.getCiudades(fillCiudades, DWRUtil.getValue('paises'));
}

function fillCiudades(data) {
DWRUtil.fillList('ciudades', data, 'key', 'value');
}

</script>

La primer funcion, execPaises, la llamaremos al inicio de la página, dentro del onLoad del body. Al ejecutarse, llamará al método getPaises de ciudadesManager y el resultado lo tendremos en la función que le pasemos como parámetro, en este caso fillPaises.
Dentro de fillPaises utilizaremos la función DWRUtil.fillList para rellenar la lista desplegable cuyo id es paises con la lista de datos recibidos, diciéndole cuál propiedad corresponde al código y cuál a la descripción.
Análogamente, la función execCuidades estará en el onChange de la lista paises, y cuando se ejecute (cada vez que cambiemos de pais) borrará todas las opciones de la lista ciudades y ejecutará el método getCiudades, pasando como parámetro el value de la lista paises (el pais elegido).
El código de las listas quedará asi:

<select id="paises" onChange="execCiudades();"></select>
<select id="ciudades"></select>

Solo nos queda codificar el bean ciudadesManager y el DTO. El manager obtiene la lista de paises o ciudades utilizando un DAO y las trasnfiera al DTO. Cabe señalar que podríamos utilizar directamente los beans Pais y Ciudad y transferirlos a la página, pero estas clases suelen tener muchas mas propiedades que no utilizaríamos y penalizaría la performance. De cualquier modo dependerá de vuestra propia aplicación como hacerlo, esta es solamente una forma mas.

public class CiudadesManager {

private DAO paisesDAO = null;
private DAO ciudadesDAO = null;

public Object[] getPaises() {
List objects = getPaisesDAO().findByNamedQuery("allPaises");
List list = new ArrayList();
for (Iterator it=objects.iterator(); it.hasNext(); ) {
Pais pais = (Pais)it.next();
ListBean bean = new ListBean(pais.getCodigo(), pais.getNombre());
list.add(bean);
}
return list.toArray();
}

public Object[] getCiudades(String idPais) throws BusinessException {
List objects = getCiudadesDAO().findByNamedQuery("byPais", new Object[]{idPais});
List list = new ArrayList();
for (Iterator it=objects.iterator(); it.hasNext(); ) {
Ciudad ciudad = (Ciudad)it.next();
ListBean bean = new ListBean(ciudad.getCodigo(), ciudad.getNombre());
list.add(bean);
}
return list.toArray();
}

public DAO getPaisesDAO() {return paisesDAO;}
public void setPaisesDAO(DAO paisesDAO) {this.paisesDAO = paisesDAO;}
public DAO getCiudadesDAO() {return ciudadesDAO;}
public void setCiudadesDAO(DAO ciudadesDAO) {this.ciudadesDAO = ciudadesDAO;}

}

public class ListBean {

private String key = null;
private String value = null;

public ListBean (String key, String value) {
this.key = key;
this.value = value;
}

public String getKey() {return key;}
public String getValue() {return value;}
public void setKey(String key) {this.key = key;}
public void setValue(String value) {this.value = value;}
}

Si todo ha sido configurado correctamente, el resultado será una lista desplegable de paises que se llena al cargar la página:

y una lista desplegable de ciudades que se refresca cada vez que cambiamos la primera:

Y todo sin que se refresque la página.

En el sitio de DWR hay numerosos ejemplos de como utilizarlo para diversas tareas, como editar tablas y formularios o validar datos, solo hace falta animarse.

jueves, agosto 11, 2005

Mule

Con el auge de SOA estuve experimentando con Mule como parte de la estrategia opensource de un cliente.
Resulta admirable que con tan solo (o casi solo) una configuración similar a esta:

<bean id="domainRouterEP" class="org.mule.impl.endpoint.MuleEndpoint">
<property name="endpointURI">
<bean class="org.mule.impl.endpoint.MuleEndpointURI">
<constructor-arg value="axis:http://${broker.host}:${broker.ws.port}/router"/>
</bean>
</property>
<property name="synchronous" value="true"/>
</bean>

<bean id="serviceDomainRouter" class="org.mule.impl.MuleDescriptor">
<property name="inboundEndpoint" ref="domainRouterEP"/>
<property name="outboundRouter">
<bean class="org.mule.routing.outbound.OutboundMessageRouter">
<property name="routers">
<list>
<bean class="org.mule.routing.outbound.StaticRecipientList"/>
</list>
</property>
</bean>
</property>
<property name="implementation" value="serviceDomainRouterImpl"/>
<property name="properties">
<map>
<entry key="axisOptions">
<map>
<entry key="wsdlServiceElement" value="RouterService"/>
<entry key="wsdlPortType" value="RouterPort"/>
</map>
</entry>
<entry key="serviceInterfaces">
<list>
<value>broker.router.ServiceDomainRouter</value>
</list>
</entry>
</map>
</property>
</bean>

<bean id="serviceDomainRouterImpl" class="broker.router.impl.ServiceDomainRouterImpl"
singleton='false'>
<property name="hub" value="${broker.hub}"/>
<property name="directory" ref="directory"/>
<property name="schemaFileName" value="request-schema.xsd"/>
</bean>
se pueda exponer un servicio via webservices, el cual derivará el mensaje a una lista (StaticRecipientList) de destinos que proveerá la implementación (ServiceDomainRouterImpl).
Este mensaje puede ser reenviado mediante diversos medios de transporte, como JMS (en mi caso), archivos, ftp, http, soap, pop/smtp.
Muy útil, como se ve, para "conectar" sistemas muy dispares y "orquestar" el buen funcionamiento entre ellos.