Skip to content

Commit

Permalink
Introduce support for finding fields in class hierarchies
Browse files Browse the repository at this point in the history
Issue: #497
  • Loading branch information
sbrannen committed Jan 20, 2018
1 parent a4d7147 commit f253ffb
Show file tree
Hide file tree
Showing 3 changed files with 348 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

import org.apiguardian.api.API;
import org.junit.platform.commons.JUnitException;
Expand Down Expand Up @@ -343,6 +344,43 @@ public static List<Field> findPublicAnnotatedFields(Class<?> clazz, Class<?> fie
// @formatter:on
}

/**
* Find all {@linkplain Field fields} of the supplied class or interface
* that are annotated or <em>meta-annotated</em> with the specified
* {@code annotationType} and match the specified {@code predicate}, using
* top-down search semantics within the type hierarchy.
*
* @see #findAnnotatedFields(Class, Class, Predicate, HierarchyTraversalMode)
*/
public static List<Field> findAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotationType,
Predicate<Field> predicate) {

return findAnnotatedFields(clazz, annotationType, predicate, HierarchyTraversalMode.TOP_DOWN);
}

/**
* Find all {@linkplain Field fields} of the supplied class or interface
* that are annotated or <em>meta-annotated</em> with the specified
* {@code annotationType} and match the specified {@code predicate}.
*
* @param clazz the class or interface in which to find the fields; never {@code null}
* @param annotationType the annotation type to search for; never {@code null}
* @param predicate the field filter; never {@code null}
* @param traversalMode the hierarchy traversal mode; never {@code null}
* @return the list of all such fields found; neither {@code null} nor mutable
*/
public static List<Field> findAnnotatedFields(Class<?> clazz, Class<? extends Annotation> annotationType,
Predicate<Field> predicate, HierarchyTraversalMode traversalMode) {

Preconditions.notNull(clazz, "Class must not be null");
Preconditions.notNull(annotationType, "annotationType must not be null");
Preconditions.notNull(predicate, "Predicate must not be null");

Predicate<Field> annotated = field -> isAnnotated(field, annotationType);

return ReflectionUtils.findFields(clazz, annotated.and(predicate), traversalMode);
}

/**
* @see org.junit.platform.commons.support.AnnotationSupport#findAnnotatedMethods(Class, Class, org.junit.platform.commons.support.HierarchyTraversalMode)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,80 @@ public static List<Constructor<?>> findConstructors(Class<?> clazz, Predicate<Co
}
}

/**
* Find all {@linkplain Field fields} of the supplied class or interface
* that match the specified {@code predicate}, using top-down search semantics
* within the type hierarchy.
*
* <p>The results will not contain fields that are <em>hidden</em>.
*
* @param clazz the class or interface in which to find the fields; never {@code null}
* @param predicate the field filter; never {@code null}
* @return an immutable list of all such fields found; never {@code null}
* but potentially empty
* @see #findFields(Class, Predicate, HierarchyTraversalMode)
*/
public static List<Field> findFields(Class<?> clazz, Predicate<Field> predicate) {
return findFields(clazz, predicate, TOP_DOWN);
}

/**
* Find all {@linkplain Field fields} of the supplied class or interface
* that match the specified {@code predicate}.
*
* <p>The results will not contain fields that are <em>hidden</em>.
*
* @param clazz the class or interface in which to find the fields; never {@code null}
* @param predicate the field filter; never {@code null}
* @param traversalMode the hierarchy traversal mode; never {@code null}
* @return an immutable list of all such fields found; never {@code null}
* but potentially empty
* @see #findFields(Class, Predicate)
*/
public static List<Field> findFields(Class<?> clazz, Predicate<Field> predicate,
HierarchyTraversalMode traversalMode) {

Preconditions.notNull(clazz, "Class must not be null");
Preconditions.notNull(predicate, "Predicate must not be null");
Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null");

// @formatter:off
return findAllFieldsInHierarchy(clazz, traversalMode).stream()
.filter(predicate)
// unmodifiable since returned by public, non-internal method(s)
.collect(toUnmodifiableList());
// @formatter:on
}

private static List<Field> findAllFieldsInHierarchy(Class<?> clazz, HierarchyTraversalMode traversalMode) {
Preconditions.notNull(clazz, "Class must not be null");
Preconditions.notNull(traversalMode, "HierarchyTraversalMode must not be null");

// @formatter:off
List<Field> localFields = getDeclaredFields(clazz).stream()
.filter(field -> !field.isSynthetic())
.collect(toList());
List<Field> superclassFields = getSuperclassFields(clazz, traversalMode).stream()
.filter(field -> !isFieldShadowedByLocalFields(field, localFields))
.collect(toList());
List<Field> interfaceFields = getInterfaceFields(clazz, traversalMode).stream()
.filter(field -> !isFieldShadowedByLocalFields(field, localFields))
.collect(toList());
// @formatter:on

List<Field> fields = new ArrayList<>();
if (traversalMode == TOP_DOWN) {
fields.addAll(superclassFields);
fields.addAll(interfaceFields);
}
fields.addAll(localFields);
if (traversalMode == BOTTOM_UP) {
fields.addAll(interfaceFields);
fields.addAll(superclassFields);
}
return fields;
}

/**
* Determine if a {@link Method} matching the supplied {@link Predicate}
* is present within the type hierarchy of the specified class, beginning
Expand Down Expand Up @@ -917,6 +991,22 @@ private static List<Method> findAllMethodsInHierarchy(Class<?> clazz, HierarchyT
return methods;
}

/**
* Custom alternative to {@link Class#getFields()} that sorts the fields
* and converts them to a mutable list.
*/
private static List<Field> getFields(Class<?> clazz) {
return toSortedMutableList(clazz.getFields());
}

/**
* Custom alternative to {@link Class#getDeclaredFields()} that sorts the
* fields and converts them to a mutable list.
*/
private static List<Field> getDeclaredFields(Class<?> clazz) {
return toSortedMutableList(clazz.getDeclaredFields());
}

/**
* Custom alternative to {@link Class#getMethods()} that sorts the methods
* and converts them to a mutable list.
Expand Down Expand Up @@ -976,6 +1066,15 @@ private static List<Method> getDefaultMethods(Class<?> clazz) {
// @formatter:on
}

private static List<Field> toSortedMutableList(Field[] fields) {
// @formatter:off
return Arrays.stream(fields)
.sorted(ReflectionUtils::defaultFieldSorter)
// Use toCollection() instead of toList() to ensure list is mutable.
.collect(toCollection(ArrayList::new));
// @formatter:on
}

private static List<Method> toSortedMutableList(Method[] methods) {
// @formatter:off
return Arrays.stream(methods)
Expand All @@ -985,6 +1084,23 @@ private static List<Method> toSortedMutableList(Method[] methods) {
// @formatter:on
}

/**
* Field comparator inspired by JUnit 4's {@code org.junit.internal.MethodSorter}
* implementation.
*/
private static int defaultFieldSorter(Field field1, Field field2) {
String name1 = field1.getName();
String name2 = field2.getName();
int comparison = Integer.compare(name1.hashCode(), name2.hashCode());
if (comparison == 0) {
comparison = name1.compareTo(name2);
if (comparison == 0) {
comparison = field1.toString().compareTo(field2.toString());
}
}
return comparison;
}

/**
* Method comparator based upon JUnit 4's {@code org.junit.internal.MethodSorter}
* implementation.
Expand Down Expand Up @@ -1027,6 +1143,40 @@ private static List<Method> getInterfaceMethods(Class<?> clazz, HierarchyTravers
return allInterfaceMethods;
}

private static List<Field> getInterfaceFields(Class<?> clazz, HierarchyTraversalMode traversalMode) {
List<Field> allInterfaceFields = new ArrayList<>();
for (Class<?> ifc : clazz.getInterfaces()) {
List<Field> localInterfaceFields = getFields(ifc);

// @formatter:off
List<Field> superinterfaceFields = getInterfaceFields(ifc, traversalMode).stream()
.filter(field -> !isFieldShadowedByLocalFields(field, localInterfaceFields))
.collect(toList());
// @formatter:on

if (traversalMode == TOP_DOWN) {
allInterfaceFields.addAll(superinterfaceFields);
}
allInterfaceFields.addAll(localInterfaceFields);
if (traversalMode == BOTTOM_UP) {
allInterfaceFields.addAll(superinterfaceFields);
}
}
return allInterfaceFields;
}

private static List<Field> getSuperclassFields(Class<?> clazz, HierarchyTraversalMode traversalMode) {
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || superclass == Object.class) {
return Collections.emptyList();
}
return findAllFieldsInHierarchy(superclass, traversalMode);
}

private static boolean isFieldShadowedByLocalFields(Field field, List<Field> localFields) {
return localFields.stream().anyMatch(local -> local.getName().equals(field.getName()));
}

private static List<Method> getSuperclassMethods(Class<?> clazz, HierarchyTraversalMode traversalMode) {
Class<?> superclass = clazz.getSuperclass();
if (superclass == null || superclass == Object.class) {
Expand Down
Loading

0 comments on commit f253ffb

Please sign in to comment.