aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/escapevelocity/ExpressionNode.java
blob: 281e998e2e0ffdbb5e88ad1e623fc094db20ec93 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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);
    }
  }
}