This repository has been archived by the owner on Jul 10, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 123
PROC-1334: Add library support for rule filtering #142
Open
josuf107
wants to merge
2
commits into
indeedeng:master
Choose a base branch
from
josuf107:jira/PROC-1334
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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); | ||
} | ||
} |
181 changes: 181 additions & 0 deletions
181
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,181 @@ | ||
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; | ||
|
||
/** | ||
* This class traverses a tomcat el syntax tree and transforms it into a new | ||
* expression that evaluates to true if the original expression could be true | ||
* given only a subset of the variables referenced. | ||
* | ||
* Expected usage is via the static destroyUnknowns method, which visits each | ||
* node in the tree to identify variables that are not in the variablesDefined | ||
* set and then transforms the expression to remove those nodes. Parents of | ||
* unknowns are recursively replaced using a | ||
* simple 3-valued logic (https://en.wikipedia.org/wiki/Three-valued_logic) with | ||
* maybeAnd, maybeOr, and maybeNot, with the semantics one would expect defined | ||
* in ProctorRuleFunctions. | ||
* | ||
* In the end the new maybeBool expression x is wrapped in a "not false" test: x | ||
* != false. The final expression will resolve to true if the original | ||
* expression is definitely true or unknown (could be true) given the known | ||
* values. | ||
*/ | ||
class NodeHunter implements NodeVisitor { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we add some javadoc in this class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay added some explanation. |
||
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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given all those code will only be used outside the proctor library. https://indeed-pte.slack.com/archives/C1F00STA9/p1688590802320449?thread_ts=1688582115.448299&cid=C1F00STA9
Can we add java doc to each of those methods to explain it is not meant to be called inside proctor lib?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
actually, can we create a subclass and only the subclass has those extra methods?