En un servicio REST (jax-rs) puedes enviar los parámetros de una petición de varias formas: en la URL (@QueryParam), en el cuerpo del mensaje con el encabezado “application/x-www-form-urlencoded” (@FormParam), como parte de la URL (@PathParam).

Pero que pasa si el cliente envía toda la información en el cuerpo del mensaje en formato JSON.
Por ejemplo:

    {
        "tipo":"URGENTE",
        "actores":[
        {
            "nombre":"Juan",
            "apellidos":"Peréz",
            "idEstado":null,
            "idMunicipio":null,
            "idColonia":null,
            "calle":"Calle 2",
            "numero":"155",
            "codigoPostal":"76800"
        }
        ],
        "demandados":[
        {
            "nombre":"María",
            "apellidos":"López",
            "idEstado":null,
            "idMunicipio":null,
            "idColonia":null,
            "calle":"Calle 4",
            "numero":"45",
            "codigoPostal":"76800"
        }
        ]
    }

Podríamos crear una clase que reflejará esta estructura, pero si solo nos interesa algún campo o la petición no tiene nada que ver con nuestro modelo, no parece la mejor opción.

En este caso, en nuestro modelo existiría una clase Actor y Demandado que comparten los mismos campos, por lo que podríamos usar la herencia.

    public class Parte {
        String id;
        String nombre;
        String apellidos;
        String idEstado;
        String idMunicipio;
        String idColonia;
        String calle;
        String numero;
        String codigoPostal;
        public Parte() {
        }
        //... getter's y setter's, métodos, etc.
    }
    public class Actor extends Parte{
        //... otras propiedades
        public Actor() {
        }
        //... otros métodos
    }
    public class Demandado extends Parte{
        //... otras propiedades
        public Actor() {
        }
        //... otros métodos
    }

en nuestro servicio REST (Apache CXF) podríamos deserializar el arreglo de Actores de la siguiente forma

    @Path("solicitar/")
    public String solicitar(String data){
        String response = null;
        try{
            ObjectMapper mapper=new ObjectMapper();
            mapper.configure(SerializationConfig.Feature.WRITE<em>DATES</em>AS<em>TIMESTAMPS, false);
            mapper.configure(Feature.FAIL</em>ON<em>UNKNOWN</em>PROPERTIES, false);
            Map<String, Object> map = mapper.readValue(data, new TypeReference<Map<String, Object>>(){});
            String tipo = (String) map.get("tipo");
            List
<Parte> actores = mapper.convertValue(map.get("actores"), new TypeReference
<List<Parte>>() {});
            //... registrar la solicitud
            response = mapper.writeValueAsString(actores);
        }catch(Exception e){
            String error = "Ocurrió un problema al hacer la solicitud";
            log.error(error, e);
            response = String.format("{\"response\":{\"status\": -1, \"data\": \"%s\"}}", error);
        }
        return response;
    }

Fuentes:
http://wiki.fasterxml.com/JacksonDataBinding
http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-DealingwithParameters

Anuncios

Hace poco escribí sobre como buscar/filtrar recursos en el servidor, para ello utilizamos un objeto Criteria -del mismo tipo que el recurso- para crear una consulta JDOQL que nos permitiera obtener los resultados.

En este artículo mostraré una forma de utilizar la clase AdvancedCriteria de SmartGwt para realizar una consulta más compleja. Al igual que la vez anterior, vamos a utilizar el framework Apache CXF para nuestros servicios REST, y Jackson para la serialización y deserialización de los objetos en formato JSON. También utilizamos Datanucleus para la persistencia de datos.

Me base en este artículo que explica cómo integrar SmartGwt con ASP.Net.

Lo primero que vamos a necesitar es una clase AdvancedCriteria del lado del servidor


public class AdvancedCriteria {

static ObjectMapper om;

{

om=new ObjectMapper();

om.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);

}

public static class JDOQLData{

public String filter="";

public Map<String,Object> params=new HashMap<String, Object>();

public Class<?> type;

&nbsp;

public JDOQLData(Class<?> type) {

this.type=type;

}

}

&nbsp;

String _constructor;

Integer _startRow;

Integer _endRow;

String fieldName;

String operator;

Object value;

AdvancedCriteria[] criteria;

&nbsp;

public AdvancedCriteria() {

}

&nbsp;

public static AdvancedCriteria fromString(String value){

try {

return om.readValue(value, AdvancedCriteria.class);

} catch (Exception e) {

throw new RuntimeException(e);

}

}

&nbsp;

// Getter's y Setter's ...

&nbsp;

public void toJDOQL(JDOQLData data){

if(this.criteria==null){

if(this.operator.equals(OperatorId.EQUALS.getValue())){

data.filter+=this.fieldName+" == ";

}else if(this.operator.equals(OperatorId.NOT_EQUAL.getValue())){

data.filter+=this.fieldName+" != ";

}else if(this.operator.equals(OperatorId.LESS_THAN.getValue())){

data.filter+=this.fieldName+" < ";

}else if(this.operator.equals(OperatorId.GREATER_THAN.getValue())){

data.filter+=this.fieldName+" > ";

}else if(this.operator.equals(OperatorId.LESS_OR_EQUAL.getValue())){

data.filter+=this.fieldName+" <= ";

}else if(this.operator.equals(OperatorId.GREATER_OR_EQUAL.getValue())){

data.filter+=this.fieldName+" >= ";

}

data.params.put(String.valueOf(data.params.size()+1), fixType(getPropertyType(data.type, this.fieldName), this.value) );

data.filter+=":"+data.params.size();

}else{

data.filter+="(";

for(int index=0;index<this.criteria.length;index++){

this.criteria[index].toJDOQL(data);

if(index+1<this.criteria.length){

if(this.operator.equals(OperatorId.AND.getValue())){

data.filter+=" && ";

}else if(this.operator.equals(OperatorId.OR.getValue())){

data.filter+=" || ";

}else if(this.operator.equals(OperatorId.NOT.getValue())){

data.filter+=" ! ";

}

}

}

data.filter+=")";

}

}

&nbsp;

private Object fixType(Class<?> type, Object value) {

return om.convertValue(value, type);

}

&nbsp;

private Class<?> getPropertyType(Class<?> clazz,String property){

try{

LinkedList<String> properties=new LinkedList<String>();

properties.addAll(Arrays.asList(property.split("\\.")));

Field field = null;

while(!properties.isEmpty()){

field = FieldUtils.getField(clazz,properties.removeFirst(),true);

clazz=field.getType();

}

return field.getType();

}catch(Exception e){

throw new RuntimeException(e);

}

}

}

Lo interesante de esta clase es que tiene un método estático fromString(String) que será utilizado por Apache CXF para crear un objeto de esta clase a partir de una cadena. También tiene un método recursivo toJDOQL(JDOQLData) que nos regresa un filtro y el listado de parámetros en un objeto JDOQLData para poder realizar la consulta en datanucleus. El método fixType y getPropertyType son para utilizar el tipo correcto -de acuerdo a la clase- en nuestro listado de parámetros.

Nuestro servicio REST quedaría de la siguiente forma


@GET

public String retrieve(@QueryParam("")Libro criteria,@QueryParam("")AdvancedCriteria advancedCriteria){

try {

List<Libro> libros = null;

if(advancedCriteria.get_constructor()!=null){

libros = Libro.getByAdvancedCriteria(advancedCriteria);

}else{

libros = Libro.getByCriteria(criteria);

}

Map<String, Object> response = new HashMap<String, Object>();

Map<String, Object> body = new HashMap<String, Object>();

body.put("status", 0);

body.put("data", libros);

response.put("response", body);

return json.writeValueAsString(response);

} catch (Exception e) {

log.error("", e);

}

return "{\"response\":{\"status\":-1, \"data\":\"Hubo un problema al buscar los Libros\"}}";

}

El framework Apache CXF se encarga de crear los objetos criteria(Libro) y advancedCriteria. Para saber cuál fue el que recibimos basta con verificar la propiedad _constructor, si es diferente de null sabemos que hemos recibido un AdvancedCriteria.

Finalmente realizamos la consulta en nuestra clase Libro


public static List<Libro> getByAdvancedCriteria(AdvancedCriteria criteria) {

List<Libro> libros;

PersistenceManager pm = PMF.get().getPersistenceManager();

Transaction tx = pm.currentTransaction();

try{

tx.begin();

JDOQLData data=new JDOQLData(Libro.class);

criteria.toJDOQL(data);

Query query = pm.newQuery(Libro.class,data.filter);

libros=(List<Libro>) query.executeWithMap(data.params);

tx.commit();

}finally{

if(tx.isActive()){

tx.rollback();

}

pm.close();

}

return libros;

}

En el cliente, utilizamos la clase AdvancedCriteria de SmartGwt para realizar la consulta


&nbsp;

AdvancedCriteria criteria=new AdvancedCriteria(OperatorId.OR,

new AdvancedCriteria[]{

new AdvancedCriteria("isbn",OperatorId.EQUALS, txtISBN.getValueAsString()),

new AdvancedCriteria(OperatorId.AND, new AdvancedCriteria[]{

new AdvancedCriteria("autor.nombre", OperatorId.EQUALS, "Gabriel García Márquez"),

new AdvancedCriteria("titulo", OperatorId.EQUALS, "Crónica de una muerte anunciada")

})

});

librosListGrid.fetchData(criteria);

&nbsp;

Finalmente al realizar el ‘fetch’ se realiza una petición a nuestro servicio con los siguientes párametros. Los cuales serán transformados a un objeto de nuestra clase AdvancedCriteria


&nbsp;

__gwt_ObjectId:793

operator:or

criteria:{"__gwt_ObjectId":788,"fieldName":"isbn","operator":"equals","value":"978-1400034956"}

criteria:{"__gwt_ObjectId":791,"operator":"or","criteria":[{"__gwt_ObjectId":789,"fieldName":"autor.nombre","operator":"equals","value":"Gabriel García Márquez"},{"__gwt_ObjectId":790,"fieldName":"titulo","operator":"equals","value":"Crónica de una muerte anunciada"}]}

_constructor:AdvancedCriteria

_operationType:fetch

_startRow:0

_endRow:75

_textMatchStyle:exact

_componentId:isc_ListGrid_0

_dataSource:isc_LibrosModule_3_0

isc_metaDataPrefix:_

isc_dataFormat:json

&nbsp;

Fuentes:
http://wiki.smartclient.com/display/Main/Integrating+with+ASP.Net+MVC
http://wiki.smartclient.com/display/Main/5.+Adding+support+for+AdvancedCriteria
http://www.objectdb.com/database/jdo/manual/chapter7#Query_Parameters
http://www.objectdb.com//database/jdo/manual/chapter7
http://stackoverflow.com/questions/935762/how-to-dynamically-build-jdo-queries-on-multiple-parameters

Para obtener el tipo de dato de las propiedades de un objeto podemos utilizar la reflexión.

Por ejemplo si tenemos las siguientes clases

public class Libro{
String isbn;
String titulo;
Autor autor;
}
public class Autor{
String id;
String nombre;
Date fechaNacimiento;
}

Un método para obtener el tipo de dato de una propiedad, incluso propiedades anidadas, sería el siguiente

private Class<?> getPropertyType(Class<?> clazz,String property){
 try{
 LinkedList<String> properties=new LinkedList<String>();
 properties.addAll(Arrays.asList(property.split("\\.")));
 Field field = null;
 while(!properties.isEmpty()){
 field = FieldUtils.getField(clazz,properties.removeFirst(),true);
 clazz=field.getType();
 }
 return field.getType();
 }catch(Exception e){
 throw new RuntimeException(e);
 }
}

Lo invocaríamos de la siguiente manera

Class<?> type=getPropertyType(Libro.class,"autor.fechaNacimiento");
System.out.println(type.toString()); //imprime java.util.Date

Notesé el uso de la clase FieldUtils de la librería Apache Commons Lang, cuyo método getField busca el campo recursivamente en la jerarquía de las clase y sus interfaces. Para agregar la librería a nuestro proyecto hay que agregar la siguiente dependencia


<dependency>
 <groupId>org.apache.commons</groupId>
 <artifactId>commons-lang3</artifactId>
 <version>3.1</version>
 </dependency>

Para listar todas las propiedades de un objeto recursivamente podemos utilizar como ejemplo el código de la clase FieldUtils.

Con la librería Jackson podemos obtener un esquema JSON de cualquier objeto. El detalle es que los tipos de datos que obtendríamos serían sólo los permitidos en JSON (string, number, boolean, object, array, null).

ObjectMapper mapper=new ObjectMapper();
System.out.println(mapper.generateJsonSchema(Libro.class));

Fuentes:
http://stackoverflow.com/questions/11125409/find-the-field-type-using-jackson-json
http://stackoverflow.com/questions/3567372/access-to-private-inherited-fields-via-reflection-in-java
http://wiki.fasterxml.com/JacksonJavaDocs
http://jackson.codehaus.org/1.8.8/javadoc/org/codehaus/jackson/map/ObjectMapper.html#generateJsonSchema(java.lang.Class)
http://wiki.fasterxml.com/JacksonTreeModel

Cuando diseñas la URL de un recurso, no queda muy claro cómo vas a realizar una búsqueda o aplicar un filtro. Por ejemplo, si tuvieramos el recurso “Productos” tendríamos el siguiente esquema para las URL’s

GET   http://localhost:8080/rest/productos   Regresa un listado de productos en formato JSON

Pero resultaría muy inefeiciente cargar TODO el listado de productos en una página. Resulta más conveniente aplicar un filtro al listado y regresar solamente un subconjunto de productos al cliente, por ejemplo, de acuerdo al tipo de producto. Para ello la forma más recomendable sería pasando un listado de parámetros en la URL de la siguiente forma

http://localhost:8080/rest/productos?tipo=OFICINA&marca=SCRIBE

Si solamente quieres realizar la paginación del listado de productos, puedes agregar párametros como startRow=0 y endRow=75 ó start=0 y limit=75 o utilizar un encabezado personalizado cómo “X-Range: 0-75”

A continuación mostrare la implementación de una búsqueda simple en un servicio REST. Para ello utilicé SmartGwt para la interfaz de usuario, Apache CXF para los servicios REST, Jackson para la serialización y Datanucleus (JDO) para la persistencia de los datos.

Primero creamos la interfaz que tendrá como fuente de datos un servicio REST

ListGrid grdProductos=new ListGrid();
grdProductos.setDataSource(new RestDataSource(){
{
setDataURL("/rest/productos");
setDataFormat(DSDataFormat.JSON);

DataSourceField fldId=new DataSourceField("id", FieldType.TEXT);
fldId.setPrimaryKey(true);
fldId.setHidden(true);
DataSourceField fldNombre=new DataSourceField("nombre", FieldType.TEXT, "Producto");
DataSourceField fldTipo=new DataSourceField("tipo", FieldType.TEXT, "Tipo");
DataSourceField fldMarca=new DataSourceField("marca", FieldType.TEXT, "Marca");
setFields(fldId,fldNombre,fldTipo,fldMarca);
}
});
Criteria criteria=new Criteria();
criteria.addCriteria("tipo","OFICINA");
criteria.addCriteria("marca", "SCRIBE");
grdProductos.setInitialCriteria(criteria);
grdProductos.setAutoFetchData(true);

Simplemente creamos un Grid para desplegar los productos y establecemos un filtro inicial (para mantener breve el ejemplo, en realidad, estos parámetros los seleccionaría el usuario).

Cuando se cargué el Grid automáticamente (autoFecthData=true) se hara un Request a la URL de nuestro servicio REST parecida a la siguiente

http://localhost:8080/rest/productos?__gwt_ObjectId=170&amp;tipo=OFICINA&marca=SCRIBE&_operationType=fetch&_startRow=0&_endRow=75&_textMatchStyle=exact&_componentId=isc_ListGrid_0&_dataSource=isc_ProductosModule_2_0&isc_metaDataPrefix=_&isc_dataFormat=json

Cómo podemos ver el Grid automáticamente envía los parámetros del filtro en la URL -incluídos los de paginación-.

Ahora vamos a ver como implementamos el servicio REST con la ayuda de Apache CXF y Jackson

@Path("productos")
public class ProductosResource {
{
json=new ObjectMapper();
log=LoggerFactory.getLogger(ProductosResource.class);
}

ObjectMapper json;
Logger log;

@GET
public String retrieve(@QueryParam("")Producto criteria){
try {
List<Producto> Productos = Producto.getByCriteria(criteria);
Map<String, Object> response = new HashMap<String, Object>();
Map<String, Object> body = new HashMap<String, Object>();
body.put("status", 0);
body.put("data", Productos);
response.put("response", body);
return json.writeValueAsString(response);
} catch (Exception e) {
log.error("", e);
}
return "{\"response\":{\"status\":-1, \"data\":\"Hubo un problema al obtener los Productos\"}}";
}
}

CXF construye automáticamente una instancia de un Producto en base a los parámetros, esta instancia la utilizaremos para filtrar el listado al comparar sus propiedades con los registros en la base de datos.

Finalmente tenemos que implementar nuestra clase Producto de la siguiente manera

@PersistenceCapable(detachable="true")
public class Producto {

@PrimaryKey
@Persistent(valueStrategy=IdGeneratorStrategy.UUIDHEX)
String id;
String nombre;
String tipo;
String marca;

public Producto() {
}

public String getId() {
return id;
}

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

public String getNombre() {
return nombre;
}

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

public String getTipo() {
return tipo;
}

public void setTipo(String tipo) {
this.tipo = tipo;
}

public String getMarca() {
return marca;
}

public void setMarca(String marca) {
this.marca = marca;
}

public static List<Producto> getByCriteria(Producto criteria) {
List<Producto> Productos;
PersistenceManager pm = PMF.get().getPersistenceManager();
Transaction tx = pm.currentTransaction();
try{
tx.begin();
StringBuffer filter=new StringBuffer();
List<Object> parameters=new ArrayList<Object>();
if(criteria.getId()!=null){
filter.append("id == :id");
parameters.add(criteria.getId());
}
if(criteria.getNombre()!=null){
if(filter.length()>0)filter.append(" && ");
filter.append("nombre == :nombre");
parameters.add(criteria.getNombre());
}
if(criteria.getMarca()!=null){
if(filter.length()>0)filter.append(" && ");
filter.append("marca == :marca");
parameters.add(criteria.getMarca());
}
Query query = pm.newQuery(Producto.class,filter.toString());
Productos=(List<Producto>) query.executeWithArray(parameters.toArray());
tx.commit();
}finally{
if(tx.isActive()){
tx.rollback();
}
pm.close();
}
return Productos;
}

}

Lo interesante es que creamos un filtro dinámico de acuerdo a los valores de los campos en el objeto Criteria.

Conclusión:

Aquí mostré como podemos implementar una búsqueda o filtro básico en nuestros servicios REST. Rara vez vamos a requerir solamente esto, usualmente se presentan casos en los que el filtro o la búsqueda es mucho más complejo. Si utilizas SmartGWT para el código del cliente, existe la clase AdvancedCriteria, que nos permite realizar filtros complejos. El detalle es mapear esta clase a una que podamos utilizar en el lado del servidor y hacer la búsqueda correspondiente mediante JDOQL. Pero esa….. esa es otra historia……

Fuentes:
http://stackoverflow.com/questions/5020704/how-to-design-restful-search-filtering
http://cxf.apache.org/docs/jax-rs-basics.html#JAX-RSBasics-Parameterbeans
https://developers.google.com/appengine/docs/java/datastore/jdo/relationships?hl=es
http://stackoverflow.com/questions/935762/how-to-dynamically-build-jdo-queries-on-multiple-parameters
http://db.apache.org/jdo/jdoql.html
http://www.elasticsearch.org/guide/reference/query-dsl/
http://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/data/AdvancedCriteria.html
http://cxf.apache.org/docs/jax-rs-advanced-features.html#JAX-RSAdvancedFeatures-FIQLsearchqueries

Uitlizo Jackson para serializar objetos al formato JSON y enviarlos al cliente mediante servicios REST. A veces olvido, sobretodo al inicio de un proyecto, que por default Jackson intenta serializar mis objetos con un BeanSerializer. Esto significa que mi clase debe ser un JavaBean, es decir, tener un constructor sin argumentos y ‘getters’ y ‘setters’ para las propiedades que me interesa sean incluídas en la respuesta. Si tu clase no cumple estas condiciones, te arrojará la siguiente excepción:

org.codehaus.jackson.map.JsonMappingException: No serializer found for class mx.com.apestudio.videohaus.server.model.Video and no properties discovered to create BeanSerializer

Tengo un proyecto en el que utilizo Jackson para serializar objetos al formato JSON. Esta aplicación la instalé en Jboss AS7 -generando un archivo war mediante Maven– pero al ejecutar la aplicación en el navegador me arrojaba el siguiente error:

java.lang.NoClassDefFoundError: Could not initialize class org.codehaus.jackson.map.ObjectMapper

ó

java.lang.NoSuchMethodError: org.codehaus.jackson.type.JavaType.<init>(Ljava/lang/Class;I)V

El problema es que existe un conflicto entre las librerías de Jackson que utiliza Jboss para la implementación de JAX-RS y las dependencias de mi proyecto. La solución es configurar Jboss para que no cargué el módulo JAX-RS. Ve al archivo <jboss_home>/standalone/configuration/standalone.xml y elimina las siguientes 2 líneas

<extension module=”org.jboss.as.jaxrs”/>
<subsystem xmlns=”urn:jboss:domain:jaxrs:1.0″/>

Fuentes:

http://community.jboss.org/message/622012
http://forum.springsource.org/

Estoy iniciando un proyecto en el que utilizo Jersey (JAX-RS) para crear los servicios REST que utilizará la aplicación. Inicialmente pensé en anotar mis entidades (clases POJO) con el atributo @XmlRootElement de JAXB, de esta forma podría regresar directamente mis objetos y Jersey se encargaría de la serialización al formato JSON(mediante Jackson). De tal forma que el método GET quedaría de la siguiente forma:

@GET
@Produces("application/json")
public List<Vehicle> retrieve(){
 DAO store=new DAO();
 return store.ofy().query(Service.class).list();
}

Elegante y sencillo (gracias a Objectify, que se encarga del acceso a datos en el AppEngine Datastore). El principal problema es que no tenemos control sobre el proceso de serialización, así que, si mas adelante queremos -por ejemplo- implementar la páginación de datos, será un proceso un poco más complicado.
Entonces ¿cómo retomamos el control sobre el proceso de serialización? Pues una opción es utilizar directamente Jackson, de está forma podremos -más adelante- agregar metadatos o cualquier cosa que necesitemos a nuestra respuesta JSON.

Así, nuestro método quedaría de la siguiente forma:

@GET
@Produces("application/json")
public String retrieve(){
 try{
 DAO store=new DAO();
 List<Vehicle> vehicles=store.ofy().query(Vehicle.class).list();
 ObjectMapper mapper=new ObjectMapper();
 Map<String,Object> jsonMap=new HashMap<String, Object>();
 jsonMap.put("vehicles", vehicles);
 return mapper.writeValueAsString(jsonMap);
 }catch(Exception e){
 e.printStackTrace();
 throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
 }
}

Si quisieramos agregar otra propiedad en la respuesta, además de los vehículos, lo haríamos simplemente agregandola a nuestro “mapa”:

jsonMap.put(“total”,vehicles.size());