Browse Source

Merge pull request #142 from zline/master

tests, fixes, new feature and tests for some discovered but not yet fixed issues
pull/148/head
kallestenflo 9 years ago
parent
commit
de06c44959
  1. 4
      json-path-assert/src/main/java/com/jayway/jsonassert/impl/JsonAsserterImpl.java
  2. 11
      json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java
  3. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java
  4. 23
      json-path/src/main/java/com/jayway/jsonpath/internal/Utils.java
  5. 51
      json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java
  6. 32
      json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java
  7. 4
      json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java
  8. 49
      json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java
  9. 27
      json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java
  10. 118
      json-path/src/test/java/com/jayway/jsonpath/DeepScanTest.java
  11. 61
      json-path/src/test/java/com/jayway/jsonpath/MultiPropTest.java
  12. 31
      json-path/src/test/java/com/jayway/jsonpath/OptionsTest.java
  13. 103
      json-path/src/test/java/com/jayway/jsonpath/PathCompilerTest.java
  14. 32
      json-path/src/test/java/com/jayway/jsonpath/TestUtils.java
  15. 54
      json-path/src/test/java/com/jayway/jsonpath/internal/token/PathTokenTest.java

4
json-path-assert/src/main/java/com/jayway/jsonassert/impl/JsonAsserterImpl.java

@ -33,7 +33,9 @@ public class JsonAsserterImpl implements JsonAsserter {
try { try {
obj = JsonPath.<T>read(jsonObject, path); obj = JsonPath.<T>read(jsonObject, path);
} catch (Exception e) { } 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)) { if (!matcher.matches(obj)) {

11
json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java

@ -488,9 +488,18 @@ public class PathCompiler {
while (inBounds(readPosition)) { while (inBounds(readPosition)) {
if (skipStrings) { if (skipStrings) {
if (charAt(readPosition) == TICK) { if (charAt(readPosition) == TICK) {
boolean escaped = false;
while (inBounds(readPosition)) { while (inBounds(readPosition)) {
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++; readPosition++;
break; break;
} }

2
json-path/src/main/java/com/jayway/jsonpath/internal/PathRef.java

@ -154,7 +154,7 @@ public abstract class PathRef implements Comparable<PathRef> {
public int compareTo(PathRef o) { public int compareTo(PathRef o) {
if(o instanceof ArrayIndexPathRef){ if(o instanceof ArrayIndexPathRef){
ArrayIndexPathRef pf = (ArrayIndexPathRef) o; ArrayIndexPathRef pf = (ArrayIndexPathRef) o;
return Integer.compare(pf.index, this.index); return Integer.valueOf(pf.index).compareTo(this.index);
} }
return super.compareTo(o); return super.compareTo(o);
} }

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

51
json-path/src/main/java/com/jayway/jsonpath/internal/token/ArrayPathToken.java

@ -44,12 +44,8 @@ public class ArrayPathToken extends PathToken {
@Override @Override
public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
if(model == null){ if (! checkArrayModel(currentPath, model, ctx))
throw new PathNotFoundException("The path " + currentPath + " is null"); return;
}
if (!ctx.jsonProvider().isArray(model)) {
throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model));
}
if(arraySliceOperation != null){ if(arraySliceOperation != null){
evaluateSliceOperation(currentPath, parent, model, ctx); evaluateSliceOperation(currentPath, parent, model, ctx);
} else { } else {
@ -60,12 +56,8 @@ public class ArrayPathToken extends PathToken {
public void evaluateIndexOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { public void evaluateIndexOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
if (model == null) { if (! checkArrayModel(currentPath, model, ctx))
throw new PathNotFoundException("The path " + currentPath + " is null"); return;
}
if (!ctx.jsonProvider().isArray(model)) {
throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model));
}
if(arrayIndexOperation.isSingleIndexOperation()){ if(arrayIndexOperation.isSingleIndexOperation()){
handleArrayIndex(arrayIndexOperation.indexes().get(0), currentPath, model, ctx); 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) { public void evaluateSliceOperation(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
if (model == null) { if (! checkArrayModel(currentPath, model, ctx))
throw new PathNotFoundException("The path " + currentPath + " is null"); return;
}
if (!ctx.jsonProvider().isArray(model)) {
throw new InvalidPathException(format("Filter: %s can only be applied to arrays. Current context is: %s", toString(), model));
}
switch (arraySliceOperation.operation()) { switch (arraySliceOperation.operation()) {
case SLICE_FROM: case SLICE_FROM:
@ -171,4 +159,31 @@ public class ArrayPathToken extends PathToken {
return false; 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;
}
} }

32
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, "']"); String evalPath = Utils.concat(currentPath, "['", property, "']");
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",
// 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(isLeaf()) {
if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){ if(ctx.options().contains(Option.DEFAULT_PATH_LEAF_TO_NULL)){
propertyVal = null; propertyVal = null;
@ -61,9 +67,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);
@ -80,9 +89,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) {
@ -154,16 +161,11 @@ public abstract class PathToken {
return prev == null; return prev == null;
} }
boolean isUpstreamDefinite(){ boolean isUpstreamDefinite() {
if(upstreamDefinite != null){ if (upstreamDefinite == null) {
return upstreamDefinite.booleanValue(); upstreamDefinite = isRoot() || prev.isTokenDefinite() && prev.isUpstreamDefinite();
}
boolean isUpstreamDefinite = isTokenDefinite();
if (isUpstreamDefinite && !isRoot()) {
isUpstreamDefinite = prev.isPathDefinite();
} }
upstreamDefinite = isUpstreamDefinite; return upstreamDefinite;
return isUpstreamDefinite;
} }
public int getTokenCount() { public int getTokenCount() {

4
json-path/src/main/java/com/jayway/jsonpath/internal/token/PredicatePathToken.java

@ -62,7 +62,9 @@ public class PredicatePathToken extends PathToken {
idx++; idx++;
} }
} else { } 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));
}
} }
} }

49
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 @@ 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,21 +42,55 @@ 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()) {
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<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
public boolean isTokenDefinite() { public boolean isTokenDefinite() {
return true; // in case of leaf multiprops will be merged, so it's kinda definite
return singlePropertyCase() || multiPropertyMergeCase();
} }
@Override @Override

27
json-path/src/main/java/com/jayway/jsonpath/internal/token/ScanPathToken.java

@ -14,6 +14,7 @@
*/ */
package com.jayway.jsonpath.internal.token; package com.jayway.jsonpath.internal.token;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.spi.json.JsonProvider; import com.jayway.jsonpath.spi.json.JsonProvider;
@ -169,11 +170,29 @@ public class ScanPathToken extends PathToken {
@Override @Override
public boolean matches(Object model) { public boolean matches(Object model) {
if (ctx.jsonProvider().isMap(model)) { if (! ctx.jsonProvider().isMap(model)) {
Collection<String> keys = ctx.jsonProvider().getPropertyKeys(model); return false;
return keys.containsAll(propertyPathToken.getProperties());
} }
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<String> keys = ctx.jsonProvider().getPropertyKeys(model);
return keys.containsAll(propertyPathToken.getProperties());
} }
} }
} }

118
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");
}
}
}

61
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 {
@ -24,6 +26,10 @@ public class MultiPropTest {
assertThat(using(conf).parse(model).read("$['a', 'b']", Map.class)) assertThat(using(conf).parse(model).read("$['a', 'b']", Map.class))
.containsEntry("a", "a-val") .containsEntry("a", "a-val")
.containsEntry("b", "b-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 @Test
@ -56,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);
}
} }

31
json-path/src/test/java/com/jayway/jsonpath/OptionsTest.java

@ -2,6 +2,7 @@ package com.jayway.jsonpath;
import org.junit.Test; import org.junit.Test;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -45,6 +46,23 @@ public class OptionsTest extends BaseTest {
Configuration conf = Configuration.builder().options(ALWAYS_RETURN_LIST).build(); 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\" : \"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<Object> 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 @Test
@ -119,4 +137,17 @@ public class OptionsTest extends BaseTest {
} catch (PathNotFoundException pnf){} } 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);
}
} }

103
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.Ignore;
import org.junit.Test; import org.junit.Test;
import java.util.List;
import static com.jayway.jsonpath.internal.PathCompiler.compile; import static com.jayway.jsonpath.internal.PathCompiler.compile;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -128,4 +130,105 @@ public class PathCompilerTest {
assertThat(compile("$..['prop']..[*]").toString()).isEqualTo("$..['prop']..[*]"); 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> result = JsonPath.read(json, "$.logs[?(@.message == '] it')].message");
assertThat(result).containsExactly("] it");
}
} }

32
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<? extends JsonPathException> 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<? extends JsonPathException> 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;
}
}
}

54
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;
}
}
Loading…
Cancel
Save