diff --git a/json-path-assert/src/main/java/com/jayway/jsonassert/impl/JsonAsserterImpl.java b/json-path-assert/src/main/java/com/jayway/jsonassert/impl/JsonAsserterImpl.java index 661310b8..e068460a 100644 --- a/json-path-assert/src/main/java/com/jayway/jsonassert/impl/JsonAsserterImpl.java +++ b/json-path-assert/src/main/java/com/jayway/jsonassert/impl/JsonAsserterImpl.java @@ -33,7 +33,9 @@ public class JsonAsserterImpl implements JsonAsserter { try { obj = JsonPath.read(jsonObject, path); } catch (Exception e) { - throw new AssertionError(String.format("Error reading JSON path [%s]", path), e); + final AssertionError assertionError = new AssertionError(String.format("Error reading JSON path [%s]", path)); + assertionError.initCause(e); + throw assertionError; } if (!matcher.matches(obj)) { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java index 4a3a36c0..a0760fc2 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java @@ -488,9 +488,18 @@ public class PathCompiler { while (inBounds(readPosition)) { if (skipStrings) { if (charAt(readPosition) == TICK) { + boolean escaped = false; while (inBounds(readPosition)) { readPosition++; - if (charAt(readPosition) == TICK && charAt(readPosition - 1) != ESCAPE) { + if (escaped) { + escaped = false; + continue; + } + if (charAt(readPosition) == ESCAPE) { + escaped = true; + continue; + } + if (charAt(readPosition) == TICK) { readPosition++; break; } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java b/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java index 391fb8a1..b5192869 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java @@ -154,7 +154,7 @@ public abstract class PathRef implements Comparable { public int compareTo(PathRef o) { if(o instanceof ArrayIndexPathRef){ ArrayIndexPathRef pf = (ArrayIndexPathRef) o; - return Integer.compare(pf.index, this.index); + return Integer.valueOf(pf.index).compareTo(this.index); } return super.compareTo(o); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java b/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java index 5a1029ff..b22cc9ec 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java @@ -272,6 +272,29 @@ public final class Utils { } } + /** + * Check if one and only one condition is true; otherwise + * throw an exception with the specified message. + * @param message error describing message + * @param expressions the boolean expressions to check + * @throws IllegalArgumentException if zero or more than one expressions are true + */ + public static void onlyOneIsTrue(final String message, final boolean ... expressions) { + if (! onlyOneIsTrueNonThrow(expressions)) { + throw new IllegalArgumentException(message); + } + } + + public static boolean onlyOneIsTrueNonThrow(final boolean ... expressions) { + int count = 0; + for (final boolean expression : expressions) { + if (expression && ++count > 1) { + return false; + } + } + return 1 == count; + } + /** *

Validate that the specified argument character sequence is * neither {@code null} nor a length of zero (no characters); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java index af6f1077..65c7784d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java @@ -44,12 +44,8 @@ public class ArrayPathToken extends PathToken { @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - if(model == null){ - throw new PathNotFoundException("The path " + currentPath + " is null"); - } - if (!ctx.jsonProvider().isArray(model)) { - throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); - } + if (! checkArrayModel(currentPath, model, ctx)) + return; if(arraySliceOperation != null){ evaluateSliceOperation(currentPath, parent, model, ctx); } else { @@ -60,12 +56,8 @@ public class ArrayPathToken extends PathToken { public void evaluateIndexOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - if (model == null) { - throw new PathNotFoundException("The path " + currentPath + " is null"); - } - if (!ctx.jsonProvider().isArray(model)) { - throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); - } + if (! checkArrayModel(currentPath, model, ctx)) + return; if(arrayIndexOperation.isSingleIndexOperation()){ handleArrayIndex(arrayIndexOperation.indexes().get(0), currentPath, model, ctx); @@ -78,12 +70,8 @@ public class ArrayPathToken extends PathToken { public void evaluateSliceOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { - if (model == null) { - throw new PathNotFoundException("The path " + currentPath + " is null"); - } - if (!ctx.jsonProvider().isArray(model)) { - throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); - } + if (! checkArrayModel(currentPath, model, ctx)) + return; switch (arraySliceOperation.operation()) { case SLICE_FROM: @@ -171,4 +159,31 @@ public class ArrayPathToken extends PathToken { return false; } } + + /** + * Check if model is non-null and array. + * @param currentPath + * @param model + * @param ctx + * @return false if current evaluation call must be skipped, true otherwise + * @throws PathNotFoundException if model is null and evaluation must be interrupted + * @throws InvalidPathException if model is not an array and evaluation must be interrupted + */ + protected boolean checkArrayModel(String currentPath, Object model, EvaluationContextImpl ctx) { + if (model == null){ + if (! isUpstreamDefinite()) { + return false; + } else { + throw new PathNotFoundException("The path " + currentPath + " is null"); + } + } + if (!ctx.jsonProvider().isArray(model)) { + if (! isUpstreamDefinite()) { + return false; + } else { + throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); + } + } + return true; + } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java index d80a8b93..47b76383 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java @@ -49,6 +49,12 @@ public abstract class PathToken { String evalPath = Utils.concat(currentPath, "['", property, "']"); Object propertyVal = readObjectProperty(property, model, ctx); if(propertyVal == JsonProvider.UNDEFINED){ + // Conditions below heavily depend on current token type (and its logic) and are not "universal", + // so this code is quite dangerous (I'd rather rewrite it & move to PropertyPathToken and implemented + // WildcardPathToken as a dynamic multi prop case of PropertyPathToken). + // Better safe than sorry. + assert this instanceof PropertyPathToken : "only PropertyPathToken is supported"; + if(isLeaf()) { if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){ propertyVal = null; @@ -61,9 +67,12 @@ public abstract class PathToken { } } } else { - if(!isUpstreamDefinite() && - !ctx.options().contains(Option.REQUIRE_PROPERTIES) && - !ctx.options().contains(Option.SUPPRESS_EXCEPTIONS)){ + if (! (isUpstreamDefinite() && isTokenDefinite()) && + !ctx.options().contains(Option.REQUIRE_PROPERTIES) || + ctx.options().contains(Option.SUPPRESS_EXCEPTIONS)){ + // If there is some indefiniteness in the path and properties are not required - we'll ignore + // absent property. And also in case of exception suppression - so that other path evaluation + // branches could be examined. return; } else { throw new PathNotFoundException("Missing property in path " + evalPath); @@ -80,9 +89,7 @@ public abstract class PathToken { } else { String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]"; - if (!isLeaf()) { - throw new InvalidPathException("Multi properties can only be used as path leafs: " + evalPath); - } + assert isLeaf() : "non-leaf multi props handled elsewhere"; Object merged = ctx.jsonProvider().createMap(); for (String property : properties) { @@ -154,16 +161,11 @@ public abstract class PathToken { return prev == null; } - boolean isUpstreamDefinite(){ - if(upstreamDefinite != null){ - return upstreamDefinite.booleanValue(); - } - boolean isUpstreamDefinite = isTokenDefinite(); - if (isUpstreamDefinite && !isRoot()) { - isUpstreamDefinite = prev.isPathDefinite(); + boolean isUpstreamDefinite() { + if (upstreamDefinite == null) { + upstreamDefinite = isRoot() || prev.isTokenDefinite() && prev.isUpstreamDefinite(); } - upstreamDefinite = isUpstreamDefinite; - return isUpstreamDefinite; + return upstreamDefinite; } public int getTokenCount() { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java index df7af013..4a27e51e 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java @@ -62,7 +62,9 @@ public class PredicatePathToken extends PathToken { idx++; } } else { - throw new InvalidPathException(format("Filter: %s can not be applied to primitives. Current context is: %s", toString(), model)); + if (isUpstreamDefinite()) { + throw new InvalidPathException(format("Filter: %s can not be applied to primitives. Current context is: %s", toString(), model)); + } } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java index 52dcc0ed..d4633a67 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java @@ -14,12 +14,16 @@ */ package com.jayway.jsonpath.internal.token; +import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.Utils; +import java.util.ArrayList; import java.util.List; +import static com.jayway.jsonpath.internal.Utils.onlyOneIsTrueNonThrow; + /** * */ @@ -28,6 +32,9 @@ class PropertyPathToken extends PathToken { private final List properties; public PropertyPathToken(List properties) { + if (properties.isEmpty()) { + throw new InvalidPathException("Empty properties"); + } this.properties = properties; } @@ -35,21 +42,55 @@ class PropertyPathToken extends PathToken { return properties; } + public boolean singlePropertyCase() { + return properties.size() == 1; + } + + public boolean multiPropertyMergeCase() { + return isLeaf() && properties.size() > 1; + } + + public boolean multiPropertyIterationCase() { + // Semantics of this case is the same as semantics of ArrayPathToken with INDEX_SEQUENCE operation. + return ! isLeaf() && properties.size() > 1; + } + @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + // Can't assert it in ctor because isLeaf() could be changed later on. + assert onlyOneIsTrueNonThrow(singlePropertyCase(), multiPropertyMergeCase(), multiPropertyIterationCase()); + if (!ctx.jsonProvider().isMap(model)) { + if (! isUpstreamDefinite()) { + return; + } else { + String m = model == null ? "null" : model.getClass().getName(); - String m = model == null ? "null" : model.getClass().getName(); + throw new PathNotFoundException(String.format( + "Expected to find an object with property %s in path %s but found '%s'. " + + "This is not a json object according to the JsonProvider: '%s'.", + getPathFragment(), currentPath, m, ctx.configuration().jsonProvider().getClass().getName())); + } + } - throw new PathNotFoundException("Expected to find an object with property " + getPathFragment() + " but found '" + m + "'. This is not a json object according to the JsonProvider: '" + ctx.configuration().jsonProvider().getClass().getName() + "'."); + if (singlePropertyCase() || multiPropertyMergeCase()) { + handleObjectProperty(currentPath, model, ctx, properties); + return; } - handleObjectProperty(currentPath, model, ctx, properties); + assert multiPropertyIterationCase(); + final List currentlyHandledProperty = new ArrayList(1); + currentlyHandledProperty.add(null); + for (final String property : properties) { + currentlyHandledProperty.set(0, property); + handleObjectProperty(currentPath, model, ctx, currentlyHandledProperty); + } } @Override public boolean isTokenDefinite() { - return true; + // in case of leaf multiprops will be merged, so it's kinda definite + return singlePropertyCase() || multiPropertyMergeCase(); } @Override diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java index e6286ed7..07d48ad2 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java @@ -14,6 +14,7 @@ */ package com.jayway.jsonpath.internal.token; +import com.jayway.jsonpath.Option; import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.spi.json.JsonProvider; @@ -169,11 +170,29 @@ public class ScanPathToken extends PathToken { @Override public boolean matches(Object model) { - if (ctx.jsonProvider().isMap(model)) { - Collection keys = ctx.jsonProvider().getPropertyKeys(model); - return keys.containsAll(propertyPathToken.getProperties()); + if (! ctx.jsonProvider().isMap(model)) { + return false; } - return false; + + if (ctx.options().contains(Option.REQUIRE_PROPERTIES)) { + // Have to require properties defined in path when an indefinite path is evaluated, + // so have to go there and search for it. + return true; + } + + if (! propertyPathToken.isTokenDefinite()) { + // It's responsibility of PropertyPathToken code to handle indefinite scenario of properties, + // so we'll allow it to do its job. + return true; + } + + if (propertyPathToken.isLeaf() && ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)) { + // In case of DEFAULT_PATH_LEAF_TO_NULL missing properties is not a problem. + return true; + } + + Collection keys = ctx.jsonProvider().getPropertyKeys(model); + return keys.containsAll(propertyPathToken.getProperties()); } } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/DeepScanTest.java b/json-path/src/test/java/com/jayway/jsonpath/DeepScanTest.java new file mode 100644 index 00000000..6724809e --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/DeepScanTest.java @@ -0,0 +1,118 @@ +package com.jayway.jsonpath; + +import org.junit.Test; + +import static com.jayway.jsonpath.JsonPath.using; +import static com.jayway.jsonpath.TestUtils.assertEvaluationThrows; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Map; + +/** + * Deep scan is indefinite, so certain "illegal" actions become a no-op instead of a path evaluation exception. + */ +public class DeepScanTest extends BaseTest { + + @Test + public void when_deep_scanning_non_array_subscription_is_ignored() { + Object result = JsonPath.parse("{\"x\": [0,1,[0,1,2,3,null],null]}").read("$..[2][3]"); + assertThat(result).asList().containsOnly(3); + result = JsonPath.parse("{\"x\": [0,1,[0,1,2,3,null],null], \"y\": [0,1,2]}").read("$..[2][3]"); + assertThat(result).asList().containsOnly(3); + + result = JsonPath.parse("{\"x\": [0,1,[0,1,2],null], \"y\": [0,1,2]}").read("$..[2][3]"); + assertThat(result).asList().isEmpty(); + } + + @Test + public void when_deep_scanning_null_subscription_is_ignored() { + Object result = JsonPath.parse("{\"x\": [null,null,[0,1,2,3,null],null]}").read("$..[2][3]"); + assertThat(result).asList().containsOnly(3); + result = JsonPath.parse("{\"x\": [null,null,[0,1,2,3,null],null], \"y\": [0,1,null]}").read("$..[2][3]"); + assertThat(result).asList().containsOnly(3); + } + + @Test + public void when_deep_scanning_array_index_oob_is_ignored() { + Object result = JsonPath.parse("{\"x\": [0,1,[0,1,2,3,10],null]}").read("$..[4]"); + assertThat(result).asList().containsOnly(10); + + result = JsonPath.parse("{\"x\": [null,null,[0,1,2,3]], \"y\": [null,null,[0,1]]}").read("$..[2][3]"); + assertThat(result).asList().containsOnly(3); + } + + @Test + public void definite_upstream_illegal_array_access_throws() { + assertEvaluationThrows("{\"foo\": {\"bar\": null}}", "$.foo.bar.[5]", PathNotFoundException.class); + assertEvaluationThrows("{\"foo\": {\"bar\": null}}", "$.foo.bar.[5, 10]", PathNotFoundException.class); + + assertEvaluationThrows("{\"foo\": {\"bar\": 4}}", "$.foo.bar.[5]", InvalidPathException.class); + assertEvaluationThrows("{\"foo\": {\"bar\": 4}}", "$.foo.bar.[5, 10]", InvalidPathException.class); + + assertEvaluationThrows("{\"foo\": {\"bar\": []}}", "$.foo.bar.[5]", PathNotFoundException.class); + } + + @Test + public void when_deep_scanning_illegal_property_access_is_ignored() { + Object result = JsonPath.parse("{\"x\": {\"foo\": {\"bar\": 4}}, \"y\": {\"foo\": 1}}").read("$..foo"); + assertThat(result).asList().hasSize(2); + + result = JsonPath.parse("{\"x\": {\"foo\": {\"bar\": 4}}, \"y\": {\"foo\": 1}}").read("$..foo.bar"); + assertThat(result).asList().containsOnly(4); + result = JsonPath.parse("{\"x\": {\"foo\": {\"bar\": 4}}, \"y\": {\"foo\": 1}}").read("$..[*].foo.bar"); + assertThat(result).asList().containsOnly(4); + result = JsonPath.parse("{\"x\": {\"foo\": {\"baz\": 4}}, \"y\": {\"foo\": 1}}").read("$..[*].foo.bar"); + assertThat(result).asList().isEmpty(); + } + + @Test + public void when_deep_scanning_illegal_predicate_is_ignored() { + Object result = JsonPath.parse("{\"x\": {\"foo\": {\"bar\": 4}}, \"y\": {\"foo\": 1}}").read( + "$..foo[?(@.bar)].bar"); + assertThat(result).asList().containsOnly(4); + + result = JsonPath.parse("{\"x\": {\"foo\": {\"bar\": 4}}, \"y\": {\"foo\": 1}}").read( + "$..[*]foo[?(@.bar)].bar"); + assertThat(result).asList().containsOnly(4); + } + + @Test + public void when_deep_scanning_require_properties_still_counts() { + final Configuration conf = Configuration.defaultConfiguration().addOptions(Option.REQUIRE_PROPERTIES); + + Object result = JsonPath.parse("[{\"x\": {\"foo\": {\"x\": 4}, \"x\": null}, \"y\": {\"x\": 1}}, {\"x\": []}]").read( + "$..x"); + assertThat(result).asList().hasSize(5); + + // foo.bar must be found in every object node after deep scan (which is impossible) + assertEvaluationThrows("{\"foo\": {\"bar\": 4}}", "$..foo.bar", PathNotFoundException.class, conf); + + assertEvaluationThrows("{\"foo\": {\"bar\": 4}, \"baz\": 2}", "$..['foo', 'baz']", PathNotFoundException.class, conf); + } + + @Test + public void when_deep_scanning_leaf_multi_props_work() { + Object result = JsonPath.parse("[{\"a\": \"a-val\", \"b\": \"b-val\", \"c\": \"c-val\"}, [1, 5], {\"a\": \"a-val\"}]").read( + "$..['a', 'c']"); + // This is current deep scan semantics: only objects containing all properties specified in multiprops token are + // considered. + assertThat(result).asList().hasSize(1); + result = ((List)result).get(0); + + assertThat(result).isInstanceOf(Map.class); + assertThat((Map)result).hasSize(2).containsEntry("a", "a-val").containsEntry("c", "c-val"); + + // But this semantics changes when DEFAULT_PATH_LEAF_TO_NULL comes into play. + Configuration conf = Configuration.defaultConfiguration().addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL); + result = using(conf).parse("[{\"a\": \"a-val\", \"b\": \"b-val\", \"c\": \"c-val\"}, [1, 5], {\"a\": \"a-val\"}]").read( + "$..['a', 'c']"); + // todo: deep equality test, but not tied to any json provider + assertThat(result).asList().hasSize(2); + for (final Object node : (List)result) { + assertThat(node).isInstanceOf(Map.class); + assertThat((Map)node).hasSize(2).containsEntry("a", "a-val"); + } + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java b/json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java index c850eed3..2f7d45d7 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java @@ -6,6 +6,8 @@ import java.util.HashMap; import java.util.Map; import static com.jayway.jsonpath.JsonPath.using; +import static com.jayway.jsonpath.TestUtils.assertEvaluationThrows; + import static org.assertj.core.api.Assertions.assertThat; public class MultiPropTest { @@ -24,6 +26,10 @@ public class MultiPropTest { assertThat(using(conf).parse(model).read("$['a', 'b']", Map.class)) .containsEntry("a", "a-val") .containsEntry("b", "b-val"); + + // current semantics: absent props are skipped + assertThat(using(conf).parse(model).read("$['a', 'd']", Map.class)) + .hasSize(1).containsEntry("a", "a-val"); } @Test @@ -56,5 +62,60 @@ public class MultiPropTest { using(conf).parse(model).read("$['a', 'x']", Map.class); } + @Test + public void multi_props_can_be_non_leafs() { + Object result = JsonPath.parse("{\"a\": {\"v\": 5}, \"b\": {\"v\": 4}, \"c\": {\"v\": 1}}").read( + "$['a', 'c'].v"); + assertThat(result).asList().containsOnly(5, 1); + } + + @Test + public void nonexistent_non_leaf_multi_props_ignored() { + Object result = JsonPath.parse("{\"a\": {\"v\": 5}, \"b\": {\"v\": 4}, \"c\": {\"v\": 1}}").read( + "$['d', 'a', 'c', 'm'].v"); + assertThat(result).asList().containsOnly(5, 1); + } + + @Test + public void multi_props_with_post_filter() { + Object result = JsonPath.parse("{\"a\": {\"v\": 5}, \"b\": {\"v\": 4}, \"c\": {\"v\": 1, \"flag\": true}}").read( + "$['a', 'c'][?(@.flag)].v"); + assertThat(result).asList().containsOnly(1); + } + + @Test + public void deep_scan_does_not_affect_non_leaf_multi_props() { + // deep scan + multiprop is quite redundant scenario, but it's not forbidden, so we'd better check + final String json = "{\"v\": [[{}, 1, {\"a\": {\"v\": 5}, \"b\": {\"v\": 4}, \"c\": {\"v\": 1, \"flag\": true}}]]}"; + Object result = JsonPath.parse(json).read("$..['a', 'c'].v"); + assertThat(result).asList().containsOnly(5, 1); + + result = JsonPath.parse(json).read("$..['a', 'c'][?(@.flag)].v"); + assertThat(result).asList().containsOnly(1); + } + + @Test + public void multi_props_can_be_in_the_middle() { + final String json = "{\"x\": [null, {\"a\": {\"v\": 5}, \"b\": {\"v\": 4}, \"c\": {\"v\": 1}}]}"; + Object result = JsonPath.parse(json).read("$.x[1]['a', 'c'].v"); + assertThat(result).asList().containsOnly(5, 1); + result = JsonPath.parse(json).read("$.x[*]['a', 'c'].v"); + assertThat(result).asList().containsOnly(5, 1); + result = JsonPath.parse(json).read("$[*][*]['a', 'c'].v"); + assertThat(result).asList().containsOnly(5, 1); + + result = JsonPath.parse(json).read("$.x[1]['d', 'a', 'c', 'm'].v"); + assertThat(result).asList().containsOnly(5, 1); + result = JsonPath.parse(json).read("$.x[*]['d', 'a', 'c', 'm'].v"); + assertThat(result).asList().containsOnly(5, 1); + } + @Test + public void non_leaf_multi_props_can_be_required() { + final Configuration conf = Configuration.defaultConfiguration().addOptions(Option.REQUIRE_PROPERTIES); + final String json = "{\"a\": {\"v\": 5}, \"b\": {\"v\": 4}, \"c\": {\"v\": 1}}"; + + assertThat(using(conf).parse(json).read("$['a', 'c'].v")).asList().containsOnly(5, 1); + assertEvaluationThrows(json, "$['d', 'a', 'c', 'm'].v", PathNotFoundException.class, conf); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/OptionsTest.java b/json-path/src/test/java/com/jayway/jsonpath/OptionsTest.java index 8d7a243f..4553c36e 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/OptionsTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/OptionsTest.java @@ -2,6 +2,7 @@ package com.jayway.jsonpath; import org.junit.Test; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,6 +46,23 @@ public class OptionsTest extends BaseTest { Configuration conf = Configuration.builder().options(ALWAYS_RETURN_LIST).build(); assertThat(using(conf).parse("{\"foo\" : \"bar\"}").read("$.foo")).isInstanceOf(List.class); + + assertThat(using(conf).parse("{\"foo\": null}").read("$.foo")).isInstanceOf(List.class); + + assertThat(using(conf).parse("{\"foo\": [1, 4, 8]}").read("$.foo")).asList() + .containsExactly(Arrays.asList(1, 4, 8)); + } + + @Test + public void an_indefinite_path_can_be_returned_as_list() { + Configuration conf = Configuration.builder().options(ALWAYS_RETURN_LIST).build(); + + List result = using(conf).parse("{\"bar\": {\"foo\": null}}").read("$..foo"); + assertThat(result).hasSize(1); + assertThat(result.get(0)).isNull(); + + assertThat(using(conf).parse("{\"bar\": {\"foo\": [1, 4, 8]}}").read("$..foo")).asList() + .containsExactly(Arrays.asList(1, 4, 8)); } @Test @@ -119,4 +137,17 @@ public class OptionsTest extends BaseTest { } catch (PathNotFoundException pnf){} } + + @Test + public void issue_suppress_exceptions_does_not_break_indefinite_evaluation() { + Configuration conf = Configuration.builder().options(SUPPRESS_EXCEPTIONS).build(); + + assertThat(using(conf).parse("{\"foo2\": [5]}").read("$..foo2[0]")).asList().containsOnly(5); + assertThat(using(conf).parse("{\"foo\" : {\"foo2\": [5]}}").read("$..foo2[0]")).asList().containsOnly(5); + assertThat(using(conf).parse("[null, [{\"foo\" : {\"foo2\": [5]}}]]").read("$..foo2[0]")).asList().containsOnly(5); + + assertThat(using(conf).parse("[null, [{\"foo\" : {\"foo2\": [5]}}]]").read("$..foo.foo2[0]")).asList().containsOnly(5); + + assertThat(using(conf).parse("{\"aoo\" : {}, \"foo\" : {\"foo2\": [5]}, \"zoo\" : {}}").read("$[*].foo2[0]")).asList().containsOnly(5); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java b/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java index 5a1d05b6..32a14671 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java @@ -3,6 +3,8 @@ package com.jayway.jsonpath; import org.junit.Ignore; import org.junit.Test; +import java.util.List; + import static com.jayway.jsonpath.internal.PathCompiler.compile; import static org.assertj.core.api.Assertions.assertThat; @@ -128,4 +130,105 @@ public class PathCompilerTest { assertThat(compile("$..['prop']..[*]").toString()).isEqualTo("$..['prop']..[*]"); } + @Test + public void issue_predicate_can_have_escaped_backslash_in_prop() { + String json = "{\n" + + " \"logs\": [\n" + + " {\n" + + " \"message\": \"it\\\\\",\n" + + " \"id\": 2\n" + + " }\n" + + " ]\n" + + "}"; + // message: it\ -> (after json escaping) -> "it\\" -> (after java escaping) -> "\"it\\\\\"" + + List result = JsonPath.read(json, "$.logs[?(@.message == 'it\\\\')].message"); + + assertThat(result).containsExactly("it\\"); + } + + @Ignore("not ready yet (requires compiler reimplementation)") + @Test + public void issue_predicate_can_have_bracket_in_regex() { + String json = "{\n" + + " \"logs\": [\n" + + " {\n" + + " \"message\": \"(it\",\n" + + " \"id\": 2\n" + + " }\n" + + " ]\n" + + "}"; + + List result = JsonPath.read(json, "$.logs[?(@.message =~ /\\(it/)].message"); + + assertThat(result).containsExactly("(it"); + } + + @Ignore("not ready yet (requires compiler reimplementation)") + @Test + public void issue_predicate_can_have_and_in_regex() { + String json = "{\n" + + " \"logs\": [\n" + + " {\n" + + " \"message\": \"it\",\n" + + " \"id\": 2\n" + + " }\n" + + " ]\n" + + "}"; + + List result = JsonPath.read(json, "$.logs[?(@.message =~ /&&|it/)].message"); + + assertThat(result).containsExactly("it"); + } + + @Ignore("not ready yet (requires compiler reimplementation)") + @Test + public void issue_predicate_can_have_and_in_prop() { + String json = "{\n" + + " \"logs\": [\n" + + " {\n" + + " \"message\": \"&& it\",\n" + + " \"id\": 2\n" + + " }\n" + + " ]\n" + + "}"; + + List result = JsonPath.read(json, "$.logs[?(@.message == '&& it')].message"); + + assertThat(result).containsExactly("&& it"); + } + + @Ignore("not ready yet (requires compiler reimplementation)") + @Test + public void issue_predicate_brackets_must_change_priorities() { + String json = "{\n" + + " \"logs\": [\n" + + " {\n" + + " \"id\": 2\n" + + " }\n" + + " ]\n" + + "}"; + + List result = JsonPath.read(json, "$.logs[?(@.message && (@.id == 1 || @.id == 2))].id"); + assertThat(result).isEmpty(); + + result = JsonPath.read(json, "$.logs[?((@.id == 2 || @.id == 1) && @.message)].id"); + assertThat(result).isEmpty(); + } + + @Test + public void issue_predicate_can_have_square_bracket_in_prop() { + String json = "{\n" + + " \"logs\": [\n" + + " {\n" + + " \"message\": \"] it\",\n" + + " \"id\": 2\n" + + " }\n" + + " ]\n" + + "}"; + + List result = JsonPath.read(json, "$.logs[?(@.message == '] it')].message"); + + assertThat(result).containsExactly("] it"); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/TestUtils.java b/json-path/src/test/java/com/jayway/jsonpath/TestUtils.java new file mode 100644 index 00000000..1835d65b --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/TestUtils.java @@ -0,0 +1,32 @@ +package com.jayway.jsonpath; + +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.fail; + +public final class TestUtils { + private TestUtils() {} + + public static void assertEvaluationThrows(final String json, final String path, + Class expected) { + assertEvaluationThrows(json, path, expected, Configuration.defaultConfiguration()); + } + + /** + * Shortcut for expected exception testing during path evaluation. + * + * @param conf conf to use during evaluation + * @param json json to parse + * @param path jsonpath do evaluate + * @param expected expected exception class (reference comparison, not an instanceof) + */ + public static void assertEvaluationThrows(final String json, final String path, + Class expected, final Configuration conf) { + try { + using(conf).parse(json).read(path); + fail("Should throw " + expected.getName()); + } catch (JsonPathException exc) { + if (exc.getClass() != expected) + throw exc; + } + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/token/PathTokenTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/token/PathTokenTest.java new file mode 100644 index 00000000..e1e68dc4 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/internal/token/PathTokenTest.java @@ -0,0 +1,54 @@ +package com.jayway.jsonpath.internal.token; + +import com.jayway.jsonpath.BaseTest; +import com.jayway.jsonpath.internal.token.PathToken; +import com.jayway.jsonpath.internal.token.PropertyPathToken; +import com.jayway.jsonpath.internal.token.ScanPathToken; +import com.jayway.jsonpath.internal.token.WildcardPathToken; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +import java.util.Arrays; + +public class PathTokenTest extends BaseTest { + + @Test + public void is_upstream_definite_in_simple_case() { + assertThat(makePathReturningTail(makePPT("foo")).isUpstreamDefinite()).isTrue(); + + assertThat(makePathReturningTail(makePPT("foo"), makePPT("bar")).isUpstreamDefinite()).isTrue(); + + assertThat(makePathReturningTail(makePPT("foo", "foo2"), makePPT("bar")).isUpstreamDefinite()).isFalse(); + + assertThat(makePathReturningTail(new WildcardPathToken(), makePPT("bar")).isUpstreamDefinite()).isFalse(); + + assertThat(makePathReturningTail(new ScanPathToken(), makePPT("bar")).isUpstreamDefinite()).isFalse(); + } + + @Test + public void is_upstream_definite_in_complex_case() { + assertThat(makePathReturningTail(makePPT("foo"), makePPT("bar"), makePPT("baz")).isUpstreamDefinite()).isTrue(); + + assertThat(makePathReturningTail(makePPT("foo"), new WildcardPathToken()).isUpstreamDefinite()).isTrue(); + + assertThat(makePathReturningTail(new WildcardPathToken(), makePPT("bar"), makePPT("baz")).isUpstreamDefinite()).isFalse(); + } + + + private PathToken makePPT(final String ... properties) { + return new PropertyPathToken(Arrays.asList(properties)); + } + + private PathToken makePathReturningTail(final PathToken ... tokens) { + PathToken last = null; + for (final PathToken token : tokens) { + if (last != null) { + last.appendTailToken(token); + } + last = token; + } + return last; + } +}