Skip to content

Commit e6ae47f

Browse files
authored
Merge pull request #3 from sidhant92/antlr
Boolean Expression Evaluator
2 parents eebd3dc + ba87114 commit e6ae47f

16 files changed

Lines changed: 1719 additions & 1309 deletions

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,63 @@ private final List<Pair<DataType, Object>> items;
135135
```
136136

137137

138+
## Applications
139+
140+
### Boolean Expression Evaluator
141+
142+
The library can be used to evaluate a boolean expression.
143+
144+
The following Data Types are supported:
145+
1. String
146+
2. Integer
147+
3. Long
148+
4. Decimal
149+
5. Boolean
150+
6. Semantic Version
151+
152+
Usage examples:
153+
154+
Simple Numerical Comparison
155+
```
156+
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
157+
final Map<String, Object> data = new HashMap<>();
158+
data.put("age", 26);
159+
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("age >= 27", data);
160+
assertTrue(booleanOptional.isPresent());
161+
assertFalse(booleanOptional.get());
162+
```
163+
Boolean Comparison
164+
```
165+
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
166+
final Map<String, Object> data = new HashMap<>();
167+
data.put("age", 25);
168+
data.put("name", "sid");
169+
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("name = sid AND age = 25", data);
170+
assertTrue(booleanOptional.isPresent());
171+
assertTrue(booleanOptional.get());
172+
```
173+
Nested Boolean Comparison
174+
```
175+
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
176+
final Map<String, Object> data = new HashMap<>();
177+
data.put("age", 25);
178+
data.put("name", "sid");
179+
data.put("num", 45);
180+
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("name:sid AND (age = 25 OR num = 44)", data);
181+
assertTrue(booleanOptional.isPresent());
182+
assertTrue(booleanOptional.get());
183+
```
184+
App Version Comparison
185+
```
186+
final BooleanExpressionEvaluator booleanExpressionEvaluator = new BooleanExpressionEvaluator();
187+
final Map<String, Object> data = new HashMap<>();
188+
data.put("app_version", "1.5.9");
189+
final Try<Boolean> result = booleanExpressionEvaluator.evaluate("app_version < 1.5.10", data);
190+
assertTrue(booleanOptional.isPresent());
191+
assertTrue(booleanOptional.get());
192+
```
193+
194+
The return type is `Try<Boolean>`. Failure means that parsing has failed and any fallback can be used.
195+
196+
138197
[For a complete list of examples please check out the test file](src/test/java/com/github/sidhant92/boolparser/application/BooleanExpressionEvaluatorTest.java)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.github.sidhant92.boolparser.application;
2+
3+
import java.util.Map;
4+
import org.apache.commons.lang3.tuple.Pair;
5+
import com.github.sidhant92.boolparser.constant.ContainerDataType;
6+
import com.github.sidhant92.boolparser.constant.DataType;
7+
import com.github.sidhant92.boolparser.constant.Operator;
8+
import com.github.sidhant92.boolparser.domain.BooleanToken;
9+
import com.github.sidhant92.boolparser.domain.InToken;
10+
import com.github.sidhant92.boolparser.domain.NumericRangeToken;
11+
import com.github.sidhant92.boolparser.domain.NumericToken;
12+
import com.github.sidhant92.boolparser.domain.StringToken;
13+
import com.github.sidhant92.boolparser.domain.Token;
14+
import com.github.sidhant92.boolparser.operator.OperatorService;
15+
import com.github.sidhant92.boolparser.parser.BoolExpressionParser;
16+
import io.vavr.control.Try;
17+
import lombok.extern.slf4j.Slf4j;
18+
19+
/**
20+
* @author sidhant.aggarwal
21+
* @since 07/02/2023
22+
*/
23+
@Slf4j
24+
public class BooleanExpressionEvaluator {
25+
private final BoolExpressionParser boolExpressionParser;
26+
27+
private final OperatorService operatorService;
28+
29+
public BooleanExpressionEvaluator(final BoolExpressionParser boolExpressionParser) {
30+
this.boolExpressionParser = boolExpressionParser;
31+
operatorService = new OperatorService();
32+
}
33+
34+
public Try<Boolean> evaluate(final String expression, final Map<String, Object> data) {
35+
final Try<Token> tokenOptional = boolExpressionParser.parseExpression(expression);
36+
return tokenOptional.map(node -> evaluateToken(node, data));
37+
}
38+
39+
private boolean evaluateToken(final Token token, final Map<String, Object> data) {
40+
switch (token.getTokenType()) {
41+
case STRING:
42+
return evaluateStringToken((StringToken) token, data);
43+
case NUMERIC:
44+
return evaluateNumericToken((NumericToken) token, data);
45+
case NUMERIC_RANGE:
46+
return evaluateNumericRangeToken((NumericRangeToken) token, data);
47+
case IN:
48+
return evaluateInToken((InToken) token, data);
49+
case BOOLEAN:
50+
return evaluateBooleanNode((BooleanToken) token, data);
51+
default:
52+
return false;
53+
}
54+
}
55+
56+
private boolean evaluateStringToken(final StringToken stringToken, final Map<String, Object> data) {
57+
if (checkFieldDataMissing(stringToken.getField(), data)) {
58+
return false;
59+
}
60+
final Object fieldData = data.get(stringToken.getField());
61+
return operatorService.evaluate(Operator.EQUALS, ContainerDataType.primitive, DataType.STRING, fieldData, stringToken.getValue());
62+
}
63+
64+
private boolean evaluateNumericToken(final NumericToken numericToken, final Map<String, Object> data) {
65+
if (checkFieldDataMissing(numericToken.getField(), data)) {
66+
return false;
67+
}
68+
final Object fieldData = data.get(numericToken.getField());
69+
return operatorService.evaluate(numericToken.getOperator(), ContainerDataType.primitive, numericToken.getDataType(), fieldData,
70+
numericToken.getValue());
71+
}
72+
73+
private boolean evaluateNumericRangeToken(final NumericRangeToken numericRangeToken, final Map<String, Object> data) {
74+
if (checkFieldDataMissing(numericRangeToken.getField(), data)) {
75+
return false;
76+
}
77+
final Object fieldData = data.get(numericRangeToken.getField());
78+
return operatorService.evaluate(Operator.GREATER_THAN_EQUAL, ContainerDataType.primitive, numericRangeToken.getFromDataType(), fieldData,
79+
numericRangeToken.getFromValue()) && operatorService.evaluate(Operator.LESS_THAN_EQUAL,
80+
ContainerDataType.primitive,
81+
numericRangeToken.getToDataType(), fieldData,
82+
numericRangeToken.getToValue());
83+
}
84+
85+
private boolean evaluateInToken(final InToken inToken, final Map<String, Object> data) {
86+
if (checkFieldDataMissing(inToken.getField(), data)) {
87+
return false;
88+
}
89+
final Object fieldData = data.get(inToken.getField());
90+
final DataType dataType = inToken.getItems().get(0).getLeft();
91+
final Object[] values = inToken.getItems()
92+
.stream()
93+
.map(Pair::getRight)
94+
.toArray();
95+
return operatorService.evaluate(Operator.IN, ContainerDataType.primitive, dataType, fieldData, values);
96+
}
97+
98+
private boolean evaluateBooleanNode(final BooleanToken booleanToken, final Map<String, Object> data) {
99+
switch (booleanToken.getOperator()) {
100+
case AND:
101+
return evaluateToken(booleanToken.getLeft(), data) && evaluateToken(booleanToken.getRight(), data);
102+
case OR:
103+
return evaluateToken(booleanToken.getLeft(), data) || evaluateToken(booleanToken.getRight(), data);
104+
default:
105+
return !evaluateToken(booleanToken.getLeft(), data);
106+
}
107+
}
108+
109+
private boolean checkFieldDataMissing(final String field, final Map<String, Object> data) {
110+
if (!data.containsKey(field)) {
111+
log.error("Error data not found for field {}", field);
112+
return true;
113+
}
114+
return false;
115+
}
116+
}

src/main/java/com/github/sidhant92/boolparser/constant/Operator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public enum Operator {
1919
GREATER_THAN_EQUAL,
2020
LESS_THAN,
2121
LESS_THAN_EQUAL,
22-
NOT_EQUAL;
22+
NOT_EQUAL,
23+
IN;
2324

2425
public static Optional<Operator> getOperatorFromSymbol(final String symbol) {
2526
return OperatorFactory.getAllOperators().stream().filter(operator -> operator.getSymbol().equals(symbol)).map(AbstractOperator::getOperator)

src/main/java/com/github/sidhant92/boolparser/datatype/AbstractDataType.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ public AbstractDataType(final Class<T> clazz) {
1919

2020
public boolean defaultIsValid(final Object value, final ObjectMapper objectMapper) {
2121
try {
22+
if (clazz.isInstance(value)) {
23+
return true;
24+
}
2225
return objectMapper.convertValue(value, clazz) != null;
2326
} catch (final Exception ex) {
2427
log.error("Unable to convert value = {} to type = {}", value, clazz);
@@ -28,7 +31,10 @@ public boolean defaultIsValid(final Object value, final ObjectMapper objectMappe
2831

2932
public Optional<T> defaultGetValue(final Object value, final ObjectMapper objectMapper) {
3033
try {
31-
return (Optional<T>) Optional.of(objectMapper.convertValue(value, clazz));
34+
if (clazz.isInstance(value)) {
35+
return Optional.of(clazz.cast(value));
36+
}
37+
return Optional.of(objectMapper.convertValue(value, clazz));
3238
} catch (final Exception ex) {
3339
log.error("Unable to convert value = {} to type = {}", value, clazz);
3440
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.github.sidhant92.boolparser.exception;
2+
3+
public class ExpressionEvaluationException extends RuntimeException {
4+
@Override
5+
public String getMessage() {
6+
return "Error evaluating boolean expression";
7+
}
8+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.github.sidhant92.boolparser.operator;
2+
3+
import java.util.Arrays;
4+
import com.github.sidhant92.boolparser.constant.ContainerDataType;
5+
import com.github.sidhant92.boolparser.constant.DataType;
6+
import com.github.sidhant92.boolparser.constant.Operator;
7+
import lombok.AllArgsConstructor;
8+
9+
/**
10+
* @author sidhant.aggarwal
11+
* @since 05/03/2023
12+
*/
13+
@AllArgsConstructor
14+
public class InOperator extends AbstractOperator {
15+
private final EqualsOperator equalsOperator;
16+
17+
@Override
18+
public <T extends Comparable<? super T>> boolean evaluate(final ContainerDataType containerDataType, final DataType dataType,
19+
final Object leftOperand, final Object... rightOperands) {
20+
return Arrays
21+
.stream(rightOperands).anyMatch(a -> equalsOperator.evaluate(ContainerDataType.primitive, dataType, leftOperand, a));
22+
}
23+
24+
@Override
25+
public Operator getOperator() {
26+
return Operator.IN;
27+
}
28+
29+
@Override
30+
public String getSymbol() {
31+
return "IN";
32+
}
33+
}

src/main/java/com/github/sidhant92/boolparser/operator/OperatorFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ private OperatorFactory() {
1818
}
1919

2020
public static void initialize() {
21-
operatorMap.put(Operator.EQUALS, new EqualsOperator());
21+
final EqualsOperator equalsOperator = new EqualsOperator();
22+
operatorMap.put(Operator.EQUALS, equalsOperator);
2223
operatorMap.put(Operator.GREATER_THAN, new GreaterThanOperator());
2324
operatorMap.put(Operator.GREATER_THAN_EQUAL, new GreaterThanEqualOperator());
2425
operatorMap.put(Operator.LESS_THAN, new LessThanOperator());
2526
operatorMap.put(Operator.LESS_THAN_EQUAL, new LessThanEqualOperator());
2627
operatorMap.put(Operator.NOT_EQUAL, new NotEqualsOperator());
28+
operatorMap.put(Operator.IN, new InOperator(equalsOperator));
2729
}
2830

2931
public static AbstractOperator getOperator(final Operator operator) {

src/main/java/com/github/sidhant92/boolparser/parser/antlr/BoolParser.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public Try<Token> parseExpression(final String expression) {
1616
private Token getNode(final String expression) {
1717
BooleanExpressionLexer filterLexer = new BooleanExpressionLexer(CharStreams.fromString(expression));
1818
CommonTokenStream commonTokenStream = new CommonTokenStream(filterLexer);
19-
BooleanExpressionBoolParser filterParser = new BooleanExpressionBoolParser(commonTokenStream);
20-
BooleanExpressionBoolParser.ParseContext filterContext = filterParser.parse();
19+
BooleanExpressionParser filterParser = new BooleanExpressionParser(commonTokenStream);
20+
BooleanExpressionParser.ParseContext filterContext = filterParser.parse();
2121

2222
ParseTreeWalker parseTreeWalker = new ParseTreeWalker();
2323
final BooleanFilterListener listener = new BooleanFilterListener();

0 commit comments

Comments
 (0)