"Filtering Collections with JXPath or JEXL"


There was another interesting question on the commons-user list today. Someone asked about using Commons JEXL to filter the contents of a Collection. There are an almost infinite number of ways to do this, but two ways pop to mind: using Commons JXPath to filter a Collection and writing a custom JexlPredicate to apply an arbitrary JEXL expression to a Collection using CollectionUtils from Commons Collections.

To demonstrate both methods, assume that you have a collection of Person objects that you created with the following code. Each Person object has two properties name and age.

       Person person1 = new Person();
      person1.setName( "Bill" );
      person1.setAge( 12 );

      Person person2 = new Person();
      person2.setName( "Amy" );
      person2.setAge( 18 );

      Person person3 = new Person();
      person3.setName( "Doug" );
      person3.setAge( 25 );

      List personList =
          Arrays.asList( new Person[] { person1, person2, person3 } );

You can filter this collection using JXPath and an XPath expression that references the age property AS IF it were an XML attribute. Create a new JXPathContext by passing the Collection to the static newContext() method. Then pass an XPath expression to the iterate() method. This will select all objects which match the XPath expression – ".[@age >= 18]". This is an unorthodox use of XPath syntax, but it works well.

       JXPathContext context = JXPathContext.newContext( personList );
      Iterator iterator = context.iterate(".[@age >= 18]");
      while( iterator.hasNext() ) {
          Object o  = (Object) iterator.next();
          System.out.println( "Person: " + ((Person) o).getName());
      }

If you don’t feel comfortable writing XPath expression to filter your objects, you can use Commons JEXL with Commons Collections and write a custom JexlPredicate that takes an expression and evaluates each element in a Collection. Here’s code that filters a collection of Person beans and returns only those instances that have an age >= 18. Note how the selection logic is encapsulated in a Predicate object which is applied to each element of a Collection through a call to CollectionUtils.select().

      Predicate predicate = new JexlPredicate( "object", "object.age >= 18" );
     Collection canVote = CollectionUtils.select( personList, predicate );

     // Using Java 1.5 syntax because I am very lazy....
     for( Object o : canVote ) {
          System.out.println( "Person: " + ((Person) o).getName() );
     }

The JexlPredicate from this example would be the following class. I not terribly happy with the lack of error handling, but it does work.

import org.apache.commons.collections.Predicate;
import org.apache.commons.jexl.Expression;
import org.apache.commons.jexl.ExpressionFactory;
import org.apache.commons.jexl.JexlContext;
import org.apache.commons.jexl.JexlHelper;

public class JexlPredicate implements Predicate {

  private String variable;
  private Expression e;

  public JexlPredicate(String variable, String expression) {
      this.variable = variable;
      try {
          this.e = ExpressionFactory.createExpression(expression);
      } catch (Exception e) {
          throw new RuntimeException("Error creating JEXL expression", e);
      }
  }

  public boolean evaluate(Object o) {
      JexlContext jc = JexlHelper.createContext();
      jc.getVars().put(variable, o);
            Boolean b;
      try {
          b = (Boolean) e.evaluate(jc);
      } catch (Exception e) {
          throw new RuntimeException("Error evaluating JEXL expression", e);
      }
      return b.booleanValue();
  }

}

There are a few more thing that could be done, you could modify the JexlPredicate to take a JexlContext in the constructor and call methods in a JEXL expression. There are actually a few more ways to do this, but for now, that’s all…