From 46007c151e4cd79891eed4d289ff372ba39d5b8c Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sun, 26 Mar 2017 01:49:46 +0430 Subject: [PATCH] LANG-1317: Adds MethodUtils#findAnnotation and extend MethodUtils#getMethodsWithAnnotation for non-public, super-class and interface methods (closes #261) --- .../commons/lang3/reflect/MethodUtils.java | 158 +++++++++++++++++- .../lang3/reflect/MethodUtilsTest.java | 136 +++++++++++++++ .../commons/lang3/reflect/testbed/Foo.java | 1 + .../commons/lang3/reflect/testbed/Parent.java | 7 + .../lang3/reflect/testbed/PublicChild.java | 16 ++ 5 files changed, 310 insertions(+), 8 deletions(-) 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 c6f979583..55b5cb1be 100644 --- a/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java +++ b/src/main/java/org/apache/commons/lang3/reflect/MethodUtils.java @@ -830,7 +830,7 @@ public static Set getOverrideHierarchy(final Method method, final Interf } /** - * Gets all methods of the given class that are annotated with the given annotation. + * Gets all class level public methods of the given class that are annotated with the given annotation. * @param cls * the {@link Class} to query * @param annotationCls @@ -841,12 +841,11 @@ public static Set getOverrideHierarchy(final Method method, final Interf * @since 3.4 */ public static Method[] getMethodsWithAnnotation(final Class cls, final Class annotationCls) { - final List annotatedMethodsList = getMethodsListWithAnnotation(cls, annotationCls); - return annotatedMethodsList.toArray(new Method[annotatedMethodsList.size()]); + return getMethodsWithAnnotation(cls, annotationCls, false, false); } /** - * Gets all methods of the given class that are annotated with the given annotation. + * Gets all class level public methods of the given class that are annotated with the given annotation. * @param cls * the {@link Class} to query * @param annotationCls @@ -857,16 +856,159 @@ public static Method[] getMethodsWithAnnotation(final Class cls, final Class< * @since 3.4 */ public static List getMethodsListWithAnnotation(final Class cls, final Class annotationCls) { + return getMethodsListWithAnnotation(cls, annotationCls, false, false); + } + + /** + * Gets all methods of the given class that are annotated with the given annotation. + * @param cls + * the {@link Class} to query + * @param annotationCls + * the {@link java.lang.annotation.Annotation} that must be present on a method to be matched + * @param searchSupers + * determines if also a lookup in the entire inheritance hierarchy of the given class should be performed + * @param ignoreAccess + * determines if also non public methods should be considered + * @return an array of Methods (possibly empty). + * @throws IllegalArgumentException + * if the class or annotation are {@code null} + * @since 3.6 + */ + public static Method[] getMethodsWithAnnotation(final Class cls, final Class annotationCls, + boolean searchSupers, boolean ignoreAccess) { + final List annotatedMethodsList = getMethodsListWithAnnotation(cls, annotationCls, searchSupers, + ignoreAccess); + return annotatedMethodsList.toArray(new Method[annotatedMethodsList.size()]); + } + + /** + * Gets all methods of the given class that are annotated with the given annotation. + * @param cls + * the {@link Class} to query + * @param annotationCls + * the {@link Annotation} that must be present on a method to be matched + * @param searchSupers + * determines if also a lookup in the entire inheritance hierarchy of the given class should be performed + * @param ignoreAccess + * determines if also non public methods should be considered + * @return a list of Methods (possibly empty). + * @throws IllegalArgumentException + * if the class or annotation are {@code null} + * @since 3.6 + */ + public static List getMethodsListWithAnnotation(final Class cls, + final Class annotationCls, + boolean searchSupers, boolean ignoreAccess) { + Validate.isTrue(cls != null, "The class must not be null"); Validate.isTrue(annotationCls != null, "The annotation class must not be null"); - final Method[] allMethods = cls.getMethods(); + List> classes = (searchSupers ? getAllSuperclassesAndInterfaces(cls) + : new ArrayList>()); + classes.add(0, cls); final List annotatedMethods = new ArrayList<>(); - for (final Method method : allMethods) { - if (method.getAnnotation(annotationCls) != null) { - annotatedMethods.add(method); + for (Class acls : classes) { + final Method[] methods = (ignoreAccess ? acls.getDeclaredMethods() : acls.getMethods()); + for (final Method method : methods) { + if (method.getAnnotation(annotationCls) != null) { + annotatedMethods.add(method); + } } } return annotatedMethods; } + /** + *

Gets the annotation object that is present on the given method or any equivalent method in + * super classes and interfaces, with the given annotation type. Returns null if the annotation + * type was not present on any of them.

+ * + *

Stops searching for an annotation once the first annotation of the specified type has been + * found. i.e, additional annotations of the specified type will be silently ignored.

+ * @param + * the annotation type + * @param method + * the {@link Method} to query + * @param annotationCls + * the {@link Annotation} to check if is present on the method + * @param searchSupers + * determines if lookup in the entire inheritance hierarchy of the given class if was not directly present + * @param ignoreAccess + * determines if underlying method has to be accessible + * @return the first matching annotation, or {@code null} if not found + * @throws IllegalArgumentException + * if the method or annotation are {@code null} + * @since 3.6 + */ + public static A getAnnotation(final Method method, final Class annotationCls, + boolean searchSupers, boolean ignoreAccess) { + + Validate.isTrue(method != null, "The method must not be null"); + Validate.isTrue(annotationCls != null, "The annotation class must not be null"); + if(!ignoreAccess && !MemberUtils.isAccessible(method)) { + return null; + } + + A annotation = method.getAnnotation(annotationCls); + + if(annotation == null && searchSupers) { + Class mcls = method.getDeclaringClass(); + List> classes = getAllSuperclassesAndInterfaces(mcls); + for (Class acls : classes) { + Method equivalentMethod; + try { + equivalentMethod = (ignoreAccess ? acls.getDeclaredMethod(method.getName(), method.getParameterTypes()) + : acls.getMethod(method.getName(), method.getParameterTypes())); + } catch (NoSuchMethodException e) { + // If not found, just keep on search + continue; + } + annotation = equivalentMethod.getAnnotation(annotationCls); + if (annotation != null) { + break; + } + } + } + + return annotation; + } + + /** + *

Gets a combination of {@link ClassUtils#getAllSuperclasses}(Class)} and + * {@link ClassUtils#getAllInterfaces}(Class)}, one from superclasses, one + * from interfaces, and so on in a breadth first way.

+ * + * @param cls the class to look up, may be {@code null} + * @return the combined {@code List} of superclasses and interfaces in order + * going up from this one + * {@code null} if null input + * @since 3.6 + */ + private static List> getAllSuperclassesAndInterfaces(final Class cls) { + if (cls == null) { + return null; + } + + final List> classes = new ArrayList<>(); + List> allSuperclasses = ClassUtils.getAllSuperclasses(cls); + int sci = 0; + List> allInterfaces = ClassUtils.getAllInterfaces(cls); + int ifi = 0; + while (ifi < allInterfaces.size() || + sci < allSuperclasses.size()) { + Class acls; + if (ifi >= allInterfaces.size()) { + acls = allSuperclasses.get(sci++); + } else if (sci >= allSuperclasses.size()) { + acls = allInterfaces.get(ifi++); + } else if (ifi < sci) { + acls = allInterfaces.get(ifi++); + } else if (sci < ifi) { + acls = allSuperclasses.get(sci++); + } else { + acls = allInterfaces.get(ifi++); + } + classes.add(acls); + } + return classes; + } } 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 df57a13fd..15d7cd766 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/reflect/MethodUtilsTest.java @@ -47,6 +47,7 @@ import org.apache.commons.lang3.reflect.testbed.Annotated; import org.apache.commons.lang3.reflect.testbed.GenericConsumer; import org.apache.commons.lang3.reflect.testbed.GenericParent; +import org.apache.commons.lang3.reflect.testbed.PublicChild; import org.apache.commons.lang3.reflect.testbed.StringParameterizedChild; import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.Assert; @@ -681,6 +682,125 @@ public void testGetMethodsWithAnnotation() throws NoSuchMethodException { assertThat(methodsWithAnnotation, hasItemInArray(MethodUtilsTest.class.getMethod("testGetMethodsListWithAnnotation"))); } + @Test + public void testGetMethodsWithAnnotationSearchSupersAndIgnoreAccess() throws NoSuchMethodException { + assertArrayEquals(new Method[0], MethodUtils.getMethodsWithAnnotation(Object.class, Annotated.class, + true, true)); + + final Method[] methodsWithAnnotation = MethodUtils.getMethodsWithAnnotation(PublicChild.class, Annotated.class, + true, true); + assertEquals(4, methodsWithAnnotation.length); + assertEquals("PublicChild", methodsWithAnnotation[0].getDeclaringClass().getSimpleName()); + assertEquals("PublicChild", methodsWithAnnotation[1].getDeclaringClass().getSimpleName()); + assertTrue(methodsWithAnnotation[0].getName().endsWith("AnnotatedMethod")); + assertTrue(methodsWithAnnotation[1].getName().endsWith("AnnotatedMethod")); + assertEquals("Foo.doIt", + methodsWithAnnotation[2].getDeclaringClass().getSimpleName() + '.' + + methodsWithAnnotation[2].getName()); + assertEquals("Parent.parentProtectedAnnotatedMethod", + methodsWithAnnotation[3].getDeclaringClass().getSimpleName() + '.' + + methodsWithAnnotation[3].getName()); + } + + @Test + public void testGetMethodsWithAnnotationNotSearchSupersButIgnoreAccess() throws NoSuchMethodException { + assertArrayEquals(new Method[0], MethodUtils.getMethodsWithAnnotation(Object.class, Annotated.class, + false, true)); + + final Method[] methodsWithAnnotation = MethodUtils.getMethodsWithAnnotation(PublicChild.class, Annotated.class, + false, true); + assertEquals(2, methodsWithAnnotation.length); + assertEquals("PublicChild", methodsWithAnnotation[0].getDeclaringClass().getSimpleName()); + assertEquals("PublicChild", methodsWithAnnotation[1].getDeclaringClass().getSimpleName()); + assertTrue(methodsWithAnnotation[0].getName().endsWith("AnnotatedMethod")); + assertTrue(methodsWithAnnotation[1].getName().endsWith("AnnotatedMethod")); + } + + @Test + public void testGetMethodsWithAnnotationSearchSupersButNotIgnoreAccess() throws NoSuchMethodException { + assertArrayEquals(new Method[0], MethodUtils.getMethodsWithAnnotation(Object.class, Annotated.class, + true, false)); + + final Method[] methodsWithAnnotation = MethodUtils.getMethodsWithAnnotation(PublicChild.class, Annotated.class, + true, false); + assertEquals(2, methodsWithAnnotation.length); + assertEquals("PublicChild.publicAnnotatedMethod", + methodsWithAnnotation[0].getDeclaringClass().getSimpleName() + '.' + + methodsWithAnnotation[0].getName()); + assertEquals("Foo.doIt", + methodsWithAnnotation[1].getDeclaringClass().getSimpleName() + '.' + + methodsWithAnnotation[1].getName()); + } + + @Test + public void testGetMethodsWithAnnotationNotSearchSupersAndNotIgnoreAccess() throws NoSuchMethodException { + assertArrayEquals(new Method[0], MethodUtils.getMethodsWithAnnotation(Object.class, Annotated.class, + false, false)); + + final Method[] methodsWithAnnotation = MethodUtils.getMethodsWithAnnotation(PublicChild.class, Annotated.class, + false, false); + assertEquals(1, methodsWithAnnotation.length); + assertEquals("PublicChild.publicAnnotatedMethod", + methodsWithAnnotation[0].getDeclaringClass().getSimpleName() + '.' + + methodsWithAnnotation[0].getName()); + } + + @Test + public void testGetAnnotationSearchSupersAndIgnoreAccess() throws NoSuchMethodException { + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentNotAnnotatedMethod"), + Annotated.class, true, true)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("doIt"), Annotated.class, + true, true)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentProtectedAnnotatedMethod"), + Annotated.class, true, true)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getDeclaredMethod("privateAnnotatedMethod"), + Annotated.class, true, true)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("publicAnnotatedMethod"), + Annotated.class, true, true)); + } + + @Test + public void testGetAnnotationNotSearchSupersButIgnoreAccess() throws NoSuchMethodException { + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentNotAnnotatedMethod"), + Annotated.class, false, true)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("doIt"), Annotated.class, + false, true)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentProtectedAnnotatedMethod"), + Annotated.class, false, true)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getDeclaredMethod("privateAnnotatedMethod"), + Annotated.class, false, true)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("publicAnnotatedMethod"), + Annotated.class, false, true)); + } + + @Test + public void testGetAnnotationSearchSupersButNotIgnoreAccess() throws NoSuchMethodException { + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentNotAnnotatedMethod"), + Annotated.class, true, false)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("doIt"), Annotated.class, + true, false)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentProtectedAnnotatedMethod"), + Annotated.class, true, false)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getDeclaredMethod("privateAnnotatedMethod"), + Annotated.class, true, false)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("publicAnnotatedMethod"), + Annotated.class, true, false)); + } + + @Test + public void testGetAnnotationNotSearchSupersAndNotIgnoreAccess() throws NoSuchMethodException { + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentNotAnnotatedMethod"), + Annotated.class, false, false)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("doIt"), Annotated.class, + false, false)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("parentProtectedAnnotatedMethod"), + Annotated.class, false, false)); + assertNull(MethodUtils.getAnnotation(PublicChild.class.getDeclaredMethod("privateAnnotatedMethod"), + Annotated.class, false, false)); + assertNotNull(MethodUtils.getAnnotation(PublicChild.class.getMethod("publicAnnotatedMethod"), + Annotated.class, false, false)); + } + @Test(expected = IllegalArgumentException.class) public void testGetMethodsWithAnnotationIllegalArgumentException1() { MethodUtils.getMethodsWithAnnotation(FieldUtilsTest.class, null); @@ -724,6 +844,22 @@ public void testGetMethodsListWithAnnotationIllegalArgumentException3() { MethodUtils.getMethodsListWithAnnotation(null, null); } + @Test(expected = IllegalArgumentException.class) + public void testGetAnnotationIllegalArgumentException1() { + MethodUtils.getAnnotation(FieldUtilsTest.class.getDeclaredMethods()[0], null, true, + true); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetAnnotationIllegalArgumentException2() { + MethodUtils.getAnnotation(null, Annotated.class, true, true); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetAnnotationIllegalArgumentException3() { + MethodUtils.getAnnotation(null, null, true, true); + } + private void expectMatchingAccessibleMethodParameterTypes(final Class cls, final String methodName, final Class[] requestTypes, final Class[] actualTypes) { final Method m = MethodUtils.getMatchingAccessibleMethod(cls, methodName, diff --git a/src/test/java/org/apache/commons/lang3/reflect/testbed/Foo.java b/src/test/java/org/apache/commons/lang3/reflect/testbed/Foo.java index 551199360..be24ac6ac 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/testbed/Foo.java +++ b/src/test/java/org/apache/commons/lang3/reflect/testbed/Foo.java @@ -21,5 +21,6 @@ public interface Foo { public static final String VALUE = "foo"; + @Annotated void doIt(); } diff --git a/src/test/java/org/apache/commons/lang3/reflect/testbed/Parent.java b/src/test/java/org/apache/commons/lang3/reflect/testbed/Parent.java index 70447b444..eeee5e398 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/testbed/Parent.java +++ b/src/test/java/org/apache/commons/lang3/reflect/testbed/Parent.java @@ -28,4 +28,11 @@ class Parent implements Foo { @Override public void doIt() { } + + @Annotated + protected void parentProtectedAnnotatedMethod() { + } + + public void parentNotAnnotatedMethod() { + } } diff --git a/src/test/java/org/apache/commons/lang3/reflect/testbed/PublicChild.java b/src/test/java/org/apache/commons/lang3/reflect/testbed/PublicChild.java index c91a06f58..ce2a1ec04 100644 --- a/src/test/java/org/apache/commons/lang3/reflect/testbed/PublicChild.java +++ b/src/test/java/org/apache/commons/lang3/reflect/testbed/PublicChild.java @@ -20,4 +20,20 @@ */ public class PublicChild extends Parent { static final String VALUE = "child"; + + @Override + public void parentProtectedAnnotatedMethod() { + } + + @Override + public void parentNotAnnotatedMethod() { + } + + @Annotated + private void privateAnnotatedMethod() { + } + + @Annotated + public void publicAnnotatedMethod() { + } }