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.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef; 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 * 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 { public class PassthruPathFunction implements PathFunction {
@Override @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; 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.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef; 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 * 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 * 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 * @param ctx
* Eval context, state bag used as the path is traversed, maintains the result of executing * Eval context, state bag used as the path is traversed, maintains the result of executing
* *
* @param parameters
* @return * @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; package com.jayway.jsonpath.internal.function;
import com.jayway.jsonpath.InvalidPathException; 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.Average;
import com.jayway.jsonpath.internal.function.numeric.Max; import com.jayway.jsonpath.internal.function.numeric.Max;
import com.jayway.jsonpath.internal.function.numeric.Min; import com.jayway.jsonpath.internal.function.numeric.Min;
import com.jayway.jsonpath.internal.function.numeric.StandardDeviation; import com.jayway.jsonpath.internal.function.numeric.StandardDeviation;
import com.jayway.jsonpath.internal.function.numeric.Sum; 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.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -35,9 +39,17 @@ public class PathFunctionFactory {
map.put("min", Min.class); map.put("min", Min.class);
map.put("max", Max.class); map.put("max", Max.class);
// Text Functions
map.put("concat", Concatenate.class);
// Network functions
map.put("getjson", HttpLoader.class);
// JSON Entity Functions // JSON Entity Functions
map.put("length", Length.class); map.put("length", Length.class);
map.put("size", Length.class); map.put("size", Length.class);
map.put("append", Append.class);
FUNCTIONS = Collections.unmodifiableMap(map); 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; package com.jayway.jsonpath.internal.function.numeric;
import com.jayway.jsonpath.JsonPathException;
import com.jayway.jsonpath.internal.EvaluationContext; import com.jayway.jsonpath.internal.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef; 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.PathFunction;
import java.util.List;
/** /**
* Defines the pattern for processing numerical values via an abstract implementation that iterates over the collection * 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 * 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(); protected abstract Number getValue();
@Override @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)){ if(ctx.configuration().jsonProvider().isArray(model)){
Iterable<?> objects = ctx.configuration().jsonProvider().toIterable(model); Iterable<?> objects = ctx.configuration().jsonProvider().toIterable(model);
for (Object obj : objects) { for (Object obj : objects) {
if (obj instanceof Number) { if (obj instanceof Number) {
Number value = (Number) obj; 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); next(value);
} }
} }
}
if (count != 0) {
return getValue(); 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.EvaluationContext;
import com.jayway.jsonpath.internal.PathRef; 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 * Provides the length of a JSONArray Object
@ -11,7 +15,7 @@ import com.jayway.jsonpath.internal.PathRef;
public class Length implements PathFunction { public class Length implements PathFunction {
@Override @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)){ if(ctx.configuration().jsonProvider().isArray(model)){
return ctx.configuration().jsonProvider().length(model); return ctx.configuration().jsonProvider().length(model);
} else if(ctx.configuration().jsonProvider().isMap(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){ if(resultIndex == 0){
throw new PathNotFoundException("No results for path: " + path.toString()); 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){ if (value != null && unwrap){
value = jsonProvider().unwrap(value); 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; package com.jayway.jsonpath.internal.path;
import com.jayway.jsonpath.internal.PathRef; 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.PathFunction;
import com.jayway.jsonpath.internal.function.PathFunctionFactory; 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 * 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 functionName;
private final String pathFragment; private final String pathFragment;
private final List<Parameter> functionParams;
public FunctionPathToken(String pathFragment) { public FunctionPathToken(String pathFragment, List<Parameter> parameters) {
this.pathFragment = pathFragment; this.pathFragment = pathFragment + ((parameters != null && parameters.size() > 0) ? "(...)" : "()");
if(pathFragment.endsWith("()")){ if(null != pathFragment){
functionName = pathFragment.substring(0, pathFragment.length()-2); functionName = pathFragment;
functionParams = parameters;
} else { } else {
functionName = null; functionName = null;
functionParams = null;
} }
} }
@Override @Override
public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) {
PathFunction pathFunction = PathFunctionFactory.newFunction(functionName); PathFunction pathFunction = PathFunctionFactory.newFunction(functionName);
Object result = pathFunction.invoke(currentPath, parent, model, ctx); evaluateParameters(currentPath, parent, model, ctx);
ctx.addResult(currentPath, parent, result); 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.Path;
import com.jayway.jsonpath.internal.Utils; import com.jayway.jsonpath.internal.Utils;
import com.jayway.jsonpath.internal.filter.FilterCompiler; 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.ArrayList;
import java.util.Collection; import java.util.Collection;
@ -23,10 +25,16 @@ public class PathCompiler {
private static final char OPEN_SQUARE_BRACKET = '['; private static final char OPEN_SQUARE_BRACKET = '[';
private static final char CLOSE_SQUARE_BRACKET = ']'; private static final char CLOSE_SQUARE_BRACKET = ']';
private static final char OPEN_PARENTHESIS = '('; 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 WILDCARD = '*';
private static final char PERIOD = '.'; private static final char PERIOD = '.';
private static final char SPACE = ' '; 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 BEGIN_FILTER = '?';
private static final char COMMA = ','; private static final char COMMA = ',';
private static final char SPLIT = ':'; 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() { 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 '@'"); throw new InvalidPathException("Path must start with '$' or '@'");
} }
@ -152,26 +176,54 @@ public class PathCompiler {
int readPosition = startPosition; int readPosition = startPosition;
int endPosition = 0; int endPosition = 0;
boolean isFunction = false;
while (path.inBounds(readPosition)) { while (path.inBounds(readPosition)) {
char c = path.charAt(readPosition); char c = path.charAt(readPosition);
if (c == SPACE) { if (c == SPACE) {
throw new InvalidPathException("Use bracket notion ['my prop'] if your property contains blank characters. position: " + path.position()); 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; endPosition = readPosition;
break; break;
} }
else if (c == OPEN_PARENTHESIS) {
isFunction = true;
endPosition = readPosition++;
break;
}
readPosition++; readPosition++;
} }
if (endPosition == 0) { if (endPosition == 0) {
endPosition = path.length(); 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(); String property = path.subSequence(startPosition, endPosition).toString();
if(property.endsWith("()")){ if(isFunction){
appender.appendPathToken(PathTokenFactory.createFunctionPathToken(property)); appender.appendPathToken(PathTokenFactory.createFunctionPathToken(property, functionParameters));
} else { } else {
appender.appendPathToken(PathTokenFactory.createSinglePropertyPathToken(property, SINGLE_QUOTE)); appender.appendPathToken(PathTokenFactory.createSinglePropertyPathToken(property, SINGLE_QUOTE));
} }
@ -179,6 +231,151 @@ public class PathCompiler {
return path.currentIsTail() || readNextToken(appender); 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) { 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); 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; package com.jayway.jsonpath.internal.path;
import com.jayway.jsonpath.Predicate; import com.jayway.jsonpath.Predicate;
import com.jayway.jsonpath.internal.function.Parameter;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@ -45,7 +46,7 @@ public class PathTokenFactory {
return new PredicatePathToken(predicate); return new PredicatePathToken(predicate);
} }
public static PathToken createFunctionPathToken(String function) { public static PathToken createFunctionPathToken(String function, List<Parameter> parameters) {
return new FunctionPathToken((function)); 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. * @return the unwrapped value.
*/ */
Object unwrap(Object obj); 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. * Created by mattg on 6/27/15.
*/ */
public class BaseFunctionTest { public class BaseFunctionTest {
protected static final String NUMBER_SERIES = "{\"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 = "{\"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\" ]}";
/** /**
* Verify the function returns the correct result based on the input expectedValue * 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) { protected void verifyFunction(Configuration conf, String pathExpr, String json, Object expectedValue) {
Object result = using(conf).parse(json).read(pathExpr); 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) { protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) {
verifyFunction(conf, pathExpr, NUMBER_SERIES, 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 { protected String getResourceAsText(String resourceName) throws IOException {
return new Scanner(BaseFunctionTest.class.getResourceAsStream(resourceName), "UTF-8").useDelimiter("\\A").next(); 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.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;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static org.junit.Assert.assertEquals;
import static org.junit.runners.Parameterized.Parameters; import static org.junit.runners.Parameterized.Parameters;
/** /**
@ -45,26 +47,72 @@ public class NumericPathFunctionTest extends BaseFunctionTest {
verifyMathFunction(conf, "$.numbers.avg()", 5.5); 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 @Test
public void testSumOfDouble() { public void testSumOfDouble() {
verifyMathFunction(conf, "$.numbers.sum()", (10d * (10d + 1d)) / 2d); 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 @Test
public void testMaxOfDouble() { public void testMaxOfDouble() {
verifyMathFunction(conf, "$.numbers.max()", 10d); 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 @Test
public void testMinOfDouble() { public void testMinOfDouble() {
verifyMathFunction(conf, "$.numbers.min()", 1d); 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 @Test
public void testStdDevOfDouble() { public void testStdDevOfDouble() {
verifyMathFunction(conf, "$.numbers.stddev()", 2.8722813232690143d); 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 * Expect that for an invalid function name we'll get back the original input to the function
*/ */

Loading…
Cancel
Save