From 861c0b34bd491da26596e1b891e71d67fb043efb Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 05:44:51 -0500 Subject: [PATCH] Modified abstract aggregation and the path compiler to handle nested functions, working implementation committed still need to support literal function arguments. At the present time the document passed for parameter function parsing is the root document. Additionally, the braces still exist within the tokenizer - need to handle error conditions and see the consiquence of removing those from the implementation --- .../jsonpath/internal/function/Length.java | 4 +- .../jsonpath/internal/function/Parameter.java | 36 ++++++++++++ .../function/PassthruPathFunction.java | 4 +- .../internal/function/PathFunction.java | 5 +- .../function/numeric/AbstractAggregation.java | 14 ++++- .../internal/path/FunctionPathToken.java | 20 +++++-- .../jsonpath/internal/path/PathCompiler.java | 55 +++++++++++++++---- .../jsonpath/internal/path/PathToken.java | 2 +- .../internal/path/PathTokenFactory.java | 4 +- .../internal/function/NestedFunctionTest.java | 2 +- 10 files changed, 123 insertions(+), 23 deletions(-) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java 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/Length.java index 5ef7ecbb..28674ca0 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/Length.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; + /** * Provides the length of a JSONArray Object * @@ -11,7 +13,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/function/Parameter.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java new file mode 100644 index 00000000..ea016bf4 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Parameter.java @@ -0,0 +1,36 @@ +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 final Path path; + private Object cachedValue; + private Boolean evaluated = false; + + public Parameter(Path path) { + this.path = 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; + } +} 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/numeric/AbstractAggregation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java index 036da19b..0f5cb954 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 @@ -2,8 +2,11 @@ package com.jayway.jsonpath.internal.function.numeric; 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,7 +33,7 @@ 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) { if(ctx.configuration().jsonProvider().isArray(model)){ Iterable objects = ctx.configuration().jsonProvider().toIterable(model); @@ -42,6 +45,15 @@ public abstract class AbstractAggregation implements PathFunction { } return getValue(); } + else if (parameters != null) { + for (Parameter param : parameters) { + if (param.getCachedValue() instanceof Number) { + Number value = (Number)param.getCachedValue(); + next(value); + } + } + return getValue(); + } return null; } } 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 9c5c2cef..4371993b 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,7 +1,7 @@ package com.jayway.jsonpath.internal.path; -import 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; @@ -18,9 +18,9 @@ public class FunctionPathToken extends PathToken { private final String functionName; private final String pathFragment; - private final List functionParams; + private final List functionParams; - public FunctionPathToken(String pathFragment, List parameters) { + public FunctionPathToken(String pathFragment, List parameters) { this.pathFragment = pathFragment; if(null != pathFragment){ functionName = pathFragment; @@ -34,10 +34,22 @@ public class FunctionPathToken extends PathToken { @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); + evaluateParameters(currentPath, parent, model, ctx); + Object result = pathFunction.invoke(currentPath, parent, model, ctx, functionParams); ctx.addResult(currentPath, parent, result); } + private void evaluateParameters(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + + if (null != functionParams) { + for (Parameter param : functionParams) { + if (!param.hasEvaluated()) { + param.setCachedValue(param.getPath().evaluate(ctx.rootDocument(), ctx.rootDocument(), ctx.configuration()).getValue()); + } + } + } + } + /** * Return the actual value by indicating true. If this return was false then we'd return the value in an array which * isn't what is desired - true indicates the raw value is returned. 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 dd4e22c4..bb9089f7 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 @@ -5,6 +5,7 @@ import com.jayway.jsonpath.Predicate; import com.jayway.jsonpath.internal.CharacterIndex; import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.internal.filter.FilterCompiler; +import com.jayway.jsonpath.internal.function.Parameter; import java.util.ArrayList; import java.util.Collection; @@ -29,6 +30,9 @@ public class PathCompiler { 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 = ':'; @@ -73,9 +77,21 @@ public class PathCompiler { } } + private void readWhitespace() { + while (path.inBounds()) { + char c = path.currentChar(); + if (c != SPACE && c != TAB && c != LF && c != CR) { + break; + } + path.incrementPosition(1); + } + } + //[$ | @] private RootPathToken readContextToken() { + readWhitespace(); + if (!path.currentCharIs(DOC_CONTEXT) && !path.currentCharIs(EVAL_CONTEXT)) { throw new InvalidPathException("Path must start with '$' or '@'"); } @@ -177,13 +193,17 @@ public class PathCompiler { } - List functionParameters = null; + List functionParameters = null; if (isFunction) { - // read the next token to determine if we have a simple no-args function call - char c = path.charAt(readPosition++); - if (c != CLOSE_PARENTHESIS) { - // parse the arguments of the function - arguments that are inner queries will be single quoted parameters - functionParameters = parseFunctionParameters(readPosition); + 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) { + // parse the arguments of the function - arguments that are inner queries will be single quoted parameters + functionParameters = parseFunctionParameters(readPosition); + } else { + path.setPosition(readPosition + 1); + } } else { path.setPosition(readPosition); @@ -203,9 +223,9 @@ public class PathCompiler { return path.currentIsTail() || readNextToken(appender); } - private List parseFunctionParameters(int readPosition) { + private List parseFunctionParameters(int readPosition) { PathToken currentToken; - List parameters = new ArrayList(); + List parameters = new ArrayList(); StringBuffer parameter = new StringBuffer(); Boolean insideParameter = false; int braceCount = 0, parenCount = 1; @@ -221,24 +241,37 @@ public class PathCompiler { // inner parse the parameter expression to pass along to the function LinkedList predicates = new LinkedList(); PathCompiler compiler = new PathCompiler(parameter.toString(), predicates); - parameters.add(compiler.compile()); + Path path = compiler.compile(); + parameters.add(new Parameter(path)); + parameter.delete(0, parameter.length()); } } else if (c == COMMA && braceCount == 0) { parameter.delete(0, parameter.length()); } else { - parameter.append(c); if (c == CLOSE_PARENTHESIS) { parenCount--; if (parenCount == 0) { + if (parameter.length() > 0) { + // inner parse the parameter expression to pass along to the function + LinkedList predicates = new LinkedList(); + PathCompiler compiler = new PathCompiler(parameter.toString(), predicates); + parameters.add(new Parameter(compiler.compile())); + } break; } + else { + parameter.append(c); + } } else if (c == OPEN_PARENTHESIS) { parenCount++; + parameter.append(c); + } + else { + parameter.append(c); } - } readPosition++; } 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 70319046..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,7 +1,7 @@ package com.jayway.jsonpath.internal.path; import com.jayway.jsonpath.Predicate; -import com.jayway.jsonpath.internal.Path; +import com.jayway.jsonpath.internal.function.Parameter; import java.util.Collection; import java.util.List; @@ -46,7 +46,7 @@ public class PathTokenFactory { return new PredicatePathToken(predicate); } - public static PathToken createFunctionPathToken(String function, List parameters) { + public static PathToken createFunctionPathToken(String function, List parameters) { return new FunctionPathToken(function, parameters); } } 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 index 3f8bfd22..6878087e 100644 --- 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 @@ -29,6 +29,6 @@ public class NestedFunctionTest extends BaseFunctionTest { @Test public void testAverageOfDoubles() { - verifyMathFunction(conf, "$.sum({$.numbers.min()}, {$.numbers.max()})", 5.5); + verifyMathFunction(conf, "$.avg({$.numbers.min()}, {$.numbers.max()})", 5.5); } }