Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PROC-1334: Add support for rule evaluation for partial contexts
- Loading branch information
Joseph Barratt
committed
Sep 26, 2023
1 parent
56a8f50
commit ef0d7e5
Showing
6 changed files
with
633 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
proctor-common/src/main/java/com/indeed/proctor/common/RuleFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package com.indeed.proctor.common; | ||
|
||
import com.indeed.proctor.common.el.filter.PartialExpressionFactory; | ||
|
||
import javax.annotation.Nonnull; | ||
import javax.annotation.concurrent.NotThreadSafe; | ||
import javax.el.FunctionMapper; | ||
import javax.el.ValueExpression; | ||
|
||
import java.util.Map; | ||
|
||
/** | ||
* Leverages RuleEvaluator to determine whether a given rule *could* match the | ||
* given context; useful for tools that search allocations. | ||
**/ | ||
@NotThreadSafe | ||
public class RuleFilter { | ||
@Nonnull | ||
private final PartialExpressionFactory expressionFactory; | ||
private final RuleEvaluator ruleEvaluator; | ||
|
||
RuleFilter( | ||
final FunctionMapper functionMapper, | ||
@Nonnull final Map<String, Object> testConstantsMap | ||
) { | ||
this.expressionFactory = new PartialExpressionFactory(testConstantsMap.keySet()); | ||
this.ruleEvaluator = new RuleEvaluator( | ||
expressionFactory, | ||
functionMapper, | ||
testConstantsMap); | ||
} | ||
|
||
public static RuleFilter createDefaultRuleFilter(final Map<String, Object> testConstantsMap) { | ||
return new RuleFilter(RuleEvaluator.FUNCTION_MAPPER, testConstantsMap); | ||
} | ||
|
||
public boolean ruleCanMatch(final String rule, final Map<String, Object> values) { | ||
expressionFactory.setContextVariables(values.keySet()); | ||
final Map<String, ValueExpression> localContext = ProctorUtils | ||
.convertToValueExpressionMap(expressionFactory, values); | ||
return ruleEvaluator.evaluateBooleanRuleWithValueExpr(rule, localContext); | ||
} | ||
} |
163 changes: 163 additions & 0 deletions
163
proctor-common/src/main/java/com/indeed/proctor/common/el/filter/NodeHunter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
package com.indeed.proctor.common.el.filter; | ||
|
||
import com.indeed.proctor.common.ProctorRuleFunctions.MaybeBool; | ||
import org.apache.el.parser.AstAnd; | ||
import org.apache.el.parser.AstFunction; | ||
import org.apache.el.parser.AstIdentifier; | ||
import org.apache.el.parser.AstLiteralExpression; | ||
import org.apache.el.parser.AstNot; | ||
import org.apache.el.parser.AstNotEqual; | ||
import org.apache.el.parser.AstOr; | ||
import org.apache.el.parser.Node; | ||
import org.apache.el.parser.NodeVisitor; | ||
import org.apache.el.parser.SimpleNode; | ||
|
||
import java.lang.reflect.InvocationTargetException; | ||
import java.util.Collections; | ||
import java.util.IdentityHashMap; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.Stack; | ||
|
||
class NodeHunter implements NodeVisitor { | ||
private final Set<Node> initialUnknowns = Collections.newSetFromMap(new IdentityHashMap<>()); | ||
private final Map<Node, Node> replacements = new IdentityHashMap<>(); | ||
private final Set<String> variablesDefined; | ||
|
||
NodeHunter(final Set<String> variablesDefined) { | ||
this.variablesDefined = variablesDefined; | ||
} | ||
|
||
public static Node destroyUnknowns( | ||
final Node node, | ||
final Set<String> variablesDefined | ||
) throws Exception { | ||
final NodeHunter nodeHunter = new NodeHunter(variablesDefined); | ||
node.accept(nodeHunter); | ||
if (nodeHunter.initialUnknowns.isEmpty()) { | ||
// Nothing to do here | ||
return node; | ||
} | ||
nodeHunter.calculateReplacements(); | ||
final Node result = nodeHunter.replaceNodes(node); | ||
// At this point result is a maybebool, we need to convert it to a bool | ||
final Node resultIsNotFalse = nodeHunter.wrapIsNotFalse(result); | ||
return resultIsNotFalse; | ||
} | ||
|
||
private void calculateReplacements() { | ||
final Stack<Node> nodesToDestroy = new Stack<>(); | ||
initialUnknowns.forEach(nodesToDestroy::push); | ||
while (!nodesToDestroy.isEmpty()) { | ||
final Node nodeToDestroy = nodesToDestroy.pop(); | ||
if (nodeToDestroy instanceof AstAnd) { | ||
// Replace simple "and" with maybeAnd | ||
replaceWithFunction(nodeToDestroy, "maybeAnd"); | ||
} else if (nodeToDestroy instanceof AstOr) { | ||
// Replace simple "or" with maybeOr | ||
replaceWithFunction(nodeToDestroy, "maybeOr"); | ||
} else if (nodeToDestroy instanceof AstNot) { | ||
// Replace simple "not" with maybeNot | ||
replaceWithFunction(nodeToDestroy, "maybeNot"); | ||
// } else if (nodeToDestroy instanceof AstEqual || nodeToDestroy instanceof AstNotEqual) { | ||
// TODO: if someone compares two bools using == that would be | ||
// weird, but we could handle it by making sure any cases that mix | ||
// maybeBool and bool are promoted to maybeBool like we do with the | ||
// other logical operators | ||
} else if (!replacements.containsKey(nodeToDestroy)) { | ||
// Anything else propagate the unknown value | ||
// | ||
// TODO: If a bool is used as an argument to a function we | ||
// could try and do the function if the maybeBool is true or | ||
// false, and only propagate the unknown if any argument is | ||
// unknown, but that seems rare and very complicated so I | ||
// haven't handled that case here. | ||
final AstLiteralExpression replacement = new AstLiteralExpression(1); | ||
replacement.setImage(MaybeBool.UNKNOWN.name()); | ||
replacements.put(nodeToDestroy, replacement); | ||
} | ||
if (nodeToDestroy.jjtGetParent() != null) { | ||
nodesToDestroy.push(nodeToDestroy.jjtGetParent()); | ||
} | ||
} | ||
} | ||
|
||
private void replaceWithFunction(final Node node, final String function) { | ||
final AstFunction replacement = new AstFunction(27); | ||
replacement.setPrefix("proctor"); | ||
replacement.setLocalName(function); | ||
replacement.setImage("proctor:" + function); | ||
for (int i = 0; i < node.jjtGetNumChildren(); i++) { | ||
final Node child = node.jjtGetChild(i); | ||
if (replacements.containsKey(child)) { | ||
replacement.jjtAddChild(replacements.get(child), i); | ||
} else { | ||
final AstFunction replacementChild = new AstFunction(27); | ||
replacementChild.setPrefix("proctor"); | ||
replacementChild.setLocalName("toMaybeBool"); | ||
replacementChild.setImage("proctor:toMaybeBool"); | ||
replacementChild.jjtAddChild(child, 0); | ||
replacement.jjtAddChild(replacementChild, i); | ||
} | ||
} | ||
replacements.put(node, replacement); | ||
} | ||
|
||
private Node replaceNodes( | ||
final Node node | ||
) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { | ||
if (replacements.containsKey(node)) { | ||
Node newNode = node; | ||
while (replacements.containsKey(newNode)) { | ||
newNode = replacements.get(newNode); | ||
} | ||
return newNode; | ||
} | ||
final SimpleNode asSimpleNode = (SimpleNode) node; | ||
for (int i = 0; i < asSimpleNode.jjtGetNumChildren(); i++) { | ||
final Node newChild = replaceNodes(asSimpleNode.jjtGetChild(i)); | ||
asSimpleNode.jjtAddChild(newChild, i); | ||
newChild.jjtSetParent(asSimpleNode); | ||
} | ||
return node; | ||
} | ||
|
||
@Override | ||
public void visit(final Node node) throws Exception { | ||
if (node instanceof AstIdentifier) { | ||
String variable = node.getImage(); | ||
if (!variablesDefined.contains(variable)) { | ||
initialUnknowns.add(node); | ||
} | ||
} | ||
} | ||
|
||
private Node wrapIsNotFalse(final Node node) { | ||
final Node resultIsNotFalse = new AstNotEqual(9); | ||
final AstLiteralExpression literalFalse = new AstLiteralExpression(1); | ||
literalFalse.setImage(MaybeBool.FALSE.name()); | ||
literalFalse.jjtSetParent(resultIsNotFalse); | ||
resultIsNotFalse.jjtSetParent(node.jjtGetParent()); | ||
node.jjtSetParent(resultIsNotFalse); | ||
resultIsNotFalse.jjtAddChild(node, 0); | ||
resultIsNotFalse.jjtAddChild(literalFalse, 1); | ||
return resultIsNotFalse; | ||
} | ||
|
||
// handy utility for debugging | ||
public static void printAST(final Node node) { | ||
final StringBuilder stringBuilder = new StringBuilder().append("\n"); | ||
printAST(stringBuilder, 0, node); | ||
System.err.println(stringBuilder.toString()); | ||
} | ||
|
||
private static void printAST(final StringBuilder stringBuilder, final int depth, final Node node) { | ||
for (int i = 0; i < depth; i++) { | ||
stringBuilder.append(" "); | ||
} | ||
stringBuilder.append(node).append('(').append(node.getClass().getSimpleName()).append(")\n"); | ||
for (int i = 0; i < node.jjtGetNumChildren(); i++) { | ||
printAST(stringBuilder, depth + 1, node.jjtGetChild(i)); | ||
} | ||
} | ||
} |
Oops, something went wrong.