Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
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;

Expand All @@ -42,20 +43,93 @@ public boolean isRootPath() {
}

@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;
root.evaluate("", op, document, ctx);
} catch (EvaluationAbortException abort){};
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);

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) {
}
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -62,6 +63,7 @@ public EvaluationContextImpl(Path path, Object rootDocument, Configuration confi
this.valueResult = configuration.jsonProvider().createArray();
this.pathResult = configuration.jsonProvider().createArray();
this.updateOperations = new ArrayList<PathRef>();
this.paramsRootDocument = paramsRootDocument;
}

public HashMap<Path, Object> documentEvalCache() {
Expand Down Expand Up @@ -111,6 +113,10 @@ public Object rootDocument() {
return rootDocument;
}

public Object paramsRootDocument() {
return paramsRootDocument;
}

public Collection<PathRef> updateOperations(){

Collections.sort(updateOperations);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ private void evaluateParameters(String currentPath, PathRef parent, Object model
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ PathToken appendTailToken(PathToken next) {
return next;
}

PathToken remove() {
prev.next = next;
if (next == null) {
return prev;
}
next.prev = prev;
return next;
}

void handleObjectProperty(String currentPath, Object model, EvaluationContextImpl ctx, List<String> properties) {

if(properties.size() == 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ public RootPathToken append(PathToken next) {
return this;
}

public PathToken chop() {
// Remove tail.
PathToken oldTail = tail;
tail = tail.remove();
return oldTail;
}

public PathTokenAppender getPathTokenAppender(){
return new PathTokenAppender(){
@Override
Expand Down Expand Up @@ -76,4 +83,8 @@ public boolean isTokenDefinite() {
public boolean isFunctionPath() {
return (tail instanceof FunctionPathToken);
}

public char getRootToken() {
return rootToken.charAt(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
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}";

/**
* Verify the function returns the correct result based on the input expectedValue
Expand All @@ -28,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);
}
Expand All @@ -40,6 +46,10 @@ protected void verifyTextFunction(Configuration conf, String pathExpr, Object ex
verifyFunction(conf, pathExpr, TEXT_SERIES, expectedValue);
}

protected void verifyExampleFunction(Configuration conf, String pathExpr, Object expectedValue) {
verifyFunction(conf, pathExpr, EXAMPLE_SERIES, expectedValue);
}

protected String getResourceAsText(String resourceName) throws IOException {
return new Scanner(BaseFunctionTest.class.getResourceAsStream(resourceName), "UTF-8").useDelimiter("\\A").next();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Configurations;
import com.jayway.jsonpath.JsonPathException;
import net.minidev.json.JSONArray;
import org.junit.Test;

Expand Down Expand Up @@ -104,6 +105,19 @@ public void testPredicateWithFunctionCallTwoMatches() {
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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.jayway.jsonpath.internal.function;

import static org.junit.runners.Parameterized.Parameters;

import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.Configurations;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Defines functional tests around executing functions on result sets.
*/
@RunWith(Parameterized.class)
public class ResultSetFunctionTest extends BaseFunctionTest {

private static final Logger logger = LoggerFactory.getLogger(ResultSetFunctionTest.class);

private Configuration conf = Configurations.GSON_CONFIGURATION;

public ResultSetFunctionTest(Configuration conf) {
logger.debug("Testing with configuration {}", conf.getClass().getName());
this.conf = conf;
}

@Parameters
public static Iterable<Configuration> configurations() {
return Configurations.configurations();
}

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