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

com.google.escapevelocity.ExpressionNode Maven / Gradle / Ivy

The newest version!
/*
 * 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]}.
 *
 * 

Nontrivial expressions are represented by a tree of {@link ExpressionNode} objects. For * example, in {@code #if ($foo.bar < 3)}, the expression {@code $foo.bar < 3} is parsed into * a tree that we might describe as

 * {@link BinaryExpressionNode}(
 *     {@link ReferenceNode.MemberReferenceNode}(
 *         {@link ReferenceNode.PlainReferenceNode}("foo"),
 *         "bar"),
 *     {@link Operator#LESS},
 *     {@link ConstantExpressionNode}(3))
 * 
* * @author [email protected] (Éamonn McManus) */ abstract class ExpressionNode extends Node { ExpressionNode(String resourceName, int lineNumber) { super(resourceName, lineNumber); } @Override final void render(EvaluationContext context, StringBuilder output) { Object rendered = evaluate(context); if (rendered == null) { if (isSilent()) { // $!foo for example return; } throw evaluationException("Null value for " + this); } if (rendered instanceof Node) { // A macro's $bodyContent, or $x when we earlier did #define ($x) ... #end ((Node) rendered).render(context, output); } else { output.append(rendered); } } /** * Returns the source form of this node. This may not be exactly how it appears in the template. * For example both {@code $x} and {@code ${x}} end up being the same kind of node, and its * {@code toString()} is {@code "$x"}. * *

This method is used in error messages. It is not invoked in normal template evaluation. */ @Override public abstract String toString(); /** * Returns the result of evaluating this node in the given context. This result may be used as * part of a further operation, for example evaluating {@code 2 + 3} to 5 in order to set * {@code $x} to 5 in {@code #set ($x = 2 + 3)}. Or it may be used directly as part of the * template output, for example evaluating replacing {@code name} by {@code Fred} in * {@code My name is $name.}. * *

This has to be an {@code Object} rather than a {@code String} (or rather than appending to * a {@code StringBuilder}) because it can potentially participate in other operations. In the * preceding example, the nodes representing {@code 2} and {@code 3} and the node representing * {@code 2 + 3} all return {@code Integer}. As another example, in {@code #if ($foo.bar < 3)} * the value {@code $foo} is itself an {@link ExpressionNode} which evaluates to the object * referenced by the {@code foo} variable, and {@code $foo.bar} is another {@link ExpressionNode} * that takes the value from {@code foo} and looks for the property {@code bar} in it. * * @param context the context of the evaluation, for example the {@code $variables} that are in * scope * @param undefinedIsFalse whether an undefined plain reference like {@code $foo} is considered * to be false. This is the case when evaluating the condition in an {@code #if}. Everywhere * else, an undefined reference causes an exception. */ abstract Object evaluate(EvaluationContext context, boolean undefinedIsFalse); final Object evaluate(EvaluationContext context) { return evaluate(context, /* undefinedIsFalse= */ false); } /** * 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 * what Velocity actually implements. */ boolean isTrue(EvaluationContext context, boolean undefinedIsFalse) { Object value = evaluate(context, undefinedIsFalse); if (value instanceof Boolean) { return (Boolean) value; } else { return value != null; } } /** * True if a null value for this expression is silently translated to an empty string when * substituted into template text. Otherwise it results in an exception. */ boolean isSilent() { return false; } /** * The integer result of evaluating this expression, or null if the expression evaluates to null. * * @throws EvaluationException if evaluating the expression produces an exception, or if it * yields a value that is neither an integer nor null. */ Integer intValue(EvaluationContext context) { Object value = evaluate(context); if (value == null) { return null; } if (!(value instanceof Integer)) { throw evaluationException("Arithmetic 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 public String toString() { return operandString(lhs) + " " + op + " " + operandString(rhs); } // Restore the parentheses in, for example, (2 + 3) * 4. private String operandString(ExpressionNode operand) { String s = String.valueOf(operand); if (operand instanceof BinaryExpressionNode) { BinaryExpressionNode binaryOperand = (BinaryExpressionNode) operand; if (binaryOperand.op.precedence < op.precedence) { return "(" + s + ")"; } } return s; } @Override Object evaluate(EvaluationContext context, boolean undefinedIsFalse) { switch (op) { case OR: return lhs.isTrue(context, undefinedIsFalse) || rhs.isTrue(context, undefinedIsFalse); case AND: return lhs.isTrue(context, undefinedIsFalse) && rhs.isTrue(context, undefinedIsFalse); case EQUAL: return equal(context); case NOT_EQUAL: return !equal(context); case PLUS: return plus(context); default: // fall out } Integer lhsInt = lhs.intValue(context); Integer rhsInt = rhs.intValue(context); if (lhsInt == null || rhsInt == null) { return nullOperand(lhsInt == null); } 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 MINUS: return lhsInt - rhsInt; case TIMES: return lhsInt * rhsInt; case DIVIDE: return (rhsInt == 0) ? null : lhsInt / rhsInt; case REMAINDER: return (rhsInt == 0) ? null : lhsInt % rhsInt; default: throw new AssertionError(op); } } // Mimic Velocity's null-handling. private Void nullOperand(boolean leftIsNull) { if (op.isInequality()) { // If both are null we'll only complain about the left one. String operand = leftIsNull ? "Left operand " + lhs : "Right operand " + rhs; throw evaluationException(operand + " of " + op + " must not be null"); } return null; } /** * 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()); } private Object plus(EvaluationContext context) { Object lhsValue = lhs.evaluate(context); Object rhsValue = rhs.evaluate(context); if (lhsValue instanceof String || rhsValue instanceof String) { // Velocity's treatment of null is all over the map. In a string concatenation, a null // reference is replaced by the the source text of the reference, for example "$foo". The // toString() that we have for the various ExpressionNode subtypes reproduces this at least // in our test cases. if (lhsValue == null) { lhsValue = lhs.toString(); } if (rhsValue == null) { rhsValue = rhs.toString(); } return new StringBuilder().append(lhsValue).append(rhsValue).toString(); } if (lhsValue == null || rhsValue == null) { return null; } if (!(lhsValue instanceof Integer) || !(rhsValue instanceof Integer)) { throw evaluationException( "Operands of + must both be integers, or at least one must be a string: " + show(lhsValue) + " + " + show(rhsValue)); } return (Integer) lhsValue + (Integer) rhsValue; } } /** * 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 public String toString() { if (expr instanceof BinaryExpressionNode) { return "!(" + expr + ")"; } return "!" + expr; } @Override Object evaluate(EvaluationContext context, boolean undefinedIsFalse) { return !expr.isTrue(context, undefinedIsFalse); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy