Skip to content

Commit 86af065

Browse files
committed
Allow J() convenience call on all versions of Java 6+
1 parent ef046ea commit 86af065

File tree

1 file changed

+124
-34
lines changed

1 file changed

+124
-34
lines changed

src/java/RJavaTools.java

Lines changed: 124 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@
1717
// You should have received a copy of the GNU General Public License
1818
// along with rJava. If not, see <http://www.gnu.org/licenses/>.
1919

20-
import java.lang.reflect.Method ;
21-
import java.lang.reflect.Field ;
20+
import java.lang.invoke.MethodHandles;
21+
import java.lang.invoke.MethodHandles.Lookup;
22+
import java.lang.reflect.AccessibleObject;
2223
import java.lang.reflect.Constructor ;
24+
import java.lang.reflect.Executable;
25+
import java.lang.reflect.Field ;
2326
import java.lang.reflect.InvocationTargetException ;
24-
import java.lang.reflect.Modifier ;
2527
import java.lang.reflect.Member ;
26-
28+
import java.lang.reflect.Method ;
29+
import java.lang.reflect.Modifier ;
30+
import java.util.ArrayDeque;
31+
import java.util.Arrays;
32+
import java.util.Deque;
33+
import java.util.HashSet;
2734
import java.util.Vector ;
2835

2936

@@ -35,6 +42,23 @@
3542
*/
3643
public class RJavaTools {
3744

45+
/* dual path for Java 8 and 9+ to check actual accessibility */
46+
private static final Method CAN_ACCESS;
47+
/* Java 8 accessibility checker */
48+
private static final Lookup LOOKUP = MethodHandles.publicLookup();
49+
50+
static {
51+
Method canAccess = null;
52+
try {
53+
canAccess = Executable.class.getMethod("canAccess", Object.class);
54+
} catch (NoSuchMethodException e) {
55+
// Java 8-
56+
} catch (SecurityException e) {
57+
// Java 8-
58+
}
59+
CAN_ACCESS = canAccess;
60+
}
61+
3862
/**
3963
* Returns an inner class of the class with the given simple name
4064
*
@@ -328,7 +352,97 @@ public static boolean hasMethod(Object o, String name) {
328352
return classHasMethod(o.getClass(), name, false);
329353
}
330354

355+
/**
356+
* Tests whether a given executable is accessible without actually calling {@link AccessibleObject#isAccessible()} which is unreliable.
357+
*
358+
* @param executable the given method/constructor
359+
* @param o the target object
360+
* @return true if the method can be accessed from RJavaTools
361+
*/
362+
public static boolean canAccess(Executable executable, Object o){
363+
try {
364+
if (CAN_ACCESS != null) {
365+
/* Java 9+ path */
366+
return (boolean) (Boolean) CAN_ACCESS.invoke(executable, o);
367+
} else {
368+
/* Java 8 path */
369+
if (executable instanceof Method)
370+
LOOKUP.unreflect((Method) executable);
371+
else
372+
LOOKUP.unreflectConstructor((Constructor) executable);
373+
return true;
374+
}
375+
} catch (Exception e) {
376+
return false;
377+
}
378+
}
331379

380+
/**
381+
* Resolves a bridge method to the override one
382+
* @param bridgeMethod the potentially bridge method
383+
* @return a method that isn't a bridge method
384+
*/
385+
public static Method resolveBridge(Method bridgeMethod) {
386+
// accounts for bridge methods which may have slightly different parameters
387+
Method best = null;
388+
if (bridgeMethod.isBridge()) {
389+
Class<?>[] bridgeParams = bridgeMethod.getParameterTypes();
390+
// look for non-bridge methods in the same class that accepts wider parameters
391+
nextMethod:
392+
for (Method method: bridgeMethod.getDeclaringClass().getDeclaredMethods()) {
393+
if (!method.isBridge() && !method.isSynthetic() && method.getName().equals(bridgeMethod.getName())
394+
&& method.getParameterCount() == bridgeMethod.getParameterCount()) {
395+
Class<?>[] testParams = method.getParameterTypes();
396+
// Bridge params should be supertypes of target params (erasure widening).
397+
for (int i = 0; i < testParams.length; i++) {
398+
if (!testParams[i].isAssignableFrom(bridgeParams[i])) continue nextMethod;
399+
}
400+
if (bridgeMethod.getReturnType().isAssignableFrom(method.getReturnType()) && (best == null || isMoreSpecific(method, best))) best = method;
401+
}
402+
}
403+
}
404+
405+
return best == null ? bridgeMethod : best;
406+
}
407+
408+
/**
409+
* Finds a mathod that accepts the given instance and is accessible from RJavaTools class.
410+
* <p>Avoids calling isAccessible/setAccessible that fails on Java 17+
411+
*
412+
* @param method The most specific, possibly non-accessible, method
413+
* @param target the instance upon which the method is being invoked (null for static)
414+
* @return
415+
*/
416+
public static Method findAccessible(Method method, Object target) {
417+
if (canAccess(method, target)) return method;
418+
419+
Deque<Class<?>> supers = new ArrayDeque<Class<?>>();
420+
HashSet<Class<?>> visited = new HashSet<Class<?>>();
421+
supers.add(target.getClass());
422+
423+
while (!supers.isEmpty()) {
424+
Class<?> c = supers.pop();
425+
if (visited.add(c)) {
426+
Method superMethod;
427+
try {
428+
// first update the method to be non-bridge
429+
method = resolveBridge(method);
430+
superMethod = c.getMethod(method.getName(), method.getParameterTypes());
431+
/* if an accessible executable is found stop here */
432+
if (canAccess(superMethod, target)) return superMethod;
433+
if (!c.isInterface() && !c.isArray()) supers.add(c.getSuperclass());
434+
if (!c.isArray()) supers.addAll(Arrays.asList(c.getInterfaces()));
435+
} catch (NoSuchMethodException e1) {
436+
/* executable not found in current class (and any of its ancestors) */
437+
} catch (SecurityException e1) {
438+
/* executable not found in current class (and any of its ancestors) */
439+
}
440+
}
441+
}
442+
443+
/* No accessible executable found, fail later on invoke/newInstance */
444+
return method;
445+
}
332446

333447
/**
334448
* 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
343457

344458
Constructor cons = getConstructor( o_clazz, clazzes, is_null );
345459

346-
/* enforcing accessibility (workaround for bug 128) */
347-
boolean access = cons.isAccessible();
348-
if (!access) {
349-
try { /* since JDK-17 this may fail */
350-
cons.setAccessible( true );
351-
} catch (Throwable e) {
352-
access = true; /* nothing we can do, just let it fail below */
353-
}
354-
}
355-
356460
Object o;
357461
try{
358462
o = cons.newInstance( args ) ;
359463
} catch( InvocationTargetException e){
360464
/* the target exception is much more useful than the reflection wrapper */
361-
throw e.getTargetException() ;
362-
} finally{
363-
if (!access)
364-
cons.setAccessible( access );
465+
throw e.getCause() ;
365466
}
366467
return o;
367468
}
@@ -377,32 +478,21 @@ static boolean[] arg_is_null(Object[] args){
377478

378479
/**
379480
* Invoke a method of a given class
380-
* <p>First the appropriate method is resolved by getMethod and
381-
* then invokes the method
481+
* <p>First the appropriate method is resolved by getMethod.
482+
* <p>Then, if the method is not accessible, tries to find an accessible equivalent.
483+
* <p>Finally it invokes the method
382484
*/
383485
public static Object invokeMethod( Class o_clazz, Object o, String name, Object[] args, Class[] clazzes) throws Throwable {
384486

385487
Method m = getMethod( o_clazz, name, clazzes, arg_is_null(args) );
386-
387-
/* enforcing accessibility (workaround for bug 128) */
388-
boolean access = m.isAccessible();
389-
if (!access) {
390-
try { /* since JDK-17 this may fail */
391-
m.setAccessible( true );
392-
} catch (Throwable e) {
393-
access = true; /* nothing we can do, fail later with proper error ... */
394-
}
395-
}
488+
m = findAccessible(m, o);
396489

397490
Object out;
398491
try{
399492
out = m.invoke( o, args ) ;
400493
} catch( InvocationTargetException e){
401494
/* the target exception is much more useful than the reflection wrapper */
402-
throw e.getTargetException() ;
403-
} finally{
404-
if (!access)
405-
m.setAccessible( access );
495+
throw e.getCause() ;
406496
}
407497
return out ;
408498
}

0 commit comments

Comments
 (0)