Java Predicate, supports SQL-like syntax
Libra is a Java package for creating and evaluating predicate. Java-based and SQL-like predicate are both supported. For SQL predicates, it is using ANTLR to parse the string against a predefined grammar. The Java-based predicates are implementation of Specification pattern and support numeric/text/collection related conditions.
Libra can be easily installed with Maven:
<dependency>
<groupId>org.dungba</groupId>
<artifactId>joo-libra</artifactId>
<version><!-- latest version. see above --></version>
</dependency>
By default, you can simply use SqlPredicate
class for all the functionality, which supports satisfiedBy
method to perform the evaluation. A PredicateContext
needs to be passed to the method.
PredicateContext context = new PredicateContext(customer);
SqlPredicate predicate = new SqlPredicate("customer.age > 50 AND customer.isResidence is true");
predicate.satisfiedBy(context);
You can optionally check for syntax errors:
if (predicate.hasError()) {
predicate.getCause().printStackTrace();
}
or throw the exception if any
predicate.checkForErrorAndThrow();
from 2.0.0
you can retrieve the raw value instead of letting Libra convert it to boolean
PredicateContext context = new PredicateContext(customer);
SqlPredicate predicate = new SqlPredicate("customer.asset - customer.liability");
Object rawValue = predicate.calculateLiteralValue(context);
Libra supports the following syntax for SQL predicates:
and
, or
and not
is
, is not
contains
(for both list and string) and matches
(only for string)a[0]
(this cannot be used to evaluate a Map
)'John'
1
, 1.0
true
, false
null
, undefined
, empty
_
, .
(to denote nested object) and [
, ]
(to denote array index), must starts with alphabet characters.{1, 2, 3}
. Empty list { }
is also supported.functionName(arg1, arg2...)
It's also possible to configure custom function in PredicateContext
. Built-in functions: sqrt
, avg
, sum
, min
, max
, len
.stream matching
Libra 2.0.0
supports stream-like matching, similar to anyMatch
, allMatch
and noneMatch
. The syntax is:
ANY <indexVariableName> IN <listVariableName> SATISFIES <expression>
ALL <indexVariableName> IN <listVariableName> SATISFIES <expression>
NONE <indexVariableName> IN <listVariableName> SATISFIES <expression>
listVariableName
is the name of the list variable you want to perform matching on. indexVariableName
is the name of the temporary variable used in each loop. For example: ANY $job IN jobs satisfies $job.salary > 1000
will try to find out if there is ANY element in jobs
which its salary
property is greater than 1000. Starting from Libra 2.1.0
the temporary variable name must be started with $
.
subset filtering
Libra 2.1.0
supports subset filtering from list:
WITH <indexVariableName> IN <listVariableName> SATISFIES <expression>
For example WITH $job IN jobs satisfies $job.salary > 1000
will returns a list of jobs which the salary
attribute is greater than 1000.
You can also transform the returned list element using transformation expression:
For example: $job.salary WITH $job IN jobs satisfies $job.salary > 1000
will returns a list of salary that is greater than 1000 from the job list.
examples
Some examples of SQL predicates:
name is 'John' and age > 27
employments contains 'LEGO assistant' and name is 'Anh Dzung Bui'
experiences >= 4 or (skills contains 'Java' and projects is not empty)
avg(4, 5, 6) is 5
More examples can be seen inside the test cases
Some special cases or limitations are covered here:
true
if and only they are not null and not empty
true
if and only they are not null and not zero
null
will always be considered as false
0 is false
will be evaluated as false
, since 0
and false
have different typetrue
if and only if they are not null and not empty
true
if and only if they are not null and not zero
null
variables will always be considered as false
BigDecimal
, so 0.0
, 0
or 0L
are all equalLibra currently supports a simple Constant Folding optimization. It will reduces constant-only conditional branches into a single branch. To enable the optimizations, use OptimizedAntlrSqlPredicateParser
as below:
SqlPredicate predicate = new SqlPredicate(predicateString, new OptimizedAntlrSqlPredicateParser());
This will take more time to compile the SQL but will reduce evaluation time.
The SqlPredicate
class allows you to use your own SqlPredicateParser
:
SqlPredicate predicate = new SqlPredicate(predicateString, new MyPredicateParser());
you can implement SqlPredicateParser
, or extend the AbstractAntlrSqlPredicateParser
to use your own grammar. For the former, the interface has only one method public Predicate parse(String predicate) throws MalformedSyntaxException
, so you can even use lambda expression to construct it, like:
SqlPredicate predicate = new SqlPredicate(predicateString, predicate -> {
return something;
});
or use method reference:
SqlPredicate predicate = new SqlPredicate(predicateString, this::parseSql);
It is better to cache the parsed version of sql and if possible, try to load all of them at startup. If you keep the SqlPredicate
objects, they will contain the parsed predicate to be reused.
The runtime evaluation is quite fast (2 millions ops/sec with Java object or 5 millions ops/sec with Map
). You can also consider using Map
because it's significantly faster.
This library is distributed under MIT license. See LICENSE