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

Commit ed925ab

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

5 files changed

Lines changed: 542 additions & 1 deletion

File tree

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
@@ -77,4 +77,44 @@ public static boolean versionInRange(final ReleaseVersion version, final String
7777
}
7878
return inRange(version, start, end);
7979
}
80+
81+
public static MaybeBool maybeAnd(final MaybeBool op1, final MaybeBool op2) {
82+
if (MaybeBool.FALSE == op1 || MaybeBool.FALSE == op2) {
83+
return MaybeBool.FALSE;
84+
}
85+
if (MaybeBool.TRUE == op1 && MaybeBool.TRUE == op2) {
86+
return MaybeBool.TRUE;
87+
}
88+
return MaybeBool.UNKNOWN;
89+
}
90+
91+
public static MaybeBool maybeOr(final MaybeBool op1, final MaybeBool op2) {
92+
if (MaybeBool.TRUE == op1 || MaybeBool.TRUE == op2) {
93+
return MaybeBool.TRUE;
94+
}
95+
if (MaybeBool.FALSE == op1 && MaybeBool.FALSE == op2) {
96+
return MaybeBool.FALSE;
97+
}
98+
return MaybeBool.UNKNOWN;
99+
}
100+
101+
public static MaybeBool maybeNot(final MaybeBool maybeBool) {
102+
if (MaybeBool.TRUE == maybeBool) {
103+
return MaybeBool.FALSE;
104+
}
105+
if (MaybeBool.FALSE == maybeBool) {
106+
return MaybeBool.TRUE;
107+
}
108+
return MaybeBool.UNKNOWN;
109+
}
110+
111+
public static MaybeBool toMaybeBool(final boolean b) {
112+
return b ? MaybeBool.TRUE : MaybeBool.FALSE;
113+
}
114+
115+
public enum MaybeBool {
116+
TRUE,
117+
FALSE,
118+
UNKNOWN;
119+
}
80120
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.indeed.proctor.common.el.LibraryFunctionMapperBuilder;
44
import com.indeed.proctor.common.el.MulticontextReadOnlyVariableMapper;
5+
import com.indeed.proctor.common.el.PartialExpressionBuilder;
56
import org.apache.commons.lang3.ClassUtils;
67
import org.apache.commons.lang3.StringUtils;
78
import org.apache.el.ExpressionFactoryImpl;
@@ -21,7 +22,10 @@
2122
import javax.el.MapELResolver;
2223
import javax.el.ValueExpression;
2324
import javax.el.VariableMapper;
25+
26+
import java.util.HashSet;
2427
import java.util.Map;
28+
import java.util.Set;
2529

2630
/**
2731
* A nice tidy packaging of javax.el stuff.
@@ -153,6 +157,50 @@ public boolean evaluateBooleanRuleWithValueExpr(final String rule, @Nonnull fina
153157
+ " from rule " + rule);
154158
}
155159

160+
/**
161+
* @deprecated Use evaluateBooleanRulePartialWithValueExpr(String, Map) instead, it's more efficient
162+
*/
163+
@Deprecated
164+
public boolean evaluateBooleanRulePartial(final String rule, @Nonnull final Map<String, Object> values) throws IllegalArgumentException {
165+
final Map<String, ValueExpression> localContext = ProctorUtils.convertToValueExpressionMap(expressionFactory, values);
166+
return evaluateBooleanRulePartialWithValueExpr(rule, localContext);
167+
}
168+
169+
public boolean evaluateBooleanRulePartialWithValueExpr(final String rule, @Nonnull final Map<String, ValueExpression> values) throws IllegalArgumentException {
170+
if (StringUtils.isBlank(rule)) {
171+
return true;
172+
}
173+
if (!rule.startsWith("${") || !rule.endsWith("}")) {
174+
LOGGER.error("Invalid rule '" + rule + "'"); // TODO: should this be an exception?
175+
return false;
176+
}
177+
final String bareRule = ProctorUtils.removeElExpressionBraces(rule);
178+
if (StringUtils.isBlank(bareRule) || "true".equalsIgnoreCase(bareRule)) {
179+
return true; // always passes
180+
}
181+
if ("false".equalsIgnoreCase(bareRule)) {
182+
return false;
183+
}
184+
185+
final ELContext elContext = createElContext(values);
186+
final Set<String> variablesDefined = new HashSet<>();
187+
variablesDefined.addAll(values.keySet());
188+
variablesDefined.addAll(testConstants.keySet());
189+
final PartialExpressionBuilder builder = new PartialExpressionBuilder(rule, elContext, variablesDefined);
190+
final ValueExpression ve = builder.createValueExpression(boolean.class);
191+
checkRuleIsBooleanType(rule, elContext, ve);
192+
193+
final Object result = ve.getValue(elContext);
194+
195+
if (result instanceof Boolean) {
196+
return ((Boolean) result);
197+
}
198+
// this should never happen, evaluateRule throws ELException when it cannot coerce to Boolean
199+
throw new IllegalArgumentException("Received non-boolean return value: "
200+
+ (result == null ? "null" : result.getClass().getCanonicalName())
201+
+ " from rule " + rule);
202+
}
203+
156204
/**
157205
* @throws IllegalArgumentException if type of expression is not boolean
158206
*/
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.indeed.proctor.common.el;
2+
3+
import com.google.common.collect.ImmutableList;
4+
import com.indeed.proctor.common.ProctorRuleFunctions.MaybeBool;
5+
import org.apache.el.parser.AstAnd;
6+
import org.apache.el.parser.AstFunction;
7+
import org.apache.el.parser.AstIdentifier;
8+
import org.apache.el.parser.AstLiteralExpression;
9+
import org.apache.el.parser.AstNot;
10+
import org.apache.el.parser.AstNotEqual;
11+
import org.apache.el.parser.AstOr;
12+
import org.apache.el.parser.ELParserTreeConstants;
13+
import org.apache.el.parser.Node;
14+
import org.apache.el.parser.NodeVisitor;
15+
import org.apache.el.parser.SimpleNode;
16+
17+
import java.lang.reflect.Constructor;
18+
import java.lang.reflect.InvocationTargetException;
19+
import java.util.Collections;
20+
import java.util.IdentityHashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Set;
24+
import java.util.Stack;
25+
import java.util.stream.Collectors;
26+
27+
public class NodeHunter implements NodeVisitor {
28+
private static final List<String> NODE_TYPES = ImmutableList.copyOf(ELParserTreeConstants.jjtNodeName)
29+
.stream()
30+
.map(nodeName -> "Ast" + nodeName)
31+
.collect(Collectors.toList());
32+
private static final Map<String, Integer> NODE_TYPE_IDS = NODE_TYPES.stream()
33+
.collect(Collectors.toMap(nodeType -> nodeType, NODE_TYPES::indexOf));
34+
35+
private final Set<Node> initialUnknowns = Collections.newSetFromMap(new IdentityHashMap<>());
36+
private final Map<Node, Node> replacements = new IdentityHashMap<>();
37+
private final Set<String> variablesDefined;
38+
39+
NodeHunter(final Set<String> variablesDefined) {
40+
this.variablesDefined = variablesDefined;
41+
}
42+
43+
public static Node destroyUnknowns(
44+
final Node node,
45+
final Set<String> variablesDefined
46+
) throws Exception {
47+
final NodeHunter nodeHunter = new NodeHunter(variablesDefined);
48+
node.accept(nodeHunter);
49+
if (nodeHunter.initialUnknowns.isEmpty()) {
50+
// Nothing to do here
51+
return node;
52+
}
53+
nodeHunter.calculateReplacements();
54+
final Node result = nodeHunter.replaceNodes(node);
55+
// At this point result is a maybebool, we need to convert it to a bool
56+
final Node resultIsNotFalse = nodeHunter.wrapIsNotFalse(result);
57+
return resultIsNotFalse;
58+
}
59+
60+
private void calculateReplacements() {
61+
final Stack<Node> nodesToDestroy = new Stack<>();
62+
initialUnknowns.forEach(nodesToDestroy::push);
63+
while (!nodesToDestroy.isEmpty()) {
64+
final Node nodeToDestroy = nodesToDestroy.pop();
65+
if (nodeToDestroy instanceof AstAnd) {
66+
// Replace simple "and" with maybeAnd
67+
replaceWithFunction(nodeToDestroy, "maybeAnd");
68+
} else if (nodeToDestroy instanceof AstOr) {
69+
// Replace simple "or" with maybeOr
70+
replaceWithFunction(nodeToDestroy, "maybeOr");
71+
} else if (nodeToDestroy instanceof AstNot) {
72+
// Replace simple "not" with maybeNot
73+
replaceWithFunction(nodeToDestroy, "maybeNot");
74+
// } else if (nodeToDestroy instanceof AstEqual || nodeToDestroy instanceof AstNotEqual) {
75+
// TODO: if someone compares two bools using == that would be
76+
// weird, but we could handle it by making sure any cases that mix
77+
// maybeBool and bool are promoted to maybeBool like we do with the
78+
// other logical operators
79+
} else if (!replacements.containsKey(nodeToDestroy)) {
80+
// Anything else propagate the unknown value
81+
//
82+
// TODO: If a bool is used as an argument to a function we
83+
// could try and do the function if the maybeBool is true or
84+
// false, and only propagate the unknown if any argument is
85+
// unknown, but that seems rare and very complicated so I
86+
// haven't handled that case here.
87+
final AstLiteralExpression replacement = new AstLiteralExpression(1);
88+
replacement.setImage(MaybeBool.UNKNOWN.name());
89+
replacements.put(nodeToDestroy, replacement);
90+
}
91+
if (nodeToDestroy.jjtGetParent() != null) {
92+
nodesToDestroy.push(nodeToDestroy.jjtGetParent());
93+
}
94+
}
95+
}
96+
97+
private void replaceWithFunction(final Node node, final String function) {
98+
final AstFunction replacement = new AstFunction(27);
99+
replacement.setPrefix("proctor");
100+
replacement.setLocalName(function);
101+
replacement.setImage("proctor:" + function);
102+
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
103+
final Node child = node.jjtGetChild(i);
104+
if (replacements.containsKey(child)) {
105+
replacement.jjtAddChild(replacements.get(child), i);
106+
} else {
107+
final AstFunction replacementChild = new AstFunction(27);
108+
replacementChild.setPrefix("proctor");
109+
replacementChild.setLocalName("toMaybeBool");
110+
replacementChild.setImage("proctor:toMaybeBool");
111+
replacementChild.jjtAddChild(child, 0);
112+
replacement.jjtAddChild(replacementChild, i);
113+
}
114+
}
115+
replacements.put(node, replacement);
116+
}
117+
118+
private Node replaceNodes(
119+
final Node node
120+
) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
121+
if (replacements.containsKey(node)) {
122+
Node newNode = node;
123+
while (replacements.containsKey(newNode)) {
124+
newNode = replacements.get(newNode);
125+
}
126+
return newNode;
127+
}
128+
final Class<?> nodeClass = node.getClass();
129+
final Constructor<?> constructor = nodeClass.getConstructor(int.class);
130+
final SimpleNode newNode = (SimpleNode) constructor.newInstance(NODE_TYPE_IDS.get(nodeClass.getSimpleName()));
131+
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
132+
final Node newChild = replaceNodes(node.jjtGetChild(i));
133+
newChild.jjtSetParent(newNode);
134+
newNode.jjtAddChild(newChild, i);
135+
}
136+
newNode.jjtSetParent(node.jjtGetParent());
137+
newNode.setImage(node.getImage());
138+
if (newNode instanceof AstFunction) {
139+
((AstFunction) newNode).setPrefix(((AstFunction) node).getPrefix());
140+
((AstFunction) newNode).setLocalName(((AstFunction) node).getLocalName());
141+
}
142+
return newNode;
143+
}
144+
145+
@Override
146+
public void visit(final Node node) throws Exception {
147+
if (node instanceof AstIdentifier) {
148+
String variable = node.getImage();
149+
if (!variablesDefined.contains(variable)) {
150+
initialUnknowns.add(node);
151+
}
152+
}
153+
}
154+
155+
private Node wrapIsNotFalse(final Node node) {
156+
final Node resultIsNotFalse = new AstNotEqual(9);
157+
final AstLiteralExpression literalFalse = new AstLiteralExpression(1);
158+
literalFalse.setImage(MaybeBool.FALSE.name());
159+
literalFalse.jjtSetParent(resultIsNotFalse);
160+
resultIsNotFalse.jjtSetParent(node.jjtGetParent());
161+
node.jjtSetParent(resultIsNotFalse);
162+
resultIsNotFalse.jjtAddChild(node, 0);
163+
resultIsNotFalse.jjtAddChild(literalFalse, 1);
164+
return resultIsNotFalse;
165+
}
166+
167+
// handy utility for debugging
168+
public static void printAST(final Node node) {
169+
final StringBuilder stringBuilder = new StringBuilder().append("\n");
170+
printAST(stringBuilder, 0, node);
171+
System.err.println(stringBuilder.toString());
172+
}
173+
174+
private static void printAST(final StringBuilder stringBuilder, final int depth, final Node node) {
175+
for (int i = 0; i < depth; i++) {
176+
stringBuilder.append(" ");
177+
}
178+
stringBuilder.append(node).append('(').append(node.getClass().getSimpleName()).append(")\n");
179+
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
180+
printAST(stringBuilder, depth + 1, node.jjtGetChild(i));
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)