Browse Source

Merge pull request #167 from mgreenwood1001/NestedJsonPathFunctionParameters

Nested json path function parameters
pull/182/head
kallestenflo 9 years ago
parent
commit
51379c648f
  1. 9
      json-path/src/main/java/com/jayway/jsonpath/internal/function/ParamType.java
  2. 66
      json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java
  3. 4
      json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruPathFunction.java
  4. 5
      json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunction.java
  5. 12
      json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java
  6. 49
      json-path/src/main/java/com/jayway/jsonpath/internal/function/http/HttpLoader.java
  7. 31
      json-path/src/main/java/com/jayway/jsonpath/internal/function/json/Append.java
  8. 21
      json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java
  9. 37
      json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Concatenate.java
  10. 8
      json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Length.java
  11. 3
      json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java
  12. 42
      json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java
  13. 207
      json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java
  14. 2
      json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java
  15. 5
      json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java
  16. 2
      json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProvider.java
  17. 13
      json-path/src/test/java/com/jayway/jsonpath/internal/function/BaseFunctionTest.java
  18. 122
      json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java
  19. 48
      json-path/src/test/java/com/jayway/jsonpath/internal/function/NumericPathFunctionTest.java

9
json-path/src/main/java/com/jayway/jsonpath/internal/function/ParamType.java

@ -0,0 +1,9 @@
package com.jayway.jsonpath.internal.function;
/**
* Created by mgreenwood on 12/11/15.
*/
public enum ParamType {
JSON,
PATH
}

66
json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java

@ -0,0 +1,66 @@
package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.internal.Path;
/**
* Created by matt@mjgreenwood.net on 12/10/15.
*/
public class Parameter {
private ParamType type;
private Path path;
private Object cachedValue;
private Boolean evaluated = false;
private String json;
public Parameter() {}
public Parameter(String json) {
this.json = json;
this.type = ParamType.JSON;
}
public Parameter(Path path) {
this.path = path;
this.type = ParamType.PATH;
}
public Object getCachedValue() {
return cachedValue;
}
public void setCachedValue(Object cachedValue) {
this.cachedValue = cachedValue;
}
public Path getPath() {
return path;
}
public void setEvaluated(Boolean evaluated) {
this.evaluated = evaluated;
}
public boolean hasEvaluated() {
return evaluated;
}
public ParamType getType() {
return type;
}
public void setType(ParamType type) {
this.type = type;
}
public void setPath(Path path) {
this.path = path;
}
public String getJson() {
return json;
}
public void setJson(String json) {
this.json = json;
}
}

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

@ -3,6 +3,8 @@ package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import java.util.List;
/**
* Defines the default behavior which is to return the model that is provided as input as output
*
@ -11,7 +13,7 @@ import com.jayway.jsonpath.internal.PathRef;
public class PassthruPathFunction implements PathFunction {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) {
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
return model;
}
}

5
json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunction.java

@ -3,6 +3,8 @@ package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import java.util.List;
/**
* Defines the pattern by which a function can be executed over the result set in the particular path
* being grabbed. The Function's input is the content of the data from the json path selector and its output
@ -27,7 +29,8 @@ public interface PathFunction {
* @param ctx
* Eval context, state bag used as the path is traversed, maintains the result of executing
*
* @param parameters
* @return
*/
Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx);
Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters);
}

12
json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java

@ -1,11 +1,15 @@
package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.InvalidPathException;
import com.jayway.jsonpath.internal.function.http.HttpLoader;
import com.jayway.jsonpath.internal.function.json.Append;
import com.jayway.jsonpath.internal.function.numeric.Average;
import com.jayway.jsonpath.internal.function.numeric.Max;
import com.jayway.jsonpath.internal.function.numeric.Min;
import com.jayway.jsonpath.internal.function.numeric.StandardDeviation;
import com.jayway.jsonpath.internal.function.numeric.Sum;
import com.jayway.jsonpath.internal.function.text.Concatenate;
import com.jayway.jsonpath.internal.function.text.Length;
import java.util.Collections;
import java.util.HashMap;
@ -35,9 +39,17 @@ public class PathFunctionFactory {
map.put("min", Min.class);
map.put("max", Max.class);
// Text Functions
map.put("concat", Concatenate.class);
// Network functions
map.put("getjson", HttpLoader.class);
// JSON Entity Functions
map.put("length", Length.class);
map.put("size", Length.class);
map.put("append", Append.class);
FUNCTIONS = Collections.unmodifiableMap(map);
}

49
json-path/src/main/java/com/jayway/jsonpath/internal/function/http/HttpLoader.java

@ -0,0 +1,49 @@
package com.jayway.jsonpath.internal.function.http;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.util.List;
/**
* Dirt simple http get method just to demo URL loading
*
* Created by mgreenwood on 12/11/15.
*/
public class HttpLoader implements PathFunction {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
if (parameters != null && parameters.size() == 1) {
try {
URL url = new URL(parameters.get(0).getCachedValue().toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
StringBuffer result = new StringBuffer();
while ((line = rd.readLine()) != null) {
result.append(line);
}
rd.close();
Object jsonResult = ctx.configuration().jsonProvider().parse(result.toString());
return jsonResult;
} catch (ProtocolException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}

31
json-path/src/main/java/com/jayway/jsonpath/internal/function/json/Append.java

@ -0,0 +1,31 @@
package com.jayway.jsonpath.internal.function.json;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import com.jayway.jsonpath.spi.json.JsonProvider;
import java.util.List;
/**
* Appends JSON structure to the current document so that you can utilize the JSON added thru another function call.
* If there are multiple parameters then this function call will add each element that is json to the structure
*
* Created by mgreenwood on 12/14/15.
*/
public class Append implements PathFunction {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
JsonProvider jsonProvider = ctx.configuration().jsonProvider();
if (parameters != null && parameters.size() > 0) {
for (Parameter param : parameters) {
if (jsonProvider.isArray(model)) {
int len = jsonProvider.length(model);
jsonProvider.setArrayIndex(model, len, param.getCachedValue());
}
}
}
return model;
}
}

21
json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java

@ -1,9 +1,13 @@
package com.jayway.jsonpath.internal.function.numeric;
import com.jayway.jsonpath.JsonPathException;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import java.util.List;
/**
* Defines the pattern for processing numerical values via an abstract implementation that iterates over the collection
* of JSONArray entities and verifies that each is a numerical value and then passes that along the abstract methods
@ -30,18 +34,31 @@ public abstract class AbstractAggregation implements PathFunction {
protected abstract Number getValue();
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) {
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
int count = 0;
if(ctx.configuration().jsonProvider().isArray(model)){
Iterable<?> objects = ctx.configuration().jsonProvider().toIterable(model);
for (Object obj : objects) {
if (obj instanceof Number) {
Number value = (Number) obj;
count++;
next(value);
}
}
}
if (parameters != null) {
for (Parameter param : parameters) {
if (param.getCachedValue() instanceof Number) {
Number value = (Number)param.getCachedValue();
count++;
next(value);
}
}
}
if (count != 0) {
return getValue();
}
return null;
throw new JsonPathException("Aggregation function attempted to calculate value using empty array");
}
}

37
json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Concatenate.java

@ -0,0 +1,37 @@
package com.jayway.jsonpath.internal.function.text;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import java.util.List;
/**
* String function concat - simple takes a list of arguments and/or an array and concatenates them together to form a
* single string
*
* Created by mgreenwood on 12/11/15.
*/
public class Concatenate implements PathFunction {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
StringBuffer result = new StringBuffer();
if(ctx.configuration().jsonProvider().isArray(model)){
Iterable<?> objects = ctx.configuration().jsonProvider().toIterable(model);
for (Object obj : objects) {
if (obj instanceof String) {
result.append(obj.toString());
}
}
}
if (parameters != null) {
for (Parameter param : parameters) {
if (param.getCachedValue() != null) {
result.append(param.getCachedValue().toString());
}
}
}
return result.toString();
}
}

8
json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java → json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Length.java

@ -1,7 +1,11 @@
package com.jayway.jsonpath.internal.function;
package com.jayway.jsonpath.internal.function.text;
import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import java.util.List;
/**
* Provides the length of a JSONArray Object
@ -11,7 +15,7 @@ import com.jayway.jsonpath.internal.PathRef;
public class Length implements PathFunction {
@Override
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) {
public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx, List<Parameter> parameters) {
if(ctx.configuration().jsonProvider().isArray(model)){
return ctx.configuration().jsonProvider().length(model);
} else if(ctx.configuration().jsonProvider().isMap(model)){

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

@ -130,7 +130,8 @@ public class EvaluationContextImpl implements EvaluationContext {
if(resultIndex == 0){
throw new PathNotFoundException("No results for path: " + path.toString());
}
Object value = jsonProvider().getArrayIndex(valueResult, 0);
int len = jsonProvider().length(valueResult);
Object value = (len > 0) ? jsonProvider().getArrayIndex(valueResult, len-1) : null;
if (value != null && unwrap){
value = jsonProvider().unwrap(value);
}

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

@ -1,9 +1,12 @@
package com.jayway.jsonpath.internal.path;
import com.jayway.jsonpath.internal.PathRef;
import com.jayway.jsonpath.internal.function.Parameter;
import com.jayway.jsonpath.internal.function.PathFunction;
import com.jayway.jsonpath.internal.function.PathFunctionFactory;
import java.util.List;
/**
* Token representing a Function call to one of the functions produced via the FunctionFactory
*
@ -15,21 +18,48 @@ public class FunctionPathToken extends PathToken {
private final String functionName;
private final String pathFragment;
private final List<Parameter> functionParams;
public FunctionPathToken(String pathFragment) {
this.pathFragment = pathFragment;
if(pathFragment.endsWith("()")){
functionName = pathFragment.substring(0, pathFragment.length()-2);
public FunctionPathToken(String pathFragment, List<Parameter> parameters) {
this.pathFragment = pathFragment + ((parameters != null && parameters.size() > 0) ? "(...)" : "()");
if(null != pathFragment){
functionName = pathFragment;
functionParams = parameters;
} else {
functionName = null;
functionParams = null;
}
}
@Override
public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
PathFunction pathFunction = PathFunctionFactory.newFunction(functionName);
Object result = pathFunction.invoke(currentPath, parent, model, ctx);
ctx.addResult(currentPath, parent, result);
evaluateParameters(currentPath, parent, model, ctx);
Object result = pathFunction.invoke(currentPath, parent, model, ctx, functionParams);
ctx.addResult(currentPath + "." + functionName, parent, result);
if (!isLeaf()) {
next().evaluate(currentPath, parent, result, ctx);
}
}
private void evaluateParameters(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
if (null != functionParams) {
for (Parameter param : functionParams) {
if (!param.hasEvaluated()) {
switch (param.getType()) {
case PATH:
param.setCachedValue(param.getPath().evaluate(ctx.rootDocument(), ctx.rootDocument(), ctx.configuration()).getValue());
param.setEvaluated(true);
break;
case JSON:
param.setCachedValue(ctx.configuration().jsonProvider().parse(param.getJson()));
param.setEvaluated(true);
break;
}
}
}
}
}
/**

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

@ -6,6 +6,8 @@ import com.jayway.jsonpath.internal.CharacterIndex;
import com.jayway.jsonpath.internal.Path;
import com.jayway.jsonpath.internal.Utils;
import com.jayway.jsonpath.internal.filter.FilterCompiler;
import com.jayway.jsonpath.internal.function.ParamType;
import com.jayway.jsonpath.internal.function.Parameter;
import java.util.ArrayList;
import java.util.Collection;
@ -23,10 +25,16 @@ public class PathCompiler {
private static final char OPEN_SQUARE_BRACKET = '[';
private static final char CLOSE_SQUARE_BRACKET = ']';
private static final char OPEN_PARENTHESIS = '(';
private static final char CLOSE_PARENTHESIS = ')';
private static final char OPEN_BRACE = '{';
private static final char CLOSE_BRACE = '}';
private static final char WILDCARD = '*';
private static final char PERIOD = '.';
private static final char SPACE = ' ';
private static final char TAB = '\t';
private static final char CR = '\r';
private static final char LF = '\r';
private static final char BEGIN_FILTER = '?';
private static final char COMMA = ',';
private static final char SPLIT = ':';
@ -71,10 +79,26 @@ public class PathCompiler {
}
}
private void readWhitespace() {
while (path.inBounds()) {
char c = path.currentChar();
if (!isWhitespace(c)) {
break;
}
path.incrementPosition(1);
}
}
private Boolean isPathContext(char c) {
return (c == DOC_CONTEXT || c == EVAL_CONTEXT);
}
//[$ | @]
private RootPathToken readContextToken() {
if (!path.currentCharIs(DOC_CONTEXT) && !path.currentCharIs(EVAL_CONTEXT)) {
readWhitespace();
if (!isPathContext(path.currentChar())) {
throw new InvalidPathException("Path must start with '$' or '@'");
}
@ -152,26 +176,54 @@ public class PathCompiler {
int readPosition = startPosition;
int endPosition = 0;
boolean isFunction = false;
while (path.inBounds(readPosition)) {
char c = path.charAt(readPosition);
if (c == SPACE) {
throw new InvalidPathException("Use bracket notion ['my prop'] if your property contains blank characters. position: " + path.position());
}
if (c == PERIOD || c == OPEN_SQUARE_BRACKET) {
else if (c == PERIOD || c == OPEN_SQUARE_BRACKET) {
endPosition = readPosition;
break;
}
else if (c == OPEN_PARENTHESIS) {
isFunction = true;
endPosition = readPosition++;
break;
}
readPosition++;
}
if (endPosition == 0) {
endPosition = path.length();
}
path.setPosition(endPosition);
List<Parameter> functionParameters = null;
if (isFunction) {
if (path.inBounds(readPosition+1)) {
// read the next token to determine if we have a simple no-args function call
char c = path.charAt(readPosition + 1);
if (c != CLOSE_PARENTHESIS) {
path.setPosition(endPosition+1);
// parse the arguments of the function - arguments that are inner queries or JSON document(s)
String functionName = path.subSequence(startPosition, endPosition).toString();
functionParameters = parseFunctionParameters(functionName);
} else {
path.setPosition(readPosition + 1);
}
}
else {
path.setPosition(readPosition);
}
}
else {
path.setPosition(endPosition);
}
String property = path.subSequence(startPosition, endPosition).toString();
if(property.endsWith("()")){
appender.appendPathToken(PathTokenFactory.createFunctionPathToken(property));
if(isFunction){
appender.appendPathToken(PathTokenFactory.createFunctionPathToken(property, functionParameters));
} else {
appender.appendPathToken(PathTokenFactory.createSinglePropertyPathToken(property, SINGLE_QUOTE));
}
@ -179,6 +231,151 @@ public class PathCompiler {
return path.currentIsTail() || readNextToken(appender);
}
/**
* Parse the parameters of a function call, either the caller has supplied JSON data, or the caller has supplied
* another path expression which must be evaluated and in turn invoked against the root document. In this tokenizer
* we're only concerned with parsing the path thus the output of this function is a list of parameters with the Path
* set if the parameter is an expression. If the parameter is a JSON document then the value of the cachedValue is
* set on the object.
*
* Sequence for parsing out the parameters:
*
* This code has its own tokenizer - it does some rudimentary level of lexing in that it can distinguish between JSON block parameters
* and sub-JSON blocks - it effectively regex's out the parameters into string blocks that can then be passed along to the appropriate parser.
* Since sub-jsonpath expressions can themselves contain other function calls this routine needs to be sensitive to token counting to
* determine the boundaries. Since the Path parser isn't aware of JSON processing this uber routine is needed.
*
* Parameters are separated by COMMAs ','
*
* <pre>
* doc = {"numbers": [1,2,3,4,5,6,7,8,9,10]}
*
* $.sum({10}, $.numbers.avg())
* </pre>
*
* The above is a valid function call, we're first summing 10 + avg of 1...10 (5.5) so the total should be 15.5
*
* @return
* An ordered list of parameters that are to processed via the function. Typically functions either process
* an array of values and/or can consume parameters in addition to the values provided from the consumption of
* an array.
*/
private List<Parameter> parseFunctionParameters(String funcName) {
PathToken currentToken;
ParamType type = null;
// Parenthesis starts at 1 since we're marking the start of a function call, the close paren will denote the
// last parameter boundary
Integer groupParen = 1, groupBracket = 0, groupBrace = 0, groupQuote = 0;
Boolean endOfStream = false;
char priorChar = 0;
List<Parameter> parameters = new ArrayList<Parameter>();
StringBuffer parameter = new StringBuffer();
while (path.inBounds() && !endOfStream) {
char c = path.currentChar();
path.incrementPosition(1);
// we're at the start of the stream, and don't know what type of parameter we have
if (type == null) {
if (isWhitespace(c)) {
continue;
}
if (c == OPEN_BRACE || isDigit(c) || DOUBLE_QUOTE == c) {
type = ParamType.JSON;
}
else if (isPathContext(c)) {
type = ParamType.PATH; // read until we reach a terminating comma and we've reset grouping to zero
}
}
switch (c) {
case DOUBLE_QUOTE:
if (priorChar != '\\' && groupQuote > 0) {
if (groupQuote == 0) {
throw new InvalidPathException("Unexpected quote '\"' at character position: " + path.position());
}
groupQuote--;
}
else {
groupQuote++;
}
break;
case OPEN_PARENTHESIS:
groupParen++;
break;
case OPEN_BRACE:
groupBrace++;
break;
case OPEN_SQUARE_BRACKET:
groupBracket++;
break;
case CLOSE_BRACE:
if (0 == groupBrace) {
throw new InvalidPathException("Unexpected close brace '}' at character position: " + path.position());
}
groupBrace--;
break;
case CLOSE_SQUARE_BRACKET:
if (0 == groupBracket) {
throw new InvalidPathException("Unexpected close bracket ']' at character position: " + path.position());
}
groupBracket--;
break;
// In either the close paren case where we have zero paren groups left, capture the parameter, or where
// we've encountered a COMMA do the same
case CLOSE_PARENTHESIS:
groupParen--;
if (0 != groupParen) {
parameter.append(c);
}
case COMMA:
// In this state we've reach the end of a function parameter and we can pass along the parameter string
// to the parser
if ((0 == groupQuote && 0 == groupBrace && 0 == groupBracket
&& ((0 == groupParen && CLOSE_PARENTHESIS == c) || 1 == groupParen))) {
endOfStream = (0 == groupParen);
if (null != type) {
Parameter param = null;
switch (type) {
case JSON:
// parse the json and set the value
param = new Parameter(parameter.toString());
break;
case PATH:
LinkedList<Predicate> predicates = new LinkedList<Predicate>();
PathCompiler compiler = new PathCompiler(parameter.toString(), predicates);
param = new Parameter(compiler.compile());
break;
}
if (null != param) {
parameters.add(param);
}
parameter.delete(0, parameter.length());
type = null;
}
}
break;
}
if (type != null && !(c == COMMA && 0 == groupBrace && 0 == groupBracket && 1 == groupParen)) {
parameter.append(c);
}
priorChar = c;
}
if (0 != groupBrace || 0 != groupParen || 0 != groupBracket) {
throw new InvalidPathException("Arguments to function: '" + funcName + "' are not closed properly.");
}
return parameters;
}
private boolean isWhitespace(char c) {
return (c == SPACE || c == TAB || c == LF || c == CR);
}
//
// [?], [?,?, ..]
//

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

@ -205,7 +205,7 @@ public abstract class PathToken {
}
public void invoke(PathFunction pathFunction, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
ctx.addResult(currentPath, parent, pathFunction.invoke(currentPath, parent, model, ctx));
ctx.addResult(currentPath, parent, pathFunction.invoke(currentPath, parent, model, ctx, null));
}
public abstract void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx);

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

@ -1,6 +1,7 @@
package com.jayway.jsonpath.internal.path;
import com.jayway.jsonpath.Predicate;
import com.jayway.jsonpath.internal.function.Parameter;
import java.util.Collection;
import java.util.List;
@ -45,7 +46,7 @@ public class PathTokenFactory {
return new PredicatePathToken(predicate);
}
public static PathToken createFunctionPathToken(String function) {
return new FunctionPathToken((function));
public static PathToken createFunctionPathToken(String function, List<Parameter> parameters) {
return new FunctionPathToken(function, parameters);
}
}

2
json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProvider.java

@ -163,6 +163,4 @@ public interface JsonProvider {
* @return the unwrapped value.
*/
Object unwrap(Object obj);
}

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

@ -12,11 +12,8 @@ import static org.assertj.core.api.Assertions.assertThat;
* Created by mattg on 6/27/15.
*/
public class BaseFunctionTest {
protected static final String NUMBER_SERIES = "{\"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}";
protected static final String TEXT_SERIES = "{\"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ]}";
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\" ]}";
/**
* Verify the function returns the correct result based on the input expectedValue
@ -32,13 +29,17 @@ public class BaseFunctionTest {
*/
protected void verifyFunction(Configuration conf, String pathExpr, String json, Object expectedValue) {
Object result = using(conf).parse(json).read(pathExpr);
assertThat(result).isEqualTo(expectedValue);
assertThat(conf.jsonProvider().unwrap(result)).isEqualTo(expectedValue);
}
protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) {
verifyFunction(conf, pathExpr, NUMBER_SERIES, expectedValue);
}
protected void verifyTextFunction(Configuration conf, String pathExpr, Object expectedValue) {
verifyFunction(conf, pathExpr, TEXT_SERIES, expectedValue);
}
protected String getResourceAsText(String resourceName) throws IOException {
return new Scanner(BaseFunctionTest.class.getResourceAsStream(resourceName), "UTF-8").useDelimiter("\\A").next();
}

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

@ -0,0 +1,122 @@
package com.jayway.jsonpath.internal.function;
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;
import static com.jayway.jsonpath.JsonPath.using;
import static org.junit.Assert.assertTrue;
/**
* Created by matt@mjgreenwood.net on 12/10/15.
*/
@RunWith(Parameterized.class)
public class NestedFunctionTest extends BaseFunctionTest {
private static final Logger logger = LoggerFactory.getLogger(NumericPathFunctionTest.class);
private Configuration conf = Configurations.GSON_CONFIGURATION;
public NestedFunctionTest(Configuration conf) {
logger.debug("Testing with configuration {}", conf.getClass().getName());
this.conf = conf;
}
@Parameterized.Parameters
public static Iterable<Configuration> configurations() {
return Configurations.configurations();
}
@Test
public void testParameterAverageFunctionCall() {
verifyMathFunction(conf, "$.avg($.numbers.min(), $.numbers.max())", 5.5);
}
@Test
public void testArrayAverageFunctionCall() {
verifyMathFunction(conf, "$.numbers.avg()", 5.5);
}
/**
* This test calculates the following:
*
* For each number in $.numbers 1 -> 10 add each number up,
* then add 1 (min), 10 (max)
*
* Alternatively 1+2+3+4+5+6+7+8+9+10+1+10 == 66
*/
@Test
public void testArrayAverageFunctionCallWithParameters() {
verifyMathFunction(conf, "$.numbers.sum($.numbers.min(), $.numbers.max())", 66.0);
}
@Test
public void testJsonInnerArgumentArray() {
verifyMathFunction(conf, "$.sum(5, 3, $.numbers.max(), 2)", 20.0);
}
@Test
public void testStringConcat() {
verifyTextFunction(conf, "$.text.concat()", "abcdef");
}
@Test
public void testStringConcatWithJSONParameter() {
verifyTextFunction(conf, "$.text.concat(\"-\", \"ghijk\")", "abcdef-ghijk");
}
@Test
public void testLoadFunction() {
verifyTextFunction(conf, "$.getjson($.urls[0])[0].total", 264);
}
@Test
public void testAppendNumber() {
verifyMathFunction(conf, "$.numbers.append(11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 0).avg()", 10.0);
}
/**
* Aggregation function should ignore text values
*/
@Test
public void testAppendTextAndNumberThenSum() {
verifyMathFunction(conf, "$.numbers.append(\"0\", \"11\").sum()", 55.0);
}
@Test
public void testErrantCloseBraceNegative() {
try {
using(conf).parse(this.NUMBER_SERIES).read("$.numbers.append(0, 1, 2}).avg()");
assert(false);
}
catch (Exception e) {
assertTrue(e.getMessage().startsWith("Unexpected close brace"));
}
}
@Test
public void testErrantCloseBracketNegative() {
try {
using(conf).parse(this.NUMBER_SERIES).read("$.numbers.append(0, 1, 2]).avg()");
assert(false);
}
catch (Exception e) {
assertTrue(e.getMessage().startsWith("Unexpected close bracket"));
}
}
@Test
public void testUnclosedFunctionCallNegative() {
try {
using(conf).parse(this.NUMBER_SERIES).read("$.numbers.append(0, 1, 2");
assert(false);
}
catch (Exception e) {
assertTrue(e.getMessage().startsWith("Arguments to function: 'append'"));
}
}
}

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

@ -2,12 +2,14 @@ package com.jayway.jsonpath.internal.function;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.runners.Parameterized.Parameters;
/**
@ -45,26 +47,72 @@ public class NumericPathFunctionTest extends BaseFunctionTest {
verifyMathFunction(conf, "$.numbers.avg()", 5.5);
}
@Test
public void testAverageOfEmptyListNegative() {
try {
verifyMathFunction(conf, "$.empty.avg()", null);
} catch (JsonPathException e) {
assertEquals(e.getMessage(), "Aggregation function attempted to calculate value using empty array");
}
}
@Test
public void testSumOfDouble() {
verifyMathFunction(conf, "$.numbers.sum()", (10d * (10d + 1d)) / 2d);
}
@Test
public void testSumOfEmptyListNegative() {
try {
verifyMathFunction(conf, "$.empty.sum()", null);
} catch (JsonPathException e) {
assertEquals(e.getMessage(), "Aggregation function attempted to calculate value using empty array");
}
}
@Test
public void testMaxOfDouble() {
verifyMathFunction(conf, "$.numbers.max()", 10d);
}
@Test
public void testMaxOfEmptyListNegative() {
try {
verifyMathFunction(conf, "$.empty.max()", null);
} catch (JsonPathException e) {
assertEquals(e.getMessage(), "Aggregation function attempted to calculate value using empty array");
}
}
@Test
public void testMinOfDouble() {
verifyMathFunction(conf, "$.numbers.min()", 1d);
}
@Test
public void testMinOfEmptyListNegative() {
try {
verifyMathFunction(conf, "$.empty.min()", null);
} catch (JsonPathException e) {
assertEquals(e.getMessage(), "Aggregation function attempted to calculate value using empty array");
}
}
@Test
public void testStdDevOfDouble() {
verifyMathFunction(conf, "$.numbers.stddev()", 2.8722813232690143d);
}
@Test
public void testStddevOfEmptyListNegative() {
try {
verifyMathFunction(conf, "$.empty.stddev()", null);
} catch (JsonPathException e) {
assertEquals(e.getMessage(), "Aggregation function attempted to calculate value using empty array");
}
}
/**
* Expect that for an invalid function name we'll get back the original input to the function
*/

Loading…
Cancel
Save