From 76cfa0e30fc9cc671807bc60aaa6f8d3d589e462 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Thu, 10 Dec 2015 17:16:59 -0500 Subject: [PATCH 01/12] Initiial implementation of PathCompiler changes to support functions that take other JsonPath elements - need to decide whether path parameters are relative to the function location (probably not) Example: $.sum({$.numbers.min()}, {$.numbers.max()}) You could also do something such as: $.numbers.add({$.numbers.min()}) where add for each element in the array took another JsonPath parameter that we'd add to resulting in an array result with the min value of numbers added to each element in the number's array. Obviously there's better examples than the above - but these changes allow the PathCompiler to parse the function parameters for nested JsonPath's --- .../internal/path/FunctionPathToken.java | 12 ++- .../jsonpath/internal/path/PathCompiler.java | 76 ++++++++++++++++++- .../internal/path/PathTokenFactory.java | 5 +- .../internal/function/NestedFunctionTest.java | 34 +++++++++ .../function/NumericPathFunctionTest.java | 2 +- 5 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java 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..9c5c2cef 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.Path; import com.jayway.jsonpath.internal.PathRef; 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,13 +18,16 @@ public class FunctionPathToken extends PathToken { private final String functionName; private final String pathFragment; + private final List functionParams; - public FunctionPathToken(String pathFragment) { + public FunctionPathToken(String pathFragment, List parameters) { this.pathFragment = pathFragment; - if(pathFragment.endsWith("()")){ - functionName = pathFragment.substring(0, pathFragment.length()-2); + if(null != pathFragment){ + functionName = pathFragment; + functionParams = parameters; } else { functionName = null; + functionParams = null; } } 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 eccd1189..dd4e22c4 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 @@ -22,6 +22,9 @@ 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 = '.'; @@ -151,26 +154,48 @@ 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) { + // 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); + } + 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)); } @@ -178,6 +203,49 @@ public class PathCompiler { return path.currentIsTail() || readNextToken(appender); } + private List parseFunctionParameters(int readPosition) { + PathToken currentToken; + List parameters = new ArrayList(); + StringBuffer parameter = new StringBuffer(); + Boolean insideParameter = false; + int braceCount = 0, parenCount = 1; + while (path.inBounds(readPosition)) { + char c = path.charAt(readPosition); + + if (c == OPEN_BRACE) { + braceCount++; + } + else if (c == CLOSE_BRACE) { + braceCount--; + if (0 == braceCount && 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(compiler.compile()); + } + } + else if (c == COMMA && braceCount == 0) { + parameter.delete(0, parameter.length()); + } + else { + parameter.append(c); + if (c == CLOSE_PARENTHESIS) { + parenCount--; + if (parenCount == 0) { + break; + } + } + else if (c == OPEN_PARENTHESIS) { + parenCount++; + } + + } + readPosition++; + } + path.setPosition(readPosition); + return parameters; + } + // // [?], [?,?, ..] // 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..70319046 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.Path; 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/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..3f8bfd22 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java @@ -0,0 +1,34 @@ +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; + +/** + * 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 testAverageOfDoubles() { + verifyMathFunction(conf, "$.sum({$.numbers.min()}, {$.numbers.max()})", 5.5); + } +} 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..3b9a3cb7 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 @@ -57,7 +57,7 @@ public class NumericPathFunctionTest extends BaseFunctionTest { @Test public void testMinOfDouble() { - verifyMathFunction(conf, "$.numbers.min()", 1d); + verifyMathFunction(conf, "$.numbers.min(foobar)", 1d); } @Test From 861c0b34bd491da26596e1b891e71d67fb043efb Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 05:44:51 -0500 Subject: [PATCH 02/12] 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); } } From 1fa2b0e192fd0386a47e006287764c1fe130da75 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 06:22:11 -0500 Subject: [PATCH 03/12] added test, removed foobar from min test - derp --- .../function/numeric/AbstractAggregation.java | 6 ++--- .../jsonpath/internal/path/PathCompiler.java | 17 ++++++++++++-- .../internal/function/NestedFunctionTest.java | 22 +++++++++++++++++-- .../function/NumericPathFunctionTest.java | 2 +- 4 files changed, 38 insertions(+), 9 deletions(-) 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 0f5cb954..8af57b44 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 @@ -43,17 +43,15 @@ public abstract class AbstractAggregation implements PathFunction { next(value); } } - return getValue(); } - else if (parameters != null) { + if (parameters != null) { for (Parameter param : parameters) { if (param.getCachedValue() instanceof Number) { Number value = (Number)param.getCachedValue(); next(value); } } - return getValue(); } - return null; + return getValue(); } } 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 bb9089f7..4dbb0b51 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 @@ -223,6 +223,20 @@ 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. + * + * @param readPosition + * The current position within the stream we've advanced - TODO remove the need for this... + * @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(int readPosition) { PathToken currentToken; List parameters = new ArrayList(); @@ -230,7 +244,7 @@ public class PathCompiler { Boolean insideParameter = false; int braceCount = 0, parenCount = 1; while (path.inBounds(readPosition)) { - char c = path.charAt(readPosition); + char c = path.charAt(readPosition++); if (c == OPEN_BRACE) { braceCount++; @@ -273,7 +287,6 @@ public class PathCompiler { parameter.append(c); } } - readPosition++; } path.setPosition(readPosition); return 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 6878087e..ef13e37a 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 @@ -28,7 +28,25 @@ public class NestedFunctionTest extends BaseFunctionTest { } @Test - public void testAverageOfDoubles() { - verifyMathFunction(conf, "$.avg({$.numbers.min()}, {$.numbers.max()})", 5.5); + 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); } } 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 3b9a3cb7..f3903aa0 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 @@ -57,7 +57,7 @@ public class NumericPathFunctionTest extends BaseFunctionTest { @Test public void testMinOfDouble() { - verifyMathFunction(conf, "$.numbers.min(foobar)", 1d); + verifyMathFunction(conf, "$.numbers.min()", 1d); } @Test From b0ec61f7a1c171e37d88136625374001915d0947 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 06:25:45 -0500 Subject: [PATCH 04/12] missing dot in $avg -> $.avg - fixing CI --- .../jayway/jsonpath/internal/function/NestedFunctionTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ef13e37a..c763e5eb 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,7 +29,7 @@ public class NestedFunctionTest extends BaseFunctionTest { @Test public void testParameterAverageFunctionCall() { - verifyMathFunction(conf, "$avg({$.numbers.min()}, {$.numbers.max()})", 5.5); + verifyMathFunction(conf, "$.avg({$.numbers.min()}, {$.numbers.max()})", 5.5); } @Test From fadc946032e4664321b91dc6a0394930d022e2e7 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 07:35:27 -0500 Subject: [PATCH 05/12] removed braces added support for JSON - need to parse JSON now... --- .../jsonpath/internal/function/ParamType.java | 9 ++ .../jsonpath/internal/function/Parameter.java | 17 ++- .../jsonpath/internal/path/PathCompiler.java | 136 ++++++++++++------ .../internal/function/NestedFunctionTest.java | 4 +- 4 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/ParamType.java 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 index ea016bf4..c275fd81 100644 --- 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 @@ -6,10 +6,13 @@ import com.jayway.jsonpath.internal.Path; * Created by matt@mjgreenwood.net on 12/10/15. */ public class Parameter { - private final Path path; + private ParamType type; + private Path path; private Object cachedValue; private Boolean evaluated = false; + public Parameter() {} + public Parameter(Path path) { this.path = path; } @@ -33,4 +36,16 @@ public class Parameter { 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; + } } 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 4dbb0b51..bc079bfa 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.ParamType; import com.jayway.jsonpath.internal.function.Parameter; import java.util.ArrayList; @@ -80,19 +81,23 @@ public class PathCompiler { private void readWhitespace() { while (path.inBounds()) { char c = path.currentChar(); - if (c != SPACE && c != TAB && c != LF && c != CR) { + if (!isWhitespace(c)) { break; } path.incrementPosition(1); } } + private Boolean isPathContext(char c) { + return (c == DOC_CONTEXT || c == EVAL_CONTEXT); + } + //[$ | @] private RootPathToken readContextToken() { readWhitespace(); - if (!path.currentCharIs(DOC_CONTEXT) && !path.currentCharIs(EVAL_CONTEXT)) { + if (!isPathContext(path.currentChar())) { throw new InvalidPathException("Path must start with '$' or '@'"); } @@ -199,7 +204,7 @@ public class PathCompiler { // 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 + // parse the arguments of the function - arguments that are inner queries or JSON document(s) functionParameters = parseFunctionParameters(readPosition); } else { path.setPosition(readPosition + 1); @@ -230,6 +235,23 @@ public class PathCompiler { * 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 + * * @param readPosition * The current position within the stream we've advanced - TODO remove the need for this... * @return @@ -239,59 +261,93 @@ public class PathCompiler { */ private List parseFunctionParameters(int readPosition) { 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; + Boolean endOfStream = false; List parameters = new ArrayList(); StringBuffer parameter = new StringBuffer(); - Boolean insideParameter = false; - int braceCount = 0, parenCount = 1; - while (path.inBounds(readPosition)) { + while (path.inBounds(readPosition) && !endOfStream) { char c = path.charAt(readPosition++); - if (c == OPEN_BRACE) { - braceCount++; - } - else if (c == CLOSE_BRACE) { - braceCount--; - if (0 == braceCount && 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); - Path path = compiler.compile(); - parameters.add(new Parameter(path)); - parameter.delete(0, parameter.length()); + // 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) { + type = ParamType.JSON; + } + else if (isPathContext(c)) { + type = ParamType.PATH; // read until we reach a terminating comma and we've reset grouping to zero } } - else if (c == COMMA && braceCount == 0) { - parameter.delete(0, parameter.length()); - } - else { - if (c == CLOSE_PARENTHESIS) { - parenCount--; - if (parenCount == 0) { - if (parameter.length() > 0) { - // inner parse the parameter expression to pass along to the function + + switch (c) { + case OPEN_PARENTHESIS: + groupParen++; + break; + case OPEN_BRACE: + groupBrace++; + break; + case OPEN_SQUARE_BRACKET: + groupBracket++; + break; + + case CLOSE_BRACE: + groupBrace--; + break; + case CLOSE_SQUARE_BRACKET: + 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 (null != type && (0 == groupBrace && 0 == groupBracket && ((0 == groupParen && CLOSE_PARENTHESIS == c) || 1 == groupParen))) { + Parameter param = new Parameter(); + param.setType(type); + if (type == ParamType.JSON) { + // parse the json and set the value + param.setCachedValue(parameter.toString()); + param.setEvaluated(true); + } + else if (type == ParamType.PATH) { LinkedList predicates = new LinkedList(); PathCompiler compiler = new PathCompiler(parameter.toString(), predicates); - parameters.add(new Parameter(compiler.compile())); + param.setPath(compiler.compile()); } - break; - } - else { - parameter.append(c); + parameters.add(param); + parameter.delete(0, parameter.length()); + + type = null; + endOfStream = (0 == groupParen); } - } - else if (c == OPEN_PARENTHESIS) { - parenCount++; - parameter.append(c); - } - else { - parameter.append(c); - } + break; + } + + if (type != null && !(c == COMMA && 0 == groupBrace && 0 == groupBracket && 1 == groupParen)) { + parameter.append(c); } } path.setPosition(readPosition); return parameters; } + private boolean isWhitespace(char c) { + return (c == SPACE || c == TAB || c == LF || c == CR); + } + // // [?], [?,?, ..] // 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 c763e5eb..28fb7adb 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,7 +29,7 @@ public class NestedFunctionTest extends BaseFunctionTest { @Test public void testParameterAverageFunctionCall() { - verifyMathFunction(conf, "$.avg({$.numbers.min()}, {$.numbers.max()})", 5.5); + verifyMathFunction(conf, "$.avg($.numbers.min(), $.numbers.max())", 5.5); } @Test @@ -47,6 +47,6 @@ public class NestedFunctionTest extends BaseFunctionTest { */ @Test public void testArrayAverageFunctionCallWithParameters() { - verifyMathFunction(conf, "$.numbers.sum({$.numbers.min()}, {$.numbers.max()})", 66.0); + verifyMathFunction(conf, "$.numbers.sum($.numbers.min(), $.numbers.max())", 66.0); } } From f932aaf22f6c7299e31c4bce47ad40ef7cc66ef5 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 07:58:01 -0500 Subject: [PATCH 06/12] working version with JSON + path expressions - need to handle error cases with tests --- .../jsonpath/internal/function/Parameter.java | 15 ++++++ .../internal/path/FunctionPathToken.java | 11 +++- .../jsonpath/internal/path/PathCompiler.java | 52 ++++++++++++------- .../internal/function/NestedFunctionTest.java | 5 ++ 4 files changed, 63 insertions(+), 20 deletions(-) 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 index c275fd81..e8eabf97 100644 --- 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 @@ -10,11 +10,18 @@ public class Parameter { 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() { @@ -48,4 +55,12 @@ public class Parameter { 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/path/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/FunctionPathToken.java index 4371993b..5fe851e1 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 @@ -44,7 +44,16 @@ public class FunctionPathToken extends PathToken { if (null != functionParams) { for (Parameter param : functionParams) { if (!param.hasEvaluated()) { - param.setCachedValue(param.getPath().evaluate(ctx.rootDocument(), ctx.rootDocument(), ctx.configuration()).getValue()); + 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 bc079bfa..71586743 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 @@ -265,8 +265,9 @@ public class PathCompiler { // 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; + 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(readPosition) && !endOfStream) { @@ -278,7 +279,7 @@ public class PathCompiler { continue; } - if (c == OPEN_BRACE) { + if (c == OPEN_BRACE || isDigit(c) || DOUBLE_QUOTE == c) { type = ParamType.JSON; } else if (isPathContext(c)) { @@ -287,6 +288,14 @@ public class PathCompiler { } switch (c) { + case DOUBLE_QUOTE: + if (priorChar != '\\' && groupQuote > 0) { + groupQuote--; + } + else { + groupQuote++; + } + break; case OPEN_PARENTHESIS: groupParen++; break; @@ -314,24 +323,28 @@ public class PathCompiler { 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 (null != type && (0 == groupBrace && 0 == groupBracket && ((0 == groupParen && CLOSE_PARENTHESIS == c) || 1 == groupParen))) { - Parameter param = new Parameter(); - param.setType(type); - if (type == ParamType.JSON) { - // parse the json and set the value - param.setCachedValue(parameter.toString()); - param.setEvaluated(true); - } - else if (type == ParamType.PATH) { - LinkedList predicates = new LinkedList(); - PathCompiler compiler = new PathCompiler(parameter.toString(), predicates); - param.setPath(compiler.compile()); - } - parameters.add(param); - parameter.delete(0, parameter.length()); - - type = null; + if ((0 == groupQuote && 0 == groupBrace && 0 == groupBracket && ((0 == groupParen && CLOSE_PARENTHESIS == c) || 1 == groupParen))) { endOfStream = (0 == groupParen); + + if (null != type) { + Parameter param = null; + if (type == ParamType.JSON) { + // parse the json and set the value + param = new Parameter(parameter.toString()); + } else if (type == ParamType.PATH) { + LinkedList predicates = new LinkedList(); + PathCompiler compiler = new PathCompiler(parameter.toString(), predicates); + param = new Parameter(compiler.compile()); + } + if (null != param) { + parameters.add(param); + } else { + // TODO: raise error... + } + parameter.delete(0, parameter.length()); + + type = null; + } } break; } @@ -339,6 +352,7 @@ public class PathCompiler { if (type != null && !(c == COMMA && 0 == groupBrace && 0 == groupBracket && 1 == groupParen)) { parameter.append(c); } + priorChar = c; } path.setPosition(readPosition); return 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 28fb7adb..e078a620 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 @@ -49,4 +49,9 @@ public class NestedFunctionTest extends BaseFunctionTest { 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); + } } From f127edabe16bf9bb027986496d30032c32b402dd Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 08:09:34 -0500 Subject: [PATCH 07/12] Test for string, added concat function --- .../function/PathFunctionFactory.java | 6 +++ .../internal/function/text/Concatenate.java | 37 +++++++++++++++++++ .../internal/function/{ => text}/Length.java | 4 +- .../internal/function/BaseFunctionTest.java | 4 ++ .../internal/function/NestedFunctionTest.java | 10 +++++ 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/text/Concatenate.java rename json-path/src/main/java/com/jayway/jsonpath/internal/function/{ => text}/Length.java (80%) 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..5dd5b836 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 @@ -6,6 +6,8 @@ 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,10 +37,14 @@ public class PathFunctionFactory { map.put("min", Min.class); map.put("max", Max.class); + // Text Functions + map.put("concat", Concatenate.class); + // JSON Entity Functions map.put("length", Length.class); map.put("size", Length.class); + FUNCTIONS = Collections.unmodifiableMap(map); } 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 80% 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 28674ca0..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,9 @@ -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; 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..8f67608f 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 @@ -39,6 +39,10 @@ public class BaseFunctionTest { 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 index e078a620..b4c13502 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 @@ -54,4 +54,14 @@ public class NestedFunctionTest extends BaseFunctionTest { 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"); + } } From b919644425613279e01f08273767d8d23c6b011c Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 21:58:41 -0500 Subject: [PATCH 08/12] adds an example for HttpLoading via getjson (not intended to be merged, just an example) also fixes bug #164 min/max providing wrong value --- .../function/PathFunctionFactory.java | 4 ++ .../internal/function/http/HttpLoader.java | 49 +++++++++++++++++++ .../function/numeric/AbstractAggregation.java | 9 +++- .../internal/path/EvaluationContextImpl.java | 2 +- .../internal/path/FunctionPathToken.java | 5 +- .../spi/json/AbstractJsonProvider.java | 14 ++++++ .../jsonpath/spi/json/JsonProvider.java | 7 +++ .../internal/function/BaseFunctionTest.java | 4 +- .../internal/function/NestedFunctionTest.java | 5 ++ .../function/NumericPathFunctionTest.java | 48 ++++++++++++++++++ 10 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/http/HttpLoader.java 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 5dd5b836..921f9e98 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,6 +1,7 @@ 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.numeric.Average; import com.jayway.jsonpath.internal.function.numeric.Max; import com.jayway.jsonpath.internal.function.numeric.Min; @@ -40,6 +41,9 @@ public class PathFunctionFactory { // 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); 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/numeric/AbstractAggregation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java index 8af57b44..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,5 +1,6 @@ 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; @@ -34,12 +35,14 @@ public abstract class AbstractAggregation implements PathFunction { @Override 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); } } @@ -48,10 +51,14 @@ public abstract class AbstractAggregation implements PathFunction { for (Parameter param : parameters) { if (param.getCachedValue() instanceof Number) { Number value = (Number)param.getCachedValue(); + count++; next(value); } } } - return getValue(); + if (count != 0) { + return getValue(); + } + throw new JsonPathException("Aggregation function attempted to calculate value using empty array"); } } 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..fa43f365 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,7 @@ public class EvaluationContextImpl implements EvaluationContext { if(resultIndex == 0){ throw new PathNotFoundException("No results for path: " + path.toString()); } - Object value = jsonProvider().getArrayIndex(valueResult, 0); + Object value = jsonProvider().getLastElement(valueResult); 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 5fe851e1..ad8bd421 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 @@ -36,7 +36,10 @@ public class FunctionPathToken extends PathToken { PathFunction pathFunction = PathFunctionFactory.newFunction(functionName); evaluateParameters(currentPath, parent, model, ctx); Object result = pathFunction.invoke(currentPath, parent, model, ctx, functionParams); - ctx.addResult(currentPath, parent, result); + 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) { diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java index 56cfb47c..71872db7 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java @@ -43,6 +43,20 @@ public abstract class AbstractJsonProvider implements JsonProvider { return ((List) obj).get(idx); } + /** + * Extracts the last value from an array + * + * @param obj an array + * @return the entry at the given index + */ + public Object getLastElement(Object obj) { + if (null != obj) { + int len = this.length(obj); + return unwrap(getArrayIndex(obj, len-1)); + } + return null; + } + public final Object getArrayIndex(Object obj, int idx, boolean unwrap){ return getArrayIndex(obj, idx); } 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..e1caa582 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 @@ -165,4 +165,11 @@ public interface JsonProvider { Object unwrap(Object obj); + /** + * Get the last element of the array + * + * @param obj an array + * @return the unwrapped value + */ + Object getLastElement(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 8f67608f..6514174b 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,8 +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\" ]}"; 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 b4c13502..38f50e68 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 @@ -64,4 +64,9 @@ public class NestedFunctionTest extends BaseFunctionTest { public void testStringConcatWithJSONParameter() { verifyTextFunction(conf, "$.text.concat(\"-\", \"ghijk\")", "abcdef-ghijk"); } + + @Test + public void testLoadFunction() { + verifyTextFunction(conf, "$.getjson($.urls[0])[0].total", 264); + } } 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 */ From 01bdbe2cc37e563a4e31b65bd799bc45d8e43f67 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Fri, 11 Dec 2015 23:09:48 -0500 Subject: [PATCH 09/12] fixing broken tests changing format of functions path expr with/without args removing errant unwrap call - added to test --- .../com/jayway/jsonpath/internal/path/FunctionPathToken.java | 2 +- .../com/jayway/jsonpath/spi/json/AbstractJsonProvider.java | 2 +- .../jayway/jsonpath/internal/function/BaseFunctionTest.java | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) 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 ad8bd421..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 @@ -21,7 +21,7 @@ public class FunctionPathToken extends PathToken { private final List functionParams; public FunctionPathToken(String pathFragment, List parameters) { - this.pathFragment = pathFragment; + this.pathFragment = pathFragment + ((parameters != null && parameters.size() > 0) ? "(...)" : "()"); if(null != pathFragment){ functionName = pathFragment; functionParams = parameters; diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java index 71872db7..65b89467 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java @@ -52,7 +52,7 @@ public abstract class AbstractJsonProvider implements JsonProvider { public Object getLastElement(Object obj) { if (null != obj) { int len = this.length(obj); - return unwrap(getArrayIndex(obj, len-1)); + return getArrayIndex(obj, len-1); } return null; } 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 6514174b..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 @@ -15,9 +15,6 @@ public class BaseFunctionTest { protected static final String NUMBER_SERIES = "{\"empty\": [], \"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}"; protected static final String TEXT_SERIES = "{\"urls\": [\"http://api.worldbank.org/countries/all/?format=json\", \"http://api.worldbank.org/countries/all/?format=json\"], \"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ]}"; - - - /** * Verify the function returns the correct result based on the input expectedValue * @@ -32,7 +29,7 @@ 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) { From 5d8e2091410661e958493657eb70964d18cd8379 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Mon, 14 Dec 2015 07:16:17 -0500 Subject: [PATCH 10/12] Added an append function such that JSON can be appended to the current document and then a function executed over the result. --- .../function/PathFunctionFactory.java | 2 ++ .../internal/function/json/Append.java | 31 +++++++++++++++++++ .../internal/function/NestedFunctionTest.java | 5 +++ 3 files changed, 38 insertions(+) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/json/Append.java 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 921f9e98..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 @@ -2,6 +2,7 @@ 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; @@ -47,6 +48,7 @@ public class PathFunctionFactory { // 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/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/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/internal/function/NestedFunctionTest.java index 38f50e68..1b36b362 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 @@ -69,4 +69,9 @@ public class NestedFunctionTest extends BaseFunctionTest { 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); + } } From 5e2ef13c640d1d9c0aa926e61e692582d8529c83 Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Mon, 14 Dec 2015 09:05:57 -0500 Subject: [PATCH 11/12] added negative test cases - removed readPosition from function parameter parser, ready for review --- .../jsonpath/internal/path/PathCompiler.java | 49 ++++++++++++------- .../internal/function/NestedFunctionTest.java | 45 +++++++++++++++++ 2 files changed, 76 insertions(+), 18 deletions(-) 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 71586743..4deebc2a 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 @@ -204,8 +204,10 @@ public class PathCompiler { // 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) - functionParameters = parseFunctionParameters(readPosition); + String functionName = path.subSequence(startPosition, endPosition).toString(); + functionParameters = parseFunctionParameters(functionName); } else { path.setPosition(readPosition + 1); } @@ -252,14 +254,12 @@ public class PathCompiler { * * 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 * - * @param readPosition - * The current position within the stream we've advanced - TODO remove the need for this... * @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(int readPosition) { + private List parseFunctionParameters(String funcName) { PathToken currentToken; ParamType type = null; @@ -270,8 +270,9 @@ public class PathCompiler { char priorChar = 0; List parameters = new ArrayList(); StringBuffer parameter = new StringBuffer(); - while (path.inBounds(readPosition) && !endOfStream) { - char c = path.charAt(readPosition++); + 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) { @@ -290,6 +291,9 @@ public class PathCompiler { switch (c) { case DOUBLE_QUOTE: if (priorChar != '\\' && groupQuote > 0) { + if (groupQuote == 0) { + throw new InvalidPathException("Unexpected quote '\"' at character position: " + path.position()); + } groupQuote--; } else { @@ -307,9 +311,15 @@ public class PathCompiler { 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; @@ -323,26 +333,27 @@ public class PathCompiler { 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))) { + if ((0 == groupQuote && 0 == groupBrace && 0 == groupBracket + && ((0 == groupParen && CLOSE_PARENTHESIS == c) || 1 == groupParen))) { endOfStream = (0 == groupParen); if (null != type) { Parameter param = null; - if (type == ParamType.JSON) { - // parse the json and set the value - param = new Parameter(parameter.toString()); - } else if (type == ParamType.PATH) { - LinkedList predicates = new LinkedList(); - PathCompiler compiler = new PathCompiler(parameter.toString(), predicates); - param = new Parameter(compiler.compile()); + 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); - } else { - // TODO: raise error... } parameter.delete(0, parameter.length()); - type = null; } } @@ -354,7 +365,9 @@ public class PathCompiler { } priorChar = c; } - path.setPosition(readPosition); + if (0 != groupBrace || 0 != groupParen || 0 != groupBracket) { + throw new InvalidPathException("Arguments to function: '" + funcName + "' are not closed properly."); + } return 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 1b36b362..bed77869 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 @@ -8,6 +8,9 @@ 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. */ @@ -74,4 +77,46 @@ public class NestedFunctionTest extends BaseFunctionTest { 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'")); + } + } + } From e6d2546c42269ea4ca595f027fa21ff13340125a Mon Sep 17 00:00:00 2001 From: Matthew J Greenwood Date: Mon, 14 Dec 2015 11:49:28 -0500 Subject: [PATCH 12/12] removed getLastElement --- .../internal/path/EvaluationContextImpl.java | 3 ++- .../jsonpath/spi/json/AbstractJsonProvider.java | 14 -------------- .../com/jayway/jsonpath/spi/json/JsonProvider.java | 9 --------- 3 files changed, 2 insertions(+), 24 deletions(-) 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 fa43f365..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().getLastElement(valueResult); + 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/spi/json/AbstractJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java index 65b89467..56cfb47c 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/json/AbstractJsonProvider.java @@ -43,20 +43,6 @@ public abstract class AbstractJsonProvider implements JsonProvider { return ((List) obj).get(idx); } - /** - * Extracts the last value from an array - * - * @param obj an array - * @return the entry at the given index - */ - public Object getLastElement(Object obj) { - if (null != obj) { - int len = this.length(obj); - return getArrayIndex(obj, len-1); - } - return null; - } - public final Object getArrayIndex(Object obj, int idx, boolean unwrap){ return getArrayIndex(obj, idx); } 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 e1caa582..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,13 +163,4 @@ public interface JsonProvider { * @return the unwrapped value. */ Object unwrap(Object obj); - - - /** - * Get the last element of the array - * - * @param obj an array - * @return the unwrapped value - */ - Object getLastElement(Object obj); }