1

I have a list of java objects, and I want to use stream to filter them at runtime. However, the variable I want to filter on is only known at runtime.

For eg, the user says I want a list of all cats whose fur length is longer than 3cm, I should be able to do

cats.stream().filter(cat -> cat.getFurLength() > 3).collect(Collectors.toList());

The getFurLength() getter however should be dynamically invoked - if the user instead wants to filter by eye colour then I should be able to call

cats.stream().filter(cat -> cat.getEyeColour() == Colour.BLUE).collect(Collectors.toList());

How do I achieve this without writing all possible filters beforehand?

Ideally the user should send something like:

{
  eyeColour:{
    operator: "equal_to",
    value: "BLUE"
  },
  furLength: {
    operator: "greater_than",
    value: 3
  }
}

and the code should be able to generate the filters dynamically based on these criteria.

2
  • You need to have a mapping somewhere which evaluates what user input can result in what Predicate and generalize the statement to use cats.stream().filter(predicateAsInput).collect(Collectors.toList()); Commented Apr 28, 2019 at 15:38
  • Deserialize the json into a class containing the three items field, operator, and value. Generate a Predicate<Cat> from that by extracting the actual value using reflection to create a Function <Cat, ValueType> and then apply the operator to see if the value matches. Commented Apr 28, 2019 at 16:00

3 Answers 3

1

Assuming your Cat class follows JavaBean convention you could use java.beans.PropertyDescriptor to access getter Method based on property name.

This allows us to learn what type of value we are dealing with. If it is numeric we can handle greater_than and other operators, but if it is non-numeric we should handle only equals_to operator.

"Simplified" and very limited solution could look like:

NOTE:
- Solution doesn't support primitive numeric types like int. Use Integer, Double etc. instead.
- I am converting all numbers to BigDecimal and use compareTo to simplify numerical type comparison, if you get any bugs for big numbers or very precise ones feel free to replace it with proper type comparison).
- for equality check it compares string representation of objects (result of toString()), so for Color you can't use BLUE but your JSON would need to hold java.awt.Color[r=0,g=0,b=255])

class PredicatesUtil {

    static <T> Predicate<T> filters(Class<T> clazz, String filtersJson) {

        JSONObject jsonObject = new JSONObject(filtersJson);

        List<Predicate<T>> predicateList = new ArrayList<>();
        for (String property : jsonObject.keySet()) {
            JSONObject filterSettings = jsonObject.getJSONObject(property);

            try {
                String operator = filterSettings.getString("operator");
                String value = filterSettings.getString("value");
                predicateList.add(propertyPredicate(clazz, property, operator, value));
            } catch (IntrospectionException e) {
                throw new RuntimeException(e);
            }
        }
        return combinePredicatesUsingAND(predicateList);
    }

    static <T> Predicate<T> combinePredicatesUsingAND(List<Predicate<T>> predicateList) {
        return t -> {
            for (Predicate<T> pr : predicateList) {
                if (!pr.test(t))
                    return false;
            }
            return true;
        };
    }

    static <T> Predicate<T> propertyPredicate(Class<T> clazz, String property,
                                              String operator, String value)
            throws IntrospectionException {

        final Method m = new PropertyDescriptor(property, clazz).getReadMethod();
        final Class<?> returnType = m.getReturnType();

        return obj -> {
            try {
                Object getterValue = m.invoke(obj);
                if (Number.class.isAssignableFrom(returnType)) {
                    BigDecimal getValue = new BigDecimal(getterValue.toString());
                    BigDecimal numValue = new BigDecimal(value);

                    int compared = getValue.compareTo(numValue);
                    if (operator.equalsIgnoreCase("equal_to")) {
                        return compared == 0;
                    } else if (operator.equalsIgnoreCase("lesser_than")) {
                        return compared < 0;
                    } else if (operator.equalsIgnoreCase("greater_than")) {
                        return compared > 0;
                    } else {
                        throw new RuntimeException("not recognized operator for numeric type: " + operator);
                    }
                } else {
                    //System.out.println("testing non-numeric, only euals_to");
                    if (operator.equalsIgnoreCase("equal_to")) {
                        return value.equalsIgnoreCase(getterValue.toString());
                    }
                    throw new RuntimeException("not recognized operator: " + operator);
                }
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        };
    }
}

which can be used like:

class Cat {
    private Color eyeColour;
    private Integer furLength;

    Cat(Color eyeColor, Integer furLength) {
        this.eyeColour = eyeColor;
        this.furLength = furLength;
    }

    public Color getEyeColour() {
        return eyeColour;
    }

    public Integer getFurLength() {
        return furLength;
    }

    public void setEyeColour(Color eyeColour) {
        this.eyeColour = eyeColour;
    }

    public void setFurLength(Integer furLength) {
        this.furLength = furLength;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "eyeColor=" + eyeColour +
                ", furLength=" + furLength +
                '}';
    }
}
class CatsDemo {
    public static void main(String[] args) {

        String json = 
                "{\n" +
                "  eyeColour:{\n" +
                "    operator: \"equal_to\",\n" +
                "    value: \"java.awt.Color[r=0,g=0,b=255]\"\n" +
                "  },\n" +
                "  furLength: {\n" +
                "    operator: \"greater_than\",\n" +
                "    value: \"3\"\n" +
                "  }\n" +
                "}";

        List<Cat> cats = List.of(
                new Cat(Color.blue, 1),
                new Cat(Color.blue, 2),
                new Cat(Color.blue, 3),
                new Cat(Color.blue, 4),
                new Cat(Color.blue, 5),
                new Cat(Color.yellow, 1),
                new Cat(Color.yellow, 2),
                new Cat(Color.yellow, 3),
                new Cat(Color.yellow, 4),
                new Cat(Color.yellow, 5)
        );

        cats.stream()
            .filter(PredicatesUtil.filters(Cat.class, json))
            .forEach(System.out::println);
    }
}

Output:

Cat{eyeColor=java.awt.Color[r=0,g=0,b=255], furLength=4}
Cat{eyeColor=java.awt.Color[r=0,g=0,b=255], furLength=5}
Sign up to request clarification or add additional context in comments.

Comments

0

Make it reusable with a function.

List<Cat> filterCats(cats, Predicate<Cat> filter) {
    return cats.stream().filter(filter).collect(Collectors.toList());
}

And then use it with:

filterCats(cats, cat -> cat.getEyeColour() == Colour.BLUE)

Or,

filterCats(cats, cat -> cat.getFurLength() > 3)

2 Comments

Question is, how do you create cat -> cat.getEyeColour() == Colour.BLUE from user data like JSON. To quote OP "How do I achieve this without writing all possible filters beforehand?"
Lots of if-statemnts? Don't think there's any ready-made shortcut for something like this as OP is probably looking for.
0

For what it's worth: Apache Commons BeanUtils library is specialized in accessing bean properties in a dynamic way.

See BeanPropertyValueEqualsPredicate for an understanding. This is only a solution for equality matches.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.