From c759a20188b9295687ea52c075e6ff4b7cc5219d Mon Sep 17 00:00:00 2001 From: Elias Ross Date: Fri, 29 May 2015 10:24:48 -0700 Subject: [PATCH 1/2] Allow JSON values to be directly used in criteria matching This is useful for matching against arrays of primitives, for example. --- .../com/jayway/jsonassert/JsonAssertTest.java | 9 ++++ .../java/com/jayway/jsonpath/Criteria.java | 46 +++++++++++++++++-- .../java/com/jayway/jsonpath/FilterTest.java | 20 ++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java b/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java index e4b66492..9889e64b 100644 --- a/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java +++ b/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java @@ -38,6 +38,8 @@ public class JsonAssertTest { " \"bicycle\": {\n" + " \"color\": \"red\",\n" + " \"price\": 19.95\n," + + " \"gears\": [23, 50]\n," + + " \"extra\": {\"x\": 0}\n," + " \"nullValue\": null\n" + " }\n" + " }\n" + @@ -55,6 +57,13 @@ public class JsonAssertTest { with(JSON).assertNotDefined("$.store.bicycle[?(@.color == 'red' )]"); } + @Test + public void assert_gears() throws Exception { + with(JSON).assertThat("$.store.bicycle[?(@.gears == [23, 50])]", is(collectionWithSize(equalTo(1)))); + with(JSON).assertThat("$.store.bicycle[?(@.gears == [23, 77])]", is(collectionWithSize(equalTo(0)))); + with(JSON).assertThat("$.store.bicycle[?(@.extra == {\"x\":0})]", is(collectionWithSize(equalTo(1)))); + } + @Test(expected = AssertionError.class) public void failed_error_message() throws Exception { 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 67a95f16..bc7af80c 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -17,6 +17,7 @@ package com.jayway.jsonpath; import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.PathCompiler; import com.jayway.jsonpath.internal.token.PredicateContextImpl; +import com.jayway.jsonpath.spi.json.JsonProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -59,7 +60,7 @@ public class Criteria implements Predicate { EQ { @Override boolean eval(Object expected, Object model, PredicateContext ctx) { - boolean res = (0 == safeCompare(expected, model)); + boolean res = (0 == safeCompare(expected, model, ctx)); if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res); return res; } @@ -72,7 +73,7 @@ public class Criteria implements Predicate { NE { @Override boolean eval(Object expected, Object model, PredicateContext ctx) { - boolean res = (0 != safeCompare(expected, model)); + boolean res = (0 != safeCompare(expected, model, ctx)); if (logger.isDebugEnabled()) logger.debug("[{}] {} [{}] => {}", model, name(), expected, res); return res; } @@ -710,6 +711,14 @@ public class Criteria implements Predicate { return (string != null && !string.isEmpty() && string.charAt(0) == '\'' && string.charAt(string.length() - 1) == '\''); } + private static boolean isJson(String string) { + if (string == null || string.length() <= 1) + return false; + char c0 = string.charAt(0); + char c1 = string.charAt(string.length() - 1); + return (c0 == '[' && c1 == ']') || (c0 == '{' && c1 == '}'); + } + private static boolean isPattern(String string) { return (string != null && !string.isEmpty() @@ -727,7 +736,6 @@ public class Criteria implements Predicate { return Pattern.compile(regex, flags); } - /** * Parse the provided criteria * @@ -755,6 +763,28 @@ public class Criteria implements Predicate { return Criteria.create(left, operator, right); } + /** + * Wrapper for JSON to be parsed as a String. + */ + private static class JsonValue { + final String value; + volatile Object jsonValue; + JsonValue(String value) { this.value = value; } + + Object parsed(PredicateContext ctx) { + if (jsonValue == null) { + JsonProvider provider = ctx.configuration().jsonProvider(); + jsonValue = provider.parse(value); + } + return jsonValue; + } + + @Override + public String toString() { + return getClass().getSimpleName() + " " + value; + } + } + /** * Creates a new criteria * @@ -797,6 +827,8 @@ public class Criteria implements Predicate { rightPrepared = rightPath; } else if (isString(right)) { rightPrepared = right.substring(1, right.length() - 1); + } else if (isJson(right)) { + rightPrepared = new JsonValue(right); } else if (isPattern(right)) { rightPrepared = compilePattern(right); } @@ -809,6 +841,10 @@ public class Criteria implements Predicate { } private static int safeCompare(Object left, Object right) throws ValueCompareException { + return safeCompare(left, right, null); + } + + private static int safeCompare(Object left, Object right, PredicateContext ctx) throws ValueCompareException { if (left == right) { return 0; @@ -841,6 +877,10 @@ public class Criteria implements Predicate { Boolean e = (Boolean) left; Boolean a = (Boolean) right; return e.compareTo(a); + } else if (left instanceof JsonValue) { + notNull(ctx, "ctx"); + JsonValue json = (JsonValue) left; + return right.equals(json.parsed(ctx)) ? 0 : -1; } else { logger.debug("Can not compare a {} with a {}", left.getClass().getName(), right.getClass().getName()); throw new ValueCompareException(); 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 45956b52..be99d514 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java @@ -240,6 +240,26 @@ public class FilterTest extends BaseTest { assertThat(filter(where("int-key").regex(Pattern.compile("^string$"))).apply(createPredicateContext(json))).isEqualTo(false); } + //---------------------------------------------------------------------------- + // + // JSON equality + // + //---------------------------------------------------------------------------- + @Test + public void json_evals() { + String nest = "{\"a\":true}"; + String arr = "[1,2]"; + String json = "{\"foo\":" + arr + ", \"bar\":" + nest + "}"; + Object tree = Configuration.defaultConfiguration().jsonProvider().parse(json); + Predicate.PredicateContext context = createPredicateContext(tree); + Filter farr = Filter.parse("[?(@.foo == " + arr + ")]"); + Filter fobjF = Filter.parse("[?(@.foo == " + nest + ")]"); + Filter fobjT = Filter.parse("[?(@.bar == " + nest + ")]"); + assertThat(farr.apply(context)).isEqualTo(true); + assertThat(fobjF.apply(context)).isEqualTo(false); + assertThat(fobjT.apply(context)).isEqualTo(true); + } + //---------------------------------------------------------------------------- // // IN From fb9c621e5c4212fc7363eec5a927331e6f6596a8 Mon Sep 17 00:00:00 2001 From: Elias Ross Date: Sat, 27 Jun 2015 17:21:18 -0700 Subject: [PATCH 2/2] unescape strings in comparison, to deal with tabs, etc. --- .../com/jayway/jsonassert/JsonAssertTest.java | 2 + .../java/com/jayway/jsonpath/Criteria.java | 40 ++++++++++++++++--- .../jsonpath/ValueCompareException.java | 14 ++++++- .../java/com/jayway/jsonpath/BaseTest.java | 1 + .../com/jayway/jsonpath/InlineFilterTest.java | 7 ++-- 5 files changed, 55 insertions(+), 9 deletions(-) diff --git a/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java b/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java index 9889e64b..63b8996c 100644 --- a/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java +++ b/json-path-assert/src/test/java/com/jayway/jsonassert/JsonAssertTest.java @@ -40,6 +40,7 @@ public class JsonAssertTest { " \"price\": 19.95\n," + " \"gears\": [23, 50]\n," + " \"extra\": {\"x\": 0}\n," + + " \"escape\" : \"Esc\\b\\f\\n\\r\\t\\u002A\",\n" + " \"nullValue\": null\n" + " }\n" + " }\n" + @@ -62,6 +63,7 @@ public class JsonAssertTest { with(JSON).assertThat("$.store.bicycle[?(@.gears == [23, 50])]", is(collectionWithSize(equalTo(1)))); with(JSON).assertThat("$.store.bicycle[?(@.gears == [23, 77])]", is(collectionWithSize(equalTo(0)))); with(JSON).assertThat("$.store.bicycle[?(@.extra == {\"x\":0})]", is(collectionWithSize(equalTo(1)))); + with(JSON).assertThat("$.store.bicycle[?(@.escape == 'Esc\\b\\f\\n\\r\\t\\u002A')]", is(collectionWithSize(equalTo(1)))); } @Test(expected = AssertionError.class) 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 bc7af80c..28c75744 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -49,6 +49,7 @@ public class Criteria implements Predicate { CriteriaType.GT.toString(), CriteriaType.REGEX.toString() }; + private static final char BS = '\\'; private Object left; private CriteriaType criteriaType; @@ -844,6 +845,38 @@ public class Criteria implements Predicate { return safeCompare(left, right, null); } + private static String unescape(String s) { + if (s.indexOf(BS) == - 1) + return s; + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == BS) { + char c2 = s.charAt(++i); + switch (c2) { + case 'b': c2 = '\b'; break; + case 'f': c2 = '\f'; break; + case 'n': c2 = '\n'; break; + case 'r': c2 = '\r'; break; + case 't': c2 = '\t'; break; + case 'u': + try { + String hex = s.substring(i + 1, i + 5); + c2 = (char)Integer.parseInt(hex, 16); + i += 4; + } catch (Exception e) { + throw new ValueCompareException("\\u parse failed", e); + } + break; + } + sb.append(c2); + } else { + sb.append(c); + } + } + return sb.toString(); + } + private static int safeCompare(Object left, Object right, PredicateContext ctx) throws ValueCompareException { if (left == right) { @@ -860,11 +893,8 @@ public class Criteria implements Predicate { } else if (leftNullish && rightNullish) { return 0; } else if (left instanceof String && right instanceof String) { - String exp = (String) left; - if (exp.contains("\'")) { - exp = exp.replace("\\'", "'"); - } - return exp.compareTo((String) right); + String expected = unescape((String) left); + return expected.compareTo((String) right); } else if (left instanceof Number && right instanceof Number) { return new BigDecimal(left.toString()).compareTo(new BigDecimal(right.toString())); } else if (left instanceof String && right instanceof Number) { diff --git a/json-path/src/main/java/com/jayway/jsonpath/ValueCompareException.java b/json-path/src/main/java/com/jayway/jsonpath/ValueCompareException.java index 862ae13a..b9cffccb 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/ValueCompareException.java +++ b/json-path/src/main/java/com/jayway/jsonpath/ValueCompareException.java @@ -15,4 +15,16 @@ package com.jayway.jsonpath; public class ValueCompareException extends JsonPathException { -} \ No newline at end of file + + public ValueCompareException() { + } + + public ValueCompareException(String message) { + super(message); + } + + public ValueCompareException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java b/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java index 21fa0e53..ababb1e0 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java @@ -78,6 +78,7 @@ public class BaseTest { " ],\n" + " \"bicycle\" : {\n" + " \"foo\" : \"baz\",\n" + + " \"escape\" : \"Esc\\b\\f\\n\\r\\t\\n\\t\\u002A\",\n" + " \"color\" : \"red\",\n" + " \"display-price\" : 19.95,\n" + " \"foo:bar\" : \"fooBar\",\n" + diff --git a/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java index 2f8cd215..f56c0d02 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java @@ -11,6 +11,7 @@ import static org.assertj.core.api.Assertions.assertThat; public class InlineFilterTest extends BaseTest { private static ReadContext reader = JsonPath.parse(JSON_DOCUMENT); + private static int bookCount = 4; @Test public void root_context_can_be_referred_in_predicate() { @@ -23,13 +24,13 @@ public class InlineFilterTest extends BaseTest { public void multiple_context_object_can_be_refered() { List all = reader.read("store.book[ ?(@.category == @.category) ]", List.class); - assertThat(all.size()).isEqualTo(4); + assertThat(all.size()).isEqualTo(bookCount); List all2 = reader.read("store.book[ ?(@.category == @['category']) ]", List.class); - assertThat(all2.size()).isEqualTo(4); + assertThat(all2.size()).isEqualTo(bookCount); List all3 = reader.read("store.book[ ?(@ == @) ]", List.class); - assertThat(all3.size()).isEqualTo(4); + assertThat(all3.size()).isEqualTo(bookCount); List none = reader.read("store.book[ ?(@.category != @.category) ]", List.class); assertThat(none.size()).isEqualTo(0);