org.perfectable.introspection.query.FieldQuery Maven / Gradle / Ivy
package org.perfectable.introspection.query;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Comparator;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.Nullable;
import static java.util.Objects.requireNonNull;
/**
* Iterable-like container that searches for fields in class.
*
* This is straightforward method to search for class fields that have specified characteristics in provided class.
*
*
Instances of this class are immutable, each filtering produces new, modified instance. To obtain unrestricted
* query, use {@link #of}.
*
*
To obtain results either iterate this class with {@link #iterator} (or in enhanced-for loop) or use one of
* {@link #stream()}, {@link #unique()}, {@link #option()} or {@link #isPresent()}.
*
*
All fields present in class are searched, either declared in it directly, or inherited from parent classes.
*
*
Example usage, which processes all non-static, public fields in class "User" that have annotation "Inject" on
* them. Before passing field to the method, it is marked as {@link Field#setAccessible accessible}.
*
* FieldQuery.of(User.class)
* .requiringModifier(Modifier.PUBLIC)
* .excludingModifier(Modifier.STATIC)
* .annotatedBy(Inject.class)
* .asAccessible()
* .stream()
* .forEach(this::process);
*
*/
@SuppressWarnings({
"DesignForExtension", // class is closed because of package-private constructor
"ClassDataAbstractionCoupling"
})
public abstract class FieldQuery extends MemberQuery {
/**
* This simple Null Object Pattern for this query.
*
* @return Query which will produce no results.
*/
public static FieldQuery empty() {
return Empty.INSTANCE;
}
/**
* Queries for fields in specified class.
*
* @param type class to search fields in.
* @return query that returns all fields in specified class.
*/
public static FieldQuery of(Class> type) {
requireNonNull(type);
return new Complete<>(type);
}
/**
* Joins results of two queries.
*
* Resulting query will return all fields returned by this query, followed by all fields
* returned by provided query.
*
* @param other secondary query that will be joined.
* @return Query that contains results from this and provided {@code other} query.
*/
public FieldQuery join(FieldQuery other) {
requireNonNull(other);
return Composite.composite(this, other);
}
/**
* Restricts query to fields with specified name.
*/
@Override
public FieldQuery named(String name) {
requireNonNull(name);
return new Named(this, name);
}
/**
* Restricts query to fields with name that matches specified pattern.
*/
@Override
public FieldQuery nameMatching(Pattern namePattern) {
requireNonNull(namePattern);
return new NameMatching(this, namePattern);
}
/**
* Restricts query to fields which matches arbitrary filter.
*/
@Override
public FieldQuery filter(Predicate super Field> filter) {
requireNonNull(filter);
return new Predicated(this, filter);
}
@Override
public FieldQuery sorted(Comparator super Field> comparator) {
requireNonNull(comparator);
return new Sorted(this, comparator);
}
/**
* Restricts query to fields with type that is subtype of specified type.
*
*
In other words, this method will restrict query to fields that when {@link Field#get get} from object, will
* return instances of specified type (or null).
*
* @param type type to restrict
* @return copy of this query that will also filter by type
*/
public FieldQuery typed(Type type) {
return typed(TypeFilter.subtypeOf(type));
}
/**
* Restricts query to fields which have type that matches specified arbitrary type filter.
*
* @param typeFilter filter of type to restrict
* @return copy of this query that will also filter by type
*/
public FieldQuery typed(TypeFilter typeFilter) {
requireNonNull(typeFilter);
return new Typed(this, typeFilter);
}
/**
* Restricts query to fields which are annotated by specified annotation filter.
*/
@Override
public FieldQuery annotatedWith(AnnotationFilter annotationFilter) {
requireNonNull(annotationFilter);
return new Annotated(this, annotationFilter);
}
/**
* Restricts query to fields that have specified modifier on them.
*/
@Override
public FieldQuery requiringModifier(int requiredModifier) {
return new RequiringModifier(this, requiredModifier);
}
/**
* Restricts query to fields that don't have specified modifier.
*/
@Override
public FieldQuery excludingModifier(int excludedModifier) {
return new ExcludingModifier(this, excludedModifier);
}
/**
* Makes query mark field as {Field#setAccessible accessible} when iterating.
*/
@Override
public FieldQuery asAccessible() {
return new AccessibleMarking(this);
}
FieldQuery() {
// package extension only
}
private static final class Complete extends FieldQuery {
private final InheritanceQuery chain;
Complete(Class type) {
this.chain = InheritanceQuery.of(type);
}
@Override
public Stream stream() {
return this.chain.stream()
.flatMap(testedClass -> Stream.of(testedClass.getDeclaredFields()));
}
@Override
public boolean contains(@Nullable Object candidate) {
if (!(candidate instanceof Field)) {
return false;
}
Field candidateField = (Field) candidate;
@SuppressWarnings("unchecked")
Class super X> declaringClass = (Class super X>) candidateField.getDeclaringClass();
return chain.contains(declaringClass);
}
}
private abstract static class Filtered extends FieldQuery {
private final FieldQuery parent;
Filtered(FieldQuery parent) {
this.parent = parent;
}
protected abstract boolean matches(Field candidate);
@Override
public Stream stream() {
return this.parent.stream()
.filter(this::matches);
}
@Override
public boolean contains(@Nullable Object candidate) {
if (!(candidate instanceof Field)) {
return false;
}
Field candidateField = (Field) candidate;
return matches(candidateField) && parent.contains(candidate);
}
}
private static final class Sorted extends FieldQuery {
private final FieldQuery parent;
private final Comparator super Field> comparator;
Sorted(FieldQuery parent, Comparator super Field> comparator) {
this.parent = parent;
this.comparator = comparator;
}
@Override
public FieldQuery sorted(Comparator super Field> nextComparator) {
@SuppressWarnings("unchecked")
Comparator<@Nullable Object> casted = (Comparator<@Nullable Object>) nextComparator;
return new Sorted(parent, this.comparator.thenComparing(casted));
}
@Override
public Stream stream() {
return this.parent.stream()
.sorted(comparator);
}
@Override
public boolean contains(@Nullable Object candidate) {
return parent.contains(candidate);
}
}
private static final class Named extends Filtered {
private final String name;
Named(FieldQuery parent, String name) {
super(parent);
this.name = name;
}
@Override
protected boolean matches(Field candidate) {
return this.name.equals(candidate.getName());
}
}
private static final class NameMatching extends Filtered {
private final Pattern namePattern;
NameMatching(FieldQuery parent, Pattern namePattern) {
super(parent);
this.namePattern = namePattern;
}
@Override
protected boolean matches(Field candidate) {
return this.namePattern.matcher(candidate.getName()).matches();
}
}
private static final class Predicated extends Filtered {
private final Predicate super Field> filter;
Predicated(FieldQuery parent, Predicate super Field> filter) {
super(parent);
this.filter = filter;
}
@Override
protected boolean matches(Field candidate) {
return this.filter.test(candidate);
}
}
private static final class Typed extends Filtered {
private final TypeFilter typeFilter;
Typed(FieldQuery parent, TypeFilter typeFilter) {
super(parent);
this.typeFilter = typeFilter;
}
@Override
protected boolean matches(Field candidate) {
return this.typeFilter.matches(candidate.getType());
}
}
private static final class Annotated extends Filtered {
private final AnnotationFilter annotationFilter;
Annotated(FieldQuery parent, AnnotationFilter annotationFilter) {
super(parent);
this.annotationFilter = annotationFilter;
}
@Override
protected boolean matches(Field candidate) {
return this.annotationFilter.matches(candidate);
}
}
private static final class RequiringModifier extends Filtered {
private final int requiredModifier;
RequiringModifier(FieldQuery parent, int requiredModifier) {
super(parent);
this.requiredModifier = requiredModifier;
}
@Override
protected boolean matches(Field candidate) {
return (candidate.getModifiers() & this.requiredModifier) != 0;
}
}
private static final class ExcludingModifier extends Filtered {
private final int excludedModifier;
ExcludingModifier(FieldQuery parent, int excludedModifier) {
super(parent);
this.excludedModifier = excludedModifier;
}
@Override
protected boolean matches(Field candidate) {
return (candidate.getModifiers() & this.excludedModifier) == 0;
}
}
private static final class AccessibleMarking extends FieldQuery {
private final FieldQuery parent;
AccessibleMarking(FieldQuery parent) {
this.parent = parent;
}
@Override
public Stream stream() {
return this.parent.stream()
.peek(element -> element.setAccessible(true));
}
@Override
public boolean contains(@Nullable Object candidate) {
return parent.contains(candidate);
}
}
private static final class Empty extends FieldQuery {
static final Empty INSTANCE = new Empty();
@Override
public Stream stream() {
return Stream.of();
}
@Override
public boolean contains(@Nullable Object candidate) {
return false;
}
}
private static final class Composite extends FieldQuery {
private final ImmutableList components;
private Composite(ImmutableList components) {
this.components = components;
}
private static Composite composite(FieldQuery... components) {
return new Composite(ImmutableList.copyOf(components));
}
@Override
public Stream stream() {
return components.stream().flatMap(FieldQuery::stream);
}
@Override
public boolean contains(@Nullable Object candidate) {
return components.stream().anyMatch(component -> component.contains(candidate));
}
@Override
public Composite join(FieldQuery other) {
ImmutableList newComponents =
ImmutableList.builder()
.addAll(components).add(other).build();
return new Composite(newComponents);
}
}
}