From 74cb6d69445a69760c787164596a517f13744b4c Mon Sep 17 00:00:00 2001 From: Nicholas Rahn Date: Fri, 4 Mar 2016 11:12:25 +0100 Subject: [PATCH] Completed implementation of running functions on result sets. All unit tests pass. Added additional unit tests for functions on result set. --- .../jsonpath/internal/path/CompiledPath.java | 90 +++++++++++++++---- .../internal/path/EvaluationContextImpl.java | 8 +- .../internal/path/FunctionPathToken.java | 2 +- .../jsonpath/internal/path/RootPathToken.java | 4 + .../internal/function/BaseFunctionTest.java | 7 +- .../function/JSONEntityPathFunctionTest.java | 14 +++ .../function/ResultSetFunctionTest.java | 26 +++++- 7 files changed, 129 insertions(+), 22 deletions(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java index 039463ae..33e012ac 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/CompiledPath.java +++ b/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); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java index 3814419e..66b9893d 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java +++ b/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 updateOperations; private final HashMap documentEvalCache = new HashMap(); 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(); + this.paramsRootDocument = paramsRootDocument; } public HashMap documentEvalCache() { @@ -111,6 +113,10 @@ public class EvaluationContextImpl implements EvaluationContext { return rootDocument; } + public Object paramsRootDocument() { + return paramsRootDocument; + } + public Collection updateOperations(){ Collections.sort(updateOperations); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java index 8e9055ed..5882a446 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java +++ b/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: diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java index 87fcfbab..c87252a4 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/RootPathToken.java +++ b/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); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java index 48d6de3f..dc97ca49 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java +++ b/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); } diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/JSONEntityPathFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/JSONEntityPathFunctionTest.java index 3017cbb6..f1a4489f 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/JSONEntityPathFunctionTest.java +++ b/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); } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/ResultSetFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/ResultSetFunctionTest.java index 204cc560..109585ce 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/ResultSetFunctionTest.java +++ b/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); + } }