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:
- Logic operators:
and
,or
andnot
- Comparison operators: >, >=, <, <=, =, ==, !=,
is
,is not
- Parenthesises
- List and string operators:
contains
(for both list and string) andmatches
(only for string) - Array indexing:
a[0]
(this cannot be used to evaluate aMap
) - String literals, single quoted, e.g:
'John'
- Numeric literals:
1
,1.0
- Boolean literals:
true
,false
- Other literals:
null
,undefined
,empty
- Variables: alphanumerics,
_
,.
(to denote nested object) and[
,]
(to denote array index), must starts with alphabet characters. - List:
{1, 2, 3}
. Empty list{ }
is also supported. - Function:
functionName(arg1, arg2...)
It's also possible to configure custom function inPredicateContext
. Built-in functions:sqrt
,avg
,sum
,min
,max
,len
. - Stream matching: See below
- Subset filtering: See below
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:
- Literals, if stand alone in their own branch, will be converted into predicate according to their types:
- String & list literals will be considered as
true
if and only they are not null and not empty - Number literals will be considered as
true
if and only they are not null and not zero null
will always be considered asfalse
- String & list literals will be considered as
- If literals are compared with any other type, the comparison will be as normal
0 is false
will be evaluated asfalse
, since0
andfalse
have different type
- Variables, if stand alone in their own branch, will be converted into predicate according to their types:
- String & list variables will be considered as
true
if and only if they are not null and not empty - Number variables will be considered as
true
if and only if they are not null and not zero - Boolean variables will be considered as their own values
null
variables will always be considered asfalse
- String & list variables will be considered as
- When comparing number, they will be converted into
BigDecimal
, so0.0
,0
or0L
are all equal
Libra 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