aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/com/google/escapevelocity/MethodFinder.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/escapevelocity/MethodFinder.java')
-rw-r--r--src/main/java/com/google/escapevelocity/MethodFinder.java172
1 files changed, 172 insertions, 0 deletions
diff --git a/src/main/java/com/google/escapevelocity/MethodFinder.java b/src/main/java/com/google/escapevelocity/MethodFinder.java
new file mode 100644
index 0000000..f8f91f5
--- /dev/null
+++ b/src/main/java/com/google/escapevelocity/MethodFinder.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2019 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 static com.google.common.reflect.Reflection.getPackageName;
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Table;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Finds public methods in a class. For each one, it determines the public class or interface in
+ * which it is declared. This avoids a problem with reflection, where we get an exception if we call
+ * a {@code Method} in a non-public class, even if the {@code Method} is public and if there is a
+ * public ancestor class or interface that declares it. We need to use the {@code Method} from the
+ * public ancestor.
+ *
+ * <p>Because looking for these methods is relatively expensive, an instance of this class will keep
+ * a cache of methods it previously discovered.
+ */
+class MethodFinder {
+
+ /**
+ * For a given class and name, returns all public methods of that name in the class, as previously
+ * determined by {@link #publicMethodsWithName}. The set of methods for a given class and name is
+ * saved the first time it is searched for, and returned directly thereafter. It may be empty.
+ *
+ * <p>Currently we add the entry for any given (class, name) pair on demand. An alternative would
+ * be to add all the methods for a given class at once. With the current scheme, we may end up
+ * calling {@link Class#getMethods()} several times for the same class, if methods of the
+ * different names are called at different times. With an all-at-once scheme, we might end up
+ * computing and storing information about a bunch of methods that will never be called. Because
+ * the profiling that led to the creation of this class revealed that {@link #visibleMethods} in
+ * particular is quite expensive, it's probably best to avoid calling it unnecessarily.
+ */
+ private final Table<Class<?>, String, ImmutableSet<Method>> methodCache = HashBasedTable.create();
+
+ /**
+ * Returns the set of public methods with the given name in the given class. Here, "public
+ * methods" means public methods in public classes or interfaces. If {@code startClass} is not
+ * itself public, its methods are effectively not public either, but inherited methods may still
+ * appear in the returned set, with the {@code Method} objects belonging to public ancestors. More
+ * than one ancestor may define an appropriate method, but it doesn't matter because invoking any
+ * of those {@code Method} objects will have the same effect.
+ */
+ synchronized ImmutableSet<Method> publicMethodsWithName(Class<?> startClass, String name) {
+ ImmutableSet<Method> cachedMethods = methodCache.get(startClass, name);
+ if (cachedMethods == null) {
+ cachedMethods = uncachedPublicMethodsWithName(startClass, name);
+ methodCache.put(startClass, name, cachedMethods);
+ }
+ return cachedMethods;
+ }
+
+ private ImmutableSet<Method> uncachedPublicMethodsWithName(Class<?> startClass, String name) {
+ // Class.getMethods() only returns public methods, so no need to filter explicitly for public.
+ Set<Method> methods =
+ Arrays.stream(startClass.getMethods())
+ .filter(m -> m.getName().equals(name))
+ .collect(toSet());
+ if (!classIsPublic(startClass)) {
+ methods =
+ methods.stream()
+ .map(m -> visibleMethod(m, startClass))
+ .filter(Objects::nonNull)
+ .collect(toSet());
+ // It would be a bit simpler to use ImmutableSet.toImmutableSet() here, but there've been
+ // problems in the past with versions of Guava that don't have that method.
+ }
+ return ImmutableSet.copyOf(methods);
+ }
+
+ private static final String THIS_PACKAGE = getPackageName(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 public methods of $foreach without
+ // having to make the ForEachVar class public. We can invoke those methods from the same
+ // package since ForEachVar is package-protected.
+ return methodInClass;
+ }
+ Method methodInSuperclass = visibleMethod(method, in.getSuperclass());
+ if (methodInSuperclass != null) {
+ return methodInSuperclass;
+ }
+ for (Class<?> superinterface : in.getInterfaces()) {
+ Method methodInSuperinterface = visibleMethod(method, superinterface);
+ if (methodInSuperinterface != null) {
+ return methodInSuperinterface;
+ }
+ }
+ 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) {
+ return Modifier.isPublic(c.getModifiers()) && classIsExported(c);
+ }
+
+ private static boolean classIsExported(Class<?> c) {
+ if (CLASS_GET_MODULE_METHOD == null) {
+ return true; // There are no modules, so all classes are exported.
+ }
+ try {
+ String pkg = getPackageName(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;
+ }
+}