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

MPEN-145: Added partial matching #156

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,44 @@ public static boolean versionInRange(
}
return inRange(version, start, end);
}

public static MaybeBool maybeAnd(final MaybeBool op1, final MaybeBool op2) {
if (MaybeBool.FALSE == op1 || MaybeBool.FALSE == op2) {
return MaybeBool.FALSE;
}
if (MaybeBool.TRUE == op1 && MaybeBool.TRUE == op2) {
return MaybeBool.TRUE;
}
return MaybeBool.UNKNOWN;
}

public static MaybeBool maybeOr(final MaybeBool op1, final MaybeBool op2) {
if (MaybeBool.TRUE == op1 || MaybeBool.TRUE == op2) {
return MaybeBool.TRUE;
}
if (MaybeBool.FALSE == op1 && MaybeBool.FALSE == op2) {
return MaybeBool.FALSE;
}
return MaybeBool.UNKNOWN;
}

public static MaybeBool maybeNot(final MaybeBool maybeBool) {
if (MaybeBool.TRUE == maybeBool) {
return MaybeBool.FALSE;
}
if (MaybeBool.FALSE == maybeBool) {
return MaybeBool.TRUE;
}
return MaybeBool.UNKNOWN;
}

public static MaybeBool toMaybeBool(final boolean b) {
return b ? MaybeBool.TRUE : MaybeBool.FALSE;
}

public enum MaybeBool {
TRUE,
FALSE,
UNKNOWN;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.indeed.proctor.common.el.LibraryFunctionMapperBuilder;
import com.indeed.proctor.common.el.MulticontextReadOnlyVariableMapper;
import com.indeed.proctor.common.el.PartialExpressionBuilder;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.el.ExpressionFactoryImpl;
Expand All @@ -21,7 +22,9 @@
import javax.el.MapELResolver;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* A nice tidy packaging of javax.el stuff.
Expand Down Expand Up @@ -159,7 +162,63 @@ public boolean evaluateBooleanRuleWithValueExpr(
+ " from rule "
+ rule);
}
/**
* @deprecated Use evaluateBooleanRulePartialWithValueExpr(String, Map) instead, it's more
* efficient
*/
@Deprecated
public boolean evaluateBooleanRulePartial(
final String rule, @Nonnull final Map<String, Object> values)
throws IllegalArgumentException {
final Map<String, ValueExpression> localContext =
ProctorUtils.convertToValueExpressionMap(expressionFactory, values);
return evaluateBooleanRulePartialWithValueExpr(rule, localContext);
}

/**
* This method should only be used for partial matching with proctor rules
* @return Evaluates a partial rule
**/
public boolean evaluateBooleanRulePartialWithValueExpr(
zacharygoodwin marked this conversation as resolved.
Show resolved Hide resolved
final String rule, @Nonnull final Map<String, ValueExpression> values)
throws IllegalArgumentException {
if (StringUtils.isBlank(rule)) {
return true;
}
if (!rule.startsWith("${") || !rule.endsWith("}")) {
LOGGER.error("Invalid rule '" + rule + "'"); // TODO: should this be an exception?
return false;
}
final String bareRule = ProctorUtils.removeElExpressionBraces(rule);
if (StringUtils.isBlank(bareRule) || "true".equalsIgnoreCase(bareRule)) {
return true; // always passes
}
if ("false".equalsIgnoreCase(bareRule)) {
return false;
}

final ELContext elContext = createElContext(values);
final Set<String> variablesDefined = new HashSet<>();
variablesDefined.addAll(values.keySet());
variablesDefined.addAll(testConstants.keySet());
final PartialExpressionBuilder builder =
new PartialExpressionBuilder(rule, elContext, variablesDefined);
final ValueExpression ve = builder.createValueExpression(boolean.class);
checkRuleIsBooleanType(rule, elContext, ve);

final Object result = ve.getValue(elContext);

if (result instanceof Boolean) {
return ((Boolean) result);
}
// this should never happen, evaluateRule throws ELException when it cannot coerce to
// Boolean
throw new IllegalArgumentException(
"Received non-boolean return value: "
+ (result == null ? "null" : result.getClass().getCanonicalName())
+ " from rule "
+ rule);
}
zacharygoodwin marked this conversation as resolved.
Show resolved Hide resolved
/** @throws IllegalArgumentException if type of expression is not boolean */
static void checkRuleIsBooleanType(
final String rule, final ELContext elContext, final ValueExpression ve) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.indeed.proctor.common.el;

import com.google.common.collect.ImmutableList;
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.ELParserTreeConstants;
import org.apache.el.parser.Node;
import org.apache.el.parser.NodeVisitor;
import org.apache.el.parser.SimpleNode;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;

public class NodeHunter implements NodeVisitor {
private static final List<String> NODE_TYPES =
ImmutableList.copyOf(ELParserTreeConstants.jjtNodeName).stream()
.map(nodeName -> "Ast" + nodeName)
.collect(Collectors.toList());
private static final Map<String, Integer> NODE_TYPE_IDS =
NODE_TYPES.stream()
.collect(Collectors.toMap(nodeType -> nodeType, NODE_TYPES::indexOf));
private static final int AST_FUNCTION_TYPE = 27;
private static final int AST_NOT_EQUAL_TYPE = 9;

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 AstFunction createFunctionReplacement(final Node node, final String function) {
final AstFunction replacement = new AstFunction(AST_FUNCTION_TYPE);
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(AST_FUNCTION_TYPE);
replacementChild.setPrefix("proctor");
replacementChild.setLocalName("toMaybeBool");
replacementChild.setImage("proctor:toMaybeBool");
replacementChild.jjtAddChild(child, 0);
replacement.jjtAddChild(replacementChild, i);
}
}

return replacement;
}

private void replaceWithFunction(final Node node, final String function) {
final AstFunction replacement = createFunctionReplacement(node, function);
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 Class<?> nodeClass = node.getClass();
final Constructor<?> constructor = nodeClass.getConstructor(int.class);
final SimpleNode newNode =
(SimpleNode) constructor.newInstance(NODE_TYPE_IDS.get(nodeClass.getSimpleName()));
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
final Node newChild = replaceNodes(node.jjtGetChild(i));
newChild.jjtSetParent(newNode);
newNode.jjtAddChild(newChild, i);
}
newNode.jjtSetParent(node.jjtGetParent());
newNode.setImage(node.getImage());
if (newNode instanceof AstFunction) {
((AstFunction) newNode).setPrefix(((AstFunction) node).getPrefix());
((AstFunction) newNode).setLocalName(((AstFunction) node).getLocalName());
}
return newNode;
}

@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(AST_NOT_EQUAL_TYPE);
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;
}
}
Loading
Loading