Home > extjs4, spring-roo > Spring Roo Controller for Extjs4 data store with pagination, sorting, filtering and server side validation.

Spring Roo Controller for Extjs4 data store with pagination, sorting, filtering and server side validation.

In this article I will describe a Spring Controller, that supports CRUD operations with server side sorting, filtering and pagination.
On the client side i have a model and a store, that handles all the communication with the server. The visual components consist of:
– Ext.grid.Panel with a Ext.PagingToolbar and ‘new’ and ‘delete’ buttons and a filter field.
– Ext.form.Panel with ‘save’ and ‘cancel’ buttons

I will not go into details about the ExtJs here but try to focus on the server side json rest controller. Most of Extjs code you can pickup in the ExtJs4 examples. I will though list the model and store here. As you can see the model has 3 fields: id, description and price.

Ext.define('Item', {
    extend: 'Ext.data.Model',
    fields: [
        {
            name: 'id',
            type: 'int',
            useNull: true
        },
        'description',
        {
            name: 'price',
            type: 'float',
            useNull: true
        }
    ]
});

And the store uses remote filtering and sorting – all done by the controller on the server. You can see more details about the filtering in one of my previous posts. And it uses a json rest proxy. I also listed the proxy exception handler, where you see how to make the form display the validation errors returned by the controller. The client side validation errors are displayed in the same way, only you get the errors like this:
form.updateRecord(newItem);
var errors = newItem.validate();

    var store = Ext.create('Ext.data.Store', {
        autoSync: true,
        model: 'Item',
        pageSize: 4,
        remoteSort: true,
        remoteFilter: true,
        proxy: {
            type: 'rest',
            url: '/cv/secure/items',
            format: 'json',
            reader: {
                type: 'json',
                root: 'data'
            }
            ,
            writer: {
                type: 'json'
            },
            afterRequest: proxyAfterRequest,
            listeners: {
                exception: proxyOnException
            }
        }
    });

    var proxyOnException = function(proxy, response, operation) {
        if (response.responseText) {
            var responseObj = Ext.decode(response.responseText);
            formPanel.getForm().markInvalid(responseObj.errors);
        }
    };

Now to the controller.
I have a Spring-Roo generated entity called Item with the same fields as the ExtJs model from before. The id field is hidden in a aspect file managed by Spring Roo.

   private String description;

    @Min(0L)
    private Double price;

The existing Rest Controller generated by Roo is not suitable for json with extjs, so I’m going to make a new rest controller for this purpose.
First lets look at the method that gets the list of items. It is the method that handles sorting, filtering and pagination.
This is an example of the query string parameters, that the ExtJs data store sends:

page:1
start:0
limit:4
sort:[{"property":"description","direction":"DESC"}]
filter:[{"property":"description","value":"r"}]

, and extjs expects to get a response like this:

{
	"total":4,
	"data":[
		{"price":3212.0,"id":3,"version":0,"description":"tgharga"},
		{"price":343.0,"id":4,"version":0,"description":"srsrg"},
		{"price":0.0,"id":2,"version":0,"description":"regerg"},
		{"price":6456.0,"id":5,"version":0,"description":"frtg"}
	]
}

Here is the method:

@RequestMapping(method = RequestMethod.GET)
    public void list(@RequestParam(value = "page", required = false) Integer page,
                     @RequestParam(value = "start", required = false) Integer start,
                     @RequestParam(value = "limit", required = false) Integer limit,
                     @RequestParam(value = "sort", required = false) String sorts,
                     @RequestParam(value = "filter", required = false) String filters,
                     Model model) {

        List<ItemSorting> sortList = ItemSorting.decodeJson(sorts);
        List<ItemFilter> itemFilterList = ItemFilter.decodeJson(filters);
        if (page != null && start != null && limit != null) {
            assert (start == (page - 1) * limit);
            model.addAttribute("total", Item.countItems(itemFilterList));
            model.addAttribute("data", Item.findItems(start, limit, sortList, itemFilterList));
        }
    }

It is quit straight forward. I made support classes, ItemSorting and ItemFilter, where the json is decoded. I will get back to those classes later.
The resulting list of items and the total number of filtered items are returned by the finder methods of the Item entity class, using jpa2 CriteriaQuery.

    public static List<Item> findItems(int firstResult, int maxResults, List<ItemSorting> itemSortings, List<ItemFilter> itemFilters) {
        CriteriaBuilder cBuilder = entityManager().getCriteriaBuilder();
        CriteriaQuery<Item> itemQuery = cBuilder.createQuery(Item.class);
        Root<Item> from = itemQuery.from(Item.class);
        // make predicates
        Predicate[] predicates = ItemFilter.makeFilterPredicates(from, itemFilters);
        // make orders
        List<Order> orders = ItemSorting.makeOrders(from, itemSortings);
        // get the items
        itemQuery.select(from).where(predicates).orderBy(orders);
        TypedQuery<Item> itemTypedQuery = entityManager().createQuery(itemQuery);
        return itemTypedQuery.setFirstResult(firstResult).setMaxResults(maxResults).getResultList();
    }

    public static Long countItems(List<ItemFilter> itemFilters) {
        CriteriaBuilder cBuilder = entityManager().getCriteriaBuilder();
        CriteriaQuery<Long> totalQuery = cBuilder.createQuery(Long.class);
        Root<Item> from = totalQuery.from(Item.class);
        // make predicates
        Predicate[] predicates = ItemFilter.makeFilterPredicates(from, itemFilters);
        // get total number of items.
        totalQuery.select(cBuilder.count(from));
        totalQuery.where(predicates);
        TypedQuery<Long> longTypedQuery = entityManager().createQuery(totalQuery);
        return longTypedQuery.getSingleResult();
    }

Following are the two helper classes ItemSorting and ItemFilter, where json from the query strings are decoded and the ordering and filtering is done for the finder methods.
First ItemSorting:

public class ItemSorting {

    private final String fieldName;
    private final String direction;

    public ItemSorting(String fieldName, String direction) {
        this.fieldName = fieldName;
        this.direction = direction;
    }

    public static List<ItemSorting> decodeJson(String json) {
        List<ItemSorting> sorts = new ArrayList<ItemSorting>();
        if (json != null) {
            List<HashMap<String,String>> sortList = new JSONDeserializer<ArrayList<HashMap<String,String>>>().deserialize(json);
            for (HashMap<String,String> sort : sortList) {
                String fieldName = sort.get("property");
                String direction = sort.get("direction");
                sorts.add(new ItemSorting(fieldName, direction));
            }
        }
        return sorts;
    }

    public static ArrayList<Order> makeOrders(Root<Item> from, List<ItemSorting> itemSortings) {
        ArrayList<Order> orders = new ArrayList<Order>();
        CriteriaBuilder cBuilder = Item.entityManager().getCriteriaBuilder();
        if (itemSortings != null && !itemSortings.isEmpty()) {
            for (ItemSorting itemSorting : itemSortings) {
                orders.add("DESC".equals(itemSorting.direction) ? cBuilder.desc(from.get(itemSorting.fieldName)) : cBuilder.asc(from.get(itemSorting.fieldName)));
            }
        }
        return orders;
    }

}

And here ItemFilter:

public class ItemFilter {

    private final String fieldName;
    private final String value;

    public ItemFilter(String fieldName, String value) {
        this.fieldName = fieldName;
        this.value = value;
    }

    public static List<ItemFilter> decodeJson(String json) {
        List<ItemFilter> itemFilters = new ArrayList<ItemFilter>();
        if (json != null) {
            List<HashMap<String,String>> filterList = new JSONDeserializer<ArrayList<HashMap<String,String>>>().deserialize(json);
            for (HashMap<String,String> filter : filterList) {
                String fieldName = filter.get("property");
                String direction = filter.get("value");
                itemFilters.add(new ItemFilter(fieldName, direction));
            }
        }
        return itemFilters;
    }

    public static Predicate[] makeFilterPredicates(Root<Item> from, List<ItemFilter> itemFilters) {
        Predicate[] predicates = new Predicate[0];
        if (itemFilters != null && !itemFilters.isEmpty()) {
            List<Predicate> predicateList = new ArrayList<Predicate>();
            for (ItemFilter itemFilter : itemFilters) {
                if ("description".equals(itemFilter.fieldName)) {
                    predicateList.add(descriptionPredicate(from, itemFilter, predicateList));
                }
            }
            predicates = predicateList.toArray(predicates);
        }
        return predicates;
    }

    private static Predicate descriptionPredicate(Root<Item> from, ItemFilter itemFilter, List<Predicate> predicateList) {
        Expression<String> path = from.get(itemFilter.fieldName);
        CriteriaBuilder cBuilder = Item.entityManager().getCriteriaBuilder();
        return cBuilder.like(path, "%" + itemFilter.value + "%");
    }

}

Now the controller is able to do sorting, filtering and pagination. Next we will make the controller able to create new items. In this case it receives some json data that it must decode and validate and then create a new item or return the validation errors.

The extjs data store sends this json data in the request body:

{"id":null,"description":"test","price":55}

When thing go well it expects a response like this:

{"message":"Created new Item", "data":{"price":55.0,"id":1,"version":0,"description":"test"}, "success":true}

, and if when the validation dont pass, the errors are returned like this:

{"success":false, "errors":{"price":"must be greater than or equal to 0"}} 

I tried different ways to get the json in the requestbody bound to item and have it validated at once, but it is currently not supported – see: https://jira.springsource.org/browse/SPR-6709
, I think when using the jackson marshaller for binding the json in the requestbody, valdation is not supported. So it must be done manually. To have the validator injected, declare a Validator and have it @Autowired.

   @RequestMapping(method = RequestMethod.POST)
    public void create(@RequestBody Item item, Model model) throws BindException {
//        validate
        final BindingResult result = new BeanPropertyBindingResult(item, "");
        ValidationUtils.invokeValidator(this.validator, item, result);
        if (result.hasErrors()) {
            model.addAttribute("success", false);
            model.addAttribute("errors", getFieldErrors(result));
        } else {
            item.persist();
            model.addAttribute("success", true);
            model.addAttribute("message", "Created new Item");
            model.addAttribute("data", item);
        }
    }

    private static Map<String, String> getFieldErrors(BindingResult result) {
        List<ObjectError> errors = result.getAllErrors();
        Map<String, String> errorMap = new HashMap<String, String>();
        for (ObjectError error : errors) {
            if (error instanceof FieldError) {
                FieldError fieldError = (FieldError) error;
                errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
            }
        }
        return errorMap;
    }

This was pretty straight forward and the last thing I will show is the update and delete methods, which are also quite simple.

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public void update(@RequestBody Item item, Model model) throws BindException {
//        validate
        final BindingResult result = new BeanPropertyBindingResult(item, "");
        ValidationUtils.invokeValidator(this.validator, item, result);
        if (result.hasErrors()) {
            model.addAttribute("success", false);
            model.addAttribute("errors", getFieldErrors(result));
        } else {
                Item pItem = Item.findItem(item.getId());
                pItem.setDescription(item.getDescription());
                pItem.setPrice(item.getPrice());
                pItem.flush();

                model.addAttribute("success", true);
                model.addAttribute("message", "Item Updated");
                model.addAttribute("data", pItem);
        }
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public void delete(@PathVariable("id") Long id, Model model) {
        Item item = Item.findItem(id);
        item.remove();
        model.addAttribute("success", true);
        model.addAttribute("message", "Item Deleted");
        model.addAttribute("data", item);
    }

Now the controller can do all the CRUD operations.

Advertisements
  1. Ahim Dahman
    June 25, 2011 at 05:21

    can you share the source code?

    • June 25, 2011 at 08:40

      Hi Ahim
      Yes, What do you need ?
      Best Regards Ole

      • Ahim Dahman
        June 26, 2011 at 06:29

        Hello,

        A basic skeleton on which to look over. Also interested about appengine & rest or appengine & json.

        Tnx !

  2. November 22, 2011 at 09:51

    Thank you for bringing light in this subject. I am trying the solution, but I am not really into ExtJs4. Is proxyAfterRequest missing in the sample code? If you could share it, it would save me a great deal of time.
    Thanks again!

  3. December 26, 2011 at 02:43

    Hi,

    Interesting article, thanks.
    However, it works only if we want to filter TEXT.

    If we want to filter numbers (as I thought you will do looking at your first piece of code introducing a model with int and float) it doesn’t work.
    For example, if we want to filter numbers small than 10, the grid will send the following filter attribute:
    filter:[{“property”:”price”,”value”:10,”comparison”:”lt”}]

    That we need to change into a filter on “price < 10".

    We will also have troubles with booleans and dates!

    Is there any FULL server-side filters handling API?

    • January 4, 2012 at 22:34

      Hi Thomas, happy new year.

      I haven’t heard of such a filter API. I think you have to handle each of your filters on the server side.
      In the method makeFilterPredicates you can add support for more filters just like the descriptionPredicate.

      Best regards
      Ole

  4. September 9, 2013 at 12:19

    Hi, I think your website might be having browser compatibility issues.
    When I look at your blog in Opera, it looks fine but
    when opening in Internet Explorer, it has some overlapping.
    I just wanted to give you a quick heads up! Other then that, superb blog!

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: