Skip to content

Commit

Permalink
#63 FieldUtils: Shorten method names, return streams, util to group b…
Browse files Browse the repository at this point in the history
…y name
  • Loading branch information
ljacqu committed Sep 26, 2023
1 parent fb2bfcd commit 6482823
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 53 deletions.
70 changes: 41 additions & 29 deletions src/main/java/ch/jalu/typeresolver/FieldUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Field> getFieldsIncludingParents(Class<?> clazz) {
return getFieldsIncludingParents(clazz, f -> true, true);
public static Stream<Field> 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<Field> getFieldsIncludingParents(Class<?> clazz, Predicate<Field> 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<Field> getFieldsIncludingParents(Class<?> clazz, Predicate<Field> fieldFilter,
boolean topParentFirst) {
public static Stream<Field> getAllFields(Class<?> clazz, boolean topParentFirst) {
LinkedList<Class<?>> 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()));
}

/**
Expand All @@ -102,8 +88,10 @@ public static List<Field> getFieldsIncludingParents(Class<?> clazz, Predicate<Fi
* @param clazz the class whose instance fields should be retrieved
* @return all non-synthetic instance fields
*/
public static List<Field> getRegularInstanceFieldsIncludingParents(Class<?> clazz) {
return getFieldsIncludingParents(clazz, FieldUtils::isRegularInstanceField, true);
public static List<Field> collectAllRegularInstanceFields(Class<?> clazz) {
return getAllFields(clazz, true)
.filter(FieldUtils::isRegularInstanceField)
.collect(Collectors.toList());
}

/**
Expand Down Expand Up @@ -143,6 +131,30 @@ public static Optional<Field> 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.
* <p>
* 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.
* <p>
* To get <b>all fields</b> 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<Field, ?, LinkedHashMap<String, Field>> collectByName(boolean firstFieldWins) {
BiConsumer<LinkedHashMap<String, Field>, 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<Class<?>> classAdder) {
Class<?> currentClass = clazz;
while (currentClass != null) {
Expand Down
100 changes: 76 additions & 24 deletions src/test/java/ch/jalu/typeresolver/FieldUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,7 +74,7 @@ void shouldReturnIfIsNonSyntheticStaticField() throws NoSuchFieldException {
@Test
void shouldGetFieldsIncludingParents() throws NoSuchFieldException {
// given / when
List<Field> allFields = FieldUtils.getFieldsIncludingParents(Class3.class);
List<Field> allFields = FieldUtils.getAllFields(Class3.class).collect(Collectors.toList());

// then
assertThat(allFields.size(), greaterThanOrEqualTo(6));
Expand Down Expand Up @@ -96,39 +102,28 @@ void shouldGetFieldsIncludingParents() throws NoSuchFieldException {
}
}

@Test
void shouldGetAllFieldsSatisfyingFilter() throws NoSuchFieldException {
// given / when
List<Field> 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<Field> allFields = FieldUtils.getFieldsIncludingParents(Class3.class,
field -> !field.getName().endsWith("a") && !field.isSynthetic(), false);
List<Field> 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<Field> allFields1 = FieldUtils.getRegularInstanceFieldsIncludingParents(Class3.class);
List<Field> allFields2 = FieldUtils.getRegularInstanceFieldsIncludingParents(InnerClass.class);
List<Field> allFields1 = FieldUtils.collectAllRegularInstanceFields(Class3.class);
List<Field> allFields2 = FieldUtils.collectAllRegularInstanceFields(InnerClass.class);

// then
assertThat(allFields1, hasSize(4));
Expand Down Expand Up @@ -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<String, Field> firstFieldsParentFirst = FieldUtils.getAllFields(ClassWithSameFieldNames.class, true)
.filter(field -> !field.isSynthetic())
.collect(FieldUtils.collectByName(true));
LinkedHashMap<String, Field> firstFieldsParentLast = FieldUtils.getAllFields(ClassWithSameFieldNames.class, false)
.filter(field -> !field.isSynthetic())
.collect(FieldUtils.collectByName(true));
LinkedHashMap<String, Field> lastFieldsParentFirst = FieldUtils.getAllFields(ClassWithSameFieldNames.class, true)
.filter(field -> !field.isSynthetic())
.collect(FieldUtils.collectByName(false));
LinkedHashMap<String, Field> 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<Field> stream = Stream.of(x2b, c2a, c2b);

// when
Map<String, List<Field>> 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 {
}

Expand Down

0 comments on commit 6482823

Please sign in to comment.