Skip to content

Commit 74cb6d6

Browse files
Completed implementation of running functions on result sets. All unit tests pass. Added additional unit tests for functions on result set.
1 parent a998881 commit 74cb6d6

File tree

7 files changed

+129
-22
lines changed

7 files changed

+129
-22
lines changed

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

Lines changed: 73 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.jayway.jsonpath.internal.EvaluationContext;
2020
import com.jayway.jsonpath.internal.Path;
2121
import com.jayway.jsonpath.internal.PathRef;
22+
import com.jayway.jsonpath.spi.json.JsonProvider;
2223
import org.slf4j.Logger;
2324
import org.slf4j.LoggerFactory;
2425

@@ -42,38 +43,93 @@ public boolean isRootPath() {
4243
}
4344

4445
@Override
45-
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration, boolean forUpdate) {
46+
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration,
47+
boolean forUpdate) {
4648
if (logger.isDebugEnabled()) {
4749
logger.debug("Evaluating path: {}", toString());
4850
}
4951

50-
EvaluationContextImpl ctx = new EvaluationContextImpl(this, rootDocument, configuration, forUpdate);
51-
try {
52-
PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP;
53-
54-
if (root.isFunctionPath()) {
55-
// Remove the functionPath and evaluate the resulting path.
56-
PathToken funcToken = root.chop();
52+
EvaluationContextImpl ctx =
53+
new EvaluationContextImpl(this, rootDocument, configuration, forUpdate, rootDocument);
54+
PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP;
55+
if (root.isFunctionPath()) {
56+
// Remove the functionPath from the path.
57+
PathToken funcToken = root.chop();
58+
try {
59+
// Evaluate the path without the tail function.
5760
root.evaluate("", op, document, ctx);
5861
// Get the value of the evaluation to use as model when evaluating the function.
5962
Object arrayModel = ctx.getValue(false);
6063

61-
// Evaluate the function on the model from the first evaluation.
62-
RootPathToken newRoot = new RootPathToken('x');
63-
newRoot.append(funcToken);
64-
CompiledPath newCPath = new CompiledPath(newRoot, true);
65-
EvaluationContextImpl newCtx = new EvaluationContextImpl(newCPath, arrayModel, configuration, false);
66-
funcToken.evaluate("", op, arrayModel, newCtx);
67-
return newCtx;
68-
} else {
64+
EvaluationContextImpl retCtx;
65+
if (!root.isPathDefinite() && isArrayOfArrays(ctx, arrayModel)) {
66+
// Special case: non-definite paths that evaluate to an array of arrays will have the function
67+
// applied to each array. An array of the results of the function call(s) will be returned.
68+
Object array = ctx.configuration().jsonProvider().createArray();
69+
for (int i = 0; i < ctx.configuration().jsonProvider().length(arrayModel); i++) {
70+
Object model = ctx.configuration().jsonProvider().getArrayIndex(arrayModel, i);
71+
EvaluationContextImpl valCtx =
72+
evaluateFunction(funcToken, model, configuration, rootDocument, op);
73+
Object val = valCtx.getValue(false);
74+
ctx.configuration().jsonProvider().setArrayIndex(array, i, val);
75+
}
76+
77+
retCtx = createFunctionEvaluationContext(funcToken, rootDocument, configuration, rootDocument);
78+
retCtx.addResult(root.getPathFragment(), op, array);
79+
} else {
80+
// Normal case: definite paths and non-definite paths that don't evaluate to an array of arrays
81+
// (such as those that evaluate to an array of numbers) will have the function applied to the
82+
// result of the original evaluation (which should be a 1-dimensional array). A single result
83+
// value will be returned.
84+
retCtx = evaluateFunction(funcToken, arrayModel, configuration, rootDocument, op);
85+
}
86+
87+
return retCtx;
88+
} catch (EvaluationAbortException abort) {
89+
} finally {
90+
// Put the functionPath back on the original path so that caching works.
91+
root.append(funcToken);
92+
}
93+
} else {
94+
try {
6995
root.evaluate("", op, document, ctx);
7096
return ctx;
97+
} catch (EvaluationAbortException abort) {
7198
}
72-
} catch (EvaluationAbortException abort){};
99+
}
73100

74101
return ctx;
75102
}
76103

104+
private boolean isArrayOfArrays(EvaluationContext ctx, Object model) {
105+
// Is the model an Array containing Arrays.
106+
JsonProvider jsonProvider = ctx.configuration().jsonProvider();
107+
if (!jsonProvider.isArray(model)) {
108+
return false;
109+
}
110+
if (jsonProvider.length(model) <= 0) {
111+
return false;
112+
}
113+
Object item = jsonProvider.getArrayIndex(model, 0);
114+
return jsonProvider.isArray(item);
115+
}
116+
117+
private EvaluationContextImpl evaluateFunction(PathToken funcToken, Object model, Configuration configuration, Object rootDocument,
118+
PathRef op) {
119+
// Evaluate the function on the given model.
120+
EvaluationContextImpl newCtx = createFunctionEvaluationContext(funcToken, model, configuration, rootDocument);
121+
funcToken.evaluate("", op, model, newCtx);
122+
return newCtx;
123+
}
124+
125+
private EvaluationContextImpl createFunctionEvaluationContext(PathToken funcToken, Object model,
126+
Configuration configuration, Object rootDocument) {
127+
RootPathToken newRoot = PathTokenFactory.createRootPathToken(root.getRootToken());
128+
newRoot.append(funcToken);
129+
CompiledPath newCPath = new CompiledPath(newRoot, true);
130+
return new EvaluationContextImpl(newCPath, model, configuration, false, rootDocument);
131+
}
132+
77133
@Override
78134
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration){
79135
return evaluate(document, rootDocument, configuration, false);

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,14 @@ public class EvaluationContextImpl implements EvaluationContext {
4545
private final Object pathResult;
4646
private final Path path;
4747
private final Object rootDocument;
48+
private final Object paramsRootDocument;
4849
private final List<PathRef> updateOperations;
4950
private final HashMap<Path, Object> documentEvalCache = new HashMap<Path, Object>();
5051
private final boolean forUpdate;
5152
private int resultIndex = 0;
5253

5354

54-
public EvaluationContextImpl(Path path, Object rootDocument, Configuration configuration, boolean forUpdate) {
55+
public EvaluationContextImpl(Path path, Object rootDocument, Configuration configuration, boolean forUpdate, Object paramsRootDocument) {
5556
notNull(path, "path can not be null");
5657
notNull(rootDocument, "root can not be null");
5758
notNull(configuration, "configuration can not be null");
@@ -62,6 +63,7 @@ public EvaluationContextImpl(Path path, Object rootDocument, Configuration confi
6263
this.valueResult = configuration.jsonProvider().createArray();
6364
this.pathResult = configuration.jsonProvider().createArray();
6465
this.updateOperations = new ArrayList<PathRef>();
66+
this.paramsRootDocument = paramsRootDocument;
6567
}
6668

6769
public HashMap<Path, Object> documentEvalCache() {
@@ -111,6 +113,10 @@ public Object rootDocument() {
111113
return rootDocument;
112114
}
113115

116+
public Object paramsRootDocument() {
117+
return paramsRootDocument;
118+
}
119+
114120
public Collection<PathRef> updateOperations(){
115121

116122
Collections.sort(updateOperations);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ private void evaluateParameters(String currentPath, PathRef parent, Object model
4949
if (!param.hasEvaluated()) {
5050
switch (param.getType()) {
5151
case PATH:
52-
param.setCachedValue(param.getPath().evaluate(ctx.rootDocument(), ctx.rootDocument(), ctx.configuration()).getValue());
52+
param.setCachedValue(param.getPath().evaluate(ctx.paramsRootDocument(), ctx.paramsRootDocument(), ctx.configuration()).getValue());
5353
param.setEvaluated(true);
5454
break;
5555
case JSON:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,8 @@ public boolean isTokenDefinite() {
8383
public boolean isFunctionPath() {
8484
return (tail instanceof FunctionPathToken);
8585
}
86+
87+
public char getRootToken() {
88+
return rootToken.charAt(0);
89+
}
8690
}

json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
public class BaseFunctionTest {
1515
protected static final String NUMBER_SERIES = "{\"empty\": [], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}";
1616
protected static final String TEXT_SERIES = "{\"urls\": [\"http://api.worldbank.org/countries/all/?format=json\", \"http://api.worldbank.org/countries/all/?format=json\"], \"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ]}";
17+
// This is the same JSON example document as is in the README.md
1718
protected static final String EXAMPLE_SERIES = "{\"store\":{\"book\":[{\"category\":\"reference\",\"author\":\"Nigel Rees\",\"title\":\"Sayings of the Century\",\"price\":8.95},{\"category\":\"fiction\",\"author\":\"Evelyn Waugh\",\"title\":\"Sword of Honour\",\"price\":12.99},{\"category\":\"fiction\",\"author\":\"Herman Melville\",\"title\":\"Moby Dick\",\"isbn\":\"0-553-21311-3\",\"price\":8.99},{\"category\":\"fiction\",\"author\":\"J. R. R. Tolkien\",\"title\":\"The Lord of the Rings\",\"isbn\":\"0-395-19395-8\",\"price\":22.99}],\"bicycle\":{\"color\":\"red\",\"price\":19.95}},\"expensive\":10}";
1819

1920
/**
@@ -29,10 +30,14 @@ public class BaseFunctionTest {
2930
* The expected value to be returned from the test
3031
*/
3132
protected void verifyFunction(Configuration conf, String pathExpr, String json, Object expectedValue) {
32-
Object result = using(conf).parse(json).read(pathExpr);
33+
Object result = executeQuery(conf, pathExpr, json);
3334
assertThat(conf.jsonProvider().unwrap(result)).isEqualTo(expectedValue);
3435
}
3536

37+
protected Object executeQuery(Configuration conf, String pathExpr, String json) {
38+
return using(conf).parse(json).read(pathExpr);
39+
}
40+
3641
protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) {
3742
verifyFunction(conf, pathExpr, NUMBER_SERIES, expectedValue);
3843
}

json-path/src/test/java/com/jayway/jsonpath/internal/function/JSONEntityPathFunctionTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.jayway.jsonpath.Configuration;
44
import com.jayway.jsonpath.Configurations;
5+
import com.jayway.jsonpath.JsonPathException;
56
import net.minidev.json.JSONArray;
67
import org.junit.Test;
78

@@ -104,6 +105,19 @@ public void testPredicateWithFunctionCallTwoMatches() {
104105
values.add(12.2d);
105106
values.add(17d);
106107
verifyFunction(conf, path, BATCH_JSON, values);
108+
109+
// Then take the average of those averages.
110+
path = path + ".avg()";
111+
verifyFunction(conf, path, BATCH_JSON, 14.6d);
112+
}
113+
114+
@Test(expected = JsonPathException.class)
115+
public void testPredicateWithFunctionCallNoMatch() {
116+
String path = "$.batches.results[?(@.values.length() >= 12)].values.avg()";
117+
118+
// This will throw an exception because a function can not be evaluated on an empty array.
119+
JSONArray values = new JSONArray();
120+
verifyFunction(conf, path, BATCH_JSON, values);
107121
}
108122

109123
}

json-path/src/test/java/com/jayway/jsonpath/internal/function/ResultSetFunctionTest.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.jayway.jsonpath.internal.function;
22

3-
import static org.junit.Assert.assertEquals;
43
import static org.junit.runners.Parameterized.Parameters;
54

65
import com.jayway.jsonpath.Configuration;
76
import com.jayway.jsonpath.Configurations;
8-
import com.jayway.jsonpath.JsonPathException;
97
import org.junit.Test;
108
import org.junit.runner.RunWith;
119
import org.junit.runners.Parameterized;
@@ -35,11 +33,35 @@ public static Iterable<Configuration> configurations() {
3533
@Test
3634
public void testMaxOfDoublesResultSet() {
3735
verifyExampleFunction(conf, "$.store.book[*].price.max()", 22.99);
36+
verifyExampleFunction(conf, "$.store..price.max()", 22.99);
37+
}
38+
39+
@Test
40+
public void testMinOfDoublesResultSet() {
41+
verifyExampleFunction(conf, "$.store.book[*].price.min()", 8.95);
42+
verifyExampleFunction(conf, "$.store..price.min()", 8.95);
3843
}
3944

4045
@Test
4146
public void testSumOfDoublesResultSet() {
4247
verifyExampleFunction(conf, "$.store.book[*].price.sum()", 53.92);
48+
verifyExampleFunction(conf, "$.store..price.sum()", 73.87);
49+
}
50+
51+
@Test
52+
public void testAvgOfDoublesResultSet() {
53+
verifyExampleFunction(conf, "$.store.book[*].price.avg()", 13.48);
54+
verifyExampleFunction(conf, "$.store..price.avg()", 14.774000000000001);
4355
}
4456

57+
@Test
58+
public void testLengthOfDoublesResultSet() {
59+
verifyExampleFunction(conf, "$.store.book[*].price.length()", 4);
60+
verifyExampleFunction(conf, "$.store..price.length()", 5);
61+
}
62+
63+
@Test
64+
public void testLengthOfBooksResultSet() {
65+
verifyExampleFunction(conf, "$.store.book.length()", 4);
66+
}
4567
}

0 commit comments

Comments
 (0)