diff --git a/README.md b/README.md index 2b6be0e9..b7826eef 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ Filters are logical expressions used to filter arrays. A typical filter would be | =~ | left matches regular expression [?(@.name =~ /foo.*?/i)] | | in | left exists in right [?(@.size in ['S', 'M'])] | | nin | left does not exists in right | +| subsetof | left is a subset of right [?(@.sizes subsetof ['S', 'M', 'L'])] | | size | size of left (array or string) should match right | | empty | left (array or string) should be empty | diff --git a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java index 30e1eda1..92f9b2bc 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -267,6 +267,33 @@ public class Criteria implements Predicate { return this; } + /** + * The subsetof operator selects objects for which the specified field is + * an array whose elements comprise a subset of the set comprised by the elements of + * the specified array. + * + * @param o the values to match against + * @return the criteria + */ + public Criteria subsetof(Object... o) { + return subsetof(Arrays.asList(o)); + } + + /** + * The subsetof operator selects objects for which the specified field is + * an array whose elements comprise a subset of the set comprised by the elements of + * the specified array. + * + * @param c the values to match against + * @return the criteria + */ + public Criteria subsetof(Collection c) { + notNull(c, "collection can not be null"); + this.criteriaType = RelationalOperator.SUBSETOF; + this.right = new ValueNode.ValueListNode(c); + return this; + } + /** * The all operator is similar to $in, but instead of matching any value * in the specified array all values in the array must be matched. diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/EvaluatorFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/EvaluatorFactory.java index 6831920c..ac47274a 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/EvaluatorFactory.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/EvaluatorFactory.java @@ -29,6 +29,7 @@ public class EvaluatorFactory { evaluators.put(RelationalOperator.CONTAINS, new ContainsEvaluator()); evaluators.put(RelationalOperator.MATCHES, new PredicateMatchEvaluator()); evaluators.put(RelationalOperator.TYPE, new TypeEvaluator()); + evaluators.put(RelationalOperator.SUBSETOF, new SubsetOfEvaluator()); } public static Evaluator createEvaluator(RelationalOperator operator){ @@ -264,4 +265,34 @@ public class EvaluatorFactory { return input; } } + + private static class SubsetOfEvaluator implements Evaluator { + @Override + public boolean evaluate(ValueNode left, ValueNode right, Predicate.PredicateContext ctx) { + ValueNode.ValueListNode rightValueListNode; + if(right.isJsonNode()){ + ValueNode vn = right.asJsonNode().asValueListNode(ctx); + if(vn.isUndefinedNode()){ + return false; + } else { + rightValueListNode = vn.asValueListNode(); + } + } else { + rightValueListNode = right.asValueListNode(); + } + ValueNode.ValueListNode leftValueListNode; + if(left.isJsonNode()){ + ValueNode vn = left.asJsonNode().asValueListNode(ctx); + if(vn.isUndefinedNode()){ + return false; + } else { + leftValueListNode = vn.asValueListNode(); + } + } else { + leftValueListNode = left.asValueListNode(); + } + return leftValueListNode.subsetof(rightValueListNode); + } + } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/RelationalOperator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/RelationalOperator.java index 4575f60b..830cc3bb 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/RelationalOperator.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/RelationalOperator.java @@ -29,7 +29,8 @@ public enum RelationalOperator { EXISTS("EXISTS"), TYPE("TYPE"), MATCHES("MATCHES"), - EMPTY("EMPTY"); + EMPTY("EMPTY"), + SUBSETOF("SUBSETOF"); private final String operatorString; diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java index 9989dd95..5135f848 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java @@ -713,6 +713,15 @@ public abstract class ValueNode { return nodes.contains(node); } + public boolean subsetof(ValueListNode right) { + for (ValueNode leftNode : nodes) { + if (!right.nodes.contains(leftNode)) { + return false; + } + } + return true; + } + public List getNodes() { return Collections.unmodifiableList(nodes); } diff --git a/json-path/src/test/java/com/jayway/jsonpath/FilterParseTest.java b/json-path/src/test/java/com/jayway/jsonpath/FilterParseTest.java index a073d9b1..948ca75f 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/FilterParseTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/FilterParseTest.java @@ -1,5 +1,6 @@ package com.jayway.jsonpath; +import java.util.Collections; import org.assertj.core.api.Assertions; import org.junit.Test; @@ -142,6 +143,15 @@ public class FilterParseTest { assertThat(filter).isEqualTo(parsed); } + @Test + public void a_subsetof_filter_can_be_serialized() { + + String filter = filter(where("a").subsetof(Collections.emptyList())).toString(); + String parsed = parse("[?(@['a'] SUBSETOF [])]").toString(); + + assertThat(filter).isEqualTo(parsed); + } + @Test public void a_exists_filter_can_be_serialized() { diff --git a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java index a9c02e76..e786ae83 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java @@ -1,5 +1,7 @@ package com.jayway.jsonpath; +import java.util.ArrayList; +import org.assertj.core.util.Lists; import org.junit.Test; import java.util.HashMap; @@ -358,6 +360,24 @@ public class FilterTest extends BaseTest { assertThat(filter(where("null-key").size(6)).apply(createPredicateContext(json))).isEqualTo(false); } + //---------------------------------------------------------------------------- + // + // SUBSETOF + // + //---------------------------------------------------------------------------- + @Test + public void array_subsetof_evals() { + // list is a superset + List list = Lists.newArrayList("a", "b", "c", "d", "e", "f", "g"); + assertThat(filter(where("string-arr").subsetof(list)).apply(createPredicateContext(json))).isEqualTo(true); + // list is exactly the same set (but in a different order) + list = Lists.newArrayList("e", "d", "b", "c", "a"); + assertThat(filter(where("string-arr").subsetof(list)).apply(createPredicateContext(json))).isEqualTo(true); + // list is missing one element + list = Lists.newArrayList("a", "b", "c", "d"); + assertThat(filter(where("string-arr").subsetof(list)).apply(createPredicateContext(json))).isEqualTo(false); + } + //---------------------------------------------------------------------------- // // EXISTS