Evaluate expressions/conditions in apex code ? (which are stored in string/text)

On the question "is it possible" the answer is obviously yes; the real question is whether the effort is justified for your project or product.

A good way to proceed with writing your own is to port already working code to Apex. An example I Googled quite quickly (not saying this is the best starting point) is A Java expression parser. Look for ones that use the implementation language directly rather than ones that build on some sort of parser generator. You are unlikely to find an exact match for your needs so will have to invest time and effort in really understanding the code as well as in porting it. And you will need lots of test cases. Plan on it taking a week not a day unless you are OK with starting with something very simple.

(Several years ago I wrote one for a product - I'm afraid it is now lost. In the end we could not use it though, because at that time in triggers there was a 20,000 script statement governor limit and in some cases we were hitting that. Much of the logic in typical expression parser is working character by character so when you get up to a substantial sized expression 20,000 is not enough statements to get the evaluation done. But now the governor limit is effectively way higher and Apex runs faster as well so this should be much less of an issue.)

But you may get lucky and find a well written Apex implementation that someone is willing to share that you can use as a starting point. I wish you good luck on that.

PS Quite often people look to invoke an existing language engine rather than write their own. But this can be problematic in that you are no longer in control of what is allowed and what is not and so that approach can create serious security holes.

PPS I did track down the parser I wrote and show the main class below to give an impression of the scale and likely style of the work.

/**
 * TODO dates, nulls
 * 
 * Precedence implemented by method call ordering:
 * 
 * unary          +expr -expr !expr
 * multiplicative * /
 * additive       + -
 * relational     < > <= >=
 * equality       == !=
 * logical AND    &&
 * logical OR     ||
 * ternary        ? :
 */
public class ExpressionInterpreter {

    // Apex won't allow an infinite loop so set a high upper bound that should never be hit
    private static final Integer LOOP_LIMIT = 25;
    private static final String LOOP_LIMIT_MESSAGE = 'Loop limit of ' + LOOP_LIMIT + ' exceeded';

    private ExpressionLexer lexer;
    private ExpressionFunctions functions;

    public ExpressionInterpreter(String expression, ExpressionSymbols symbols, ExpressionFunctions functions) {
        this.lexer = new ExpressionLexer(expression, symbols);
        this.functions = functions;
    }

    private Object function(String functionName) {
        Integer functionPosition = lexer.getMatchPosition();
        Integer openPosition = lexer.getPosition() - 1;
        List<Object> arguments = new List<Object>();
        if (lexer.consumeOnMatch(')')) {
            return functions.evaluate(functionName, arguments, functionPosition);
        }
        arguments.add(expression());

        for (boolean isAnotherArgument = true; isAnotherArgument;) {
            if (lexer.consumeOnMatch(',')) {
                arguments.add(expression());
                isAnotherArgument = true;
            } else {
                isAnotherArgument = false;
            }
            if (lexer.consumeOnMatch(')')) {
                return functions.evaluate(functionName, arguments, functionPosition);
            }
        }
        throw ExpressionException.create(
                'No closing parenthesis found for function "' + functionName + '"',
                openPosition
                );
    }

    private Object atom() {

        // Unary
        boolean negativeOp = false;
        Integer negativePosition = -1;
        boolean notOp = false;
        Integer notPosition = -1;
        if (lexer.consumeOnMatch('-')) {
            negativeOp = true;
            negativePosition = lexer.getMatchPosition();
        } else if (lexer.consumeOnMatch('+')) {
            // Nothing to do in this case
        } else if (lexer.consumeOnMatch('!')) {
            notOp = true;
            notPosition = lexer.getMatchPosition();
        }

        Object result;
        String functionName;
        // Check if there is parenthesis
        if (lexer.consumeOnMatch('(')) {
            Integer openPosition = lexer.getMatchPosition();
            result = expression();
            if (lexer.consumeOnMatch(')')) {
                // Good - a match
            } else {
                throw ExpressionException.create('Mismatched parenthesis', openPosition);
            }
        } else if ((functionName = lexer.consumeFunctionOnMatch()) != null) {
            result = function(functionName);
        } else {
            result = lexer.consumeNextValue();
        }

        if (negativeOp) {
            if (result instanceof Double) {
                return -((Double) result);
            } else {
                throw ExpressionException.create('Type mismatch: cannot negate ' + result, negativePosition);
            }
        } else if (notOp) {
            if (result instanceof Boolean) {
                return !((Boolean) result);
            } else {
                throw ExpressionException.create('Type mismatch: cannot not ' + result, notPosition);
            }
        } else {
            return result;
        }
    }

    private Object factors() {
        Object result1 = atom();
        for (Integer i = 0; i < LOOP_LIMIT; i++) {
            Integer op = lexer.consumeOnMatch(new String[] {'/', '*'});
            if (op == -1) {
                return result1;
            }
            Integer opPosition = lexer.getMatchPosition();

            Object result2 = atom();
            if (result1 instanceof Double && result2 instanceof Double) {
                if (op == 0) {
                    if ((Double) result2 == 0.0) {
                        throw ExpressionException.create('Divide by zero', opPosition);
                    }
                    result1 = (Double) result1 / (Double) result2;
                }
                if (op == 1) {
                    result1 = (Double) result1 * (Double) result2;
                }
            } else {
                throw ExpressionException.create(
                        'Type mismatch: cannot apply * or / to ' + result1 + ' and ' + result2,
                        opPosition
                        );
            }
        }
        throw ExpressionException.create(LOOP_LIMIT_MESSAGE, lexer.getPosition());
    }

    private Object summands() {
        Object result1 = factors();
        for (Integer i = 0; i < LOOP_LIMIT; i++) {
            Integer op = lexer.consumeOnMatch(new String[] {'-', '+'});
            if (op == -1) {
                return result1;
            }
            Integer opPosition = lexer.getMatchPosition();

            Object result2 = factors();
            if (result1 instanceof Double && result2 instanceof Double) {
                if (op == 0) {
                    result1 = (Double) result1 - (Double) result2;
                } else {
                    result1 = (Double) result1 + (Double) result2;
                }
            } else {
                throw ExpressionException.create(
                        'Type mismatch: cannot apply + or - to ' + result1 + ' and ' + result2,
                        opPosition
                        );
            }
        }
        throw ExpressionException.create(LOOP_LIMIT_MESSAGE, lexer.getPosition());
    }

    private Object relational() {
        Object result1 = summands();
        for (Integer i = 0; i < LOOP_LIMIT; i++) {
            Integer op = lexer.consumeOnMatch(new String[] {'>=', '<=', '>', '<'});
            if (op == -1) {
                return result1;
            }
            Integer opPosition = lexer.getMatchPosition();

            Object result2 = summands();
            if (result1 instanceof Double && result2 instanceof Double) {
                Double d1 = (Double) result1;
                Double d2 = (Double) result2;
                if (op == 0) {
                    result1 = d1 >= d2;
                } else if (op == 1) {
                    result1 = d1 <= d2;
                } else if (op == 2) {
                    result1 = d1 > d2;
                } else if (op == 3) {
                    result1 = d1 < d2;
                }
            } else {
                throw ExpressionException.create(
                        'Type mismatch: cannot apply < or > or <= or >= to ' + result1 + ' and ' + result2,
                        opPosition
                        );
            }
        }
        throw ExpressionException.create(LOOP_LIMIT_MESSAGE, lexer.getPosition());
    }

    private Object equality() {
        Object result1 = relational();
        for (Integer i = 0; i < LOOP_LIMIT; i++) {
            Integer op = lexer.consumeOnMatch(new String[] {'==', '!='});
            if (op == -1) {
                return result1;
            }
            Integer opPosition = lexer.getMatchPosition();

            Object result2 = relational();
            if ((result1 instanceof Boolean && result2 instanceof Boolean)
                    || (result1 instanceof Double && result2 instanceof Double)
                    || (result1 == null || result2 == null)
                    ) {
                if (op == 0) {
                    result1 = result1 == result2;
                } else if (op == 1) {
                    result1 = result1 != result2;
                }
            } else {
                throw ExpressionException.create(
                        'Type mismatch: cannot apply == or != to ' + result1 + ' and ' + result2,
                        opPosition
                        );
            }
        }
        throw ExpressionException.create(LOOP_LIMIT_MESSAGE, lexer.getPosition());
    }

    private Object logical() {
        Object result1 = equality();
        for (Integer i = 0; i < LOOP_LIMIT; i++) {
            Integer op = lexer.consumeOnMatch(new String[] {'&&', '||'});
            if (op == -1) {
                return result1;
            }
            Integer opPosition = lexer.getMatchPosition();

            Object result2 = equality();
            if ((result1 instanceof Boolean && result2 instanceof Boolean)
                    || (result1 instanceof Double && result2 instanceof Double)) {
                Boolean b1 = (Boolean) result1;
                Boolean b2 = (Boolean) result2;
                if (op == 0) {
                    result1 = b1 && b2;
                }
                if (op == 1) {
                    result1 = b1 || b2;
                }
            } else {
                throw ExpressionException.create(
                        'Type mismatch: cannot apply && or || to ' + result1 + ' and ' + result2,
                        opPosition
                        );
            }
        }
        throw ExpressionException.create(LOOP_LIMIT_MESSAGE, lexer.getPosition());
    }

    private Object ternary() {
        Object result1 = logical();

        boolean questionOp = lexer.consumeOnMatch('?');
        if (!questionOp) {
            return result1;
        }
        Integer questionOpPosition = lexer.getMatchPosition();

        Object result2 = logical();

        boolean colonOp = lexer.consumeOnMatch(':');
        if (!colonOp) {
            return result2;
        }

        Object result3 = logical();
        if (result1 instanceof Boolean) {
            if ((Boolean) result1) {
                return result2;
            } else {
                return result3;
            }
        } else {
            throw ExpressionException.create(
                    'Type mismatch: value preceding ? must be a boolean but was ' + result1,
                    questionOpPosition
                    );
        }
    }

    private Object expression() {
        return ternary();
    }

    /**
     * Evaluate the expression.
     */
    public Object interpret() {
        return expression();
    }
}

Further PPS

For some current work we are using an Apex port of JsonLogic. This has the advantage that the parsing of the text (finding tokens, eliminating white space) can be done via a single call to Apex JSON.deserializeUntyped and there is a nice extension model for additional operators.


This is a partial answer as I was very recently faced with this but could be a helpful starting point - evaluates expressions using Reverse Polish Notation

//  --------------------------------------------------------------------
//  rpnCalculate    : calculates an expression using reverse polish notation
//  --------------------------------------------------------------------
public static Decimal rpnCalculate(Object[] rpnExprStack) {
    // Each stack element is either a decimal or a string (+ - * or / )  . Example [0] 35 [1] 7 [2] / divides 35 by 7, returning 5
    Decimal     res = 0;
    Object[]    workRpnExprStack    = new List<Object> ();
    for (Object obj : rpnExprStack)     // make copy of stack as we'll be modifying it
        workRpnExprStack.add(obj);

    while (workRpnExprStack.size() > 1) {
        Object expr0    = workRpnExprStack[0];
        Object expr1    = workRpnExprStack[1];
        Object expr2    = workRpnExprStack.size() >2 ? workRpnExprStack[2] : null;
        //  we either have dec dec operator, operator expr, dec operator, or dec operator expr
        if (expr0 instanceof String)
            throw new FooException('[UTIL-08] Invalid RPN expression stack (dangling operator): ' + rpnExprStack);
        if (expr0 instanceOf Decimal && expr1 instanceOf Decimal && expr2 instanceOf String) {
            String operator = (String) expr2;
            if (operator == '+')
                res     = (Decimal) expr0 + (Decimal) expr1;
            else    
            if (operator == '-')
                res     = (Decimal) expr0 - (Decimal) expr1;
            else
            if (operator == '*')
                res     = (Decimal) expr0 * (Decimal) expr1;
            else
            if (operator == '/')
                res     = (Decimal) expr0 / (Decimal) expr1;
            else
                throw new FooException('[UTIL-08] Invalid RPN expression stack (unsupported operator): ' + rpnExprStack);
            workRpnExprStack.remove(0); // pop expr 0
            workRpnExprStack.remove(0); // pop expr 1
            workRpnExprStack.remove(0); // pop operator
            if (workRpnExprStack.size() > 0)
                workRpnExprStack.add(0,res);// push res to front of stack
            else
                workRpnExprStack.add(res);      
        }   
        else
            throw new FooException('[UTIL-08] Invalid RPN expression stack (expressions/operators): ' + rpnExprStack);


    }
    res     = (Decimal) workRpnExprStack[0];
    return res;
}


@isTest
private static void testRpnCalculate() {
    System.assertEquals(2.0,    rpnCalculate(new List<Object> {10,5,'/'}));
    System.assertEquals(40.0,   rpnCalculate(new List<Object> {10,5,'/',20,'*'}));
    System.assertEquals(10.0,   rpnCalculate(new List<Object> {10}));

}

It presumes the following:

  • Method is within some Utility class
  • Some exception (here called FooException) is defined
  • And the operands, where field values, have been evaluated before the method is called
  • If any operands are null, the method is not called
  • The expression is in Reverse Polish Notation as this avoided the need for writing a recursive descent parser (I was under severe time pressure when I built this)

That said, I agree with Keith C that there are examples in Java of a recursive descent parser that can handle expressions, these would need to be adapted to APEX and some utility class to fetch values of arbitrary fields from arbitrary objects


As an alternative to evaluating the expression yourself, you could let Apex do it by mimicking the Javascript eval() function. This can be done by making a callout to the executeAnonymous API method on either the Tooling or Apex API.

The trick is you need to pass any required input parameters in the eval string body. If a response is required you need a mechanism to extract it.

There are two common ways you can get a response back from executeAnonymous.

  1. Throw a deliberate exception at the end of the execute and include the response. Kevin covers this approach in EVAL() in Apex. Secure Dynamic Code Evaluation on the Salesforce1 Platform.
  2. I used a variation of this approach but returned the response via the debug log rather than an intentional exception. See Adding Eval() support to Apex.

Using my technique the Apex would be something like:

string toEval = 'string stage = \''+Stage+'\';';
toEval += 'integer amount = '+Amount+';';
toEval += 'boolean result = (stage == \'Discovery\' && amount > 2000);'
toEval += 'System.debug(LoggingLevel.Error, result);'

boolean result = soapSforceCom200608Apex.evalBoolean(toEval);

Tags:

Dynamic

Apex