javafilteringjexl

Looking for JEXL Filter feature


I know, I can do several things in JEXL, but unable to find Filter feature in it, which is indeed very useful.

How can I do something like

 var x=[{a:11,b=5},{a:1,b=15},{a:12,b=25},{a:4,b=35},{a:7,b=45}];

 return x[.a>10].b; // Which filters to {a:11,b=5} & {a:12,b=25}
                   // & hence returns [5,25]

Solution

  • First of all your syntax is not valid JEXL. I assume you meant this:

    var x = [{'a':11,'b':5}, {'a':1,'b':15}, {'a':12,'b':25}, {'a':4,'b':35}, {'a':7,'b':45}];
    

    Since you can call any Java method on any object in a JEXL script, you have (at least theoretically) full access to the Java Stream API.

    However, the Stream API isn't directly available from a raw array and we can't just call Arrays.stream(x); without some effort. The easiest way around this is to create a set instead:

    var x = {{'a':11,'b':5}, {'a':1,'b':15}, {'a':12,'b':25}, {'a':4,'b':35}, {'a':7,'b':45}};
    

    Now we can simply call stream() and work from there:

    x.stream();
    

    What we want now is something like this:

    x.stream().filter(function(m){m['a']>10});
    

    Unfortunately the method resolver in JEXL will not be able to correctly resolve Stream.filter(Predicate) with a JEXL function, as it doesn't know how to turn a JEXL function into a Predicate. A JEXL function is of type org.apache.commons.jexl3.internal.Closure.

    Thus the very least you need to do is to provide your own Predicate implementation in Java and then create a new instance in your script:

    public class MyCustomFilterPredicate implements Predicate<HashMap<String, Integer>> {
        @Override
        public boolean test(final HashMap<String, Integer> m)
        {
            return m.get("a") > 10;
        }
    }
    

    You can then create a new instance in your JEXL script:

    var filterPredicate = new('my.custom.filter.predicate.MyCustomFilterPredicate');
    

    The same goes for Stream.map(Function):

    public class MyCustomMapFunction implements Function<HashMap<String, Integer>, Integer> {
        @Override
        public Integer apply(final HashMap<String, Integer> m)
        {
            return m.get("b");
        }
    }
    

    And again create a new instance in your script:

    var mapFunction = new('my.custom.map.function.MyCustomMapFunction');
    

    Your entire script will then look like this:

    var x = {{'a':11,'b':5}, {'a':1,'b':15}, {'a':12,'b':25}, {'a':4,'b':35}, {'a':7,'b':45}};
    
    var filterPredicate = new('my.custom.filter.predicate.MyCustomFilterPredicate');
    var mapFunction = new('my.custom.map.function.MyCustomMapFunction');
    
    return x.stream().filter(filterPredicate).map(mapFunction).toArray();
    

    Of course you might have noticed that the reusability of your predicate and function implementations are rather limited. This is why I'd recommend creating implementations that wrap a JEXL Closure:

    public class MyCustomFilterPredicate implements Predicate<Object> {
        private final Closure closure;
        public MyCustomFilterPredicate(final Closure closure) {
            this.closure = closure;
        }
        @Override
        public boolean test(final Object o)
        {
            return (boolean) closure.execute(JexlEngine.getThreadContext(), o);
        }
    }
    
    public class MyCustomMapFunction implements Function<Object, Object> {
        private final Closure closure;
        public MyCustomMapFunction(final Closure closure) {
            this.closure = closure;
        }
        @Override
        public Object apply(final Object o)
        {
            return closure.execute(JexlEngine.getThreadContext(), o);
        }
    }
    

    Now you can change your script as follows and reuse these Java classes in various ways:

    var x = {{'a':11,'b':5}, {'a':1,'b':15}, {'a':12,'b':25}, {'a':4,'b':35}, {'a':7,'b':45}};
    
    var filterPredicate = new('my.custom.filter.predicate.MyCustomFilterPredicate', function(m){m['a']>10});
    var mapFunction = new('my.custom.map.function.MyCustomMapFunction', function(m){m['b']});
    
    return x.stream().filter(filterPredicate).map(mapFunction).toArray();