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();