Check if JSONObject matches JSON boolean expression

To explain correctly the problem I must start with an example let’s say I have a list of users like this

[
    { "name": "John",  "surname": "Doe",   "age": 22 },
    { "name": "Syrus", "surname": "Black", "age": 20 }
]

And I have also a JSONObject representing a condition that must be matched like this:

{
    "OR":[
        { "name": { "eq": "John"} },
        { "AND":[
            { "name": { "eq": "Syrus"} },
            { "age": { "gt": 18 } }
        ] }
    ]
}

Which should be translated in:

name = "John" OR (name = "Syrus" AND age > 18)

Now I have to make the code that given the JSONObject condition and the list of the users checks for each users if the condition is matched. At the moment this is what I have done:

import java.util.Set;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import com.query.Queryable;
import org.json.JSONArray;
import org.json.JSONObject;

public class QueryableTreeMap<K,V extends JSONObject> extends TreeMap<K,V> implements Queryable<JSONObject,JSONObject> {

    private static final long serialVersionUID = 2586026774025401270L;

    private static boolean test(Set<Map.Entry<String, Object>> condition, JSONObject value){
        boolean isValid = true;
        Iterator<Map.Entry<String, Object>> iter = condition.iterator();
        while(iter.hasNext()){
            Map.Entry<String, Object> subcond = iter.next();
            if(subcond.getKey().equals("OR")){
                //isValid = isValid || test((Set<Map.Entry<String, Object>>) subcond.getValue(), value);
            } else if(subcond.getKey().equals("AND")){
                //isValid = isValid && test((Set<Map.Entry<String, Object>>) subcond.getValue(), value);
            } else if(subcond.getKey().equals("NOT")){
            
            } else {
                
            }
        }
        return isValid;
    }

    @Override
    public JSONObject query(JSONObject query) {
        // the set containing the conditions
        Set<Map.Entry<String, Object>> entries = query.toMap().entrySet();
        // the JSONArray with containing the records that match the condition
        JSONArray array = new JSONArray();
        // for each JSONObject inside this structure
        this.forEach(new BiConsumer<K,JSONObject>(){

            @Override
            public void accept(K key, JSONObject value) {
                // testing if the current record matches the condition
                if(test(entries, value)) array.put(value);
            }
            
        });
        // returns a JSONObject containing a JSONArray that contains the records that match the condition
        return new JSONObject(array);
    }
}

I am currently stuck in the test method which should in-fact test if the given object matches the given condition.

I don’t mind changing the format of the JSON condition as long as it is a JSONObject.

At the moment i have come up with a partial solution that builds an object called Condition that represents the boolean expression inside the JSONObject (not very efficient but still a possible solution) this is obviously not working at the moment, i need help on what i should do now

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class Condition {

    private Condition left, right;
    private String boolExpr, key, value;
    private boolean not;
    private int operationType; // greater then, less then, equal to, greater or equal of, less or equal of

    public Condition(Map<String, Object> map) {
        Set<Map.Entry<String, Object>> set = map.entrySet();
        Iterator<Map.Entry<String, Object>> iterator = set.iterator();
        while(iterator.hasNext()){
            Object entry = iterator.next();
            System.out.println(entry);
            System.out.println(entry.getClass());
            if(entry instanceof Map.Entry) {
                String key = (String) ((Map.Entry) entry).getKey();
                switch(key){
                    case "AND":
                    case "OR":
                        this.boolExpr = key;
                        break;
                    case "eq":
                    case "gt":
                    case "lt":
                    case "gte":
                    case "lte":
                        this.operationType = getOperationTypeFromString(key);
                        break;
                    case "NOT":
                        this.not = true;
                        break;
                    default:
                        this.key = key;
                        break;
                }
            }
        }
    }

    public int getOperationTypeFromString(String operation) {
        switch(operation){
            case "eq":
                return 0;
            case "gt":
                return 1;
            case "lt":
                return 2;
            case "gte":
                return 3;
            case "lte":
                return 4;
            default:
                return 0;
        }
    }

}

I would prefer not to use the Condition class and just use the JSONObject instead. I am using the org.json JSON-Java parser.

Answer

Edit: code updated to use org.json.

Below is a working implementation that handles your example.

The function that actually does the work is match, which recursively traverses the filter and applies each node to the supplied object. The function returns true if the given object satisfies the given filter.

Conceptually the match function is mapping each construct in your filter language (AND, OR, EQ, LT etc) into its Java equivalent. E.g. AND maps to Stream.allMatch, NOT maps to the Java ! operator, EQ maps to Object.equals, etc. I.e. match is defining the semantics for your filter language.

I hope the code is self-explanatory – let me know if anything is unclear.

import org.json.*;

import java.io.*;
import java.nio.file.*;
import java.util.List;
import java.util.stream.*;

import static java.util.stream.Collectors.toList;

public class Test {

    public static void main(String[] args) throws IOException {
        final List<JSONObject> jsObjs =
                stream(readJsonArray("users.json"))
                        .map(JSONObject.class::cast)
                        .collect(toList());

        final JSONObject jsFilter = readJsonObject("filter.json");

        final List<JSONObject> matches = applyFilter(jsObjs, jsFilter);

        System.out.println(matches);
    }

    private static List<JSONObject> applyFilter(List<JSONObject> jsObjs, JSONObject jsFilter) {
        return jsObjs.stream()
                .filter(jsObj -> match(jsObj, jsFilter))
                .collect(toList());
    }

    private static boolean match(JSONObject jsObj, JSONObject jsFilter) {

        final String name = getSingleKey(jsFilter);
        final Object value = jsFilter.get(name);

        switch (name) {
            case "AND":
                return stream((JSONArray)value)
                        .map(JSONObject.class::cast)
                        .allMatch(jse -> match(jsObj, jse));
            case "OR":
                return stream((JSONArray)value)
                        .map(JSONObject.class::cast)
                        .anyMatch(jse -> match(jsObj, jse));
            case "NOT":
                return !match(jsObj, (JSONObject)((JSONArray)value).get(0));
            default:
                final JSONObject jsOp = (JSONObject)value;
                final String operator = getSingleKey(jsOp);
                final Object operand = jsOp.get(operator);
                switch (operator) {
                    case "eq": return jsObj.get(name).equals(operand);
                    case "lt": return (Integer)jsObj.get(name) < (Integer)operand;
                    case "gt": return (Integer)jsObj.get(name) > (Integer)operand;
                    default: throw new IllegalArgumentException("Unexpected operator: " + operator);
                }
        }
    }

    private static JSONObject readJsonObject(String fileName) throws IOException {
        try (Reader reader = Files.newBufferedReader(Paths.get(fileName))) {
            return new JSONObject(new JSONTokener(reader));
        }
    }

    private static JSONArray readJsonArray(String fileName) throws IOException {
        try (Reader reader = Files.newBufferedReader(Paths.get(fileName))) {
            return new JSONArray(new JSONTokener(reader));
        }
    }

    private static Stream<Object> stream(JSONArray jsa) {
        return StreamSupport.stream(jsa.spliterator(), false);
    }

    private static String getSingleKey(JSONObject jso) {
        if (jso.length() != 1) {
            throw new IllegalArgumentException("Expected single entry");
        } else {
            return jso.keySet().iterator().next();
        }
    }
}

Leave a Reply

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