Skip to content
This repository was archived by the owner on Jul 10, 2024. It is now read-only.

Commit ef0d7e5

Browse files
author
Joseph Barratt
committed
PROC-1334: Add support for rule evaluation for partial contexts
1 parent 56a8f50 commit ef0d7e5

File tree

6 files changed

+633
-0
lines changed

6 files changed

+633
-0
lines changed

proctor-common/src/main/java/com/indeed/proctor/common/ProctorRuleFunctions.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,44 @@ public static boolean versionInRange(
8080
}
8181
return inRange(version, start, end);
8282
}
83+
84+
public static MaybeBool maybeAnd(final MaybeBool op1, final MaybeBool op2) {
85+
if (MaybeBool.FALSE == op1 || MaybeBool.FALSE == op2) {
86+
return MaybeBool.FALSE;
87+
}
88+
if (MaybeBool.TRUE == op1 && MaybeBool.TRUE == op2) {
89+
return MaybeBool.TRUE;
90+
}
91+
return MaybeBool.UNKNOWN;
92+
}
93+
94+
public static MaybeBool maybeOr(final MaybeBool op1, final MaybeBool op2) {
95+
if (MaybeBool.TRUE == op1 || MaybeBool.TRUE == op2) {
96+
return MaybeBool.TRUE;
97+
}
98+
if (MaybeBool.FALSE == op1 && MaybeBool.FALSE == op2) {
99+
return MaybeBool.FALSE;
100+
}
101+
return MaybeBool.UNKNOWN;
102+
}
103+
104+
public static MaybeBool maybeNot(final MaybeBool maybeBool) {
105+
if (MaybeBool.TRUE == maybeBool) {
106+
return MaybeBool.FALSE;
107+
}
108+
if (MaybeBool.FALSE == maybeBool) {
109+
return MaybeBool.TRUE;
110+
}
111+
return MaybeBool.UNKNOWN;
112+
}
113+
114+
public static MaybeBool toMaybeBool(final boolean b) {
115+
return b ? MaybeBool.TRUE : MaybeBool.FALSE;
116+
}
117+
118+
public enum MaybeBool {
119+
TRUE,
120+
FALSE,
121+
UNKNOWN;
122+
}
83123
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.indeed.proctor.common;
2+
3+
import com.indeed.proctor.common.el.filter.PartialExpressionFactory;
4+
5+
import javax.annotation.Nonnull;
6+
import javax.annotation.concurrent.NotThreadSafe;
7+
import javax.el.FunctionMapper;
8+
import javax.el.ValueExpression;
9+
10+
import java.util.Map;
11+
12+
/**
13+
* Leverages RuleEvaluator to determine whether a given rule *could* match the
14+
* given context; useful for tools that search allocations.
15+
**/
16+
@NotThreadSafe
17+
public class RuleFilter {
18+
@Nonnull
19+
private final PartialExpressionFactory expressionFactory;
20+
private final RuleEvaluator ruleEvaluator;
21+
22+
RuleFilter(
23+
final FunctionMapper functionMapper,
24+
@Nonnull final Map<String, Object> testConstantsMap
25+
) {
26+
this.expressionFactory = new PartialExpressionFactory(testConstantsMap.keySet());
27+
this.ruleEvaluator = new RuleEvaluator(
28+
expressionFactory,
29+
functionMapper,
30+
testConstantsMap);
31+
}
32+
33+
public static RuleFilter createDefaultRuleFilter(final Map<String, Object> testConstantsMap) {
34+
return new RuleFilter(RuleEvaluator.FUNCTION_MAPPER, testConstantsMap);
35+
}
36+
37+
public boolean ruleCanMatch(final String rule, final Map<String, Object> values) {
38+
expressionFactory.setContextVariables(values.keySet());
39+
final Map<String, ValueExpression> localContext = ProctorUtils
40+
.convertToValueExpressionMap(expressionFactory, values);
41+
return ruleEvaluator.evaluateBooleanRuleWithValueExpr(rule, localContext);
42+
}
43+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package com.indeed.proctor.common.el.filter;
2+
3+
import com.indeed.proctor.common.ProctorRuleFunctions.MaybeBool;
4+
import org.apache.el.parser.AstAnd;
5+
import org.apache.el.parser.AstFunction;
6+
import org.apache.el.parser.AstIdentifier;
7+
import org.apache.el.parser.AstLiteralExpression;
8+
import org.apache.el.parser.AstNot;
9+
import org.apache.el.parser.AstNotEqual;
10+
import org.apache.el.parser.AstOr;
11+
import org.apache.el.parser.Node;
12+
import org.apache.el.parser.NodeVisitor;
13+
import org.apache.el.parser.SimpleNode;
14+
15+
import java.lang.reflect.InvocationTargetException;
16+
import java.util.Collections;
17+
import java.util.IdentityHashMap;
18+
import java.util.Map;
19+
import java.util.Set;
20+
import java.util.Stack;
21+
22+
class NodeHunter implements NodeVisitor {
23+
private final Set<Node> initialUnknowns = Collections.newSetFromMap(new IdentityHashMap<>());
24+
private final Map<Node, Node> replacements = new IdentityHashMap<>();
25+
private final Set<String> variablesDefined;
26+
27+
NodeHunter(final Set<String> variablesDefined) {
28+
this.variablesDefined = variablesDefined;
29+
}
30+
31+
public static Node destroyUnknowns(
32+
final Node node,
33+
final Set<String> variablesDefined
34+
) throws Exception {
35+
final NodeHunter nodeHunter = new NodeHunter(variablesDefined);
36+
node.accept(nodeHunter);
37+
if (nodeHunter.initialUnknowns.isEmpty()) {
38+
// Nothing to do here
39+
return node;
40+
}
41+
nodeHunter.calculateReplacements();
42+
final Node result = nodeHunter.replaceNodes(node);
43+
// At this point result is a maybebool, we need to convert it to a bool
44+
final Node resultIsNotFalse = nodeHunter.wrapIsNotFalse(result);
45+
return resultIsNotFalse;
46+
}
47+
48+
private void calculateReplacements() {
49+
final Stack<Node> nodesToDestroy = new Stack<>();
50+
initialUnknowns.forEach(nodesToDestroy::push);
51+
while (!nodesToDestroy.isEmpty()) {
52+
final Node nodeToDestroy = nodesToDestroy.pop();
53+
if (nodeToDestroy instanceof AstAnd) {
54+
// Replace simple "and" with maybeAnd
55+
replaceWithFunction(nodeToDestroy, "maybeAnd");
56+
} else if (nodeToDestroy instanceof AstOr) {
57+
// Replace simple "or" with maybeOr
58+
replaceWithFunction(nodeToDestroy, "maybeOr");
59+
} else if (nodeToDestroy instanceof AstNot) {
60+
// Replace simple "not" with maybeNot
61+
replaceWithFunction(nodeToDestroy, "maybeNot");
62+
// } else if (nodeToDestroy instanceof AstEqual || nodeToDestroy instanceof AstNotEqual) {
63+
// TODO: if someone compares two bools using == that would be
64+
// weird, but we could handle it by making sure any cases that mix
65+
// maybeBool and bool are promoted to maybeBool like we do with the
66+
// other logical operators
67+
} else if (!replacements.containsKey(nodeToDestroy)) {
68+
// Anything else propagate the unknown value
69+
//
70+
// TODO: If a bool is used as an argument to a function we
71+
// could try and do the function if the maybeBool is true or
72+
// false, and only propagate the unknown if any argument is
73+
// unknown, but that seems rare and very complicated so I
74+
// haven't handled that case here.
75+
final AstLiteralExpression replacement = new AstLiteralExpression(1);
76+
replacement.setImage(MaybeBool.UNKNOWN.name());
77+
replacements.put(nodeToDestroy, replacement);
78+
}
79+
if (nodeToDestroy.jjtGetParent() != null) {
80+
nodesToDestroy.push(nodeToDestroy.jjtGetParent());
81+
}
82+
}
83+
}
84+
85+
private void replaceWithFunction(final Node node, final String function) {
86+
final AstFunction replacement = new AstFunction(27);
87+
replacement.setPrefix("proctor");
88+
replacement.setLocalName(function);
89+
replacement.setImage("proctor:" + function);
90+
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
91+
final Node child = node.jjtGetChild(i);
92+
if (replacements.containsKey(child)) {
93+
replacement.jjtAddChild(replacements.get(child), i);
94+
} else {
95+
final AstFunction replacementChild = new AstFunction(27);
96+
replacementChild.setPrefix("proctor");
97+
replacementChild.setLocalName("toMaybeBool");
98+
replacementChild.setImage("proctor:toMaybeBool");
99+
replacementChild.jjtAddChild(child, 0);
100+
replacement.jjtAddChild(replacementChild, i);
101+
}
102+
}
103+
replacements.put(node, replacement);
104+
}
105+
106+
private Node replaceNodes(
107+
final Node node
108+
) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
109+
if (replacements.containsKey(node)) {
110+
Node newNode = node;
111+
while (replacements.containsKey(newNode)) {
112+
newNode = replacements.get(newNode);
113+
}
114+
return newNode;
115+
}
116+
final SimpleNode asSimpleNode = (SimpleNode) node;
117+
for (int i = 0; i < asSimpleNode.jjtGetNumChildren(); i++) {
118+
final Node newChild = replaceNodes(asSimpleNode.jjtGetChild(i));
119+
asSimpleNode.jjtAddChild(newChild, i);
120+
newChild.jjtSetParent(asSimpleNode);
121+
}
122+
return node;
123+
}
124+
125+
@Override
126+
public void visit(final Node node) throws Exception {
127+
if (node instanceof AstIdentifier) {
128+
String variable = node.getImage();
129+
if (!variablesDefined.contains(variable)) {
130+
initialUnknowns.add(node);
131+
}
132+
}
133+
}
134+
135+
private Node wrapIsNotFalse(final Node node) {
136+
final Node resultIsNotFalse = new AstNotEqual(9);
137+
final AstLiteralExpression literalFalse = new AstLiteralExpression(1);
138+
literalFalse.setImage(MaybeBool.FALSE.name());
139+
literalFalse.jjtSetParent(resultIsNotFalse);
140+
resultIsNotFalse.jjtSetParent(node.jjtGetParent());
141+
node.jjtSetParent(resultIsNotFalse);
142+
resultIsNotFalse.jjtAddChild(node, 0);
143+
resultIsNotFalse.jjtAddChild(literalFalse, 1);
144+
return resultIsNotFalse;
145+
}
146+
147+
// handy utility for debugging
148+
public static void printAST(final Node node) {
149+
final StringBuilder stringBuilder = new StringBuilder().append("\n");
150+
printAST(stringBuilder, 0, node);
151+
System.err.println(stringBuilder.toString());
152+
}
153+
154+
private static void printAST(final StringBuilder stringBuilder, final int depth, final Node node) {
155+
for (int i = 0; i < depth; i++) {
156+
stringBuilder.append(" ");
157+
}
158+
stringBuilder.append(node).append('(').append(node.getClass().getSimpleName()).append(")\n");
159+
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
160+
printAST(stringBuilder, depth + 1, node.jjtGetChild(i));
161+
}
162+
}
163+
}

0 commit comments

Comments
 (0)