NIFI-5885 ArrayOutOfBoundsException at EL 'or' and 'and' functions

EL 'or' and 'and' functions can be called multiple times within the same context using the same evaluator instance.
That happens if their subject is derived from an IteratingEvaluator such as 'anyDelineatedValues'.

And if the right hand side expression for such 'or' and 'and' contains another IteratingEvaluator,
then it can be evaluated more than the number of its candidates, ultimately an ArrayOutOfBoundsException is thrown.

This commit makes Or/AndEvaluator caching its right hand side result to prevent that happens.
For 'or' and 'and' functions, the right hand side expression is independant from their subject boolean value.
It's enough evaluating right hand side once, because it returns the same result even with different subjects.

Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com>

This closes #3212.
This commit is contained in:
Koji Kawamura 2018-12-10 14:31:16 +09:00 committed by Pierre Villard
parent a6f91a1975
commit 4e7a856f77
No known key found for this signature in database
GPG Key ID: BEE1599F0726E9CD
3 changed files with 61 additions and 5 deletions

View File

@ -27,6 +27,7 @@ public class AndEvaluator extends BooleanEvaluator {
private final Evaluator<Boolean> subjectEvaluator;
private final Evaluator<Boolean> rhsEvaluator;
private BooleanQueryResult rhsResult;
public AndEvaluator(final Evaluator<Boolean> subjectEvaluator, final Evaluator<Boolean> rhsEvaluator) {
this.subjectEvaluator = subjectEvaluator;
@ -44,9 +45,18 @@ public class AndEvaluator extends BooleanEvaluator {
return new BooleanQueryResult(false);
}
// Returning previously evaluated result.
// The same AndEvaluator can be evaluated multiple times if subjectEvaluator is IteratingEvaluator.
// In that case, it's enough to evaluate the right hand side.
if (rhsResult != null) {
return rhsResult;
}
final QueryResult<Boolean> rhsValue = rhsEvaluator.evaluate(attributes);
if (rhsValue == null) {
return new BooleanQueryResult(false);
rhsResult = new BooleanQueryResult(false);
} else {
rhsResult = new BooleanQueryResult(rhsValue.getValue());
}
return new BooleanQueryResult(rhsValue.getValue());

View File

@ -27,6 +27,7 @@ public class OrEvaluator extends BooleanEvaluator {
private final Evaluator<Boolean> subjectEvaluator;
private final Evaluator<Boolean> rhsEvaluator;
private BooleanQueryResult rhsResult;
public OrEvaluator(final Evaluator<Boolean> subjectEvaluator, final Evaluator<Boolean> rhsEvaluator) {
this.subjectEvaluator = subjectEvaluator;
@ -44,12 +45,21 @@ public class OrEvaluator extends BooleanEvaluator {
return new BooleanQueryResult(true);
}
final QueryResult<Boolean> rhsValue = rhsEvaluator.evaluate(attributes);
if (rhsValue == null) {
return new BooleanQueryResult(false);
// Returning previously evaluated result.
// The same OrEvaluator can be evaluated multiple times if subjectEvaluator is IteratingEvaluator.
// In that case, it's enough to evaluate the right hand side.
if (rhsResult != null) {
return rhsResult;
}
return new BooleanQueryResult(rhsValue.getValue());
final QueryResult<Boolean> rhsValue = rhsEvaluator.evaluate(attributes);
if (rhsValue == null) {
rhsResult = new BooleanQueryResult(false);
} else {
rhsResult = new BooleanQueryResult(rhsValue.getValue());
}
return rhsResult;
}
@Override

View File

@ -995,6 +995,42 @@ public class TestQuery {
verifyEquals("${anyDelineatedValue(${abc}, ','):equals('d')}", attributes, false);
}
@Test
public void testNestedAnyDelineatedValueOr() {
final Map<String, String> attributes = new HashMap<>();
attributes.put("abc", "a,b,c");
attributes.put("xyz", "x");
// Assert each part separately.
assertEquals("true", Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):equals('c')}",
attributes, null));
assertEquals("false", Query.evaluateExpressions("${anyDelineatedValue('${xyz}', ','):equals('z')}",
attributes, null));
// Combine them with 'or'.
assertEquals("true", Query.evaluateExpressions(
"${anyDelineatedValue('${abc}', ','):equals('c'):or(${anyDelineatedValue('${xyz}', ','):equals('z')})}",
attributes, null));
}
@Test
public void testNestedAnyDelineatedValueAnd() {
final Map<String, String> attributes = new HashMap<>();
attributes.put("abc", "2,0,1,3");
attributes.put("xyz", "x,y,z");
// Assert each part separately.
assertEquals("true", Query.evaluateExpressions("${anyDelineatedValue('${abc}', ','):gt('2')}",
attributes, null));
assertEquals("true", Query.evaluateExpressions("${anyDelineatedValue('${xyz}', ','):equals('z')}",
attributes, null));
// Combine them with 'and'.
assertEquals("true", Query.evaluateExpressions(
"${anyDelineatedValue('${abc}', ','):gt('2'):and(${anyDelineatedValue('${xyz}', ','):equals('z')})}",
attributes, null));
}
@Test
public void testAllDelineatedValues() {
final Map<String, String> attributes = new HashMap<>();