Es común encontrar combos que sólo deben mostrar algunas opciones dependiendo de la selección hecha en otro combo, esto es, una selección dependiente.

En el Showcase de SmartGwt viene un ejemplo de cómo hacer esto. El problema con el ejemplo es que no nos dejará filtrar (en realidad autocompletar) los valores que contiene el combo. Esto se debe a que, en el código del ejemplo, sobreescribimos el método getPickListFilterCriteria que es utilizado cuando escribimos algún valor en el combo.

Por ejemplo, si tenemos que mostrar las Colonias que pertenecen a un Municipio -y además permitir que el usuario filtre/busque al escribir el nombre de la colonia- lo haríamos de la siguiente manera.

        SelectItem itmMunicipio = new SelectItem("municipio", "Municipio");
        itmMunicipio.addChangedHandler(new ChangedHandler() {
            @Override
            public void onChanged(ChangedEvent event) {
                ComboBoxItem item = (ComboBoxItem) frmPartes.getItem("colonia");
                item.setPickListCriteria(new Criteria("municipio", form.getValueAsString("municipio")));
                item.fetchData();
            }
        });
        itmMunicipio.setOptionDataSource(MunicipiosDS.singleton());
        itmMunicipio.setValueField("id");
        itmMunicipio.setDisplayField("nombre");
        itmMunicipio.setDefaultValue("014"); // Querétaro
        ComboBoxItem itmColonia = new ComboBoxItem("colonia", "Colonia");
        itmColonia.setOptionDataSource(ColoniasDS.singleton());
        itmColonia.setValueField("id");
        itmColonia.setDisplayField("nombre");
        itmColonia.setAddUnknownValues(false);
        itmColonia.setTextMatchStyle(TextMatchStyle.SUBSTRING);
        itmColonia.setPickListCriteria(new Criteria("municipio", "014")); // esto es para el valor inicial
        itmColonia.setDefaultToFirstOption(true);
        itmColonia.setWidth(200);
        itmColonia.setPickListWidth(300);

Fuentes:
http://www.smartclient.com/smartgwt/showcase/#dep_selectects_db_combobox_category
http://www.smartclient.com/smartgwt/showcase/#combobox_multifield_search
http://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/docs/ComboBoxFiltering.html
http://www.smartclient.com/smartgwt/javadoc/com/smartgwt/client/widgets/form/fields/ComboBoxItem.html#setTextMatchStyle(com.smartgwt.client.types.TextMatchStyle)

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

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

Si utilizas un DataSource para enlazar los datos a un control de SmartGwt y tienes la necesidad de filtrar por un rango de fechas, lo puedes hacer de la siguiente manera:

AdvancedCriteria criteria=new AdvancedCriteria(OperatorId.AND, new Criterion[]{
new Criterion(“inicio”, OperatorId.GREATEROREQUAL, inicio),
new Criterion(“inicio”, OperatorId.LESS_THAN, fin)
});
calendario.fetchData(criteria);

Notesé que a pesar de que la clase Criterion hereda de Criteria, no podemos utilizarla directamente en el método fetchData, por ejemplo:

Criterion criteria=new Criterion();
criteria.addCriteria(“inicio”, OperatorId.GREATEROREQUAL, inicio);
criteria.addCriteria(“inicio”, OperatorId.LESS_THAN, fin);
calendario.fetchData(criteria);

arrojaría el siguiente error:

Caused by: java.lang.NullPointerException: null

    at com.smartgwt.client.data.Criterion.addCriteria(Criterion.java:218)

    at com.smartgwt.client.data.Criterion.addCriteria(Criterion.java:276)

Los objetos Criterion siempre deberán usarse como parte de un AdvancedCriteria dado que su definición es:

A criterion is part of the definition of an com.smartgwt.client.data.AdvancedCriteria object, which is used to filter records according to search criteria.