aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/escapevelocity/ReferenceNode.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/escapevelocity/ReferenceNode.java')
-rw-r--r--src/main/java/com/google/escapevelocity/ReferenceNode.java176
1 files changed, 44 insertions, 132 deletions
diff --git a/src/main/java/com/google/escapevelocity/ReferenceNode.java b/src/main/java/com/google/escapevelocity/ReferenceNode.java
index 4b43f77..622388f 100644
--- a/src/main/java/com/google/escapevelocity/ReferenceNode.java
+++ b/src/main/java/com/google/escapevelocity/ReferenceNode.java
@@ -15,16 +15,18 @@
*/
package com.google.escapevelocity;
+import static java.util.stream.Collectors.toList;
+
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Primitives;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
* A node in the parse tree that is a reference. A reference is anything beginning with {@code $},
@@ -89,21 +91,27 @@ abstract class ReferenceNode extends ExpressionNode {
if (lhsValue == null) {
throw evaluationException("Cannot get member " + id + " of null value");
}
+ // If this is a Map, then Velocity looks up the property in the map.
+ if (lhsValue instanceof Map<?, ?>) {
+ Map<?, ?> map = (Map<?, ?>) lhsValue;
+ return map.get(id);
+ }
// Velocity specifies that, given a reference .foo, it will first look for getfoo() and then
// for getFoo(), and likewise given .Foo it will look for getFoo() and then getfoo().
for (String prefix : PREFIXES) {
for (boolean changeCase : CHANGE_CASE) {
String baseId = changeCase ? changeInitialCase(id) : id;
String methodName = prefix + baseId;
- Method method;
- try {
- method = lhsValue.getClass().getMethod(methodName);
+ Optional<Method> maybeMethod =
+ context.publicMethodsWithName(lhsValue.getClass(), methodName).stream()
+ .filter(m -> m.getParameterTypes().length == 0)
+ .findFirst();
+ if (maybeMethod.isPresent()) {
+ Method method = maybeMethod.get();
if (!prefix.equals("is") || method.getReturnType().equals(boolean.class)) {
// Don't consider methods that happen to be called isFoo() but don't return boolean.
return invokeMethod(method, lhsValue, ImmutableList.of());
}
- } catch (NoSuchMethodException e) {
- // Continue with next possibility
}
}
}
@@ -206,25 +214,35 @@ abstract class ReferenceNode extends ExpressionNode {
if (lhsValue == null) {
throw evaluationException("Cannot invoke method " + id + " on null value");
}
- List<Object> argValues = new ArrayList<>();
- for (ExpressionNode arg : args) {
- argValues.add(arg.evaluate(context));
- }
- List<Method> methodsWithName = new ArrayList<>();
- for (Method method : lhsValue.getClass().getMethods()) {
- if (method.getName().equals(id) && !method.isSynthetic()) {
- methodsWithName.add(method);
+ try {
+ return evaluate(context, lhsValue, lhsValue.getClass());
+ } catch (EvaluationException e) {
+ // If this is a Class, try invoking a static method of the class it refers to.
+ // This is what Apache Velocity does. If the method exists as both an instance method of
+ // Class and a static method of the referenced class, then it is the instance method of
+ // Class that wins, again consistent with Velocity.
+ if (lhsValue instanceof Class<?>) {
+ return evaluate(context, null, (Class<?>) lhsValue);
}
+ throw e;
}
- if (methodsWithName.isEmpty()) {
- throw evaluationException("No method " + id + " in " + lhsValue.getClass().getName());
+ }
+
+ private Object evaluate(EvaluationContext context, Object lhsValue, Class<?> targetClass) {
+ List<Object> argValues = args.stream()
+ .map(arg -> arg.evaluate(context))
+ .collect(toList());
+ ImmutableSet<Method> publicMethodsWithName = context.publicMethodsWithName(targetClass, id);
+ if (publicMethodsWithName.isEmpty()) {
+ throw evaluationException("No method " + id + " in " + targetClass.getName());
}
- List<Method> compatibleMethods = new ArrayList<>();
- for (Method method : methodsWithName) {
- // TODO(emcmanus): support varargs, if it's useful
- if (compatibleArgs(method.getParameterTypes(), argValues)) {
- compatibleMethods.add(method);
- }
+ List<Method> compatibleMethods = publicMethodsWithName.stream()
+ .filter(method -> compatibleArgs(method.getParameterTypes(), argValues))
+ .collect(toList());
+ // TODO(emcmanus): support varargs, if it's useful
+ if (compatibleMethods.size() > 1) {
+ compatibleMethods =
+ compatibleMethods.stream().filter(method -> !method.isSynthetic()).collect(toList());
}
switch (compatibleMethods.size()) {
case 0:
@@ -253,7 +271,7 @@ abstract class ReferenceNode extends ExpressionNode {
Object argValue = argValues.get(i);
if (paramType.isPrimitive()) {
return primitiveIsCompatible(paramType, argValue);
- } else if (!paramType.isInstance(argValue)) {
+ } else if (argValue != null && !paramType.isInstance(argValue)) {
return false;
}
}
@@ -267,7 +285,7 @@ abstract class ReferenceNode extends ExpressionNode {
return primitiveTypeIsAssignmentCompatible(primitive, Primitives.unwrap(value.getClass()));
}
- private static final ImmutableList<Class<?>> NUMERICAL_PRIMITIVES = ImmutableList.<Class<?>>of(
+ private static final ImmutableList<Class<?>> NUMERICAL_PRIMITIVES = ImmutableList.of(
byte.class, short.class, int.class, long.class, float.class, double.class);
private static final int INDEX_OF_INT = NUMERICAL_PRIMITIVES.indexOf(int.class);
@@ -300,21 +318,9 @@ abstract class ReferenceNode extends ExpressionNode {
}
/**
- * Invoke the given method on the given target with the given arguments. The method is expected
- * to be public, but the class it is in might not be. In that case we will search up the
- * hierarchy for an ancestor that is public and has the same method, and use that to invoke the
- * method. Otherwise we would get an {@link IllegalAccessException}. More than one ancestor might
- * define the method, but it doesn't matter which one we invoke since ultimately the code that
- * will run will be the same.
+ * Invoke the given method on the given target with the given arguments.
*/
Object invokeMethod(Method method, Object target, List<Object> argValues) {
- if (!classIsPublic(target.getClass())) {
- method = visibleMethod(method, target.getClass());
- if (method == null) {
- throw evaluationException(
- "Method is not visible in class " + target.getClass().getName() + ": " + method);
- }
- }
try {
return method.invoke(target, argValues.toArray());
} catch (InvocationTargetException e) {
@@ -323,98 +329,4 @@ abstract class ReferenceNode extends ExpressionNode {
throw evaluationException(e);
}
}
-
- private static String packageNameOf(Class<?> c) {
- String name = c.getName();
- int lastDot = name.lastIndexOf('.');
- if (lastDot > 0) {
- return name.substring(0, lastDot);
- } else {
- return "";
- }
- }
-
- private static final String THIS_PACKAGE = packageNameOf(Node.class) + ".";
-
- /**
- * Returns a Method with the same name and parameter types as the given one, but that is in a
- * public class or interface. This might be the given method, or it might be a method in a
- * superclass or superinterface.
- *
- * @return a public method in a public class or interface, or null if none was found.
- */
- static Method visibleMethod(Method method, Class<?> in) {
- if (in == null) {
- return null;
- }
- Method methodInClass;
- try {
- methodInClass = in.getMethod(method.getName(), method.getParameterTypes());
- } catch (NoSuchMethodException e) {
- return null;
- }
- if (classIsPublic(in) || in.getName().startsWith(THIS_PACKAGE)) {
- // The second disjunct is a hack to allow us to use the methods of $foreach without having
- // to make the ForEachVar class public. We can invoke those methods from here since they
- // are in the same package.
- return methodInClass;
- }
- Method methodSuper = visibleMethod(method, in.getSuperclass());
- if (methodSuper != null) {
- return methodSuper;
- }
- for (Class<?> intf : in.getInterfaces()) {
- Method methodIntf = visibleMethod(method, intf);
- if (methodIntf != null) {
- return methodIntf;
- }
- }
- return null;
- }
-
- /**
- * Returns whether the given class is public as seen from this class. Prior to Java 9, a class
- * was either public or not public. But with the introduction of modules in Java 9, a class can
- * be marked public and yet not be visible, if it is not exported from the module it appears in.
- * So, on Java 9, we perform an additional check on class {@code c}, which is effectively
- * {@code c.getModule().isExported(c.getPackageName())}. We use reflection so that the code can
- * compile on earlier Java versions.
- */
- private static boolean classIsPublic(Class<?> c) {
- if (!Modifier.isPublic(c.getModifiers())) {
- return false;
- }
- if (CLASS_GET_MODULE_METHOD != null) {
- return classIsExported(c);
- }
- return true;
- }
-
- private static boolean classIsExported(Class<?> c) {
- try {
- String pkg = packageNameOf(c);
- Object module = CLASS_GET_MODULE_METHOD.invoke(c);
- return (Boolean) MODULE_IS_EXPORTED_METHOD.invoke(module, pkg);
- } catch (Exception e) {
- return false;
- }
- }
-
- private static final Method CLASS_GET_MODULE_METHOD;
- private static final Method MODULE_IS_EXPORTED_METHOD;
-
- static {
- Method classGetModuleMethod;
- Method moduleIsExportedMethod;
- try {
- classGetModuleMethod = Class.class.getMethod("getModule");
- Class<?> moduleClass = classGetModuleMethod.getReturnType();
- moduleIsExportedMethod = moduleClass.getMethod("isExported", String.class);
- } catch (Exception e) {
- classGetModuleMethod = null;
- moduleIsExportedMethod = null;
- }
- CLASS_GET_MODULE_METHOD = classGetModuleMethod;
- MODULE_IS_EXPORTED_METHOD = moduleIsExportedMethod;
- }
}