Dynamic Comparator for Objects with ComparatorChain

Good afternoon, writes a potential Java developer. I recently ran into the following common task: write a multistage comparator to sort a collection of simple objects.

After studying this issue, there was a desire to write a comparator for a class that could dynamically change when changing class fields and (or) sorting parameters that are set from the outside.

So first things first. There is some Product class:

import java.util.Formatter;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class Product {
    private String name;
    private Integer rate;
    private Double price;

    public Product(String name, Integer rate, Double price) {
        this.name = name;
        this.rate = rate;
        this.price = price;
    }
    @Override
    public String toString() {
        String shortName = name.substring(0, Math.min(name.length(), 18));
        return new Formatter().format("name: %-20s rate: %8d price: %8.2f", shortName, rate, price)
                .toString();
    }
}

If the direction (ASC, DESC) and the order of sorting are known in advance, then this problem can be easily solved by the following method:

 public static List<Product> sortMethod(List<Product> products){
        return products.stream()
                .sorted(Comparator.comparing(Product::getName)
                        .thenComparing(Comparator.comparing(Product::getRate).reversed())
                        .thenComparing(Product::getPrice))

                .toList();
    }

Similar actions can be performed using CompareToBuilder.class or other libraries not yet known to me.

But what if the composition of the class fields is not known in advance, and the sorting parameters are stored in a separate file, and can also be set arbitrarily? How to write a dynamic comparator that can work without changing the code, when changing the fields of the entity itself and sorting parameters.

We have the Product class shown above and the comparator parameters contained in the view XML file:

<sort>
    <name>asc</name>
    <price>asc</price>
    <rate>desc</rate>
</sort>

The first thing to do is to parse this file and write the parameters of our future comparator into a certain structure that will allow us to store the order of the data written to it. I used LinkedHashMap for this, where SortType is an Enum of the form:

package org.example.sort;

import lombok.Getter;

public enum SortType {
    ASC(1),
    DESC(-1);

    @Getter
    private int value;

    SortType(int i) {
        this.value = i;
    }
}

My XMLParser.class looks like this, maybe not the best and optimal, but that’s not the point of this article. I draw your attention to the fact that as method parameters getSortTypeMap() our Product.class is used and some path to a file with sorting options, which allows us to say about the dynamic construction of the arguments of the getComparatorBySortMap() method of the future ComparatorFactory.class.

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import lombok.SneakyThrows;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class XMLParser {

    @SneakyThrows
    public static LinkedHashMap<Field, SortType> 
                                      getSortTypeMap (Class<?> clazz, String path) {        
        LinkedHashMap<Field, SortType> sortTypeMap = new LinkedHashMap<>();
        List<Field> fields = List.of(clazz.getDeclaredFields());

        DocumentBuilder documentBuilder;
        Document document = null;
        try {
            documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            document = documentBuilder.parse(path);
        } catch (SAXException | ParserConfigurationException | IOException e) {
            e.printStackTrace();
        }
        XPathFactory pathFactory = XPathFactory.newInstance();
        XPath xpath = pathFactory.newXPath();
        XPathExpression expr = xpath.compile("//sort/child::*");

        NodeList nodes = (NodeList) expr.evaluate(document, XPathConstants.NODESET);
        for (int i = 0; i < nodes.getLength(); i++) {
            Node n = nodes.item(i);
            Field field =
                    fields.stream().filter(t -> t.getName().equals(n.getNodeName()))
                            .findFirst()
                            .orElse(null);
            if (field != null) {
                if (n.getTextContent().toUpperCase().equals(SortType.ASC.toString())) {
                    sortTypeMap.put(field, SortType.ASC);
                }
                if (n.getTextContent().toUpperCase().equals(SortType.DESC.toString())) {
                    sortTypeMap.put(field, SortType.DESC);
                }
            }
        }
        return sortTypeMap;
    }
}

And now the class itself, which allows you to generate the necessary comparator and based on the use ComparatorChain.class and BeanComparator.class:

import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections4.comparators.ComparatorChain;

public class ComparatorFactory <T> {
    public  ComparatorChain<T> getProductComparatorBySortMap(
            LinkedHashMap<Field, SortType> sortTypeMap) {
        if (sortTypeMap.isEmpty()) {
            return null;
        }

        ComparatorChain<T> chain = new ComparatorChain<>();
        String parameterName;
        boolean direction;
        for (Map.Entry<Field, SortType> sortParametr : sortTypeMap.entrySet()) {
            parameterName = sortParametr.getKey().getName();
            direction = sortParametr.getValue().getValue() <= 0;
            chain.addComparator(new BeanComparator<>(parameterName), direction);
        }
        return chain;
    }
}

As a result, we get the ability to generate comparators based on the data about the class and sort order, while both the class and the order can change.

I apologize in advance for possible flaws in the code and the lack of a deeper analysis of the speed of this method, possible errors and shortcomings that I could not reflect in the article due to lack of experience in commercial programming.

I will be glad to comments under my first article. And I hope that someone who needs a “young” Java developer like me will read it.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *