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 cfb57c7b..222c9dfa 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -48,7 +48,7 @@ public class Criteria { */ private static final Object NOT_SET = new Object(); - private final String key; + private final JsonPath key; private final List criteriaChain; @@ -61,17 +61,17 @@ public class Criteria { notEmpty(key, "key can not be null or empty"); this.criteriaChain = new ArrayList(); this.criteriaChain.add(this); - this.key = key; + this.key = JsonPath.compile(key); } private Criteria(List criteriaChain, String key) { notEmpty(key, "key can not be null or empty"); this.criteriaChain = criteriaChain; this.criteriaChain.add(this); - this.key = key; + this.key = JsonPath.compile(key); } - public String getKey() { + public JsonPath getKey() { return this.key; } @@ -95,15 +95,34 @@ public class Criteria { } } + private static Object readSafely(JsonPath path, Map map){ + try{ + return path.read(map); + } catch (InvalidPathException e){ + return null; + } + } - + private static boolean objectOrAnyCollectionItemMatches(final Object singleObjectOrCollection, + final Predicate predicate){ + if (singleObjectOrCollection instanceof Collection) { + Iterator it = ((Collection) singleObjectOrCollection).iterator(); + while (it.hasNext()) { + if (predicate.accept((T) it.next())) { + return true; + } + } + return false; + } + return predicate.accept((T) singleObjectOrCollection); + } boolean singleObjectApply(Map map) { for (CriteriaType key : this.criteria.keySet()) { - Object actualVal = map.get(this.key); - Object expectedVal = this.criteria.get(key); + Object actualVal = readSafely(this.key, map); + final Object expectedVal = this.criteria.get(key); if (CriteriaType.GT.equals(key)) { @@ -111,10 +130,14 @@ public class Criteria { return false; } - Number expectedNumber = (Number) expectedVal; - Number actualNumber = (Number) actualVal; + final Number expectedNumber = (Number) expectedVal; + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { - return (actualNumber.doubleValue() > expectedNumber.doubleValue()); + @Override + public boolean accept(Number value) { + return (value.doubleValue() > expectedNumber.doubleValue()); + } + }); } else if (CriteriaType.GTE.equals(key)) { @@ -122,10 +145,14 @@ public class Criteria { return false; } - Number expectedNumber = (Number) expectedVal; - Number actualNumber = (Number) actualVal; + final Number expectedNumber = (Number) expectedVal; + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { - return (actualNumber.doubleValue() >= expectedNumber.doubleValue()); + @Override + public boolean accept(Number value) { + return (value.doubleValue() >= expectedNumber.doubleValue()); + } + }); } else if (CriteriaType.LT.equals(key)) { @@ -133,10 +160,14 @@ public class Criteria { return false; } - Number expectedNumber = (Number) expectedVal; - Number actualNumber = (Number) actualVal; + final Number expectedNumber = (Number) expectedVal; + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { - return (actualNumber.doubleValue() < expectedNumber.doubleValue()); + @Override + public boolean accept(Number value) { + return (value.doubleValue() < expectedNumber.doubleValue()); + } + }); } else if (CriteriaType.LTE.equals(key)) { @@ -144,20 +175,31 @@ public class Criteria { return false; } - Number expectedNumber = (Number) expectedVal; - Number actualNumber = (Number) actualVal; + final Number expectedNumber = (Number) expectedVal; + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { - return (actualNumber.doubleValue() <= expectedNumber.doubleValue()); + @Override + public boolean accept(Number value) { + return (value.doubleValue() <= expectedNumber.doubleValue()); + } + }); } else if (CriteriaType.NE.equals(key)) { - if (expectedVal == null && actualVal == null) { - return false; - } - if (expectedVal == null) { - return true; - } else { - return !expectedVal.equals(actualVal); - } + + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { + + @Override + public boolean accept(Object value) { + if (expectedVal == null && value == null) { + return false; + } + if (expectedVal == null) { + return true; + } else { + return !expectedVal.equals(value); + } + } + }); } else if (CriteriaType.IN.equals(key)) { @@ -186,36 +228,53 @@ public class Criteria { } else if (CriteriaType.EXISTS.equals(key)) { - boolean exp = (Boolean) expectedVal; - boolean act = map.containsKey(this.key); + final boolean exp = (Boolean) expectedVal; + return objectOrAnyCollectionItemMatches(map, new Predicate() { + + @Override + public boolean accept(final Object value) { + boolean act = true; + try { + Object val = getKey().read(value); + if(val instanceof Collection){ + act = !((Collection) val).isEmpty(); + } + } catch (InvalidPathException e) { + act = false; + } + return act == exp; + + } - return act == exp; + }); } else if (CriteriaType.TYPE.equals(key)) { - Class exp = (Class) expectedVal; - Class act = null; - if (map.containsKey(this.key)) { - Object actVal = map.get(this.key); - if (actVal != null) { - act = actVal.getClass(); + final Class exp = (Class) expectedVal; + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { + + @Override + public boolean accept(Object value) { + Class act = value == null ? null : value.getClass(); + if (act == null) { + return false; + } else { + return act.equals(exp); } - } - if (act == null) { - return false; - } else { - return act.equals(exp); - } + } + }); } else if (CriteriaType.REGEX.equals(key)) { + final Pattern exp = (Pattern) expectedVal; + + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { - - Pattern exp = (Pattern) expectedVal; - String act = (String) actualVal; - if (act == null) { - return false; - } - return exp.matcher(act).matches(); + @Override + public boolean accept(String value) { + return value != null && exp.matcher(value).matches(); + } + }); + } else { throw new UnsupportedOperationException("Criteria type not supported: " + key.name()); @@ -234,11 +293,18 @@ public class Criteria { } return true; } else { - if (isValue == null) { - return (map.get(key) == null); - } else { - return isValue.equals(map.get(key)); - } + Object actualVal = readSafely(this.key, map); + return objectOrAnyCollectionItemMatches(actualVal, new Predicate() { + @Override + public boolean accept(Object value) { + if (isValue == null) { + return value == null; + } else { + return isValue.equals(value); + } + } + + }); } } else { @@ -375,6 +441,7 @@ public class Criteria { */ public Criteria in(Collection c) { notNull(c, "collection can not be null"); + checkFilterCanBeApplied(CriteriaType.IN); criteria.put(CriteriaType.IN, c); return this; } @@ -399,6 +466,7 @@ public class Criteria { */ public Criteria nin(Collection c) { notNull(c, "collection can not be null"); + checkFilterCanBeApplied(CriteriaType.NIN); criteria.put(CriteriaType.NIN, c); return this; } @@ -421,6 +489,7 @@ public class Criteria { */ public Criteria all(Collection c) { notNull(c, "collection can not be null"); + checkFilterCanBeApplied(CriteriaType.ALL); criteria.put(CriteriaType.ALL, c); return this; } @@ -432,6 +501,7 @@ public class Criteria { * @return */ public Criteria size(int s) { + checkFilterCanBeApplied(CriteriaType.SIZE); criteria.put(CriteriaType.SIZE, s); return this; } @@ -505,6 +575,16 @@ public class Criteria { criteriaChain.add(new Criteria("$and").is(asList(criteria))); return this; } + + private void checkFilterCanBeApplied(CriteriaType type){ + if (getKey().getTokenizer().size() > 2){ + throw new IllegalArgumentException("Cannot use "+type+" filter on a multi-level path expression"); + } + } + + private interface Predicate { + boolean accept(T value); + } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/Filter.java b/json-path/src/main/java/com/jayway/jsonpath/Filter.java index 8a3b8076..9de183bf 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Filter.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Filter.java @@ -101,8 +101,8 @@ public abstract class Filter { } public MapFilter addCriteria(Criteria criteria) { - Criteria existing = this.criteria.get(criteria.getKey()); - String key = criteria.getKey(); + Criteria existing = this.criteria.get(criteria.getKey().getPath()); + String key = criteria.getKey().getPath(); if (existing == null) { this.criteria.put(key, criteria); } else { 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 77cd22ca..2ee3a0e2 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 net.minidev.json.parser.JSONParser; + import org.junit.Test; import java.util.Collections; @@ -382,5 +384,41 @@ public class FilterTest { assertEquals(1, res.get(0).intValue()); } + + @Test + public void filters_can_contain_json_path_expressions() throws Exception { + Object doc = JsonModel.model(DOCUMENT).getJsonObject(); + + assertTrue(filter(where("$.store..price").gt(10)).accept(doc)); + assertFalse(filter(where("$.store..price").gte(100)).accept(doc)); + assertTrue(filter(where("$.store..category").ne("fiction")).accept(doc)); + assertFalse(filter(where("$.store.bicycle.color").ne("red")).accept(doc)); + assertTrue(filter(where("$.store.bicycle.color").ne("blue")).accept(doc)); + assertTrue(filter(where("$.store..color").exists(true)).accept(doc)); + assertFalse(filter(where("$.store..flavor").exists(true)).accept(doc)); + assertTrue(filter(where("$.store..color").regex(Pattern.compile("^r.d$"))).accept(doc)); + assertTrue(filter(where("$.store..color").type(String.class)).accept(doc)); + assertTrue(filter(where("$.store..price").is(12.99)).accept(doc)); + assertFalse(filter(where("$.store..price").is(13.99)).accept(doc)); + + } + + @Test + public void collection_based_filters_cannot_be_applied_to_multi_level_expressions(){ + + try{ + where("$.store.*").size(4); + fail("This should have thrown an exception"); + } catch(IllegalArgumentException e){ + + } + + try{ + where("$.store.*").in("foo"); + fail("This should have thrown an exception"); + } catch(IllegalArgumentException e){ + + } + } }