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.

15 comentarios:

Anónimo dijo...

De momento no funciona tan bien si quires crear una session y mantenerla ... en fin para acceder a la capa de datos , ya cumple con el principio de AJAX , supongo que la version 2.1 sea mas estable y cubra estas diferencias.

Anónimo dijo...

prueba agregando en elemento create la propiedad scope="session"

rocasaca dijo...

Gracias, excelente recurso.

Saludos, desde Costa Rica

Anónimo dijo...

Como se hace para acceder a un objeto que es el parametro de salida de un mensaje que se le envio a un Dao.
Desde ya muchas gracias.
Nicolas

Richard C. dijo...

Realmente práctico y simple, una claridez para explicar sobresaliente, ahora te tengo una consulta =) : Cómo se haría la implementación en el caso de no usar Spring?. O necesariamente DWR funciona con Spring y tendríamos que meterle mano al Ajax Puritano. Muchas Gracias anticipadamente.

Anónimo dijo...

Como se hace para imprimir el resultado de una collection q es un bean...tnego la clase q retorna el bean y el collection de ese tipo y esta bien pero cuando pasa a dwr se desaparece o no puede acceder al objeto.

pumuki dijo...

Hola,
Pues a mi me sale el siguiente error:

ERROR [DefaultConverterManager] No converter found for mypackage.ListBean

Alguien sabe que es ??

Gracias por la ayuda, un saludo

pumuki dijo...

Ya lo tengo,

me faltaba esta linea en el dwr.xml

convert converter="bean" match="mypackage.LisBean"

Un saludo

pumuki dijo...

Estoy haciendo un formulario con un select multiple, ¿como se recogen todos los valores del select multiple con DWR? con DWR.getValue("select") solo me recoge un valor. ¿Alguien puede echarme una mano?

Gracias

Unknown dijo...

Estoy haciendo un formulario con un select multiple, ¿como se recogen todos los valores del select multiple con DWR? con DWR.getValue("select") solo me recoge un valor. ¿Alguien puede echarme una mano?


¿Has probado con getValues? Creo que te retorna un java.util.Map

Martha Cortes dijo...

Tngo un problema... x más q he releido el tutorial segui todo pero cuando ya lo deployo y me meto a http://localhost:8080/proyecto/dwr/ no me muestra ninguna clase (c supone q esto muestra las clases que stan expuestas remotarmente por el dwr)... alguien ha tenido este problema? q pudiera ayudarme... :'(

Anónimo dijo...

GRACIAS AMIGO MUY BUEN APORTE...
Si nos puedieras ayudar con el war que generaste con el codigo fuera mucho mejor gracias...

Anónimo dijo...

GRACIAS! por fin!!!

Fenix086 dijo...

RE: Tngo un problema... x más q he releido el tutorial segui todo pero cuando ya lo deployo y me meto a http://localhost:8080/proyecto/dwr/ no me muestra ninguna clase (c supone q esto muestra las clases que stan expuestas remotarmente por el dwr)... alguien ha tenido este problema? q pudiera ayudarme... :'(


Hola Martha, Asegurate que el dwr.xml esté bien configurado, con las rutas que son.Esto en caso que no te aparezca nada(Error 400), Si encuentra la página pero no muestra las clases, puede ser por la version del dwr que estás manejando. Fijate bien el error que sale en consola

Buen día;

Chartrix dijo...

disculpa a mi me sale esa salida en el apache
Estado HTTP 404 - No est� disponible el Servlet dwr-invoker