Filtering Java Collections via Annotation-Driven Introspection

Since Java 8, the programming workload of iterating over collections and selecting a subcollection (filtering) based on defined constraints (predicates) has been considerably simplified using the new Stream API. Despite this new feature, some scenarios can still overwhelm developers by requiring them to implement a significant amount of very specific code to meet the filtering conditions. A common example in enterprise systems is the need to filter:

". . . a collection where each object is an element of a large graph and the attributes to be considered in the filters belong to distinct objects."

To illustrate the scenario, let's consider the small class diagram below. It is required to filter a collection of Post objects taking into account different attributes of the object graph. At this point, disregard the @Filterable annotation on some attributes; this will be covered later.

Class diagram

To present a traditional Java code for filtering Post collections, let's make some assumptions:

A way to filter a postsCollection by the text attribute of the Publication class is:

Java
 
postsCollection
	.stream()
	.filter(p -> Objects.isNull(p.getText()) || StringUtils.stripAccents(p.getText().trim().toLowerCase())
            .contains(textFilter.trim().toLowerCase()))
	.toList();


If the need is to filter the collection by the  review attribute of the Comment class, the code could be like this:

Java
 
postsCollection
	.stream()
	.filter(p -> Objects.isNull(p.getComments()) || p.getComments().isEmpty() ||
            p.getComments().stream().anyMatch(c -> StringUtils.stripAccents(c.getReview().trim().toLowerCase())
                                              .contains(textFilter.trim().toLowerCase())))
	.toList();


Note that for each change in filter requirements, the code must be adjusted accordingly. And what's worse: the need to combine multiple attributes from different graph classes can lead to code hard to maintain.

Addressing such scenarios, this article presents a generic approach to filtering collections by combining annotations and the Java Reflection API.

Introspector Filter Algorithm

The proposed approach is called Introspector Filter and its main algorithm relies on three fundaments:

It's worth explaining that all traversing operations are supported by the Reflection API. Another relevant point is that the attributes to be considered in the filtering operation must be annotated with @Filterable

The code below presents the implementation of the Introspector Filter algorithm. Its full source code is available in the GitHub repository.

Java
 
public IntrospectorFilter() {
  
    public Boolean filter(Object value, Object filter) {
		
        if (Objects.isNull(filter)) {
            return true;
        }
		
        String textFilter = StringUtils.stripAccents(filter.toString().trim().toLowerCase());
        var nodesList = new ArrayList<Node>();

        nodesList.add(new Node(0, 0, value));

        while (!nodesList.isEmpty()) { // BFS for relationships

            var node = nodesList.removeFirst();
			
            if (node.height() > this.heightBound || node.breadth() > this.breadthBound) {
                continue;
            }
			
            var fieldValue = node.value();
            var fieldValueClass = fieldValue.getClass();

            int heightHop = node.height();
            do { // Hierarchical traversing
				
                if (Objects.nonNull(
                    this.searchInRelationships(node, fieldValueClass, heightHop, textFilter, nodesList))) {
                        return true;
                }
				
                fieldValueClass = fieldValueClass.getSuperclass(); 
                heightHop++;
            } while (isValidParentClass(fieldValueClass) && heightHop <= this.heightBound);

            if (isStringOrWrapper(fieldValue) && containsTextFilter(fieldValue.toString(), textFilter)) {
                return true;
            }
        }
		
        return false;
    }
}


Three methods have been abstracted from this code to keep the focus on the main algorithm:

Let's go back to the previous small class diagram. The two codes presented for filtering a postsCollection may be replaced with the following code using the Introspector Filter Algorithm.

Java
 
postsCollection.stream().filter(p -> filter.filter(p, textFilter)).toList();


Introspector Filter Project

The implementation of the Introspector Filter Algorithm has been encapsulated in a library (jar). It can be incorporated into a Maven project by simply adding the following dependency to the pom.xml file. This implementation requires at least Java version 21 to work. But there is a Java 8 compatible version — change the version dependency to 0.1.0.

XML
 
<dependency>
    <groupId>io.github.tnas</groupId>
    <artifactId>introspectorfilter</artifactId>
    <version>1.0.0</version>
</dependency>


The project also has an example module detailing how to use the Introspector Filter as a global filter in JSF datatable components.

Conclusion

Selecting a subset of objects from a Java collection is a common task in software development. Having a flexible strategy (Introspector Filter Algorithm) or tool (Introspector Filter Library) for creating filters to be used in these selections can help developers write more concise and maintainable code. This is the proposal of Instrospector Filter Project, which is fully available in a GitHub repository.

 

 

 

 

Top