/* * Copyright (C) 2018 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.escapevelocity; import com.google.escapevelocity.Parser.Operator; /** * A node in the parse tree representing an expression. Expressions appear inside directives, * specifically {@code #set}, {@code #if}, {@code #foreach}, and macro calls. Expressions can * also appear inside indices in references, like {@code $x[$i]}. * * @author emcmanus@google.com (Éamonn McManus) */ abstract class ExpressionNode extends Node { ExpressionNode(String resourceName, int lineNumber) { super(resourceName, lineNumber); } /** * True if evaluating this expression yields a value that is considered true by Velocity's * * rules. A value is false if it is null or equal to Boolean.FALSE. * Every other value is true. * *

Note that the text at the similar link * here * states that empty collections and empty strings are also considered false, but that is not * true. */ boolean isTrue(EvaluationContext context) { Object value = evaluate(context); if (value instanceof Boolean) { return (Boolean) value; } else { return value != null; } } /** * True if this is a defined value and it evaluates to true. This is the same as {@link #isTrue} * except that it is allowed for this to be undefined variable, in which it evaluates to false. * The method is overridden for plain references so that undefined is the same as false. * The reason is to support Velocity's idiom {@code #if ($var)}, where it is not an error * if {@code $var} is undefined. */ boolean isDefinedAndTrue(EvaluationContext context) { return isTrue(context); } /** * The integer result of evaluating this expression. * * @throws EvaluationException if evaluating the expression produces an exception, or if it * yields a value that is not an integer. */ int intValue(EvaluationContext context) { Object value = evaluate(context); if (!(value instanceof Integer)) { throw evaluationException("Arithemtic is only available on integers, not " + show(value)); } return (Integer) value; } /** * Returns a string representing the given value, for use in error messages. The string * includes both the value's {@code toString()} and its type. */ private static String show(Object value) { if (value == null) { return "null"; } else { return value + " (a " + value.getClass().getName() + ")"; } } /** * Represents all binary expressions. In {@code #set ($a = $b + $c)}, this will be the type * of the node representing {@code $b + $c}. */ static class BinaryExpressionNode extends ExpressionNode { final ExpressionNode lhs; final Operator op; final ExpressionNode rhs; BinaryExpressionNode(ExpressionNode lhs, Operator op, ExpressionNode rhs) { super(lhs.resourceName, lhs.lineNumber); this.lhs = lhs; this.op = op; this.rhs = rhs; } @Override Object evaluate(EvaluationContext context) { switch (op) { case OR: return lhs.isTrue(context) || rhs.isTrue(context); case AND: return lhs.isTrue(context) && rhs.isTrue(context); case EQUAL: return equal(context); case NOT_EQUAL: return !equal(context); default: // fall out } int lhsInt = lhs.intValue(context); int rhsInt = rhs.intValue(context); switch (op) { case LESS: return lhsInt < rhsInt; case LESS_OR_EQUAL: return lhsInt <= rhsInt; case GREATER: return lhsInt > rhsInt; case GREATER_OR_EQUAL: return lhsInt >= rhsInt; case PLUS: return lhsInt + rhsInt; case MINUS: return lhsInt - rhsInt; case TIMES: return lhsInt * rhsInt; case DIVIDE: return lhsInt / rhsInt; case REMAINDER: return lhsInt % rhsInt; default: throw new AssertionError(op); } } /** * Returns true if {@code lhs} and {@code rhs} are equal according to Velocity. * *

Velocity's definition * of equality differs depending on whether the objects being compared are of the same * class. If so, equality comes from {@code Object.equals} as you would expect. But if they * are not of the same class, they are considered equal if their {@code toString()} values are * equal. This means that integer 123 equals long 123L and also string {@code "123"}. It also * means that equality isn't always transitive. For example, two StringBuilder objects each * containing {@code "123"} will not compare equal, even though the string {@code "123"} * compares equal to each of them. */ private boolean equal(EvaluationContext context) { Object lhsValue = lhs.evaluate(context); Object rhsValue = rhs.evaluate(context); if (lhsValue == rhsValue) { return true; } if (lhsValue == null || rhsValue == null) { return false; } if (lhsValue.getClass().equals(rhsValue.getClass())) { return lhsValue.equals(rhsValue); } // Funky equals behaviour specified by Velocity. return lhsValue.toString().equals(rhsValue.toString()); } } /** * A node in the parse tree representing an expression like {@code !$a}. */ static class NotExpressionNode extends ExpressionNode { private final ExpressionNode expr; NotExpressionNode(ExpressionNode expr) { super(expr.resourceName, expr.lineNumber); this.expr = expr; } @Override Object evaluate(EvaluationContext context) { return !expr.isTrue(context); } } }