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); + } }
