Browse Source

added support of multiple object properties in non-leaf path node

Semantics of this case is the same as semantics of ArrayPathToken with multiple array indices specified.
pull/142/head
Alexey Makeyev 9 years ago
parent
commit
99637ca2d7
  1. 23
      json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java
  2. 13
      json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java
  3. 37
      json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java
  4. 57
      json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java
  5. 2
      json-path/src/test/java/com/jayway/jsonpath/PathTokenTest.java

23
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;
}
/** /**
* <p>Validate that the specified argument character sequence is * <p>Validate that the specified argument character sequence is
* neither {@code null} nor a length of zero (no characters); * neither {@code null} nor a length of zero (no characters);

13
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); Object propertyVal = readObjectProperty(property, model, ctx);
if(propertyVal == JsonProvider.UNDEFINED){ if(propertyVal == JsonProvider.UNDEFINED){
// Conditions below heavily depend on current token type (and its logic) and are not "universal", // 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"; assert this instanceof PropertyPathToken : "only PropertyPathToken is supported";
if(isLeaf()) { if(isLeaf()) {
@ -60,9 +62,12 @@ public abstract class PathToken {
} }
} else { } else {
if(!isUpstreamDefinite() && if (! (isUpstreamDefinite() && isTokenDefinite()) &&
!ctx.options().contains(Option.REQUIRE_PROPERTIES) || !ctx.options().contains(Option.REQUIRE_PROPERTIES) ||
ctx.options().contains(Option.SUPPRESS_EXCEPTIONS)){ 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; return;
} else { } else {
throw new PathNotFoundException("Missing property in path " + evalPath); throw new PathNotFoundException("Missing property in path " + evalPath);
@ -78,9 +83,7 @@ public abstract class PathToken {
} else { } else {
String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]"; String evalPath = currentPath + "[" + Utils.join(", ", "'", properties) + "]";
if (!isLeaf()) { assert isLeaf() : "non-leaf multi props handled elsewhere";
throw new InvalidPathException("Multi properties can only be used as path leafs: " + evalPath);
}
Object merged = ctx.jsonProvider().createMap(); Object merged = ctx.jsonProvider().createMap();
for (String property : properties) { for (String property : properties) {

37
json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java

@ -14,12 +14,16 @@
*/ */
package com.jayway.jsonpath.internal.token; package com.jayway.jsonpath.internal.token;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.Utils; import com.jayway.jsonpath.internal.Utils;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import static com.jayway.jsonpath.internal.Utils.onlyOneIsTrueNonThrow;
/** /**
* *
*/ */
@ -28,6 +32,9 @@ public class PropertyPathToken extends PathToken {
private final List<String> properties; private final List<String> properties;
public PropertyPathToken(List<String> properties) { public PropertyPathToken(List<String> properties) {
if (properties.isEmpty()) {
throw new InvalidPathException("Empty properties");
}
this.properties = properties; this.properties = properties;
} }
@ -35,8 +42,24 @@ public class PropertyPathToken extends PathToken {
return properties; 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 @Override
public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { 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 (!ctx.jsonProvider().isMap(model)) {
if (! isUpstreamDefinite()) { if (! isUpstreamDefinite()) {
return; return;
@ -45,12 +68,24 @@ public class PropertyPathToken extends PathToken {
} }
} }
if (singlePropertyCase() || multiPropertyMergeCase()) {
handleObjectProperty(currentPath, model, ctx, properties); handleObjectProperty(currentPath, model, ctx, properties);
return;
}
assert multiPropertyIterationCase();
final List<String> currentlyHandledProperty = new ArrayList<String>(1);
currentlyHandledProperty.add(null);
for (final String property : properties) {
currentlyHandledProperty.set(0, property);
handleObjectProperty(currentPath, model, ctx, currentlyHandledProperty);
}
} }
@Override @Override
boolean isTokenDefinite() { boolean isTokenDefinite() {
return true; // in case of leaf multiprops will be merged, so it's kinda definite
return singlePropertyCase() || multiPropertyMergeCase();
} }
@Override @Override

57
json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java

@ -6,6 +6,8 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static com.jayway.jsonpath.JsonPath.using; import static com.jayway.jsonpath.JsonPath.using;
import static com.jayway.jsonpath.TestUtils.assertEvaluationThrows;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
public class MultiPropTest { public class MultiPropTest {
@ -60,5 +62,60 @@ public class MultiPropTest {
using(conf).parse(model).read("$['a', 'x']", Map.class); 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);
}
} }

2
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"), 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(); assertThat(makePathReturningTail(new WildcardPathToken(), makePPT("bar")).isUpstreamDefinite()).isFalse();

Loading…
Cancel
Save