ExtJS Grid Filter Generic Support Framework

11 11 2009

By using convention, some extension, and a Criteria API, we can make a generic service framework to support ExtJS’s grid filter plugin that will support any model object; removing the need to perform service tier and back-end development to support each grid.

Introduction
Anyone who’s used ExtJS for an extended length of time has most likely come across the grid filters extension. If you haven’t, I highly recommend it (see example and forum post). This great extension of the grid framework was so well received by the community, ExtJS is sucking it up into the core product and will be available with the 3.1 release.

PastedGraphic.J4aeCaSin4mW.jpg

I have had the opportunity to use this extension on the last couple of projects. How it was being used provided for a lot of “cutting and pasting” due to not following conventions and/or the requirement of a specific domain model type for the query mechanism. This copy and pasted structure usually involved creating a servlet or handler that was responsible for converting the request parameters into some sort of domain model object, then calling a DAO or Stateless session bean to take apart that model object to create a query, performing the query, returning the results, then marshaling of the specific type to return to ExtJS. I’m going to demonstrate how to use some convention and some minor extension to create a ExtJS grid filtering support framework that should be generic no matter which model you need to query.

Model
We’re going to use a simple model here to demonstrate this framework. We have 3 domain objects and a utility bean that will allow easy marshaling of our results into a JSON structure required by ExtJS’s reader/store classes. The 3 domain objects are Person, Fish, and WaterType. Person and Fish are self explanatory, WaterType is merely an “enumeration” of the type of water a fish is found in: saltwater or freshwater.

PastedGraphic1.4volmyGOlbHm.jpg

All but the SearchResult class are Hibernate models and have the appropriate Hibernate mapping files in our project. Let’s not harp on the design here as it is awful and may not make a lot of “business sense”. It’s merely for demonstration purposes to show filtering with a custom type (integer), a boolean, and an entity with a relationship.

ExtJs
Filters
Out of the box, the grid filtering extension supports Date, Boolean, String, Numeric, and List. Numeric doesn’t quite identify the strong types our Java backend require, so we’re going to have to add some custom pseudo types in order for our generic servlet code to be able to identify and parse the request parameters into the appropriate type. Since Numeric has all of the functionality we need, we’re merely going to extend it by creating a new file named IntegerFilter.js, with the following content:

Ext.ux.grid.filter.IntegerFilter = Ext.extend(Ext.ux.grid.filter.NumericFilter, {
        serialize: function(){
                var args = [];
                var values = this.menu.getValue();
                for(var key in values)
                        args.push({type: 'integer', comparison: key, value: values[key]});
        
                this.fireEvent('serialize', args, this);
                return args;
        }
});

The key thing to notice here is the use of “integer” as the type that is pushed during serialization. This will inform the processing servlet that this value is tied to a strongly typed integer.

Grid Filters
Next, we can get to creating our grid. There are four things to notice about our filter/json reader/column model definition. First, we name our columns in the grid/filter/store the exact same name as the attributes of our model. This is the key “convention” piece we need to follow for the servlet code to work appropriately. Second, we are using dot notation to reference a field of a related entity to the Fish entity we are displaying in our grid. Third, notice the use of the custom filter type Integer for the citationLength member. Last, notice the always sent parameter model, which defines the fully qualified classname of the model for this grid. This class will identify to the Criteria API what model we’re basing our query off of.

Here’s our filter definition:

var fishFilters = new Ext.ux.grid.GridFilters({
        filters: [
            { type: 'boolean', dataIndex: 'water.freshwater' },
            { type: 'string', dataIndex: 'name' },
            { type: 'integer', dataIndex: 'citationLength' }
        ]
    });

Grid Store
Here’s our grid store/json reader definition, again notice the use of model attribute names, dot notation, the integer type, and the model parameter specifying the fully qualified class name:

    var fishGridStore = new Ext.data.Store({
        proxy: new Ext.data.HttpProxy({
            url: 'ExtJsFilterServlet',
            timeout: 90000,
            method: 'GET'
        }),
        reader: new Ext.data.JsonReader({
            root: 'searchResult.results',
            totalProperty: 'searchResult.totalCount',
            fields: [
                { name: id },
                { name: 'name' },
                { name: 'water.freshwater', type: 'boolean' },
                { name: 'citationLength' }
            ]
        }),
        remoteSort: true,
        sortInfo: {
            field: 'name',
            direction: 'ASC'
        }
    });
 
    fishGridStore.on('beforeload', function(store){
            store.baseParams = { model: 'com.captechventures.playground.model.Fish' };
    });

Column Model
This is fairly straight forward, typical ExtJS column model setup.

    var fishCm = new Ext.grid.ColumnModel([
                            { header: 'Name', dataIndex: 'name', sortable: true}, 
                            { header: 'Fresh Water', dataIndex: 'water.freshwater', sortable: true },
                            { header: 'Citation Length', dataIndex: 'citationLength', sortable: true }
                        ]);

I won’t bother with the boring details of setting up the paging toolbar and the grid given these 3 components, however I will show you what the grid will look like in the browser.

PastedGraphic2.tMksKBEPet0g.jpg

Servlet
Criteria Objects
We need to instantiate our criteria objects based on our model. Remember, we passed in the fully qualified name of the model from the grid as a base parameter. Also, notice we’re creating 2 criteria objects. Unfortunately we’ll have to perform 2 queries here to support ExtJS’s paging toolbar functionality; one for the “count” of total records that meet the criteria and one to retrieve the specific set of results for the page the grid is “on”.

Criteria criteria = session.createCriteria(request.getParameter("model"));
 
Criteria countCriteria = session.createCriteria(request.getParameter("model"));
setupProjections(countCriteria);

Pagination Directives
We’re using Hibernate’s API to limit the result (maxResults) and to identify the first record to return (firstResult). We only need to perform this on the criteria object indended for the actual result set, not the “count” criteria object.

Integer limit = DEFAULT_LIMIT;
Integer start = DEFAULT_START;
 
if (StringUtils.isNotBlank(servletRequest.getParameter("limit"))) {
        limit = Integer.parseInt(servletRequest.getParameter("limit").trim());
}
 
if (StringUtils.isNotBlank(servletRequest.getParameter("start"))) {
        start = Integer.parseInt(servletRequest.getParameter("start").trim());
}
 
criteria.setMaxResults(limit);
criteria.setFirstResult(start);

Criteria Population
First, let’s get out our sort directives. We’ll also need a boolean identifying if any sort code as been applied. ExtJS’s grid sorting mechanism currently only support a single sort, so we will as well.

// let's get out our sort directives
String sort = servletRequest.getParameter("sort");
String dir = servletRequest.getParameter("dir");
 
// an identifier for whether or not a sort was performed
boolean sortSet = false;

Now for the meat of our work. Here we’re going to loop through the request object and retrieve all the filtering criteria that could have been set. This is standard grid filter code and is one of the biggest pieces of code I’ve seen duplicated over and over. First let’s start with our loop and local variable setup.

for (int i = 0; servletRequest.getParameter("filter[" + i + "][field]") != null; i++) {
 
        // get out the request parameters for this filter
        String prefix = "filter[" + i + "]";
        String field = servletRequest.getParameter(prefix + "[field]");
        String type = servletRequest.getParameter(prefix + "[data][type]");
        String value = servletRequest.getParameter(prefix + "[data][value]");
        String comparison = servletRequest.getParameter(prefix+ "[data][comparison]");

We now have all the data and metadata regarding a single filter to apply the appropriate API call to the Hibernate Criteria API object. First thing we need to do is determine what data type was used and parse the request value string into the appropriate object/type.

Object propVal = null;
 
// special handling for data types
if ("date".equals(type)) {
        try {
                propVal = sdf.parse(value);
        } catch (ParseException e) {
                throw new ServletException(e);
        }
} else if ("numeric".equals(type)) {
        // defaulting to integer
        propVal = Integer.parseInt(value);
} else if ("integer".equals(type)) {
        propVal = Integer.parseInt(value);
} else if ("long".equals(type)) {
        propVal = Long.parseLong(value);
} else if ("float".equals(type)) {
        propVal = Float.parseFloat(value);
} else if ("double".equals(type)) {
        propVal = Double.parseDouble(value);
} else if ("boolean".equals(type)) {
        propVal = Boolean.parseBoolean(value);
} else if ("string".equals(type)) {
        // IMPORTANT -- so partial match queries will work
        // we're going to add wildcard to the value
        propVal = "%" + value + "%";
} else {
        propVal = value;
}

You can see the use of the new IntegerFilter here where we will parse the value appropriately with the Integer class. There is a “long”, “float”, and “double” in this control statement that we didn’t create new filters for. Those would be easy to do and would follow the same pattern as IntegerFilter.js.

Now that we have a value and the name of the field, we’re ready to add criterion to our criteria object(s). Dot notation and the ability to reference related entities adds a little bit of complexity to this process, however we can take care of this fairly easily using Hibernate’s API. We pulled this code out into a helper function as it is useful in other places:

private Criteria findCriteria(Criteria original, String field) {
        String[] nested = field.split("\\.");
        Criteria toUse = original;
 
        // traverse the dots and continually create Criteria objects
        // the last criteria object will be the one we intend to use
        if (nested.length > 0) {
                for (int j = 0; j < nested.length - 1; j++) {
                        toUse = toUse.createCriteria(nested[j]);
                }
        }

        return toUse;
}

According to the API, we need a property object to set our criterion on, so we need to identify that as well. We need to take into account nested properties. This is where the convention aspect comes into play again. We take the “field” name that was provided to us by ExtJS and can create an appropriate Property from that name that will work with the Criteria API appropriately. This is also useful as a helper function:

private Property findProperty(String field) {
        String[] nested = field.split("\\.");
        // we want to use the last field if it is nested
        // our previously called findCriteria method will take care of the appropriate
        // nesting in the actual API call for the query
        if (nested.length > 0) {
                return Property.forName(nested[nested.length - 1]);
        } else {
                return Property.forName(field);
        }
}

We now have our Property and the Criteria which to act on, let’s finally add our criterion.

// determine the operation
if ("lt".equalsIgnoreCase(comparison)) {
        searchToUse.add(prop.lt(propVal));
        countToUse.add(prop.lt(propVal));
} else if ("gt".equalsIgnoreCase(comparison)) {
        searchToUse.add(prop.gt(propVal));
        countToUse.add(prop.gt(propVal));
} else if ("eq".equalsIgnoreCase(comparison)) {
        searchToUse.add(prop.eq(propVal));
        countToUse.add(prop.eq(propVal));
} else {
        // we want to use the "like" operator for strings
        if ("string".equals(type)) {
                searchToUse.add(prop.like(propVal).ignoreCase());
                countToUse.add(prop.like(propVal).ignoreCase());
        } else {
                searchToUse.add(prop.eq(propVal));
                countToUse.add(prop.eq(propVal));
        }
}

Almost done, we now need to set a sort if one is available:

if (StringUtils.isNotBlank(sort) && sort.equals(field)) {
        setupSort(searchToUse, prop, dir);
        sortSet = true;
}

Again, setting up the sort is useful elsewhere, so we’ve pulled that out into its own function:

private void setupSort(Criteria criteria, Property prop, String dir) {
        if (StringUtils.equalsIgnoreCase("asc", dir)) {
                criteria.addOrder(prop.asc());
        } else {
                criteria.addOrder(prop.desc());
        }
}

That ends our looped code. We should have populated Criteria objects at this point that we can now query against.

SearchResult result = new SearchResult();
result.setTotalCount((Integer) countCriteria.uniqueResult());
result.setResults(criteria.list());

Now we just need to marshall our SearchResult object to the output stream. For this demonstration, I’m using XStream’s JSON plugin as it was the easiest to setup in this web application. You can use any facility that will create XML or JSON that ExtJS can use in their readers:

response.setContentType("application/json");
XStream xstream = new XStream(new JsonHierarchicalStreamDriver());
xstream.setMode(XStream.NO_REFERENCES);
xstream.alias("searchResult", SearchResult.class);
xstream.toXML(result, response.getWriter());

Congrats! We now have a reusable servlet that is capable of handling any model object we throw at it from the ExtJS grid filter.

Conclusion
There are a few limitations to this framework. The first is that this is a fairly simple use case. We didn’t use a list filter nor does this framework allow for the navigation over a collection based relationship. You are also limited to what architecture your current application uses. We used Hibernate3 for this demonstration due the availability of a Criteria API. JPA1 doesn’t have a Criteria API, however it is possible to write a few classes where a majority of your need can be supported without a full blown Criteria API. I have done this and it is in use at one of my clients. Changes to your model require a change to your javascript. These are especially difficult to find/make even with the help from an IDE and usually reveal themselves unexpectedly by a tester or user.

For most cases, the only new code required when adding a new filtered ExtJS grid to your site is the javascript code you would have to write anyways. The server side is completely taken care of. This framework will work for a majority of use cases. For specialized use cases, you could always write a specialized handler but continue to use the generic servlet for all standard use cases.

This is a rather long blog with several snippets of code. I highly recommend taking a look at the sample app I’m attaching for a better picture of how this all fits together. One note regarding the attached war — I had to remove the Hibernate3.jar file from the WEB-INF/lib folder due to size limitations. Hibernate 3.3.2 was used, so you will need to add this for the example to work.

Downloads
search-web.war