Friday, March 30, 2012

Filter expressions in Rest urls leveraging QueryDsl Part I

The other day I was looking for a way to create dynamic filter expressions for use in a Restful webservice url. Clients should be able to pass arbitrary filter expressions to filter the results of a GET operation on a collection resource, which values are retrieved from a database by JPA. This idea came to me while using QueryDsl to construct where clauses in a typesafe manner. It would be nice if we could use QueryDsl-like BooleanExpressions as value of a Url query-parameter, like so:
http://www.myprettyproducts.com/product?filter="type.eq(3).and(prize.lt(500))"
In QueryDsl you use the generated QueryType classes to construct a BooleanExpression object which you can use in the where clause of a select query like this:
QProduct qProduct = QProduct.product;
  JPQLQuery query = new JPAQuery(entityManager).from(qProduct)
  .where(qProduct.type.eq(3).and(qProduct.prize.lt(500));
  return  query.list(qProduct);
Now if I could use qProduct.type.eq(3).and(qProduct.prize.lt(500) in a url and convert this string expression to Java code we are done.
Hey, wait a minute are we opening up the system for code injection? Yes, but there is a solution to this I will cover later on.
So how do we convert the expression string to Java code at runtime? I use MVEL for this part. That's a powerful evaluation language for Java applications, which evaluates a script. Now how it works is that you setup a map of key value pairs for the variables that are expected in the expression string. For the example above this would be: "type", qProduct.type and "prize", qProduct.prize. Code example:
This following code will take any valid expression string using the type and prize attributes. The conditional operators are the ones allowed by the QueryDsl QueryType metadata objects and depend on the attributes Type.
Predicate toProductPredicate(String expressionFilter){
  Map<String, Object> vars = new HashMap<String, Object>();
  vars.put("type", qProduct.type);
  vars.put("prize", qProduct.prize);
  return (Predicate)MVEL.eval(expressionFilter, vars);
}

public Collection<product> findAllProducts(String filterExpression){
  QProduct qProduct = QProduct.product;
  JPQLQuery query = new JPAQuery(entityManager).from(qProduct)
            .where(toProductPredicate(expressionString));
  return query.list(qProduct);
}

How to go about the code injection vulnerability.
You want to restrict the expression to only include terms and constructions that lead to a valid QueryDsl Predicate and nothing more. For this purpose I wrote a custom dynamic Parser using Parboild. Parboild is a socalled PEG (Parsing expression grammar) parser. See all detail in the followup post: Filter expressions in Rest urls leveraging QueryDsl Part 2