diff --git a/src/java/RJavaTools.java b/src/java/RJavaTools.java
index 3b52c85..6890b56 100644
--- a/src/java/RJavaTools.java
+++ b/src/java/RJavaTools.java
@@ -17,13 +17,20 @@
// You should have received a copy of the GNU General Public License
// along with rJava. If not, see .
-import java.lang.reflect.Method ;
-import java.lang.reflect.Field ;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodHandles.Lookup;
+import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor ;
+import java.lang.reflect.Executable;
+import java.lang.reflect.Field ;
import java.lang.reflect.InvocationTargetException ;
-import java.lang.reflect.Modifier ;
import java.lang.reflect.Member ;
-
+import java.lang.reflect.Method ;
+import java.lang.reflect.Modifier ;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashSet;
import java.util.Vector ;
@@ -35,6 +42,23 @@
*/
public class RJavaTools {
+ /* dual path for Java 8 and 9+ to check actual accessibility */
+ private static final Method CAN_ACCESS;
+ /* Java 8 accessibility checker */
+ private static final Lookup LOOKUP = MethodHandles.publicLookup();
+
+ static {
+ Method canAccess = null;
+ try {
+ canAccess = Executable.class.getMethod("canAccess", Object.class);
+ } catch (NoSuchMethodException e) {
+ // Java 8-
+ } catch (SecurityException e) {
+ // Java 8-
+ }
+ CAN_ACCESS = canAccess;
+ }
+
/**
* Returns an inner class of the class with the given simple name
*
@@ -328,7 +352,97 @@ public static boolean hasMethod(Object o, String name) {
return classHasMethod(o.getClass(), name, false);
}
+ /**
+ * Tests whether a given executable is accessible without actually calling {@link AccessibleObject#isAccessible()} which is unreliable.
+ *
+ * @param executable the given method/constructor
+ * @param o the target object
+ * @return true if the method can be accessed from RJavaTools
+ */
+ public static boolean canAccess(Executable executable, Object o){
+ try {
+ if (CAN_ACCESS != null) {
+ /* Java 9+ path */
+ return (boolean) (Boolean) CAN_ACCESS.invoke(executable, o);
+ } else {
+ /* Java 8 path */
+ if (executable instanceof Method)
+ LOOKUP.unreflect((Method) executable);
+ else
+ LOOKUP.unreflectConstructor((Constructor) executable);
+ return true;
+ }
+ } catch (Exception e) {
+ return false;
+ }
+ }
+ /**
+ * Resolves a bridge method to the override one
+ * @param bridgeMethod the potentially bridge method
+ * @return a method that isn't a bridge method
+ */
+ public static Method resolveBridge(Method bridgeMethod) {
+ // accounts for bridge methods which may have slightly different parameters
+ Method best = null;
+ if (bridgeMethod.isBridge()) {
+ Class>[] bridgeParams = bridgeMethod.getParameterTypes();
+ // look for non-bridge methods in the same class that accepts wider parameters
+ nextMethod:
+ for (Method method: bridgeMethod.getDeclaringClass().getDeclaredMethods()) {
+ if (!method.isBridge() && !method.isSynthetic() && method.getName().equals(bridgeMethod.getName())
+ && method.getParameterCount() == bridgeMethod.getParameterCount()) {
+ Class>[] testParams = method.getParameterTypes();
+ // Bridge params should be supertypes of target params (erasure widening).
+ for (int i = 0; i < testParams.length; i++) {
+ if (!testParams[i].isAssignableFrom(bridgeParams[i])) continue nextMethod;
+ }
+ if (bridgeMethod.getReturnType().isAssignableFrom(method.getReturnType()) && (best == null || isMoreSpecific(method, best))) best = method;
+ }
+ }
+ }
+
+ return best == null ? bridgeMethod : best;
+ }
+
+ /**
+ * Finds a mathod that accepts the given instance and is accessible from RJavaTools class.
+ *
Avoids calling isAccessible/setAccessible that fails on Java 17+
+ *
+ * @param method The most specific, possibly non-accessible, method
+ * @param target the instance upon which the method is being invoked (null for static)
+ * @return
+ */
+ public static Method findAccessible(Method method, Object target) {
+ if (canAccess(method, target)) return method;
+
+ Deque> supers = new ArrayDeque>();
+ HashSet> visited = new HashSet>();
+ supers.add(target.getClass());
+
+ while (!supers.isEmpty()) {
+ Class> c = supers.pop();
+ if (visited.add(c)) {
+ Method superMethod;
+ try {
+ // first update the method to be non-bridge
+ method = resolveBridge(method);
+ superMethod = c.getMethod(method.getName(), method.getParameterTypes());
+ /* if an accessible executable is found stop here */
+ if (canAccess(superMethod, target)) return superMethod;
+ if (!c.isInterface() && !c.isArray()) supers.add(c.getSuperclass());
+ if (!c.isArray()) supers.addAll(Arrays.asList(c.getInterfaces()));
+ } catch (NoSuchMethodException e1) {
+ /* executable not found in current class (and any of its ancestors) */
+ } catch (SecurityException e1) {
+ /* executable not found in current class (and any of its ancestors) */
+ }
+ }
+ }
+
+ /* No accessible executable found, fail later on invoke/newInstance */
+ return method;
+ }
/**
* Object creator. Find the best constructor based on the parameter classes
@@ -343,25 +457,12 @@ public static Object newInstance( Class o_clazz, Object[] args, Class[] clazzes
Constructor cons = getConstructor( o_clazz, clazzes, is_null );
- /* enforcing accessibility (workaround for bug 128) */
- boolean access = cons.isAccessible();
- if (!access) {
- try { /* since JDK-17 this may fail */
- cons.setAccessible( true );
- } catch (Throwable e) {
- access = true; /* nothing we can do, just let it fail below */
- }
- }
-
Object o;
try{
o = cons.newInstance( args ) ;
} catch( InvocationTargetException e){
/* the target exception is much more useful than the reflection wrapper */
- throw e.getTargetException() ;
- } finally{
- if (!access)
- cons.setAccessible( access );
+ throw e.getCause() ;
}
return o;
}
@@ -377,32 +478,21 @@ static boolean[] arg_is_null(Object[] args){
/**
* Invoke a method of a given class
- * First the appropriate method is resolved by getMethod and
- * then invokes the method
+ *
First the appropriate method is resolved by getMethod.
+ *
Then, if the method is not accessible, tries to find an accessible equivalent.
+ *
Finally it invokes the method
*/
public static Object invokeMethod( Class o_clazz, Object o, String name, Object[] args, Class[] clazzes) throws Throwable {
Method m = getMethod( o_clazz, name, clazzes, arg_is_null(args) );
-
- /* enforcing accessibility (workaround for bug 128) */
- boolean access = m.isAccessible();
- if (!access) {
- try { /* since JDK-17 this may fail */
- m.setAccessible( true );
- } catch (Throwable e) {
- access = true; /* nothing we can do, fail later with proper error ... */
- }
- }
+ m = findAccessible(m, o);
Object out;
try{
out = m.invoke( o, args ) ;
} catch( InvocationTargetException e){
/* the target exception is much more useful than the reflection wrapper */
- throw e.getTargetException() ;
- } finally{
- if (!access)
- m.setAccessible( access );
+ throw e.getCause() ;
}
return out ;
}