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 dd46f40b..3305bff9 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/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java index 2ff23e3c..2076f95f 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 @@ -44,7 +44,9 @@ public abstract class PathToken { 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. Better safe than sorry. + // 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()) { @@ -60,9 +62,12 @@ public abstract class PathToken { } } else { - if(!isUpstreamDefinite() && + 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); @@ -78,9 +83,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) { 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 86b16b78..1dc64388 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 @@ public class PropertyPathToken extends PathToken { private final List properties; public PropertyPathToken(List properties) { + if (properties.isEmpty()) { + throw new InvalidPathException("Empty properties"); + } this.properties = properties; } @@ -35,8 +42,24 @@ public 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; @@ -45,12 +68,24 @@ public class PropertyPathToken extends PathToken { } } - handleObjectProperty(currentPath, model, ctx, properties); + if (singlePropertyCase() || multiPropertyMergeCase()) { + handleObjectProperty(currentPath, model, ctx, properties); + return; + } + + 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 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/test/java/com/jayway/jsonpath/MultiPropTest.java b/json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java index aeb0bffb..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 { @@ -60,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/PathTokenTest.java b/json-path/src/test/java/com/jayway/jsonpath/PathTokenTest.java index 0548597f..b7612649 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/PathTokenTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/PathTokenTest.java @@ -19,7 +19,7 @@ public class PathTokenTest extends BaseTest { assertThat(makePathReturningTail(makePPT("foo"), makePPT("bar")).isUpstreamDefinite()).isTrue(); - // assertThat(makePathReturningTail(makePPT("foo", "foo2"), makePPT("bar")).isUpstreamDefinite()).isFalse(); + assertThat(makePathReturningTail(makePPT("foo", "foo2"), makePPT("bar")).isUpstreamDefinite()).isFalse(); assertThat(makePathReturningTail(new WildcardPathToken(), makePPT("bar")).isUpstreamDefinite()).isFalse();