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 ;
2223import java .lang .reflect .Constructor ;
24+ import java .lang .reflect .Executable ;
25+ import java .lang .reflect .Field ;
2326import java .lang .reflect .InvocationTargetException ;
24- import java .lang .reflect .Modifier ;
2527import 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 ;
2734import java .util .Vector ;
2835
2936
3542 */
3643public 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