• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static java.util.stream.Collectors.toList;
19 
20 import com.google.common.base.Joiner;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableSet;
23 import com.google.common.collect.Iterables;
24 import com.google.common.primitives.Primitives;
25 import java.lang.reflect.InvocationTargetException;
26 import java.lang.reflect.Method;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Optional;
30 
31 /**
32  * A node in the parse tree that is a reference. A reference is anything beginning with {@code $},
33  * such as {@code $x} or {@code $x[$i].foo($j)}.
34  *
35  * @author emcmanus@google.com (Éamonn McManus)
36  */
37 abstract class ReferenceNode extends ExpressionNode {
ReferenceNode(String resourceName, int lineNumber)38   ReferenceNode(String resourceName, int lineNumber) {
39     super(resourceName, lineNumber);
40   }
41 
42   /**
43    * A node in the parse tree that is a plain reference such as {@code $x}. This node may appear
44    * inside a more complex reference like {@code $x.foo}.
45    */
46   static class PlainReferenceNode extends ReferenceNode {
47     final String id;
48 
PlainReferenceNode(String resourceName, int lineNumber, String id)49     PlainReferenceNode(String resourceName, int lineNumber, String id) {
50       super(resourceName, lineNumber);
51       this.id = id;
52     }
53 
evaluate(EvaluationContext context)54     @Override Object evaluate(EvaluationContext context) {
55       if (context.varIsDefined(id)) {
56         return context.getVar(id);
57       } else {
58         throw evaluationException("Undefined reference $" + id);
59       }
60     }
61 
62     @Override
isDefinedAndTrue(EvaluationContext context)63     boolean isDefinedAndTrue(EvaluationContext context) {
64       if (context.varIsDefined(id)) {
65         return isTrue(context);
66       } else {
67         return false;
68       }
69     }
70   }
71 
72   /**
73    * A node in the parse tree that is a reference to a property of another reference, like
74    * {@code $x.foo} or {@code $x[$i].foo}.
75    */
76   static class MemberReferenceNode extends ReferenceNode {
77     final ReferenceNode lhs;
78     final String id;
79 
MemberReferenceNode(ReferenceNode lhs, String id)80     MemberReferenceNode(ReferenceNode lhs, String id) {
81       super(lhs.resourceName, lhs.lineNumber);
82       this.lhs = lhs;
83       this.id = id;
84     }
85 
86     private static final String[] PREFIXES = {"get", "is"};
87     private static final boolean[] CHANGE_CASE = {false, true};
88 
evaluate(EvaluationContext context)89     @Override Object evaluate(EvaluationContext context) {
90       Object lhsValue = lhs.evaluate(context);
91       if (lhsValue == null) {
92         throw evaluationException("Cannot get member " + id + " of null value");
93       }
94       // If this is a Map, then Velocity looks up the property in the map.
95       if (lhsValue instanceof Map<?, ?>) {
96         Map<?, ?> map = (Map<?, ?>) lhsValue;
97         return map.get(id);
98       }
99       // Velocity specifies that, given a reference .foo, it will first look for getfoo() and then
100       // for getFoo(), and likewise given .Foo it will look for getFoo() and then getfoo().
101       for (String prefix : PREFIXES) {
102         for (boolean changeCase : CHANGE_CASE) {
103           String baseId = changeCase ? changeInitialCase(id) : id;
104           String methodName = prefix + baseId;
105           Optional<Method> maybeMethod =
106               context.publicMethodsWithName(lhsValue.getClass(), methodName).stream()
107                   .filter(m -> m.getParameterTypes().length == 0)
108                   .findFirst();
109           if (maybeMethod.isPresent()) {
110             Method method = maybeMethod.get();
111             if (!prefix.equals("is") || method.getReturnType().equals(boolean.class)) {
112               // Don't consider methods that happen to be called isFoo() but don't return boolean.
113               return invokeMethod(method, lhsValue, ImmutableList.of());
114             }
115           }
116         }
117       }
118       throw evaluationException(
119           "Member " + id + " does not correspond to a public getter of " + lhsValue
120               + ", a " + lhsValue.getClass().getName());
121     }
122 
changeInitialCase(String id)123     private static String changeInitialCase(String id) {
124       int initial = id.codePointAt(0);
125       String rest = id.substring(Character.charCount(initial));
126       if (Character.isUpperCase(initial)) {
127         initial = Character.toLowerCase(initial);
128       } else if (Character.isLowerCase(initial)) {
129         initial = Character.toUpperCase(initial);
130       }
131       return new StringBuilder().appendCodePoint(initial).append(rest).toString();
132     }
133   }
134 
135   /**
136    * A node in the parse tree that is an indexing of a reference, like {@code $x[0]} or
137    * {@code $x.foo[$i]}. Indexing is array indexing or calling the {@code get} method of a list
138    * or a map.
139    */
140   static class IndexReferenceNode extends ReferenceNode {
141     final ReferenceNode lhs;
142     final ExpressionNode index;
143 
IndexReferenceNode(ReferenceNode lhs, ExpressionNode index)144     IndexReferenceNode(ReferenceNode lhs, ExpressionNode index) {
145       super(lhs.resourceName, lhs.lineNumber);
146       this.lhs = lhs;
147       this.index = index;
148     }
149 
evaluate(EvaluationContext context)150     @Override Object evaluate(EvaluationContext context) {
151       Object lhsValue = lhs.evaluate(context);
152       if (lhsValue == null) {
153         throw evaluationException("Cannot index null value");
154       }
155       if (lhsValue instanceof List<?>) {
156         Object indexValue = index.evaluate(context);
157         if (!(indexValue instanceof Integer)) {
158           throw evaluationException("List index is not an integer: " + indexValue);
159         }
160         List<?> lhsList = (List<?>) lhsValue;
161         int i = (Integer) indexValue;
162         if (i < 0 || i >= lhsList.size()) {
163           throw evaluationException(
164               "List index " + i + " is not valid for list of size " + lhsList.size());
165         }
166         return lhsList.get(i);
167       } else if (lhsValue instanceof Map<?, ?>) {
168         Object indexValue = index.evaluate(context);
169         Map<?, ?> lhsMap = (Map<?, ?>) lhsValue;
170         return lhsMap.get(indexValue);
171       } else {
172         // In general, $x[$y] is equivalent to $x.get($y). We've covered the most common cases
173         // above, but for other cases like Multimap we resort to evaluating the equivalent form.
174         MethodReferenceNode node = new MethodReferenceNode(lhs, "get", ImmutableList.of(index));
175         return node.evaluate(context);
176       }
177     }
178   }
179 
180   /**
181    * A node in the parse tree representing a method reference, like {@code $list.size()}.
182    */
183   static class MethodReferenceNode extends ReferenceNode {
184     final ReferenceNode lhs;
185     final String id;
186     final List<ExpressionNode> args;
187 
MethodReferenceNode(ReferenceNode lhs, String id, List<ExpressionNode> args)188     MethodReferenceNode(ReferenceNode lhs, String id, List<ExpressionNode> args) {
189       super(lhs.resourceName, lhs.lineNumber);
190       this.lhs = lhs;
191       this.id = id;
192       this.args = args;
193     }
194 
195     /**
196      * {@inheritDoc}
197      *
198      * <p>Evaluating a method expression such as {@code $x.foo($y)} involves looking at the actual
199      * types of {@code $x} and {@code $y}. The type of {@code $x} must have a public method
200      * {@code foo} with a parameter type that is compatible with {@code $y}.
201      *
202      * <p>Currently we don't allow there to be more than one matching method. That is a difference
203      * from Velocity, which blithely allows you to invoke {@link List#remove(int)} even though it
204      * can't really know that you didn't mean to invoke {@link List#remove(Object)} with an Object
205      * that just happens to be an Integer.
206      *
207      * <p>The method to be invoked must be visible in a public class or interface that is either the
208      * class of {@code $x} itself or one of its supertypes. Allowing supertypes is important because
209      * you may want to invoke a public method like {@link List#size()} on a list whose class is not
210      * public, such as the list returned by {@link java.util.Collections#singletonList}.
211      */
evaluate(EvaluationContext context)212     @Override Object evaluate(EvaluationContext context) {
213       Object lhsValue = lhs.evaluate(context);
214       if (lhsValue == null) {
215         throw evaluationException("Cannot invoke method " + id + " on null value");
216       }
217       try {
218         return evaluate(context, lhsValue, lhsValue.getClass());
219       } catch (EvaluationException e) {
220         // If this is a Class, try invoking a static method of the class it refers to.
221         // This is what Apache Velocity does. If the method exists as both an instance method of
222         // Class and a static method of the referenced class, then it is the instance method of
223         // Class that wins, again consistent with Velocity.
224         if (lhsValue instanceof Class<?>) {
225           return evaluate(context, null, (Class<?>) lhsValue);
226         }
227         throw e;
228       }
229     }
230 
evaluate(EvaluationContext context, Object lhsValue, Class<?> targetClass)231     private Object evaluate(EvaluationContext context, Object lhsValue, Class<?> targetClass) {
232       List<Object> argValues = args.stream()
233           .map(arg -> arg.evaluate(context))
234           .collect(toList());
235       ImmutableSet<Method> publicMethodsWithName = context.publicMethodsWithName(targetClass, id);
236       if (publicMethodsWithName.isEmpty()) {
237         throw evaluationException("No method " + id + " in " + targetClass.getName());
238       }
239       List<Method> compatibleMethods = publicMethodsWithName.stream()
240           .filter(method -> compatibleArgs(method.getParameterTypes(), argValues))
241           .collect(toList());
242           // TODO(emcmanus): support varargs, if it's useful
243       if (compatibleMethods.size() > 1) {
244         compatibleMethods =
245             compatibleMethods.stream().filter(method -> !method.isSynthetic()).collect(toList());
246       }
247       switch (compatibleMethods.size()) {
248         case 0:
249           throw evaluationException(
250               "Parameters for method " + id + " have wrong types: " + argValues);
251         case 1:
252           return invokeMethod(Iterables.getOnlyElement(compatibleMethods), lhsValue, argValues);
253         default:
254           throw evaluationException(
255               "Ambiguous method invocation, could be one of:\n  "
256               + Joiner.on("\n  ").join(compatibleMethods));
257       }
258     }
259 
260     /**
261      * Determines if the given argument list is compatible with the given parameter types. This
262      * includes an {@code Integer} argument being compatible with a parameter of type {@code int} or
263      * {@code long}, for example.
264      */
compatibleArgs(Class<?>[] paramTypes, List<Object> argValues)265     static boolean compatibleArgs(Class<?>[] paramTypes, List<Object> argValues) {
266       if (paramTypes.length != argValues.size()) {
267         return false;
268       }
269       for (int i = 0; i < paramTypes.length; i++) {
270         Class<?> paramType = paramTypes[i];
271         Object argValue = argValues.get(i);
272         if (paramType.isPrimitive()) {
273           return primitiveIsCompatible(paramType, argValue);
274         } else if (argValue != null && !paramType.isInstance(argValue)) {
275           return false;
276         }
277       }
278       return true;
279     }
280 
primitiveIsCompatible(Class<?> primitive, Object value)281     private static boolean primitiveIsCompatible(Class<?> primitive, Object value) {
282       if (value == null || !Primitives.isWrapperType(value.getClass())) {
283         return false;
284       }
285       return primitiveTypeIsAssignmentCompatible(primitive, Primitives.unwrap(value.getClass()));
286     }
287 
288     private static final ImmutableList<Class<?>> NUMERICAL_PRIMITIVES = ImmutableList.of(
289         byte.class, short.class, int.class, long.class, float.class, double.class);
290     private static final int INDEX_OF_INT = NUMERICAL_PRIMITIVES.indexOf(int.class);
291 
292     /**
293      * Returns true if {@code from} can be assigned to {@code to} according to
294      * <a href="https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.1.2">Widening
295      * Primitive Conversion</a>.
296      */
primitiveTypeIsAssignmentCompatible(Class<?> to, Class<?> from)297     static boolean primitiveTypeIsAssignmentCompatible(Class<?> to, Class<?> from) {
298       // To restate the JLS rules, f can be assigned to t if:
299       // - they are the same; or
300       // - f is char and t is a numeric type at least as wide as int; or
301       // - f comes before t in the order byte, short, int, long, float, double.
302       if (to == from) {
303         return true;
304       }
305       int toI = NUMERICAL_PRIMITIVES.indexOf(to);
306       if (toI < 0) {
307         return false;
308       }
309       if (from == char.class) {
310         return toI >= INDEX_OF_INT;
311       }
312       int fromI = NUMERICAL_PRIMITIVES.indexOf(from);
313       if (fromI < 0) {
314         return false;
315       }
316       return toI >= fromI;
317     }
318   }
319 
320   /**
321    * Invoke the given method on the given target with the given arguments.
322    */
invokeMethod(Method method, Object target, List<Object> argValues)323   Object invokeMethod(Method method, Object target, List<Object> argValues) {
324     try {
325       return method.invoke(target, argValues.toArray());
326     } catch (InvocationTargetException e) {
327       throw evaluationException(e.getCause());
328     } catch (Exception e) {
329       throw evaluationException(e);
330     }
331   }
332 }
333