diff --git a/src/main/java/ch/jalu/typeresolver/FieldUtils.java b/src/main/java/ch/jalu/typeresolver/FieldUtils.java index b8fb843..daf993a 100644 --- a/src/main/java/ch/jalu/typeresolver/FieldUtils.java +++ b/src/main/java/ch/jalu/typeresolver/FieldUtils.java @@ -5,12 +5,15 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.function.Predicate; +import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Class with utilities for processing fields. @@ -57,43 +60,26 @@ public static boolean isRegularStaticField(Field field) { * in this class's hierarchy are returned first. * * @param clazz the class whose fields (incl. its parents' fields) should be returned - * @return all fields, with top-most parent's fields first, and this class's fields last + * @return a stream of all fields, with top-most parent's fields first, and this class's fields last */ - public static List getFieldsIncludingParents(Class clazz) { - return getFieldsIncludingParents(clazz, f -> true, true); + public static Stream getAllFields(Class clazz) { + return getAllFields(clazz, true); } /** - * Returns all fields from the given class and its parents, recursively, that match the provided filter. - * The fields of the top-most parent in this class's hierarchy are returned first. + * Returns all fields from the given class and its parents, recursively. Depending on the parameter, fields are + * either returned from top-to-bottom or bottom-to-top relative to the class's hierarchy. * - * @param clazz the class whose fields (incl. its parents' fields) should be returned that match the filter - * @param fieldFilter the condition a field must fulfill in order to be part of the result - * @return all fields matching the filter, with the top-most parent's fields first, and this class's fields last - */ - public static List getFieldsIncludingParents(Class clazz, Predicate fieldFilter) { - return getFieldsIncludingParents(clazz, fieldFilter, true); - } - - /** - * Returns all fields from the given class and its parents, recursively, that match the provided filter. Depending - * on the parameter, fields are either returned from top-to-bottom or bottom-to-top relative to the class's - * hierarchy. - * - * @param clazz the class whose fields (incl. its parents' fields) should be returned that match the filter - * @param fieldFilter the condition a field must fulfill in order to be part of the result + * @param clazz the class whose fields (incl. its parents' fields) should be returned * @param topParentFirst true if the top-most parent's fields should come first, false for last - * @return all fields matching the filter, in the specified order + * @return a stream of all fields, in the specified order */ - public static List getFieldsIncludingParents(Class clazz, Predicate fieldFilter, - boolean topParentFirst) { + public static Stream getAllFields(Class clazz, boolean topParentFirst) { LinkedList> classes = new LinkedList<>(); collectParents(clazz, (topParentFirst ? classes::addFirst : classes::addLast)); return classes.stream() - .flatMap(clz -> Arrays.stream(clz.getDeclaredFields())) - .filter(fieldFilter) - .collect(Collectors.toList()); + .flatMap(clz -> Arrays.stream(clz.getDeclaredFields())); } /** @@ -102,8 +88,10 @@ public static List getFieldsIncludingParents(Class clazz, Predicate getRegularInstanceFieldsIncludingParents(Class clazz) { - return getFieldsIncludingParents(clazz, FieldUtils::isRegularInstanceField, true); + public static List collectAllRegularInstanceFields(Class clazz) { + return getAllFields(clazz, true) + .filter(FieldUtils::isRegularInstanceField) + .collect(Collectors.toList()); } /** @@ -143,6 +131,30 @@ public static Optional tryFindFieldInClassOrParent(Class clazz, String return Optional.empty(); } + /** + * Collector for a stream of fields to group them by name, with the parameter indicating whether the first + * encountered field with a given name should be retained, or the last one. This collector does not support + * parallel streams. + *

+ * Note that this collector is especially useful if you are only dealing with instance fields: for a mechanism + * that processes the instance fields of classes (e.g. for serialization), you may want to only consider the + * lowest-most declared field per any given name so that behavior can be overridden. + *

+ * To get all fields grouped by name, use {@code stream.collect(Collectors.groupingBy(Field::getName))}. + * + * @param firstFieldWins true if the first encountered field with a given name should be kept; + * false to keep the last one + * @return collector to collect names by field + */ + public static Collector> collectByName(boolean firstFieldWins) { + BiConsumer, Field> accumulator = firstFieldWins + ? (map, field) -> map.putIfAbsent(field.getName(), field) + : (map, field) -> map.put(field.getName(), field); + + return Collector.of(LinkedHashMap::new, accumulator, + (a, b) -> { throw new UnsupportedOperationException(); }); + } + private static void collectParents(Class clazz, Consumer> classAdder) { Class currentClass = clazz; while (currentClass != null) { diff --git a/src/test/java/ch/jalu/typeresolver/FieldUtilsTest.java b/src/test/java/ch/jalu/typeresolver/FieldUtilsTest.java index 0d69c20..f4a3642 100644 --- a/src/test/java/ch/jalu/typeresolver/FieldUtilsTest.java +++ b/src/test/java/ch/jalu/typeresolver/FieldUtilsTest.java @@ -7,11 +7,17 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -68,7 +74,7 @@ void shouldReturnIfIsNonSyntheticStaticField() throws NoSuchFieldException { @Test void shouldGetFieldsIncludingParents() throws NoSuchFieldException { // given / when - List allFields = FieldUtils.getFieldsIncludingParents(Class3.class); + List allFields = FieldUtils.getAllFields(Class3.class).collect(Collectors.toList()); // then assertThat(allFields.size(), greaterThanOrEqualTo(6)); @@ -96,39 +102,28 @@ void shouldGetFieldsIncludingParents() throws NoSuchFieldException { } } - @Test - void shouldGetAllFieldsSatisfyingFilter() throws NoSuchFieldException { - // given / when - List allFields = FieldUtils.getFieldsIncludingParents(Class3.class, - field -> field.getType().isPrimitive()); - - // then - assertThat(allFields, hasSize(4)); - assertThat(allFields.get(0), equalTo(Class1.class.getDeclaredField("C1A"))); - assertThat(allFields.get(1), equalTo(Class1.class.getDeclaredField("c1b"))); - assertThat(allFields.get(2), equalTo(Class2.class.getDeclaredField("c2a"))); - assertThat(allFields.get(3), equalTo(Class3.class.getDeclaredField("c3b"))); - } - @Test void shouldGetAllFieldsWithFilterAndParentsLast() throws NoSuchFieldException { // given / when - List allFields = FieldUtils.getFieldsIncludingParents(Class3.class, - field -> !field.getName().endsWith("a") && !field.isSynthetic(), false); + List allFields = FieldUtils.getAllFields(Class3.class, false) + .filter(f -> !f.isSynthetic()) + .collect(Collectors.toList()); // then - assertThat(allFields, hasSize(4)); - assertThat(allFields.get(0), equalTo(Class3.class.getDeclaredField("c3b"))); - assertThat(allFields.get(1), equalTo(Class2.class.getDeclaredField("c2b"))); - assertThat(allFields.get(2), equalTo(Class1.class.getDeclaredField("C1A"))); - assertThat(allFields.get(3), equalTo(Class1.class.getDeclaredField("c1b"))); + assertThat(allFields, hasSize(6)); + assertThat(allFields.get(0), equalTo(Class3.class.getDeclaredField("c3a"))); + assertThat(allFields.get(1), equalTo(Class3.class.getDeclaredField("c3b"))); + assertThat(allFields.get(2), equalTo(Class2.class.getDeclaredField("c2a"))); + assertThat(allFields.get(3), equalTo(Class2.class.getDeclaredField("c2b"))); + assertThat(allFields.get(4), equalTo(Class1.class.getDeclaredField("C1A"))); + assertThat(allFields.get(5), equalTo(Class1.class.getDeclaredField("c1b"))); } @Test void shouldGetAllRegularInstanceFieldsIncludingParents() throws NoSuchFieldException { // given / when - List allFields1 = FieldUtils.getRegularInstanceFieldsIncludingParents(Class3.class); - List allFields2 = FieldUtils.getRegularInstanceFieldsIncludingParents(InnerClass.class); + List allFields1 = FieldUtils.collectAllRegularInstanceFields(Class3.class); + List allFields2 = FieldUtils.collectAllRegularInstanceFields(InnerClass.class); // then assertThat(allFields1, hasSize(4)); @@ -165,6 +160,63 @@ void shouldReturnFieldFromClassOrParentIfExists() throws NoSuchFieldException { assertThat(FieldUtils.tryFindFieldInClassOrParent(Class3.class, ""), equalTo(Optional.empty())); } + @Test + void shouldCollectFieldsByName() throws NoSuchFieldException { + // given + Field c1a = Class1.class.getDeclaredField("C1A"); + Field c1b = Class1.class.getDeclaredField("c1b"); + Field c2a = Class2.class.getDeclaredField("c2a"); + Field c2b = Class2.class.getDeclaredField("c2b"); + Field c3a = Class3.class.getDeclaredField("c3a"); + Field c3b = Class3.class.getDeclaredField("c3b"); + + Field x1b = ClassWithSameFieldNames.class.getDeclaredField("c1b"); + Field x2b = ClassWithSameFieldNames.class.getDeclaredField("c2b"); + + // when + LinkedHashMap firstFieldsParentFirst = FieldUtils.getAllFields(ClassWithSameFieldNames.class, true) + .filter(field -> !field.isSynthetic()) + .collect(FieldUtils.collectByName(true)); + LinkedHashMap firstFieldsParentLast = FieldUtils.getAllFields(ClassWithSameFieldNames.class, false) + .filter(field -> !field.isSynthetic()) + .collect(FieldUtils.collectByName(true)); + LinkedHashMap lastFieldsParentFirst = FieldUtils.getAllFields(ClassWithSameFieldNames.class, true) + .filter(field -> !field.isSynthetic()) + .collect(FieldUtils.collectByName(false)); + LinkedHashMap lastFieldsParentLast = FieldUtils.getAllFields(ClassWithSameFieldNames.class, false) + .filter(field -> !field.isSynthetic()) + .collect(FieldUtils.collectByName(false)); + + // then + assertThat(firstFieldsParentFirst.keySet(), contains("C1A", "c1b", "c2a", "c2b", "c3a", "c3b")); + assertThat(firstFieldsParentFirst.values(), contains( c1a, c1b, c2a, c2b, c3a, c3b)); + assertThat(firstFieldsParentLast.keySet(), contains("c1b", "c2b", "c3a", "c3b", "c2a", "C1A")); + assertThat(firstFieldsParentLast.values(), contains( x1b, x2b, c3a, c3b, c2a, c1a )); + assertThat(lastFieldsParentFirst.keySet(), contains("C1A", "c1b", "c2a", "c2b", "c3a", "c3b")); + assertThat(lastFieldsParentFirst.values(), contains( c1a, x1b, c2a, x2b, c3a, c3b )); + assertThat(lastFieldsParentLast.keySet(), contains("c1b", "c2b", "c3a", "c3b", "c2a", "C1A")); + assertThat(lastFieldsParentLast.values(), contains( c1b, c2b, c3a, c3b, c2a, c1a )); + } + + @Test + void shouldHaveValidJavadoc_collectByName() throws NoSuchFieldException { + // given + Field x2b = ClassWithSameFieldNames.class.getDeclaredField("c2b"); + Field c2a = Class2.class.getDeclaredField("c2a"); + Field c2b = Class2.class.getDeclaredField("c2b"); + + + Stream stream = Stream.of(x2b, c2a, c2b); + + // when + Map> result = stream.collect(Collectors.groupingBy(Field::getName)); + + // then + assertThat(result.keySet(), containsInAnyOrder("c2a", "c2b")); + assertThat(result.get("c2a"), contains(c2a)); + assertThat(result.get("c2b"), contains(x2b, c2b)); + } + private class InnerClass { }