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(); } } }