Browse Source

Completed implementation of running functions on result sets. All unit tests pass. Added additional unit tests for functions on result set.

pull/197/head
Nicholas Rahn 9 years ago
parent
commit
74cb6d6944
  1. 82
      json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java
  2. 8
      json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java
  3. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java
  4. 4
      json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java
  5. 7
      json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java
  6. 14
      json-path/src/test/java/com/jayway/jsonpath/internal/function/JSONEntityPathFunctionTest.java
  7. 26
      json-path/src/test/java/com/jayway/jsonpath/internal/function/ResultSetFunctionTest.java

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

@ -19,6 +19,7 @@ import com.jayway.jsonpath.internal.EvaluationAbortException;
import com.jayway.jsonpath.internal.EvaluationContext; import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.spi.json.JsonProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -42,38 +43,93 @@ public class CompiledPath implements Path {
} }
@Override @Override
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration, boolean forUpdate) { public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration,
boolean forUpdate) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Evaluating path: {}", toString()); logger.debug("Evaluating path: {}", toString());
} }
EvaluationContextImpl ctx = new EvaluationContextImpl(this, rootDocument, configuration, forUpdate); EvaluationContextImpl ctx =
try { new EvaluationContextImpl(this, rootDocument, configuration, forUpdate, rootDocument);
PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP; PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP;
if (root.isFunctionPath()) { if (root.isFunctionPath()) {
// Remove the functionPath and evaluate the resulting path. // Remove the functionPath from the path.
PathToken funcToken = root.chop(); PathToken funcToken = root.chop();
try {
// Evaluate the path without the tail function.
root.evaluate("", op, document, ctx); root.evaluate("", op, document, ctx);
// Get the value of the evaluation to use as model when evaluating the function. // Get the value of the evaluation to use as model when evaluating the function.
Object arrayModel = ctx.getValue(false); Object arrayModel = ctx.getValue(false);
// Evaluate the function on the model from the first evaluation. EvaluationContextImpl retCtx;
RootPathToken newRoot = new RootPathToken('x'); if (!root.isPathDefinite() && isArrayOfArrays(ctx, arrayModel)) {
newRoot.append(funcToken); // Special case: non-definite paths that evaluate to an array of arrays will have the function
CompiledPath newCPath = new CompiledPath(newRoot, true); // applied to each array. An array of the results of the function call(s) will be returned.
EvaluationContextImpl newCtx = new EvaluationContextImpl(newCPath, arrayModel, configuration, false); Object array = ctx.configuration().jsonProvider().createArray();
funcToken.evaluate("", op, arrayModel, newCtx); for (int i = 0; i < ctx.configuration().jsonProvider().length(arrayModel); i++) {
return newCtx; Object model = ctx.configuration().jsonProvider().getArrayIndex(arrayModel, i);
EvaluationContextImpl valCtx =
evaluateFunction(funcToken, model, configuration, rootDocument, op);
Object val = valCtx.getValue(false);
ctx.configuration().jsonProvider().setArrayIndex(array, i, val);
}
retCtx = createFunctionEvaluationContext(funcToken, rootDocument, configuration, rootDocument);
retCtx.addResult(root.getPathFragment(), op, array);
} else {
// Normal case: definite paths and non-definite paths that don't evaluate to an array of arrays
// (such as those that evaluate to an array of numbers) will have the function applied to the
// result of the original evaluation (which should be a 1-dimensional array). A single result
// value will be returned.
retCtx = evaluateFunction(funcToken, arrayModel, configuration, rootDocument, op);
}
return retCtx;
} catch (EvaluationAbortException abort) {
} finally {
// Put the functionPath back on the original path so that caching works.
root.append(funcToken);
}
} else { } else {
try {
root.evaluate("", op, document, ctx); root.evaluate("", op, document, ctx);
return ctx; return ctx;
} catch (EvaluationAbortException abort) {
}
} }
} catch (EvaluationAbortException abort){};
return ctx; return ctx;
} }
private boolean isArrayOfArrays(EvaluationContext ctx, Object model) {
// Is the model an Array containing Arrays.
JsonProvider jsonProvider = ctx.configuration().jsonProvider();
if (!jsonProvider.isArray(model)) {
return false;
}
if (jsonProvider.length(model) <= 0) {
return false;
}
Object item = jsonProvider.getArrayIndex(model, 0);
return jsonProvider.isArray(item);
}
private EvaluationContextImpl evaluateFunction(PathToken funcToken, Object model, Configuration configuration, Object rootDocument,
PathRef op) {
// Evaluate the function on the given model.
EvaluationContextImpl newCtx = createFunctionEvaluationContext(funcToken, model, configuration, rootDocument);
funcToken.evaluate("", op, model, newCtx);
return newCtx;
}
private EvaluationContextImpl createFunctionEvaluationContext(PathToken funcToken, Object model,
Configuration configuration, Object rootDocument) {
RootPathToken newRoot = PathTokenFactory.createRootPathToken(root.getRootToken());
newRoot.append(funcToken);
CompiledPath newCPath = new CompiledPath(newRoot, true);
return new EvaluationContextImpl(newCPath, model, configuration, false, rootDocument);
}
@Override @Override
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration){ public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration){
return evaluate(document, rootDocument, configuration, false); return evaluate(document, rootDocument, configuration, false);

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

@ -45,13 +45,14 @@ public class EvaluationContextImpl implements EvaluationContext {
private final Object pathResult; private final Object pathResult;
private final Path path; private final Path path;
private final Object rootDocument; private final Object rootDocument;
private final Object paramsRootDocument;
private final List<PathRef> updateOperations; private final List<PathRef> updateOperations;
private final HashMap<Path, Object> documentEvalCache = new HashMap<Path, Object>(); private final HashMap<Path, Object> documentEvalCache = new HashMap<Path, Object>();
private final boolean forUpdate; private final boolean forUpdate;
private int resultIndex = 0; private int resultIndex = 0;
public EvaluationContextImpl(Path path, Object rootDocument, Configuration configuration, boolean forUpdate) { public EvaluationContextImpl(Path path, Object rootDocument, Configuration configuration, boolean forUpdate, Object paramsRootDocument) {
notNull(path, "path can not be null"); notNull(path, "path can not be null");
notNull(rootDocument, "root can not be null"); notNull(rootDocument, "root can not be null");
notNull(configuration, "configuration can not be null"); notNull(configuration, "configuration can not be null");
@ -62,6 +63,7 @@ public class EvaluationContextImpl implements EvaluationContext {
this.valueResult = configuration.jsonProvider().createArray(); this.valueResult = configuration.jsonProvider().createArray();
this.pathResult = configuration.jsonProvider().createArray(); this.pathResult = configuration.jsonProvider().createArray();
this.updateOperations = new ArrayList<PathRef>(); this.updateOperations = new ArrayList<PathRef>();
this.paramsRootDocument = paramsRootDocument;
} }
public HashMap<Path, Object> documentEvalCache() { public HashMap<Path, Object> documentEvalCache() {
@ -111,6 +113,10 @@ public class EvaluationContextImpl implements EvaluationContext {
return rootDocument; return rootDocument;
} }
public Object paramsRootDocument() {
return paramsRootDocument;
}
public Collection<PathRef> updateOperations(){ public Collection<PathRef> updateOperations(){
Collections.sort(updateOperations); Collections.sort(updateOperations);

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

@ -49,7 +49,7 @@ public class FunctionPathToken extends PathToken {
if (!param.hasEvaluated()) { if (!param.hasEvaluated()) {
switch (param.getType()) { switch (param.getType()) {
case PATH: case PATH:
param.setCachedValue(param.getPath().evaluate(ctx.rootDocument(), ctx.rootDocument(), ctx.configuration()).getValue()); param.setCachedValue(param.getPath().evaluate(ctx.paramsRootDocument(), ctx.paramsRootDocument(), ctx.configuration()).getValue());
param.setEvaluated(true); param.setEvaluated(true);
break; break;
case JSON: case JSON:

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

@ -83,4 +83,8 @@ public class RootPathToken extends PathToken {
public boolean isFunctionPath() { public boolean isFunctionPath() {
return (tail instanceof FunctionPathToken); return (tail instanceof FunctionPathToken);
} }
public char getRootToken() {
return rootToken.charAt(0);
}
} }

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

@ -14,6 +14,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class BaseFunctionTest { public class BaseFunctionTest {
protected static final String NUMBER_SERIES = "{\"empty\": [], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}"; protected static final String NUMBER_SERIES = "{\"empty\": [], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}";
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\" ]}"; 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\" ]}";
// This is the same JSON example document as is in the README.md
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}"; 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}";
/** /**
@ -29,10 +30,14 @@ public class BaseFunctionTest {
* The expected value to be returned from the test * The expected value to be returned from the test
*/ */
protected void verifyFunction(Configuration conf, String pathExpr, String json, Object expectedValue) { protected void verifyFunction(Configuration conf, String pathExpr, String json, Object expectedValue) {
Object result = using(conf).parse(json).read(pathExpr); Object result = executeQuery(conf, pathExpr, json);
assertThat(conf.jsonProvider().unwrap(result)).isEqualTo(expectedValue); assertThat(conf.jsonProvider().unwrap(result)).isEqualTo(expectedValue);
} }
protected Object executeQuery(Configuration conf, String pathExpr, String json) {
return using(conf).parse(json).read(pathExpr);
}
protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) { protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) {
verifyFunction(conf, pathExpr, NUMBER_SERIES, expectedValue); verifyFunction(conf, pathExpr, NUMBER_SERIES, expectedValue);
} }

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

@ -2,6 +2,7 @@ package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Configurations; import com.jayway.jsonpath.Configurations;
import com.jayway.jsonpath.JsonPathException;
import net.minidev.json.JSONArray; import net.minidev.json.JSONArray;
import org.junit.Test; import org.junit.Test;
@ -104,6 +105,19 @@ public class JSONEntityPathFunctionTest extends BaseFunctionTest {
values.add(12.2d); values.add(12.2d);
values.add(17d); values.add(17d);
verifyFunction(conf, path, BATCH_JSON, values); verifyFunction(conf, path, BATCH_JSON, values);
// Then take the average of those averages.
path = path + ".avg()";
verifyFunction(conf, path, BATCH_JSON, 14.6d);
}
@Test(expected = JsonPathException.class)
public void testPredicateWithFunctionCallNoMatch() {
String path = "$.batches.results[?(@.values.length() >= 12)].values.avg()";
// This will throw an exception because a function can not be evaluated on an empty array.
JSONArray values = new JSONArray();
verifyFunction(conf, path, BATCH_JSON, values);
} }
} }

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

@ -1,11 +1,9 @@
package com.jayway.jsonpath.internal.function; package com.jayway.jsonpath.internal.function;
import static org.junit.Assert.assertEquals;
import static org.junit.runners.Parameterized.Parameters; import static org.junit.runners.Parameterized.Parameters;
import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Configurations; import com.jayway.jsonpath.Configurations;
import com.jayway.jsonpath.JsonPathException;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
@ -35,11 +33,35 @@ public class ResultSetFunctionTest extends BaseFunctionTest {
@Test @Test
public void testMaxOfDoublesResultSet() { public void testMaxOfDoublesResultSet() {
verifyExampleFunction(conf, "$.store.book[*].price.max()", 22.99); verifyExampleFunction(conf, "$.store.book[*].price.max()", 22.99);
verifyExampleFunction(conf, "$.store..price.max()", 22.99);
}
@Test
public void testMinOfDoublesResultSet() {
verifyExampleFunction(conf, "$.store.book[*].price.min()", 8.95);
verifyExampleFunction(conf, "$.store..price.min()", 8.95);
} }
@Test @Test
public void testSumOfDoublesResultSet() { public void testSumOfDoublesResultSet() {
verifyExampleFunction(conf, "$.store.book[*].price.sum()", 53.92); verifyExampleFunction(conf, "$.store.book[*].price.sum()", 53.92);
verifyExampleFunction(conf, "$.store..price.sum()", 73.87);
}
@Test
public void testAvgOfDoublesResultSet() {
verifyExampleFunction(conf, "$.store.book[*].price.avg()", 13.48);
verifyExampleFunction(conf, "$.store..price.avg()", 14.774000000000001);
} }
@Test
public void testLengthOfDoublesResultSet() {
verifyExampleFunction(conf, "$.store.book[*].price.length()", 4);
verifyExampleFunction(conf, "$.store..price.length()", 5);
}
@Test
public void testLengthOfBooksResultSet() {
verifyExampleFunction(conf, "$.store.book.length()", 4);
}
} }

Loading…
Cancel
Save