DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Composing Custom Annotations in Spring
  • Mastering Spring: Synchronizing @Transactional and @Async Annotations With Various Propagation Strategies
  • Implementing Persistence With Clean Architecture
  • Unraveling Lombok's Code Design Pitfalls: Exploring Encapsulation Issues

Trending

  • Mocking Kafka for Local Spring Development
  • Retesting Best Practices for Agile Teams: A Quick Guide to Bug Fix Verification
  • Observability in Spring Boot 4
  • Securing Everything: Mapping the Right Identity and Access Protocol (OIDC, OAuth2, and SAML) to the Right Identity
  1. DZone
  2. Coding
  3. Java
  4. Filtering Java Collections via Annotation-Driven Introspection

Filtering Java Collections via Annotation-Driven Introspection

Learn a versatile collection filtering strategy based on Java annotations and reflection and how it can simplify the code of some predicates.

By 
Thiago Nascimento user avatar
Thiago Nascimento
DZone Core CORE ·
Oct. 28, 24 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
6.6K Views

Join the DZone community and get the full member experience.

Join For Free

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 List<Post> has been instantiated and loaded.
  • A primitive type textFilter variable has been set as the value to be searched.
  • The Apache Commons Lang StringUtils class is used to remove diacritics from strings.

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:

  • Traverse all relationships of each object using a Breadth-First Search strategy.
  • Traverse the hierarchy of each object. 
  • Check if any attribute annotated with @Filterable of each object contains the given pattern.

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:

  • isValidParentClass: Verify if the parent class is valid, considering any additional annotations that have been provided to identify the class. If no additional annotation have been provided, all parent classes are considered valid.
  • isStringOrWrapper: Check if the value of the attribute annotated with @Filterable is a String or a primitive type wrapper. When this is true, such a relationship traversing is interrupted, as there is no further way forward.
  • containsTextFilter: Check if the attribute annotated with @Filterable contains the supplied pattern.

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.

Annotation Class diagram Attribute (computing) Filter (software) Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • Composing Custom Annotations in Spring
  • Mastering Spring: Synchronizing @Transactional and @Async Annotations With Various Propagation Strategies
  • Implementing Persistence With Clean Architecture
  • Unraveling Lombok's Code Design Pitfalls: Exploring Encapsulation Issues

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook