Repository: commons-lang
Updated Branches:
  refs/heads/master 078e512e6 -> de0819cb8


LANG-1195: Enhance MethodUtils to allow invocation of private methods (closes 
#141)


Project: http://git-wip-us.apache.org/repos/asf/commons-lang/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-lang/commit/5fef9575
Tree: http://git-wip-us.apache.org/repos/asf/commons-lang/tree/5fef9575
Diff: http://git-wip-us.apache.org/repos/asf/commons-lang/diff/5fef9575

Branch: refs/heads/master
Commit: 5fef9575646f6583fd2d9ee01368b3deefe6ce82
Parents: 078e512
Author: Derek Ashmore <[email protected]>
Authored: Sat Jun 4 08:53:29 2016 -0500
Committer: pascalschumacher <[email protected]>
Committed: Sun Jun 5 20:42:52 2016 +0200

----------------------------------------------------------------------
 .../commons/lang3/reflect/MethodUtils.java      | 199 ++++++++++++++++++-
 .../commons/lang3/reflect/MethodUtilsTest.java  |  63 +++++-
 2 files changed, 250 insertions(+), 12 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-lang/blob/5fef9575/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java 
b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
index c938cb6..296a2db 100644
--- a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
+++ b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java
@@ -93,6 +93,29 @@ public class MethodUtils {
             IllegalAccessException, InvocationTargetException {
         return invokeMethod(object, methodName, ArrayUtils.EMPTY_OBJECT_ARRAY, 
null);
     }
+    
+    /**
+     * <p>Invokes a named method without parameters.</p>
+     *
+     * <p>This is a convenient wrapper for
+     * {@link #invokeMethod(Object object,boolean forceAccess,String 
methodName, Object[] args, Class[] parameterTypes)}.
+     * </p>
+     *
+     * @param object invoke method on this object
+     * @param forceAccess force access to invoke method even if it's not 
accessible
+     * @param methodName get method with this name
+     * @return The value returned by the invoked method
+     *
+     * @throws NoSuchMethodException if there is no such accessible method
+     * @throws InvocationTargetException wraps an exception thrown by the 
method invoked
+     * @throws IllegalAccessException if the requested method is not 
accessible via reflection
+     *  
+     * @since 3.5
+     */
+    public static Object invokeMethod(final Object object, final boolean 
forceAccess, final String methodName) 
+               throws NoSuchMethodException, IllegalAccessException, 
InvocationTargetException {
+        return invokeMethod(object, forceAccess, methodName, 
ArrayUtils.EMPTY_OBJECT_ARRAY, null);
+    }
 
     /**
      * <p>Invokes a named method whose parameter type matches the object 
type.</p>
@@ -123,17 +146,46 @@ public class MethodUtils {
         final Class<?>[] parameterTypes = ClassUtils.toClass(args);
         return invokeMethod(object, methodName, args, parameterTypes);
     }
-
+    
     /**
      * <p>Invokes a named method whose parameter type matches the object 
type.</p>
      *
-     * <p>This method delegates the method search to {@link 
#getMatchingAccessibleMethod(Class, String, Class[])}.</p>
+     * <p>This method supports calls to methods taking primitive parameters 
+     * via passing in wrapping classes. So, for example, a {@code Boolean} 
object
+     * would match a {@code boolean} primitive.</p>
+     *
+     * <p>This is a convenient wrapper for
+     * {@link #invokeMethod(Object object,boolean forceAccess,String 
methodName, Object[] args, Class[] parameterTypes)}.
+     * </p>
+     *
+     * @param object invoke method on this object
+     * @param methodName get method with this name
+     * @param args use these arguments - treat null as empty array
+     * @return The value returned by the invoked method
+     *
+     * @throws NoSuchMethodException if there is no such accessible method
+     * @throws InvocationTargetException wraps an exception thrown by the 
method invoked
+     * @throws IllegalAccessException if the requested method is not 
accessible via reflection
+     * 
+     * @since 3.5
+     */
+    public static Object invokeMethod(final Object object, final boolean 
forceAccess, final String methodName,
+            Object... args) throws NoSuchMethodException,
+            IllegalAccessException, InvocationTargetException {
+        args = ArrayUtils.nullToEmpty(args);
+        final Class<?>[] parameterTypes = ClassUtils.toClass(args);
+        return invokeMethod(object, forceAccess, methodName, args, 
parameterTypes);
+    }
+
+    /**
+     * <p>Invokes a named method whose parameter type matches the object 
type.</p>
      *
      * <p>This method supports calls to methods taking primitive parameters 
      * via passing in wrapping classes. So, for example, a {@code Boolean} 
object
      * would match a {@code boolean} primitive.</p>
      *
      * @param object invoke method on this object
+     * @param forceAccess force access to invoke method even if it's not 
accessible
      * @param methodName get method with this name
      * @param args use these arguments - treat null as empty array
      * @param parameterTypes match these parameters - treat null as empty array
@@ -143,21 +195,77 @@ public class MethodUtils {
      * @throws InvocationTargetException wraps an exception thrown by the 
method invoked
      * @throws IllegalAccessException if the requested method is not 
accessible via reflection
      */
-    public static Object invokeMethod(final Object object, final String 
methodName,
+    public static Object invokeMethod(final Object object, final boolean 
forceAccess, final String methodName,
             Object[] args, Class<?>[] parameterTypes)
             throws NoSuchMethodException, IllegalAccessException,
             InvocationTargetException {
         parameterTypes = ArrayUtils.nullToEmpty(parameterTypes);
         args = ArrayUtils.nullToEmpty(args);
-        final Method method = getMatchingAccessibleMethod(object.getClass(),
-                methodName, parameterTypes);
-        if (method == null) {
-            throw new NoSuchMethodException("No such accessible method: "
-                    + methodName + "() on object: "
-                    + object.getClass().getName());
+        
+        final String messagePrefix;
+        Method method = null;
+        boolean isOriginallyAccessible = false;
+        Object result = null;
+        
+        try {
+            if (forceAccess) {
+               messagePrefix = "No such method: ";
+               method = getMatchingMethod(object.getClass(),
+                        methodName, parameterTypes);
+               if (method != null) {
+                   isOriginallyAccessible = method.isAccessible();
+                   if (!isOriginallyAccessible) {
+                       method.setAccessible(true);
+                   }
+               }
+            }  else {
+               messagePrefix = "No such accessible method: ";
+               method = getMatchingAccessibleMethod(object.getClass(),
+                        methodName, parameterTypes);
+            }
+            
+            if (method == null) {
+                throw new NoSuchMethodException(messagePrefix
+                        + methodName + "() on object: "
+                        + object.getClass().getName());
+            }
+            args = toVarArgs(method, args);
+            
+            result = method.invoke(object, args);
         }
-        args = toVarArgs(method, args);
-        return method.invoke(object, args);
+        finally {
+            if (method != null && forceAccess && method.isAccessible() != 
isOriginallyAccessible) {
+                method.setAccessible(isOriginallyAccessible);
+            }
+        }
+        
+        return result;
+    }
+    
+    /**
+     * <p>Invokes a named method whose parameter type matches the object 
type.</p>
+     *
+     * <p>This method delegates the method search to {@link 
#getMatchingAccessibleMethod(Class, String, Class[])}.</p>
+     *
+     * <p>This method supports calls to methods taking primitive parameters 
+     * via passing in wrapping classes. So, for example, a {@code Boolean} 
object
+     * would match a {@code boolean} primitive.</p>
+     *
+     * @param object invoke method on this object
+     * @param methodName get method with this name
+     * @param args use these arguments - treat null as empty array
+     * @param parameterTypes match these parameters - treat null as empty array
+     * @return The value returned by the invoked method
+     *
+     * @throws NoSuchMethodException if there is no such accessible method
+     * @throws InvocationTargetException wraps an exception thrown by the 
method invoked
+     * @throws IllegalAccessException if the requested method is not 
accessible via reflection
+     */
+    public static Object invokeMethod(final Object object, final String 
methodName, 
+            Object[] args, Class<?>[] parameterTypes)
+            throws NoSuchMethodException, IllegalAccessException,
+            InvocationTargetException {
+       return invokeMethod(object, false, methodName, args, parameterTypes);
     }
 
     /**
@@ -604,6 +712,75 @@ public class MethodUtils {
         }
         return bestMatch;
     }
+    
+    /**
+     * <p>Retrieves a method whether or not it's accessible. If no such method
+     * can be found, return {@code null}.</p>
+     * @param cls The class that will be subjected to the method search
+     * @param methodName The method that we wish to call
+     * @param parameterTypes Argument class types
+     * @return The method
+     * 
+     * @since 3.5
+     */
+    public static Method getMatchingMethod(final Class<?> cls, final String 
methodName,
+            final Class<?>... parameterTypes) {
+       Validate.notNull(cls, "Null class not allowed.");
+       Validate.notEmpty(methodName, "Null or blank methodName not allowed.");
+       
+       // Address methods in superclasses
+       Method[] methodArray = cls.getDeclaredMethods();
+       List<Class<?>> superclassList = ClassUtils.getAllSuperclasses(cls);
+       for (Class<?> klass: superclassList) {
+               methodArray = ArrayUtils.addAll(methodArray, 
klass.getDeclaredMethods());
+       }
+       
+       Method inexactMatch = null;
+       for (Method method: methodArray) {
+               if (methodName.equals(method.getName()) && 
+                               ArrayUtils.isEquals(parameterTypes, 
method.getParameterTypes())) {
+                       return method;
+               } else if (methodName.equals(method.getName()) &&  
+                               ClassUtils.isAssignable(parameterTypes, 
method.getParameterTypes(), true)) {
+                       if (inexactMatch == null) {
+                               inexactMatch = method;
+                       } else if (distance(parameterTypes, 
method.getParameterTypes()) 
+                                       < distance(parameterTypes, 
inexactMatch.getParameterTypes())) {
+                               inexactMatch = method;
+                       }
+               }
+               
+       }
+       return inexactMatch;
+    }
+    
+    /**
+     * <p>Returns the aggregate number of inheritance hops between assignable 
argument class types.  Returns -1
+     * if the arguments aren't assignable.  Fills a specific purpose for 
getMatchingMethod and is not generalized.</p>
+     * @param classArray
+     * @param toClassArray
+     * @return the aggregate number of inheritance hops between assignable 
argument class types.
+     */
+    private static int distance(Class<?>[] classArray, Class<?>[] 
toClassArray) {
+       int answer=0;
+       
+       if (!ClassUtils.isAssignable(classArray, toClassArray, true)) {
+               return -1;
+       }
+       for (int offset = 0; offset < classArray.length; offset++) {
+               // Note InheritanceUtils.distance() uses different scoring 
system.
+               if (classArray[offset].equals(toClassArray[offset])) {
+                       continue;
+               } else if (ClassUtils.isAssignable(classArray[offset], 
toClassArray[offset], true) 
+                               && !ClassUtils.isAssignable(classArray[offset], 
toClassArray[offset], false)) {
+                       answer++;
+               } else {
+                       answer = answer+2;
+               }
+       }
+       
+       return answer;
+    }
 
     /**
      * Get the hierarchy of overridden methods down to {@code result} 
respecting generics.

http://git-wip-us.apache.org/repos/asf/commons-lang/blob/5fef9575/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
----------------------------------------------------------------------
diff --git 
a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java 
b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
index c7c3a69..ec755f2 100644
--- a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java
@@ -32,12 +32,14 @@ import static org.junit.Assert.fail;
 import java.lang.reflect.Method;
 import java.lang.reflect.Type;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.ClassUtils.Interfaces;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.apache.commons.lang3.mutable.Mutable;
@@ -101,11 +103,41 @@ public class MethodUtilsTest {
         public static void oneParameterStatic(final String s) {
             // empty
         }
-
+        
         @SuppressWarnings("unused")
         private void privateStuff() {
         }
 
+        @SuppressWarnings("unused")
+        private String privateStringStuff() {
+               return "privateStringStuff()";
+        }
+        
+        @SuppressWarnings("unused")
+        private String privateStringStuff(final int i) {
+               return "privateStringStuff(int)";
+        }
+        
+        @SuppressWarnings("unused")
+        private String privateStringStuff(final Integer i) {
+               return "privateStringStuff(Integer)";
+        }
+        
+        @SuppressWarnings("unused")
+        private String privateStringStuff(final double d) {
+               return "privateStringStuff(double)";
+        }
+        
+        @SuppressWarnings("unused")
+        private String privateStringStuff(final String s) {
+               return "privateStringStuff(String)";
+        }
+        
+        @SuppressWarnings("unused")
+        private String privateStringStuff(final Object s) {
+               return "privateStringStuff(Object)";
+        }
+
 
         public String foo() {
             return "foo()";
@@ -728,4 +760,33 @@ public class MethodUtilsTest {
         int[] actual = (int[])MethodUtils.invokeMethod(testBean, "unboxing", 
Integer.valueOf(1), Integer.valueOf(2));
         Assert.assertArrayEquals(new int[]{1, 2}, actual);
     }
+    
+    @Test
+    public void testInvokeMethodForceAccessNoArgs() throws Exception {
+        Method privateStringStuffMethod = 
MethodUtils.getMatchingMethod(TestBean.class, "privateStringStuff");
+        Assert.assertFalse(privateStringStuffMethod.isAccessible());
+        Assert.assertEquals("privateStringStuff()", 
MethodUtils.invokeMethod(testBean, true, "privateStringStuff"));
+        Assert.assertFalse(privateStringStuffMethod.isAccessible());
+    }
+    
+    @Test
+    public void testInvokeMethodForceAccessWithArgs() throws Exception {
+        Assert.assertEquals("privateStringStuff(Integer)", 
MethodUtils.invokeMethod(testBean, true, "privateStringStuff", 5));
+        Assert.assertEquals("privateStringStuff(double)", 
MethodUtils.invokeMethod(testBean, true, "privateStringStuff", 5.0d));
+        Assert.assertEquals("privateStringStuff(String)", 
MethodUtils.invokeMethod(testBean, true, "privateStringStuff", "Hi There"));
+        Assert.assertEquals("privateStringStuff(Object)", 
MethodUtils.invokeMethod(testBean, true, "privateStringStuff", new Date()));
+    }
+    
+    @Test
+    public void testDistance() throws Exception {
+        Method distanceMethod = 
MethodUtils.getMatchingMethod(MethodUtils.class, "distance", Class[].class, 
Class[].class);
+        distanceMethod.setAccessible(true);
+        
+        Assert.assertEquals(-1, distanceMethod.invoke(null, new 
Class[]{String.class}, new Class[]{Date.class}));
+        Assert.assertEquals(0, distanceMethod.invoke(null, new 
Class[]{Date.class}, new Class[]{Date.class}));
+        Assert.assertEquals(1, distanceMethod.invoke(null, new 
Class[]{Integer.class}, new 
Class[]{ClassUtils.wrapperToPrimitive(Integer.class)}));
+        Assert.assertEquals(2, distanceMethod.invoke(null, new 
Class[]{Integer.class}, new Class[]{Object.class}));
+        
+        distanceMethod.setAccessible(false);
+    }
 }

Reply via email to