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 1928d91d..5c0675f6 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 @@ -54,10 +54,18 @@ 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 (! isUpstreamDefinite()) { + return; + } else { + 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 (! isUpstreamDefinite()) { + return; + } else { + throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model)); + } } try { @@ -143,7 +151,9 @@ public class ArrayPathToken extends PathToken { break; } } catch (IndexOutOfBoundsException e) { - throw new PathNotFoundException("Index out of bounds when evaluating path " + currentPath); + if (isUpstreamDefinite()) { + throw new PathNotFoundException("Index out of bounds when evaluating path " + currentPath); + } } } 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 bd6c5100..bfd5af11 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 @@ -69,7 +69,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 f7824036..86b16b78 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 @@ -38,7 +38,11 @@ public class PropertyPathToken extends PathToken { @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { if (!ctx.jsonProvider().isMap(model)) { - throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath); + if (! isUpstreamDefinite()) { + return; + } else { + throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath); + } } handleObjectProperty(currentPath, model, ctx, properties); 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 5bb0d0e5..72a7f8fd 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; @@ -167,6 +168,11 @@ public class ScanPathToken extends PathToken { @Override public boolean matches(Object model) { if (ctx.jsonProvider().isMap(model)) { + 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; + } 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..e85566d2 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/DeepScanTest.java @@ -0,0 +1,88 @@ +package com.jayway.jsonpath; + +import org.junit.Test; + +import static com.jayway.jsonpath.TestUtils.assertEvaluationThrows; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 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); + } +} 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; + } + } +}