1 /*
2  * Copyright (C) 2018 Google, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.escapevelocity;
17 
18 import com.google.escapevelocity.Parser.Operator;
19 
20 /**
21  * A node in the parse tree representing an expression. Expressions appear inside directives,
22  * specifically {@code #set}, {@code #if}, {@code #foreach}, and macro calls. Expressions can
23  * also appear inside indices in references, like {@code $x[$i]}.
24  *
25  * @author emcmanus@google.com (Éamonn McManus)
26  */
27 abstract class ExpressionNode extends Node {
ExpressionNode(String resourceName, int lineNumber)28   ExpressionNode(String resourceName, int lineNumber) {
29     super(resourceName, lineNumber);
30   }
31 
32   /**
33    * True if evaluating this expression yields a value that is considered true by Velocity's
34    * <a href="http://velocity.apache.org/engine/releases/velocity-1.7/user-guide.html#Conditionals">
35    * rules</a>.  A value is false if it is null or equal to Boolean.FALSE.
36    * Every other value is true.
37    *
38    * <p>Note that the text at the similar link
39    * <a href="http://velocity.apache.org/engine/devel/user-guide.html#Conditionals">here</a>
40    * states that empty collections and empty strings are also considered false, but that is not
41    * true.
42    */
isTrue(EvaluationContext context)43   boolean isTrue(EvaluationContext context) {
44     Object value = evaluate(context);
45     if (value instanceof Boolean) {
46       return (Boolean) value;
47     } else {
48       return value != null;
49     }
50   }
51 
52   /**
53    * True if this is a defined value and it evaluates to true. This is the same as {@link #isTrue}
54    * except that it is allowed for this to be undefined variable, in which it evaluates to false.
55    * The method is overridden for plain references so that undefined is the same as false.
56    * The reason is to support Velocity's idiom {@code #if ($var)}, where it is not an error
57    * if {@code $var} is undefined.
58    */
isDefinedAndTrue(EvaluationContext context)59   boolean isDefinedAndTrue(EvaluationContext context) {
60     return isTrue(context);
61   }
62 
63   /**
64    * The integer result of evaluating this expression.
65    *
66    * @throws EvaluationException if evaluating the expression produces an exception, or if it
67    *     yields a value that is not an integer.
68    */
intValue(EvaluationContext context)69   int intValue(EvaluationContext context) {
70     Object value = evaluate(context);
71     if (!(value instanceof Integer)) {
72       throw evaluationException("Arithemtic is only available on integers, not " + show(value));
73     }
74     return (Integer) value;
75   }
76 
77   /**
78    * Returns a string representing the given value, for use in error messages. The string
79    * includes both the value's {@code toString()} and its type.
80    */
show(Object value)81   private static String show(Object value) {
82     if (value == null) {
83       return "null";
84     } else {
85       return value + " (a " + value.getClass().getName() + ")";
86     }
87   }
88 
89   /**
90    * Represents all binary expressions. In {@code #set ($a = $b + $c)}, this will be the type
91    * of the node representing {@code $b + $c}.
92    */
93   static class BinaryExpressionNode extends ExpressionNode {
94     final ExpressionNode lhs;
95     final Operator op;
96     final ExpressionNode rhs;
97 
BinaryExpressionNode(ExpressionNode lhs, Operator op, ExpressionNode rhs)98     BinaryExpressionNode(ExpressionNode lhs, Operator op, ExpressionNode rhs) {
99       super(lhs.resourceName, lhs.lineNumber);
100       this.lhs = lhs;
101       this.op = op;
102       this.rhs = rhs;
103     }
104 
evaluate(EvaluationContext context)105     @Override Object evaluate(EvaluationContext context) {
106       switch (op) {
107         case OR:
108           return lhs.isTrue(context) || rhs.isTrue(context);
109         case AND:
110           return lhs.isTrue(context) && rhs.isTrue(context);
111         case EQUAL:
112           return equal(context);
113         case NOT_EQUAL:
114           return !equal(context);
115         default: // fall out
116       }
117       int lhsInt = lhs.intValue(context);
118       int rhsInt = rhs.intValue(context);
119       switch (op) {
120         case LESS:
121           return lhsInt < rhsInt;
122         case LESS_OR_EQUAL:
123           return lhsInt <= rhsInt;
124         case GREATER:
125           return lhsInt > rhsInt;
126         case GREATER_OR_EQUAL:
127           return lhsInt >= rhsInt;
128         case PLUS:
129           return lhsInt + rhsInt;
130         case MINUS:
131           return lhsInt - rhsInt;
132         case TIMES:
133           return lhsInt * rhsInt;
134         case DIVIDE:
135           return lhsInt / rhsInt;
136         case REMAINDER:
137           return lhsInt % rhsInt;
138         default:
139           throw new AssertionError(op);
140       }
141     }
142 
143     /**
144      * Returns true if {@code lhs} and {@code rhs} are equal according to Velocity.
145      *
146      * <p>Velocity's <a
147      * href="http://velocity.apache.org/engine/releases/velocity-1.7/vtl-reference-guide.html#aifelseifelse_-_Output_conditional_on_truth_of_statements">definition
148      * of equality</a> differs depending on whether the objects being compared are of the same
149      * class. If so, equality comes from {@code Object.equals} as you would expect.  But if they
150      * are not of the same class, they are considered equal if their {@code toString()} values are
151      * equal. This means that integer 123 equals long 123L and also string {@code "123"}.  It also
152      * means that equality isn't always transitive. For example, two StringBuilder objects each
153      * containing {@code "123"} will not compare equal, even though the string {@code "123"}
154      * compares equal to each of them.
155      */
equal(EvaluationContext context)156     private boolean equal(EvaluationContext context) {
157       Object lhsValue = lhs.evaluate(context);
158       Object rhsValue = rhs.evaluate(context);
159       if (lhsValue == rhsValue) {
160         return true;
161       }
162       if (lhsValue == null || rhsValue == null) {
163         return false;
164       }
165       if (lhsValue.getClass().equals(rhsValue.getClass())) {
166         return lhsValue.equals(rhsValue);
167       }
168       // Funky equals behaviour specified by Velocity.
169       return lhsValue.toString().equals(rhsValue.toString());
170     }
171   }
172 
173   /**
174    * A node in the parse tree representing an expression like {@code !$a}.
175    */
176   static class NotExpressionNode extends ExpressionNode {
177     private final ExpressionNode expr;
178 
NotExpressionNode(ExpressionNode expr)179     NotExpressionNode(ExpressionNode expr) {
180       super(expr.resourceName, expr.lineNumber);
181       this.expr = expr;
182     }
183 
evaluate(EvaluationContext context)184     @Override Object evaluate(EvaluationContext context) {
185       return !expr.isTrue(context);
186     }
187   }
188 }
189