diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/ParamType.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/ParamType.java new file mode 100644 index 00000000..20288d47 --- /dev/null +++ b/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 +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java new file mode 100644 index 00000000..e8eabf97 --- /dev/null +++ b/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; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruPathFunction.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruPathFunction.java index 1d1a8f64..36d7da77 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruPathFunction.java +++ b/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 parameters) { return model; } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunction.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunction.java index 611c2611..7f0a3802 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunction.java +++ b/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 parameters); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java index c7e5da70..6414e0d9 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PathFunctionFactory.java +++ b/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); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/http/HttpLoader.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/http/HttpLoader.java new file mode 100644 index 00000000..acf58f07 --- /dev/null +++ b/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 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; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/json/Append.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/json/Append.java new file mode 100644 index 00000000..805226e4 --- /dev/null +++ b/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 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; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java index 036da19b..78d9da99 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java +++ b/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 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"); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Concatenate.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Concatenate.java new file mode 100644 index 00000000..6c2dc1c4 --- /dev/null +++ b/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 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(); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Length.java similarity index 70% rename from json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java rename to json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Length.java index 5ef7ecbb..e70f4e3f 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java +++ b/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 parameters) { if(ctx.configuration().jsonProvider().isArray(model)){ return ctx.configuration().jsonProvider().length(model); } else if(ctx.configuration().jsonProvider().isMap(model)){ 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 804d1e96..119f2e92 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 @@ -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); } 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 beef14db..8e9055ed 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 @@ -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 functionParams; - public FunctionPathToken(String pathFragment) { - this.pathFragment = pathFragment; - if(pathFragment.endsWith("()")){ - functionName = pathFragment.substring(0, pathFragment.length()-2); + public FunctionPathToken(String pathFragment, List 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; + } + } + } + } } /** diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java index e1ea89b3..b8039b9f 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java +++ b/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 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 ',' + * + *
+     * doc = {"numbers": [1,2,3,4,5,6,7,8,9,10]}
+     *
+     * $.sum({10}, $.numbers.avg())
+     * 
+ * + * 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 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 parameters = new ArrayList(); + 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 predicates = new LinkedList(); + 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); + } + // // [?], [?,?, ..] // diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java index d6300c40..dc22dda1 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java +++ b/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); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java index e242253d..995c73e4 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathTokenFactory.java +++ b/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 parameters) { + return new FunctionPathToken(function, parameters); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProvider.java index 92f63ccd..fa7dda41 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonProvider.java +++ b/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); - - } 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 a3c9fba4..8962b38f 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 @@ -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(); } diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java new file mode 100644 index 00000000..bed77869 --- /dev/null +++ b/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 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'")); + } + } + +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NumericPathFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NumericPathFunctionTest.java index f3903aa0..54814091 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/internal/function/NumericPathFunctionTest.java +++ b/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 */