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. 90
      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

90
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.Path;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.spi.json.JsonProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,38 +43,93 @@ public class CompiledPath implements Path {
}
@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()) {
logger.debug("Evaluating path: {}", toString());
}
EvaluationContextImpl ctx = new EvaluationContextImpl(this, rootDocument, configuration, forUpdate);
try {
PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP;
if (root.isFunctionPath()) {
// Remove the functionPath and evaluate the resulting path.
PathToken funcToken = root.chop();
EvaluationContextImpl ctx =
new EvaluationContextImpl(this, rootDocument, configuration, forUpdate, rootDocument);
PathRef op = ctx.forUpdate() ? PathRef.createRoot(rootDocument) : PathRef.NO_OP;
if (root.isFunctionPath()) {
// Remove the functionPath from the path.
PathToken funcToken = root.chop();
try {
// Evaluate the path without the tail function.
root.evaluate("", op, document, ctx);
// Get the value of the evaluation to use as model when evaluating the function.
Object arrayModel = ctx.getValue(false);
// Evaluate the function on the model from the first evaluation.
RootPathToken newRoot = new RootPathToken('x');
newRoot.append(funcToken);
CompiledPath newCPath = new CompiledPath(newRoot, true);
EvaluationContextImpl newCtx = new EvaluationContextImpl(newCPath, arrayModel, configuration, false);
funcToken.evaluate("", op, arrayModel, newCtx);
return newCtx;
} else {
EvaluationContextImpl retCtx;
if (!root.isPathDefinite() && isArrayOfArrays(ctx, arrayModel)) {
// Special case: non-definite paths that evaluate to an array of arrays will have the function
// applied to each array. An array of the results of the function call(s) will be returned.
Object array = ctx.configuration().jsonProvider().createArray();
for (int i = 0; i < ctx.configuration().jsonProvider().length(arrayModel); i++) {
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 {
try {
root.evaluate("", op, document, ctx);
return ctx;
} catch (EvaluationAbortException abort) {
}
} catch (EvaluationAbortException abort){};
}
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
public EvaluationContext evaluate(Object document, Object rootDocument, Configuration configuration){
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 Path path;
private final Object rootDocument;
private final Object paramsRootDocument;
private final List<PathRef> updateOperations;
private final HashMap<Path, Object> documentEvalCache = new HashMap<Path, Object>();
private final boolean forUpdate;
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(rootDocument, "root 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.pathResult = configuration.jsonProvider().createArray();
this.updateOperations = new ArrayList<PathRef>();
this.paramsRootDocument = paramsRootDocument;
}
public HashMap<Path, Object> documentEvalCache() {
@ -111,6 +113,10 @@ public class EvaluationContextImpl implements EvaluationContext {
return rootDocument;
}
public Object paramsRootDocument() {
return paramsRootDocument;
}
public Collection<PathRef> 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()) {
switch (param.getType()) {
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);
break;
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() {
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 {
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\" ]}";
// 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}";
/**
@ -29,10 +30,14 @@ public class BaseFunctionTest {
* The expected value to be returned from the test
*/
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);
}
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) {
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.Configurations;
import com.jayway.jsonpath.JsonPathException;
import net.minidev.json.JSONArray;
import org.junit.Test;
@ -104,6 +105,19 @@ public class JSONEntityPathFunctionTest extends BaseFunctionTest {
values.add(12.2d);
values.add(17d);
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;
import static org.junit.Assert.assertEquals;
import static org.junit.runners.Parameterized.Parameters;
import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Configurations;
import com.jayway.jsonpath.JsonPathException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@ -35,11 +33,35 @@ public class ResultSetFunctionTest extends BaseFunctionTest {
@Test
public void testMaxOfDoublesResultSet() {
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
public void testSumOfDoublesResultSet() {
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