miércoles, 7 de septiembre de 2011

RequestFactory con Objectify

A partir de GWT 2.1 RequestFactory es la nueva forma de comunicación con el servidor. Para los programadores Java, GWT ofrece maneras mas fáciles de interactuar con el servidor como son el RPC(Remote Procedure Call) y RequestFactory.
Objectify es la librería mas conveniente para acceder al Datastore de AppEngine el servicio de alojamiento de aplicaciones de Google. El AppEngine usa BigTable un almacen de datos NoSql.



Para implementar la persistencia es necesario instalar el AppEngine SDK en un anterior post instale el Plugin de Google para Eclipse.
El codigo fuente de ejemplo lo puede descargar en este enlace.
Voy a la dirección http://code.google.com/intl/es-ES/appengine/downloads.html y descargo appengine-java-sdk-1.5.3.zip lo extraigo en un directorio  y ejecuto el Eclipse IDE voy al menu Window->Preferences->Google->App Engine y agrego la ruta del SDK.
Voy a crear un nuevo proyecto que se llamara ExampleRF


Como en este proyecto voy utilizar Objectify voy a descargar la librería objectify-3.0.zip . Extraigo el zip y copio el objectify-3.0.jar en la carpeta lib del WEB-INF del proyecto y lo agrego al Build Path
Tambien debo descargar un jar adicional json-20090211.jar de este link e igualmente lo agrego al Build Path.
Algo mas que agregue es esta libreria gwt-servlet-deps.jar al directorio lib de mi aplicación esta libreria no se agrega al BuildPath. Lo puedes encontrar en el directorio de instalación del GWT

A continuación voy a configurar el archivo de módulo para que cargue los módulos RequestFactory y Objectify.

<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='examplerf'>  
  <inherits name='com.google.gwt.user.User'/>
  <inherits name="com.googlecode.objectify.Objectify" />
  <inherits name='com.google.web.bindery.requestfactory.RequestFactory' />  
  <entry-point class='com.examplerf.client.ExampleRF'/>  
  <source path='client'/>
  <source path='shared'/>
</module>

El archivo web.xml debe lucir asi:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>  
  
  <servlet>
    <servlet-name>requestFactoryServlet</servlet-name>
      <servlet-class>com.google.web.bindery.requestfactory.server.RequestFactoryServlet</servlet-class>
        <init-param>
          <param-name>symbolMapsDirectory</param-name>
          <param-value>WEB-INF/classes/symbolMaps/</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>requestFactoryServlet</servlet-name>
    <url-pattern>/gwtRequest</url-pattern>
  </servlet-mapping>
  <welcome-file-list>
    <welcome-file>ExampleRF.html</welcome-file>
  </welcome-file-list>

</web-app>

Igual que en los ejemplos anteriores debo eliminar el código y archivos de ejemplo.
Ahora voy a crear la estructura de directorios.
Voy a crear una lista de colaboradores por el momento este ejemplo no tendrá interfaz gráfica y el mantenimiento lo realizare en el entry point y el listado voy a mostrarlo en consola.
Mi clase de dominio seria Colaborador.java y lo creare en el paquete server.domain
package com.examplerf.server.domain;

import com.googlecode.objectify.annotation.Entity;

@Entity
public class Colaborador {
 private String nombres;
 private String apellidos;
 private Integer edad;  
}

La clase Colaborador es una entidad Objectify. Para que pueda funcionar con RequestFactory debe tener las propiedades ID y version. De acuerdo a David M. Chandler se puede heredar de una clase DatastoreObject.
package com.examplerf.server.domain;

import javax.persistence.Id;
import javax.persistence.PrePersist;

public class DatastoreObject {
 @Id
 private Long id;
 private Integer version = 0;
 
 /**
  * Auto-increment version # whenever persisted
  */
 @PrePersist
 void onPersist()
 {
  this.version++;
 }

 public Long getId() {
  return id;
 }

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

 public Integer getVersion() {
  return version;
 }

 public void setVersion(Integer version) {
  this.version = version;
 }
 
}
Edito la clase colaborador para que herede de DatastoreObject
package com.examplerf.server.domain;

import com.googlecode.objectify.annotation.Entity;

@Entity
public class Colaborador extends DatastoreObject {
 private String nombres;
 private String apellidos;
 private Integer edad;
 public String getNombres() {
  return nombres;
 }
 public void setNombres(String nombres) {
  this.nombres = nombres;
 }
 public String getApellidos() {
  return apellidos;
 }
 public void setApellidos(String apellidos) {
  this.apellidos = apellidos;
 }
 public Integer getEdad() {
  return edad;
 }
 public void setEdad(Integer edad) {
  this.edad = edad;
 } 
}
Los getter y setter los he generado haciendo click derecho en el lugar donde se situaran del menú contextual seleccionar Source->Generate Getters and Setters... Para la capa de persistencia el autor de turbomanage utiliza una clase genérica DAO(Data Access Object) ObjectifyDao.java y una clase de excepción TooManyResultsException.java primero esta clase se sitúa en el paquete shared
package com.examplerf.shared;
/**
 * Wrapper exception that gets thrown when Objectify get() returns too many results
 */
public class TooManyResultsException extends Exception
{

 /**
  * 
  */
 private static final long serialVersionUID = -2279521172940075176L;

 public TooManyResultsException()
 {
  super();
 }

 public TooManyResultsException(Throwable t)
 {
  super(t);
 }

 public TooManyResultsException(String msg)
 {
  super(msg);
 }

 public TooManyResultsException(String msg, Throwable t)
 {
  super(msg, t);
 }

}


Ahora copio la clase ObjectifyDao.java en el paquete server.service
package com.examplerf.server.service;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.persistence.Embedded;
import javax.persistence.Transient;

import com.examplerf.server.domain.Colaborador;
import com.examplerf.shared.TooManyResultsException;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.Query;
import com.googlecode.objectify.util.DAOBase;



/**
 * Generic DAO for use with Objectify
 * 
 * @author turbomanage
 * 
 * @param <T>
 */
public class ObjectifyDao<T> extends DAOBase
{

 static final int BAD_MODIFIERS = Modifier.FINAL | Modifier.STATIC
   | Modifier.TRANSIENT;

 static
 {
  ObjectifyService.register(Colaborador.class);  
 }

 protected Class<T> clazz;

 @SuppressWarnings("unchecked")
 public ObjectifyDao()
 {
  clazz = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
 }

 public Key<T> put(T entity)

 {
  return ofy().put(entity);
 }

 public Map<Key<T>, T> putAll(Iterable<T> entities)
 {
  return ofy().put(entities);
 }

 public void delete(T entity)
 {
  ofy().delete(entity);
 }

 public void deleteKey(Key<T> entityKey)
 {
  ofy().delete(entityKey);
 }

 public void deleteAll(Iterable<T> entities)
 {
  ofy().delete(entities);
 }

 public void deleteKeys(Iterable<Key<T>> keys)
 {
  ofy().delete(keys);
 }

 public T get(Long id) throws EntityNotFoundException
 {
  return ofy().get(this.clazz, id);
 }

 public T get(Key<T> key) throws EntityNotFoundException
 {
  return ofy().get(key);
 }

 public Map<Key<T>, T> get(Iterable<Key<T>> keys)
 {
  return ofy().get(keys);
 }

 public List<T> listAll()
 {
  Query<T> q = ofy().query(clazz);
  return q.list();
 }

 /**
  * Convenience method to get all objects matching a single property
  * 
  * @param propName
  * @param propValue
  * @return T matching Object
  * @throws TooManyResultsException
  */
 public T getByProperty(String propName, Object propValue)
   throws TooManyResultsException
 {
  Query<T> q = ofy().query(clazz);
  q.filter(propName, propValue);
  Iterator<T> fetch = q.limit(2).list().iterator();
  if (!fetch.hasNext())
  {
   return null;
  }
  T obj = fetch.next();
  if (fetch.hasNext())
  {
   throw new TooManyResultsException(q.toString()
     + " returned too many results");
  }
  return obj;
 }

 public List<T> listByProperty(String propName, Object propValue)
 {
  Query<T> q = ofy().query(clazz);
  q.filter(propName, propValue);
  return q.list();
 }

 public List<Key<T>> listKeysByProperty(String propName, Object propValue)
 {
  Query<T> q = ofy().query(clazz);
  q.filter(propName, propValue);
  return q.listKeys();
 }

 public T getByExample(T exampleObj) throws TooManyResultsException
 {
  Query<T> q = buildQueryByExample(exampleObj);
  Iterator<T> fetch = q.limit(2).list().iterator();
  if (!fetch.hasNext())
  {
   return null;
  }
  T obj = fetch.next();
  if (fetch.hasNext())
  {
   throw new TooManyResultsException(q.toString()
     + " returned too many results");
  }
  return obj;
 }

 public List<T> listByExample(T exampleObj)
 {
  Query<T> queryByExample = buildQueryByExample(exampleObj);
  return queryByExample.list();
 }

 public Key<T> getKey(Long id)
 {
  return new Key<T>(this.clazz, id);
 }

 public Key<T> key(T obj)
 {
  return ObjectifyService.factory().getKey(obj);
 }

 public List<T> listChildren(Object parent)
 {
  return ofy().query(clazz).ancestor(parent).list();
 }

 public List<Key<T>> listChildKeys(Object parent)
 {
  return ofy().query(clazz).ancestor(parent).listKeys();
 }

 protected Query<T> buildQueryByExample(T exampleObj)
 {
  Query<T> q = ofy().query(clazz);

  // Add all non-null properties to query filter
  for (Field field : clazz.getDeclaredFields())
  {
   // Ignore transient, embedded, array, and collection properties
   if (field.isAnnotationPresent(Transient.class)
     || (field.isAnnotationPresent(Embedded.class))
     || (field.getType().isArray())
     || (field.getType().isArray())
     || (Collection.class.isAssignableFrom(field.getType()))
     || ((field.getModifiers() & BAD_MODIFIERS) != 0))
    continue;

   field.setAccessible(true);

   Object value;
   try
   {
    value = field.get(exampleObj);
   } catch (IllegalArgumentException e)
   {
    throw new RuntimeException(e);
   } catch (IllegalAccessException e)
   {
    throw new RuntimeException(e);
   }
   if (value != null)
   {
    q.filter(field.getName(), value);
   }
  }

  return q;
 }

 /*
  * Application-specific methods to retrieve items owned by a specific user
  */
 

}

Se puede notar que la clase Colaborador se ha registrado en la clase ObjectifyDao. Todas las clases de entidad necesitan registrarse en esta clase.
static
 {
  ObjectifyService.register(Colaborador.class);  
 }
Ahora voy a crear la clase ColaboradorDao en el paquete server.service que manejara las operaciones CRUD(Create Update Delete) del Colaborador.
package com.examplerf.server.service;

import com.examplerf.server.domain.Colaborador;
import com.google.appengine.api.datastore.EntityNotFoundException;

public class ColaboradorDao extends ObjectifyDao<Colaborador> {
 
 public void save(Colaborador colaborador){
  this.put(colaborador);
 }
 public void remove(Long id){
  try {
   Colaborador c = this.get(id);
   this.delete(c);   
  } catch (EntityNotFoundException e) {   
   e.printStackTrace();
  }
 }
 public Colaborador fetch(Long id){
  Colaborador c = null;
  try {
    c = this.get(id);
  } catch (EntityNotFoundException e) {   
   e.printStackTrace();
  }
  return c;
 }
}

Utilizando RequestFactory se puede exponer ColaboradorDao directamente como servicio. Para ello es necesario una clase DaoServiceLocator en el paquete server.locator.
package com.examplerf.server.locator;

import com.google.web.bindery.requestfactory.shared.ServiceLocator;

/**
 * Generic locator service that can be referenced in the @Service annotation
 * for any RequestFactory service stub
 *  
 * @author turbomanage
 */
public class DaoServiceLocator implements ServiceLocator {

 @Override
 public Object getInstance(Class<?> clazz) {
  try {
   return clazz.newInstance();
  } catch (InstantiationException e) {
   throw new RuntimeException(e);
  } catch (IllegalAccessException e) {
   throw new RuntimeException(e);
  }
 }

}
En RequestFactory se declara un proxy interface por cada entidad. En este ejemplo la interfaz será ColaboradorProxy. Pero antes RequestFactory requiere una clase Generic Entity Locator para que pueda relacionar la Entidad(la clase de dominio en el servidor) con su respectivo proxy interface(EntityProxy en el cliente). Esta clase es ObjectifyLocator y va en el paquete server.locator.
package com.examplerf.server.locator;

import com.examplerf.server.domain.DatastoreObject;
import com.google.web.bindery.requestfactory.shared.Locator;
import com.googlecode.objectify.util.DAOBase;

/**
 * Generic @Locator for objects that extend DatastoreObject
 */
public class ObjectifyLocator extends Locator<DatastoreObject, Long> {
  @Override
  public DatastoreObject create(Class<? extends DatastoreObject> clazz) {
    try {
      return clazz.newInstance();
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public DatastoreObject find(Class<? extends DatastoreObject> clazz, Long id) {
    DAOBase daoBase = new DAOBase();
    return daoBase.ofy().find(clazz, id);
  }

  @Override
  public Class<DatastoreObject> getDomainType() {
    // Never called
    return null;
  }

  @Override
  public Long getId(DatastoreObject domainObject) {
    return domainObject.getId();
  }

  @Override
  public Class<Long> getIdType() {
    return Long.class;
  }

  @Override
  public Object getVersion(DatastoreObject domainObject) {
    return domainObject.getVersion();
  }
}
Ahora ya puedo crear mi proxy interface ColaboradorProxy en el paquete shared.proxy.
package com.examplerf.shared.proxy;

import com.examplerf.server.domain.Colaborador;
import com.examplerf.server.locator.ObjectifyLocator;
import com.google.web.bindery.requestfactory.shared.EntityProxy;
import com.google.web.bindery.requestfactory.shared.ProxyFor;

@ProxyFor(value = Colaborador.class, locator = ObjectifyLocator.class)
public interface ColaboradorProxy extends EntityProxy {
 String getNombres();
 void setNombres(String nombres);
 String getApellidos();
 void setApellidos(String apellidos);
 Integer getEdad();
 void setEdad(Integer edad);
}
Ahora voy a crear el servicio ColaboradorService en el paquete shared.sevice.
package com.examplerf.shared.service;

import java.util.List;

import com.examplerf.server.locator.DaoServiceLocator;
import com.examplerf.server.service.ColaboradorDao;
import com.examplerf.shared.proxy.ColaboradorProxy;
import com.google.web.bindery.requestfactory.shared.Request;
import com.google.web.bindery.requestfactory.shared.RequestContext;
import com.google.web.bindery.requestfactory.shared.Service;

@Service(value = ColaboradorDao.class, locator = DaoServiceLocator.class)
public interface ColaboradorService extends RequestContext {
 Request<List<ColaboradorProxy>> listAll();
 Request<ColaboradorProxy> fetch(Long id);
 Request<Void> save(ColaboradorProxy colaborador);
 Request<Void> remove(Long id); 
}
Como puede notar en el servicio se utiliza la interfaz ColaboradorProxy no la clase de dominio Colaborador, RequestFactory se encarga de hacer el mapeo entre estos. Ahora voy a crear la interfaz donde se definen los servicios ExampleRFRequestFactory en el paquete shared.service.
package com.examplerf.shared.service;

import com.google.web.bindery.requestfactory.shared.RequestFactory;

public interface ExampleRFRequestFactory extends RequestFactory {
 public ColaboradorService colaboradorService();
}
Ahora voy a utilizar mis servicios desde el entry point utilizando SimpleEventBus para este sencillo ejemplo.
package com.examplerf.client;

import com.examplerf.shared.proxy.ColaboradorProxy;
import com.examplerf.shared.service.ColaboradorService;
import com.examplerf.shared.service.ExampleRFRequestFactory;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.web.bindery.event.shared.SimpleEventBus;
import com.google.web.bindery.requestfactory.shared.Receiver;

public class ExampleRF implements EntryPoint {
 private final ExampleRFRequestFactory myRF = GWT.create(ExampleRFRequestFactory.class);
 public void onModuleLoad() {
  myRF.initialize(new SimpleEventBus());
  
  guardar();
 }
 public void guardar(){
  ColaboradorService cs = myRF.colaboradorService();
  
  ColaboradorProxy colaborador = cs.create(ColaboradorProxy.class);
  colaborador.setNombres("Juan");
  colaborador.setApellidos("Perez");
  colaborador.setEdad(27);
  
  cs.save(colaborador).fire(new Receiver<Void>() {

   @Override
   public void onSuccess(Void response) {
    GWT.log("Se guardo correctamente");    
   }
  });
 }
}
Ahora ejecuto la aplicación click derecho en el proyecto Run As -> Web Application Me muestra "Se guardo correctamente" Los datos ingresados se pueden ver en la consola local del appengine. Ir a http://localhost:8888/_ah/admin luego click en el boton List Entities y veran la lista de entradas. Esta consola sólo esta visible si esta iniciado el servidor Jetty. Es decir si detiene la ejecución de la aplicación en Hosted Mode tampoco podrá acceder a la consola local.

Ahora agrego las funciones par editar, eliminar y listar
package com.examplerf.client;

import java.util.List;

import com.examplerf.shared.proxy.ColaboradorProxy;
import com.examplerf.shared.service.ColaboradorService;
import com.examplerf.shared.service.ExampleRFRequestFactory;
import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;

import com.google.web.bindery.event.shared.SimpleEventBus;
import com.google.web.bindery.requestfactory.shared.Receiver;
import com.google.web.bindery.requestfactory.shared.Request;

public class ExampleRF implements EntryPoint {
 private final ExampleRFRequestFactory myRF = GWT.create(ExampleRFRequestFactory.class);
 public void onModuleLoad() {
  myRF.initialize(new SimpleEventBus());
  
  //guardar();
  //editar();  
  //eliminar();
  listar();
 }
 public void guardar(){
  ColaboradorService cs = myRF.colaboradorService();
  
  ColaboradorProxy colaborador = cs.create(ColaboradorProxy.class);
  colaborador.setNombres("marcelo");
  colaborador.setApellidos("bailon");
  colaborador.setEdad(30);  
  
  cs.save(colaborador).fire(new Receiver<Void>() {

   @Override
   public void onSuccess(Void response) {
    GWT.log("Se guardo correctamente");    
   }
  });
 }
 public void editar(){   
  ColaboradorService cs = myRF.colaboradorService();
  
  Request<ColaboradorProxy> colaborador = cs.fetch(new Long(2));
  colaborador.fire(new Receiver<ColaboradorProxy>() {

   @Override
   public void onSuccess(ColaboradorProxy response) {
    GWT.log(response.getNombres());
    
    ColaboradorService cs2 = myRF.colaboradorService();
    //para poder editar
    ColaboradorProxy colaborador2 = cs2.edit(response);
    colaborador2.setNombres("Marcelo");
    colaborador2.setApellidos("bailon");
    
    cs2.save(colaborador2).fire(new Receiver<Void>() {

     @Override
     public void onSuccess(Void response) {
      GWT.log("Se edito correctamente");      
     }
    });
   }
  });

 }
 public void eliminar(){
  myRF.colaboradorService().remove(new Long(1)).fire(new Receiver<Void>() {

   @Override
   public void onSuccess(Void response) {
    GWT.log("Se elimino correctamente");    
   }
  });
  
 }
 public void listar(){
  myRF.colaboradorService().listAll().fire(new Receiver<List<ColaboradorProxy>>() {

   @Override
   public void onSuccess(List<ColaboradorProxy> response) {
    for(ColaboradorProxy c : response){
     GWT.log(c.getNombres() +" "+c.getApellidos());
    }    
   }
  });
 }
}

2 comentarios:

  1. Disculpa, ¿Que se debe hacer con el HTML? No aparece ejemplo del Html en tu publicación. Saludos.

    ResponderEliminar
  2. El enlace del codigo fuente esta al inicio del post
    http://www.2shared.com/file/cDedheRb/ExampleRF.html
    descomprimelo y dentro del folder war esta el html
    Saludos.

    ResponderEliminar