aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/escapevelocity/ExpressionNode.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/escapevelocity/ExpressionNode.java')
-rw-r--r--src/main/java/com/google/escapevelocity/ExpressionNode.java188
1 files changed, 188 insertions, 0 deletions
diff --git a/src/main/java/com/google/escapevelocity/ExpressionNode.java b/src/main/java/com/google/escapevelocity/ExpressionNode.java
new file mode 100644
index 0000000..281e998
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/ExpressionNode.java
@@ -0,0 +1,188 @@
+/*
+ * 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
+ * <a href="http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html#Conditionals">
+ * rules</a>. A value is false if it is null or equal to Boolean.FALSE.
+ * Every other value is true.
+ *
+ * <p>Note that the text at the similar link
+ * <a href="http://velocity.apache.org/engine/devel/user-guide.html#Conditionals">here</a>
+ * 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.
+ *
+ * <p>Velocity's <a
+ * href="http://velocity.apache.org/engine/releases/velocity-1.7/vtl-reference-guide.html#aifelseifelse_-_Output_conditional_on_truth_of_statements">definition
+ * of equality</a> 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);
+ }
+ }
+}