All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.indeed.proctor.common.RuleEvaluator Maven / Gradle / Ivy

The newest version!
package com.indeed.proctor.common;

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;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.el.ArrayELResolver;
import javax.el.BeanELResolver;
import javax.el.CompositeELResolver;
import javax.el.ELContext;
import javax.el.ELResolver;
import javax.el.ExpressionFactory;
import javax.el.FunctionMapper;
import javax.el.ListELResolver;
import javax.el.MapELResolver;
import javax.el.ValueExpression;
import javax.el.ValueReference;
import javax.el.VariableMapper;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/**
 * A nice tidy packaging of javax.el stuff.
 *
 * @author ketan
 * @author pwp
 */
public class RuleEvaluator {
    private static final Logger LOGGER = LogManager.getLogger(RuleEvaluator.class);

    static final FunctionMapper FUNCTION_MAPPER = defaultFunctionMapperBuilder().build();

    static final ExpressionFactory EXPRESSION_FACTORY = new ExpressionFactoryImpl();

    @Nonnull final ExpressionFactory expressionFactory;
    @Nonnull final CompositeELResolver elResolver;
    @Nonnull private final Map testConstants;
    @Nonnull private final FunctionMapper functionMapper;

    RuleEvaluator(
            @Nonnull final ExpressionFactory expressionFactory,
            @Nonnull final FunctionMapper functionMapper,
            @Nonnull final Map testConstantsMap) {
        this.expressionFactory = expressionFactory;

        this.functionMapper = functionMapper;

        elResolver = constructStandardElResolver();

        testConstants =
                ProctorUtils.convertToValueExpressionMap(expressionFactory, testConstantsMap);
    }

    public static RuleEvaluator createDefaultRuleEvaluator(
            final Map testConstantsMap) {
        return new RuleEvaluator(EXPRESSION_FACTORY, FUNCTION_MAPPER, testConstantsMap);
    }

    @Nonnull
    private static CompositeELResolver constructStandardElResolver() {
        final CompositeELResolver elResolver = new CompositeELResolver();
        elResolver.add(new ArrayELResolver());
        elResolver.add(new ListELResolver());
        elResolver.add(new MapELResolver());
        elResolver.add(new BeanELResolver()); // this must be last, because it throws Exception
        return elResolver;
    }

    public static LibraryFunctionMapperBuilder defaultFunctionMapperBuilder() {
        final LibraryFunctionMapperBuilder builder =
                new LibraryFunctionMapperBuilder()
                        .add("indeed", ProctorRuleFunctions.class) // backwards compatibility
                        .add("fn", LegacyTaglibFunctions.class)
                        .add("proctor", ProctorRuleFunctions.class);
        return builder;
    }

    @Nonnull
    ELContext createElContext(@Nonnull final Map localContext) {
        @SuppressWarnings("unchecked")
        final VariableMapper variableMapper =
                new MulticontextReadOnlyVariableMapper(testConstants, localContext);
        return createELContext(variableMapper);
    }

    @Nonnull
    ELContext createELContext(@Nonnull final VariableMapper variableMapper) {
        return new ELContext() {
            @Nonnull
            @Override
            public ELResolver getELResolver() {
                return elResolver;
            }

            @Nonnull
            @Override
            public FunctionMapper getFunctionMapper() {
                return functionMapper;
            }

            @Nonnull
            @Override
            public VariableMapper getVariableMapper() {
                return variableMapper;
            }
        };
    }

    /**
     * @deprecated Use evaluateBooleanRuleWithValueExpr(String, Map) instead, it's more efficient
     */
    @Deprecated
    public boolean evaluateBooleanRule(final String rule, @Nonnull final Map values)
            throws IllegalArgumentException {
        final Map localContext =
                ProctorUtils.convertToValueExpressionMap(expressionFactory, values);
        return evaluateBooleanRuleWithValueExpr(rule, localContext);
    }

    public boolean evaluateBooleanRuleWithValueExpr(
            final String rule, @Nonnull final Map localContext)
            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 ProctorUtils.ElExpressionClassification ec =
                ProctorUtils.clasifyElExpression(rule, true);
        if (ec == ProctorUtils.ElExpressionClassification.EMPTY
                || ec == ProctorUtils.ElExpressionClassification.CONSTANT_TRUE) {
            return true; //  always passes
        }
        if (ec == ProctorUtils.ElExpressionClassification.CONSTANT_FALSE) {
            return false;
        }

        final ELContext elContext = createElContext(localContext);
        final ValueExpression ve =
                expressionFactory.createValueExpression(elContext, rule, 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);
    }
    /**
     * @deprecated Use evaluateBooleanRulePartialWithValueExpr(String, Map) instead, it's more
     *     efficient
     */
    @Deprecated
    public boolean evaluateBooleanRulePartial(
            final String rule, @Nonnull final Map values)
            throws IllegalArgumentException {
        final Map 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(
            final String rule, @Nonnull final Map 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 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);
    }

    /** @throws IllegalArgumentException if type of expression is not boolean */
    static void checkRuleIsBooleanType(
            final String rule, final ELContext elContext, final ValueExpression ve) {
        // apache-el is an expression language, not a rule language, and it is very lenient
        // sadly that means it will just evaluate to false when users make certain mistakes, e.g. by
        // coercing String value "xyz" to boolean false, instead of throwing an exception.
        // To support users writing rules, be more strict here in requiring the type of the
        // value to be expected before coercion
        Class type = ve.getType(elContext);

        // if object is map check what the base value type is
        if (Objects.equals(type, Object.class)) {
            final ValueReference valueReference = ve.getValueReference(elContext);
            final Object base = valueReference.getBase();
            final Object property = valueReference.getProperty();
            try {
                if (base instanceof Map) {
                    final Map map = (Map) base;
                    type = map.get(property).getClass();
                }
            } catch (final NullPointerException e) {
                throw new IllegalArgumentException(
                        "Received nested value which does not exist in context map: value "
                                + property
                                + " in rule "
                                + rule);
            }
        }

        if (ClassUtils.isPrimitiveWrapper(type)) {
            type = ClassUtils.wrapperToPrimitive(type);
        }
        // allow null to be coerced for historic reasons
        if ((type != null) && (type != boolean.class)) {
            throw new IllegalArgumentException(
                    "Received non-boolean return value: " + type + " from rule " + rule);
        }
    }

    /**
     * @param expectedType class to coerce result to, use primitive instead of wrapper, e.g.
     *     boolean.class instead of Boolean.class.
     * @return null or a Boolean value representing the expression evaluation result
     * @throws RuntimeException: E.g. PropertyNotFound or other ELException when not of expectedType
     * @deprecated Use evaluateBooleanRule() instead, it checks against more errors
     */
    @CheckForNull
    @Deprecated
    public Object evaluateRule(
            final String rule, final Map values, final Class expectedType) {
        final ELContext elContext =
                createElContext(
                        ProctorUtils.convertToValueExpressionMap(expressionFactory, values));
        final ValueExpression ve =
                expressionFactory.createValueExpression(elContext, rule, expectedType);
        return ve.getValue(elContext);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy