viernes, 20 de junio de 2008

AJAX con DWR en NetBeans 6.1

Una de las bibliotecas más fáciles que he visto para programar en AJAX es el Direct Web Remoting - DWR.
En este post veremos algunas de sus características que nos ayudará a tener aplicaciones enriquecidas con ajax. Para ello usaremos:

  1. NetBeans 6.1
  2. DWR

Si deseas saber lo que es AJAX, puedes revisar mi anterior post llamado (justamente) AJAX.

Instalando DWR en NetBeans

La biblioteca DWR consta únicamente de un archivo .jar. Este lo podemos descargar de aquí: http://getahead.org/dwr/download. A la fecha de este post la versión del DWR es la 2.0.4.

Guardaremos el archivo en una carpeta que será destinada para las bibliotecas de los proyectos. Yo, en Windows, lo guardo en d:\proys\lib\DWR, y en Linux lo guardo en ~/proys/lib

Adicionalmente DWR necesita de la biblioteca commons-logging. Esta la puedes descargar de aquí: http://commons.apache.org/downloads/download_logging.cgi

Descomprimamos el archivo descargado de commons-logging en la misma carpeta lib.

Entramos a la opción Tools > Libraries:


Hacemos clic en "New Library" para crear una nueva biblioteca, y llamaremos DWR.

Clic en OK. Ahora agregamos los archivos .jar correspondientes. Hacemos clic en Add Jar/Folder y seleccionamos el archivo commons-logging-1.1.1.jar y dwr.jar


Clic en OK parar cerrar la ventana.

El proyecto Web

Ahora crearemos un aplicación web, que no usará ningún framework. Será una aplicación totalmente "simple". La llamaremos DwrSamples.

En las propiedades del proyecto web creado, entramos a sus propiedades haciendo clic derecho sobre el ícono del proyecto, y seleccionamos"Properties". En esta ventana seleccionamos del margen izquierdo la categoría "Libraries". Luego agregamos la biblioteca DWR haciendo clic en el botón "Add Library". Después de esto, deberá lucir así:



Crearemos una clase a la que llamaremos Calculadora y estará en el paquete logica. Esta clase, básicamente, tendrá los métodos que realizarán las operaciones de manera asíncrona.

package logica;
public class Calculadora {

public int sumar(int a, int b) {
return a + b;
}

public int restar(int a, int b) {
return a - b;
}
}


Ahora, necesitamos que esta clase sea leíble por DWR, por lo que usaremos anotaciones para "publicar" la clase como objeto javascript, pero sólo publicaremos el método sumar() para que sea ajax. Usaremos la anotación de DWR @RemoteProxy para la clase Calculadora, y @RemoteMethod para el método sumar(). Deberá lucir así:

package logica;

import org.directwebremoting.annotations.RemoteMethod;
import org.directwebremoting.annotations.RemoteProxy;

@RemoteProxy
public class Calculadora {

@RemoteMethod
public int sumar(int a, int b) {
return a + b;
}

public int restar(int a, int b) {
return a - b;
}
}



Ahora, abrirmos el archivo web.xml (podemos presionar Shift + Alt + O para abrir el buscador de archivos y escribir web para que nos seleccione el archivo que estamos buscando). En la barra superior hacemos clic en el botón "Servlets" para visualizar los servlets de nuestra aplicación. Una vez allí, hacemos clic en el botón "Add Servlet".
El nuevo servlet que crearemos, le pondremos el nombre "DWR", la clase del servlet será "org.directwebremoting.servlet.DwrServlet", y el patrón URL será "/dwr/*", es decir, este serlet responderá las peticiones en la dirección "/dwr/". Deberá lucir esta ventana así:



Hacemos clic en "OK". Luego, agregaremos un parámetro de inicio. Hacemos clic en el botón "Add.." de la sección "Initialization Parameter".

El parámetro de inicio lo llamaremos classes, y tendrá una clase llamada logica.Calculadora.

Clic en OK, y deberá lucir así la ventana de los servlets.

O si lo prefieres en xml, el archivo web.xml deberá lucir así.
Ahora, crearemos nuestra interfaz. Abrimos el archivo index.jsp, le pondremos un formulario:
        <h2>Calculadora</h2>
<form action="">
Valor 1: <input type="text" name="valor1" id="valor1"/><br/>
Valor 2: <input type="text" name="valor2" id="valor2"/><br/>
<input type="button" value="Sumar" onclick="sumar()"/><br/>
Resultado:<div id="suma"></div>
</form>

Nota que el botón no es un submit, sino un tipo "button". Tampoco olvidar los atributos id de los tags. Estos nos ayudará a identificar un tag en toda la página.

Ahora, necesitamos importar las bibliotecas de DWR. Para ello, agregamos las siguientes lineas en la cabecera del jsp.
        <script type="text/javascript" src="<%=pageContext.getServletContext().getContextPath()  %>/dwr/interface/Calculadora.js"></script>
<script type="text/javascript" src="<%=pageContext.getServletContext().getContextPath() %>/dwr/engine.js"></script>
<script type="text/javascript" src="<%=pageContext.getServletContext().getContextPath() %>/dwr/util.js"></script>



Nota que estos javascript están bajo la carpeta /dwr, que es el servlet que hemos creado párrafos arriba. Además, hay un Calculadora.js, que tiene el mismo nombre de nuestra clase java que hemos creado.

Crearemos la función sumar() que es llamada desde el botón "sumar" de nuestro formulario:
            function sumar(){
var valor1=dwr.util.getValue("valor1");
var valor2=dwr.util.getValue("valor2");
Calculadora.sumar(valor1,valor2,mostrarSuma);
}


En las dos primeras líneas estamos obteniendo los valores de los tag "valor1" y "valor2". Este nombre es el nombrado en los atributos id de los input:text. Por ello es importante que los ID identifiquen a un único tag en toda la página.

La tercera linea llama al objeto Calculadora. Este objeto es el que DWR creó como contraparte a nuestra clase java Calculadora.

Nota que llama al método sumar() y recibe tres parámetros. Los dos primeros son los mismos parámetros que hemos declarado en nuestra clase java Calculadora. Pero el tercer parámetro, que se llama mostrarSuma, es el nombre de una función en javascript que se encargará de recibir y manejar el resultado que devuelto por el método sumar() de java. Así es la convención de DWR. Notar que se le está pasando solo el nombre, sin paréntesis.

La función javascript mostrarSuma() será la siguiente:
            function mostrarSuma(resultado){
dwr.util.setValue("suma",resultado);
}


Vemos que el resultado devuelto por el método de java sumar() es el parámetro de esta función javascript. Tomamos el valor y lo mostramos en el tag que tiene nombre "suma" (que es un <div>).

El index.jsp completo es el siguiente:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JSP Page</title>

<script type="text/javascript" src="<%=pageContext.getServletContext().getContextPath() %>/dwr/interface/Calculadora.js"></script>
<script type="text/javascript" src="<%=pageContext.getServletContext().getContextPath() %>/dwr/engine.js"></script>
<script type="text/javascript" src="<%=pageContext.getServletContext().getContextPath() %>/dwr/util.js"></script>
<script type="text/javascript">
function sumar(){
var valor1=dwr.util.getValue("valor1");
var valor2=dwr.util.getValue("valor2");
Calculadora.sumar(valor1,valor2,mostrarSuma);
}
function mostrarSuma(resultado){
dwr.util.setValue("suma",resultado);
}
</script>
</head>
<body>

<h2>Calculadora</h2>
<form action="">
Valor 1: <input type="text" name="valor1" id="valor1"/><br/>
Valor 2: <input type="text" name="valor2" id="valor2"/><br/>
<input type="button" value="Sumar" onclick="sumar()"/><br/>
Resultado:<div id="suma"></div>
</form>
</body>
</html>


Lo corremos y probamos:


¡Magia!

Con base de datos

A esta altura verás la simpleza del DWR, y que una aplicación con base de datos no sería de lo más difícil. Haríamos una clase que accede la base de datos y lo muestre en la web. Pero ¿cómo lo mostramos en una tabla?. Aquí mostraremos el ejemplo:

Crearemos una clase llamada PersonasService y lo guardaremos en el paquete logica. Esta clase tendrá la anotación @RemoteProxy pero con un parámetro que cambiará el nombre del objeto javascript. Es decir, no se llamará PersonasService en javascript, sino, se llamará solamente Personas.
package logica;

import org.directwebremoting.annotations.RemoteProxy;
@RemoteProxy(name = "Personas")
public class PersonasService {


}


Ahora, crearemos un método llamado getLista() que devolverá una lista de objetos Persona que tendrá los registros de la base de datos. Para ello, primero crearemos la clase beans.Persona.
package beans;

import java.util.Date;


public class Persona {


private int id;

private String nombre;

private String titulo;

private boolean viajeroFrecuente;

private Date ultimaActualizacion;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getNombre() {
return nombre;
}

public void setNombre(String nombre) {
this.nombre = nombre;
}

public String getTitulo() {
return titulo;
}

public void setTitulo(String titulo) {
this.titulo = titulo;
}

public Date getUltimaActualizacion() {
return ultimaActualizacion;
}

public void setUltimaActualizacion(Date ultimaActualizacion) {
this.ultimaActualizacion = ultimaActualizacion;
}

public boolean isViajeroFrecuente() {
return viajeroFrecuente;
}

public void setViajeroFrecuente(boolean viajeroFrecuente) {
this.viajeroFrecuente = viajeroFrecuente;
}
}

Por alguna razón, los beans con anotaciones no son convertidos por DWR a objetos JavaScript. Al menos con esta versión. He seguido la documentación que indica cómo usar un bean con @DataTransferObject y nada. Si alguien lo puede lograr, lo agradeceré un montón.

Pero para poder enviar beans en DWR, crearemos un archivo llamado dwr.xml y lo guardamos dentro del directorio WEB-INF (en la misma ubicación del archivo web.xml). En ese archivo colocaremos lo siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN"
"http://directwebremoting.org/schema/dwr20.dtd">
<dwr>
<allow>

<convert converter="bean" match="beans.Persona">
</convert>
</allow>
</dwr>


Ahora, volvemos a la clase PersonasService. Hacemos clic derecho sobre el fondo del código fuente, y seleccionamos Enterprise Resource > Use Database.

Hacemos clic en el botón "Add"

Pondremos como nombre de la referencia, el valor de travelDS. Además, hacemos clic en el botón "Add" de "Project Data sources", escribimos el nombre del JNDI jdbc/travel y seleccionamos de la lista el URL del JDBC referido a la base de datos travel.

Clic en OK

Clic en OK

Clic en OK

Veremos que NetBeans nos ha creado un método llamado getTravelDS()
    private DataSource getTravelDS() throws NamingException {
Context c = new InitialContext();
return (DataSource) c.lookup("java:comp/env/travelDS");
}

Pues ya con esto, podemos hacer nuestro manejo a la base de datos.

Ahora sí, crearemos el método getLista():
    @RemoteMethod
public List<Persona> getLista() {
try {
DataSource ds = getTravelDS();
Connection conn = ds.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM person");
List<Persona> lista = new ArrayList<Persona>();
while (rs.next()) {
Persona p = new Persona();
p.setId(rs.getInt("PERSONID"));
p.setNombre(rs.getString("NAME"));
p.setTitulo(rs.getString("JOBTITLE"));
p.setViajeroFrecuente(rs.getBoolean("FREQUENTFLYER"));
p.setUltimaActualizacion(rs.getDate("LASTUPDATED"));
lista.add(p);
}
return lista;

} catch (SQLException ex) {
Logger.getLogger(PersonasService.class.getName()).log(Level.SEVERE, null, ex);
} catch (NamingException ex) {
Logger.getLogger(PersonasService.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}


La interfaz web


Ahora, para terminar, lo más importante de una aplicación: la interfaz web.
Crearemos una tabla donde se colocarán los datos de los registros obtenidos de la base de datos. Esta tabla será así:
        <input type="button" onclick="mostrarPersonas()" value="Mostrar personas"/>
<input type="button" onclick="limpiarCuadro()" value="Limpiar cuadro"/>
<table border="1">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Cargo</th>

</tr>
</thead>

<tbody id="personas">
<tr id="pattern" style="display:none">
<td id="id"></td>
<td id="nombre"></td>
<td id="titulo"></td>
</tr>
</tbody>
</table>


Nota que hay una fila que está invisible (style="display:none"). Esta será nuestra plantilla llamada "pattern". Cuando llenemos los datos de la base de datos, lo que haremos será duplicar esta fila por cada registro.

Vemos también que hay un botón que se llama "Mostrar Personas" que llama a la función JavaScript mostrarPersonas(), que es la está aquí:
            function mostrarPersonas(){
Personas.getLista(listarPersonas);
}


Es decir, estamos llamando al método getLista() del objeto JavaScript Personas que el DWR creó. El método correspondiente en java no tiene parámetros, pero aquí estamos pasándole un argumento. Este argumento es el nombre de la función JavaScript que procesará el resultado devuelto por el método java. Y la función listarPersonas() está aquí:
            function listarPersonas(data){
limpiarCuadro();
for(var i=0;i<data.length;i++){
fila=data[i];
var $id=fila.id;

dwr.util.cloneNode("pattern",{idSuffix:$id});
dwr.util.setValue("id"+$id,fila.id);
dwr.util.setValue("nombre"+$id,fila.nombre);
dwr.util.setValue("titulo"+$id,fila.titulo);
$("pattern"+$id).style.display="";
}
}


Lo que primero hace es limpiar el cuadro (luego presento la función que limpia la tabla). Vemos también que está recibiendo un parámetro. Como vimos en el ejemplo anterior, este es el resultado que está devolviendo el objeto java.
Lo que hacemos es recorrer todas filas de la lista, tomamos una fila y obtenemos el ID de ese objeto.
Este ID nos permitirá identificar a esa única fila, porque sabemos que cada ID de la tabla es única.
Luego clonamos la plantilla:
dwr.util.cloneNode("pattern",{idSuffix:$id});

Al clonarla, hacemos que el ID de la fila creada tenga como subfijo el ID del objeto java. Es decir, al clonar una fila, toda la fila queda idéntica al patrón, incluyendo los nombres de los ID, que según el patrón es "pattern". Si el primero registro de la tabla tiene el valor "1" en el campo "ID", cuando clone la fila pondrá como nombre del ID "pattern" y terminará con el valor "1", o sea, se llamará finalmente "pattern1", y las celdas "nombre1", "titulo1" . Para la segunda fila será "pattern2" y las celdas "nombre2","titulo2"... y así sucesivamente.
Ya clonamos la fila pattern, ahora colocaremos los valores a cada celda. Como ya tenemos el ID unico de cada celda, simplemente colocaremos el valor en ellas
dwr.util.setValue("id"+$id,fila.id);
dwr.util.setValue("nombre"+$id,fila.nombre);
dwr.util.setValue("titulo"+$id,fila.titulo);

Pero como también se clonó el estilo - que indica que debe estar oculto - le indicamos que se muestre.
$("pattern"+$id).style.display="";

...y listo.
A continuación las funciones que borran el contenido de la tabla:
            function limpiarCuadro(){
dwr.util.removeAllRows("personas",{filter:filtroBorrado});
}

function filtroBorrado(tr){
return (tr.id!="pattern");
}


Existe la función filtroBorrado() para evitar que la fila "pattern" sea borrada. Sino, ¿como hacemos la clonación?
Y el resultado es el ya esperado...


Para terminar...

Revisa la documentación de DWR, ahí encontrarás muchas cosas muy interesantes.

Recursos

El proyecto utilizado para este post lo puedes descargar de aquí http://diesil-java.googlecode.com/files/dwrsamples.tar.gz.