From 89e71a93535bf2bdaaaf2e215b4ab8d3494b183c Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 19:59:29 -0400 Subject: [PATCH 1/9] initial commit of function support providing math / JSON helper routines in path execution --- .../src/test/resources/lotto.json | 40 ++++++++++++++++- .../java/com/jayway/jsonpath/Criteria.java | 33 +++++++------- .../jsonpath/internal/PathCompiler.java | 44 ++++++++++++++++--- .../jsonpath/internal/token/PathToken.java | 20 ++++++++- 4 files changed, 111 insertions(+), 26 deletions(-) diff --git a/json-path-assert/src/test/resources/lotto.json b/json-path-assert/src/test/resources/lotto.json index 853e62d9..eb4e4691 100644 --- a/json-path-assert/src/test/resources/lotto.json +++ b/json-path-assert/src/test/resources/lotto.json @@ -1 +1,39 @@ -{"lotto":{"lottoId":5,"winning-numbers":[2,45,34,23,7,5,3],"winners":[{"winnerId":23,"numbers":[2,45,34,23,3,5]},{"winnerId":54,"numbers":[52,3,12,11,18,22]}]}} \ No newline at end of file +{ + "lotto": { + "maxAverage": 100, + "lottoId": 5, + "winning-numbers": [ + 2, + 45, + 34, + 23, + 7, + 5, + 3 + ], + "winners": [ + { + "winnerId": 23, + "numbers": [ + 2, + 45, + 34, + 23, + 3, + 5 + ] + }, + { + "winnerId": 54, + "numbers": [ + 52, + 3, + 12, + 11, + 18, + 22 + ] + } + ] + } +} \ No newline at end of file diff --git a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java index 67a95f16..68572b1a 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -39,7 +39,7 @@ public class Criteria implements Predicate { private static final Logger logger = LoggerFactory.getLogger(Criteria.class); - private static final String[] OPERATORS = { + public static final String[] OPERATORS = { CriteriaType.EQ.toString(), CriteriaType.GTE.toString(), CriteriaType.LTE.toString(), @@ -314,24 +314,23 @@ public class Criteria implements Predicate { abstract boolean eval(Object expected, Object model, PredicateContext ctx); + /** + * Retrieve the CriteriaType based on the incoming string token - assumes the CriteriaType toString() + * method will match the string token presented + * + * @param str + * The string token representing the CriteriaType + * + * @return + * The enum type CriteriaType + */ public static CriteriaType parse(String str) { - if ("==".equals(str)) { - return EQ; - } else if (">".equals(str)) { - return GT; - } else if (">=".equals(str)) { - return GTE; - } else if ("<".equals(str)) { - return LT; - } else if ("<=".equals(str)) { - return LTE; - } else if ("!=".equals(str)) { - return NE; - } else if ("=~".equals(str)) { - return REGEX; - } else { - throw new UnsupportedOperationException("CriteriaType " + str + " can not be parsed"); + for (CriteriaType value : values()) { + if (value.toString().matches(str)) { + return value; + } } + throw new UnsupportedOperationException("CriteriaType " + str + " can not be parsed"); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java index d59c171d..99d2bb61 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java @@ -17,13 +17,7 @@ package com.jayway.jsonpath.internal; import com.jayway.jsonpath.Filter; import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Predicate; -import com.jayway.jsonpath.internal.token.ArrayPathToken; -import com.jayway.jsonpath.internal.token.PathToken; -import com.jayway.jsonpath.internal.token.PredicatePathToken; -import com.jayway.jsonpath.internal.token.PropertyPathToken; -import com.jayway.jsonpath.internal.token.RootPathToken; -import com.jayway.jsonpath.internal.token.ScanPathToken; -import com.jayway.jsonpath.internal.token.WildcardPathToken; +import com.jayway.jsonpath.internal.token.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,6 +41,9 @@ public class PathCompiler { private static final char BRACKET_OPEN = '['; private static final char BRACKET_CLOSE = ']'; private static final char SPACE = ' '; + private static final char PERCENT = '%'; + private static final char PAREN_OPEN = '('; + private static final char PAREN_CLOSE = ')'; private static final Cache cache = new Cache(200); @@ -107,6 +104,11 @@ public class PathCompiler { fragment = path.substring(i, i + positions); i += positions; break; + case PERCENT: + positions = fastForwardUntilCloseParens(path, i); + fragment = path.substring(i, i + positions); + i += positions; + break; case PERIOD: i++; if (path.charAt(i) == PERIOD) { @@ -185,6 +187,33 @@ public class PathCompiler { return skipCount; } + private static int fastForwardUntilCloseParens(String s, int index) { + int skipCount = 0; + int nestedParens = 0; + + //First char is always '[' no need to check it + index++; + skipCount++; + + while (index < s.length()) { + char current = s.charAt(index); + + index++; + skipCount++; + + if (current == PAREN_CLOSE && nestedParens == 0) { + break; + } + if (current == PAREN_OPEN) { + nestedParens++; + } + if (current == PAREN_CLOSE) { + nestedParens--; + } + } + return skipCount; + } + private static int fastForwardUntilClosed(String s, int index) { int skipCount = 0; int nestedBrackets = 0; @@ -242,6 +271,7 @@ public class PathCompiler { else if ("..".equals(pathFragment)) return new ScanPathToken(); else if ("[*]".equals(pathFragment)) return new WildcardPathToken(); else if (".*".equals(pathFragment)) return new WildcardPathToken(); + else if (".%".equals(pathFragment) || pathFragment.startsWith("['%")) return new FunctionPathToken(pathFragment); else if ("[?]".equals(pathFragment)) return new PredicatePathToken(filterList.poll()); else if (FILTER_PATTERN.matcher(pathFragment).matches()) { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java index 720728a2..bfd31564 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java @@ -14,11 +14,17 @@ */ package com.jayway.jsonpath.internal.token; +import com.jayway.jsonpath.Function; import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.Utils; +import com.jayway.jsonpath.internal.function.FunctionFactory; +import com.jayway.jsonpath.internal.function.numeric.Average; +import com.jayway.jsonpath.internal.function.Length; +import com.jayway.jsonpath.internal.function.PassthruFunction; +import com.jayway.jsonpath.internal.function.numeric.Sum; import com.jayway.jsonpath.spi.json.JsonProvider; import java.util.List; @@ -67,7 +73,11 @@ public abstract class PathToken { PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, property) : PathRef.NO_OP; if (isLeaf()) { ctx.addResult(evalPath, pathRef, propertyVal); - } else { +// } else if (isFunction()) { +// Function function = FunctionFactory.newFunction(next.getPathFragment()); +// next.invoke(function, evalPath, pathRef, propertyVal, ctx); + } + else { next().evaluate(evalPath, pathRef, propertyVal, ctx); } } else { @@ -143,6 +153,10 @@ public abstract class PathToken { return next == null; } +// boolean isFunction() { +// return null != next && next.getPathFragment().startsWith("['%"); +// } + boolean isRoot() { return prev == null; } @@ -201,6 +215,10 @@ public abstract class PathToken { return super.equals(obj); } + public void invoke(Function function, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + ctx.addResult(currentPath, parent, function.invoke(currentPath, parent, model, ctx)); + } + public abstract void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); abstract boolean isTokenDefinite(); From 1a0ea4b5593cc48fdbb454be7769a93d358e1a60 Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 20:00:34 -0400 Subject: [PATCH 2/9] initial commit of function support providing math / JSON helper routines in path execution --- .../jayway/jsonpath/AggregationMapReduce.java | 23 ++++++ .../java/com/jayway/jsonpath/Function.java | 33 +++++++++ .../internal/function/FunctionFactory.java | 74 +++++++++++++++++++ .../jsonpath/internal/function/Length.java | 23 ++++++ .../internal/function/PassthruFunction.java | 18 +++++ .../function/numeric/AbstractAggregation.java | 52 +++++++++++++ .../internal/function/numeric/Average.java | 26 +++++++ .../internal/function/numeric/Max.java | 22 ++++++ .../internal/function/numeric/Min.java | 22 ++++++ .../function/numeric/StandardDeviation.java | 24 ++++++ .../internal/function/numeric/Sum.java | 20 +++++ .../internal/token/FunctionPathToken.java | 50 +++++++++++++ .../jsonpath/functions/BaseFunctionTest.java | 36 +++++++++ .../functions/JSONEntityFunctionTest.java | 23 ++++++ .../functions/NumericFunctionTest.java | 62 ++++++++++++++++ 15 files changed, 508 insertions(+) create mode 100644 json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/Function.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java create mode 100644 json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java create mode 100644 json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java create mode 100644 json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java create mode 100644 json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java diff --git a/json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java b/json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java new file mode 100644 index 00000000..f147eb57 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java @@ -0,0 +1,23 @@ +package com.jayway.jsonpath; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * Defines a pattern for taking a collection of input streams and executing the same path operation across all files + * map / reducing the results as they come in to provide an aggregation function on top of the + * + * Created by matt@mjgreenwood.net on 6/26/15. + */ +public class AggregationMapReduce { + + public static void main(String args[]) throws IOException { + ReadContext ctx = JsonPath.parse(new File("/home/mattg/dev/JsonPath/json-path-assert/src/test/resources/lotto.json")); + List numbers = ctx.read("$.lotto.winners..numbers.%sum()"); + + Object value = ctx.read("$.lotto.winners.[?(@.winnerId > $.lotto.winners.%length())].numbers.%avg()"); + System.out.println(numbers); + System.out.println(value); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/Function.java b/json-path/src/main/java/com/jayway/jsonpath/Function.java new file mode 100644 index 00000000..4256f8cf --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/Function.java @@ -0,0 +1,33 @@ +package com.jayway.jsonpath; + +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; + +/** + * 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 + * is defined via the functions behavior. Thus transformations in types can take place. Additionally, functions + * can accept multiple selectors in order to produce their output. + * + * Created by matt@mjgreenwood.net on 6/26/15. + */ +public interface Function { + + /** + * Invoke the function and output a JSON object (or scalar) value which will be the result of executing the path + * + * @param currentPath + * The current path location inclusive of the function name + * @param parent + * The path location above the current function + * + * @param model + * The JSON model as input to this particular function + * + * @param ctx + * Eval context, state bag used as the path is traversed, maintains the result of executing + * + * @return + */ + Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx); +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java new file mode 100644 index 00000000..f224d679 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java @@ -0,0 +1,74 @@ +package com.jayway.jsonpath.internal.function; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.internal.function.numeric.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Implements a factory that given a name of the function will return the Function implementation, or null + * if the value is not obtained. + * + * Leverages the function's name in order to determine which function to execute which is maintained internally + * here via a static map + * + * Created by mattg on 6/27/15. + */ +public class FunctionFactory { + + public static final Map FUNCTIONS; + + static { + // New functions should be added here and ensure the name is not overridden + Map map = new HashMap(); + + // Math Functions + map.put("avg", Average.class); + map.put("stddev", StandardDeviation.class); + map.put("sum", Sum.class); + map.put("min", Min.class); + map.put("max", Max.class); + + // JSON Entity Functions + map.put("length", Length.class); + + FUNCTIONS = Collections.unmodifiableMap(map); + } + + /** + * Either provides a pass thru function when the function cannot be properly mapped or otherwise returns the function + * implementation based on the name using the internal FUNCTION map + * + * @see #FUNCTIONS + * @see Function + * + * @param pathFragment + * The path fragment that is currently being processed which is believed to be the name of a function + * + * @return + * The implementation of a function + * + * @throws InvalidPathException + */ + public static Function newFunction(String pathFragment) throws InvalidPathException { + Function result = new PassthruFunction(); + if (null != pathFragment) { + String name = pathFragment.replaceAll("['%\\]\\[\\(\\)]", "").trim().toLowerCase(); + + if (null != name && FUNCTIONS.containsKey(name) && Function.class.isAssignableFrom(FUNCTIONS.get(name))) { + try { + result = (Function)FUNCTIONS.get(name).newInstance(); + } catch (InstantiationException e) { + throw new InvalidPathException("Function of name: " + name + " cannot be created", e); + } catch (IllegalAccessException e) { + throw new InvalidPathException("Function of name: " + name + " cannot be created", e); + } + } + } + return result; + + } +} 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 new file mode 100644 index 00000000..747b93b4 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java @@ -0,0 +1,23 @@ +package com.jayway.jsonpath.internal.function; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; +import net.minidev.json.JSONArray; + +/** + * Provides the length of a JSONArray Object + * + * Created by mattg on 6/26/15. + */ +public class Length implements Function { + + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { + if (model instanceof JSONArray) { + JSONArray array = (JSONArray)model; + return Integer.valueOf(array.size()); + } + return null; + } +} \ No newline at end of file diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java new file mode 100644 index 00000000..fadda31d --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java @@ -0,0 +1,18 @@ +package com.jayway.jsonpath.internal.function; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; + +/** + * Defines the default behavior which is to return the model that is provided as input as output + * + * Created by mattg on 6/26/15. + */ +public class PassthruFunction implements Function { + + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { + return model; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java new file mode 100644 index 00000000..8322194b --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java @@ -0,0 +1,52 @@ +package com.jayway.jsonpath.internal.function.numeric; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; +import net.minidev.json.JSONArray; + +import java.util.Iterator; + +/** + * 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 + * + * + * Created by mattg on 6/26/15. + */ +public abstract class AbstractAggregation implements Function { + + /** + * Defines the next value in the array to the mathmatical function + * + * @param value + * The numerical value to process next + */ + protected abstract void next(Number value); + + /** + * Obtains the value generated via the series of next value calls + * + * @return + * A numerical answer based on the input value provided + */ + protected abstract Number getValue(); + + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { + if (model instanceof JSONArray) { + JSONArray array = (JSONArray)model; + Double num = 0d; + Iterator it = array.iterator(); + while (it.hasNext()) { + Object next = it.next(); + if (next instanceof Number) { + Number value = (Number) next; + next(value); + } + } + return getValue(); + } + return null; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java new file mode 100644 index 00000000..f4c6788e --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java @@ -0,0 +1,26 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Provides the average of a series of numbers in a JSONArray + * + * Created by mattg on 6/26/15. + */ +public class Average extends AbstractAggregation { + + private Double summation = 0d; + private Double count = 0d; + + @Override + protected void next(Number value) { + count++; + summation += value.doubleValue(); + } + + @Override + protected Number getValue() { + if (count != 0d) { + return summation / count; + } + return 0d; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java new file mode 100644 index 00000000..27570bf6 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java @@ -0,0 +1,22 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Defines the summation of a series of JSONArray numerical values + * + * Created by mattg on 6/26/15. + */ +public class Max extends AbstractAggregation { + private Double max = Double.MIN_VALUE; + + @Override + protected void next(Number value) { + if (max < value.doubleValue()) { + max = value.doubleValue(); + } + } + + @Override + protected Number getValue() { + return max; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java new file mode 100644 index 00000000..d626d48e --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java @@ -0,0 +1,22 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Defines the summation of a series of JSONArray numerical values + * + * Created by mattg on 6/26/15. + */ +public class Min extends AbstractAggregation { + private Double min = Double.MAX_VALUE; + + @Override + protected void next(Number value) { + if (min < value.doubleValue()) { + min = value.doubleValue(); + } + } + + @Override + protected Number getValue() { + return min; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java new file mode 100644 index 00000000..d659f2d6 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java @@ -0,0 +1,24 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Provides the standard deviation of a series of numbers + * + * Created by mattg on 6/27/15. + */ +public class StandardDeviation extends AbstractAggregation { + private Double sumSq = 0d; + private Double sum = 0d; + private Double count = 0d; + + @Override + protected void next(Number value) { + sum += value.doubleValue(); + sumSq += value.doubleValue()*value.doubleValue(); + count++; + } + + @Override + protected Number getValue() { + return Math.sqrt(sumSq/count - sum*sum/count/count); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java new file mode 100644 index 00000000..3996bb43 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java @@ -0,0 +1,20 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Defines the summation of a series of JSONArray numerical values + * + * Created by mattg on 6/26/15. + */ +public class Sum extends AbstractAggregation { + private Double summation = 0d; + + @Override + protected void next(Number value) { + summation += value.doubleValue(); + } + + @Override + protected Number getValue() { + return summation; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java new file mode 100644 index 00000000..7765e292 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java @@ -0,0 +1,50 @@ +package com.jayway.jsonpath.internal.token; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.function.FunctionFactory; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Token representing a Function call to one of the functions produced via the FunctionFactory + * + * @see FunctionFactory + * + * Created by mattg on 6/27/15. + */ +public class FunctionPathToken extends PathToken { + + private final String functionName; + private final String pathFragment; + + public FunctionPathToken(String pathFragment) { + this.pathFragment = pathFragment; + Matcher matcher = Pattern.compile(".*?\\%(\\w+)\\(.*?").matcher(pathFragment); + if (matcher.matches()) { + functionName = matcher.group(1); + } + else { + // We'll end up throwing an error from the factory when we get that far + functionName = null; + } + } + + @Override + public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + Function function = FunctionFactory.newFunction(functionName); + ctx.addResult(currentPath, parent, function.invoke(currentPath, parent, model, ctx)); + } + + @Override + boolean isTokenDefinite() { + return false; + } + + @Override + String getPathFragment() { + return pathFragment; + } + +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java new file mode 100644 index 00000000..100be413 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java @@ -0,0 +1,36 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; + +import static com.jayway.jsonpath.JsonPath.using; +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\" ]}"; + + + /** + * Verify the function returns the correct result based on the input expectedValue + * + * @param pathExpr + * The path expression to execute + * + * @param json + * The json document (actual content) to parse + * + * @param expectedValue + * The expected value to be returned from the test + */ + protected void verifyFunction(String pathExpr, String json, Object expectedValue) { + Configuration conf = Configuration.defaultConfiguration(); + assertThat(using(conf).parse(json).read(pathExpr)).isEqualTo(expectedValue); + } + + protected void verifyMathFunction(String pathExpr, Object expectedValue) { + verifyFunction(pathExpr, NUMBER_SERIES, expectedValue); + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java new file mode 100644 index 00000000..254d2e9f --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java @@ -0,0 +1,23 @@ +package com.jayway.jsonpath.functions; + +import org.junit.Test; + +/** + * Verifies methods that are helper implementations of functions for manipulating JSON entities, i.e. + * length, etc. + * + * Created by mattg on 6/27/15. + */ +public class JSONEntityFunctionTest extends BaseFunctionTest { + @Test + public void testLengthOfTextArray() { + // The length of JSONArray is an integer + System.out.println(TEXT_SERIES); + verifyFunction("$['text'].%length()", TEXT_SERIES, 6); + } + @Test + public void testLengthOfNumberArray() { + // The length of JSONArray is an integer + verifyFunction("$.numbers.%length()", NUMBER_SERIES, 10); + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java new file mode 100644 index 00000000..fcfa394b --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java @@ -0,0 +1,62 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; +import net.minidev.json.JSONArray; +import org.junit.Test; + +import java.util.Arrays; + +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Defines functional tests around executing: + * + * - sum + * - avg + * - stddev + * + * for each of the above, executes the test and verifies that the results are as expected based on a static input + * and static output. + * + * Created by mattg on 6/26/15. + */ +public class NumericFunctionTest extends BaseFunctionTest { + + @Test + public void testAverageOfDoubles() { + verifyMathFunction("$.numbers.%average()", (10d * (10d + 1d)) / 2d); + } + + @Test + public void testSumOfDouble() { + verifyMathFunction("$.numbers.%sum()", (10d * (10d + 1d)) / 2d); + } + + @Test + public void testMaxOfDouble() { + verifyMathFunction("$.numbers.%max()", 10d); + } + + @Test + public void testMinOfDouble() { + verifyMathFunction("$.numbers.%min()", 1d); + } + + @Test + public void testStdDevOfDouble() { + verifyMathFunction("$.numbers.%stddev()", 1d); + } + + /** + * Expect that for an invalid function name we'll get back the original input to the function + */ + @Test + public void testInvalidFunctionNameNegative() { + Configuration conf = Configuration.defaultConfiguration(); + JSONArray numberSeries = new JSONArray(); + numberSeries.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); + assertThat(using(conf).parse(NUMBER_SERIES).read("$.numbers.%foo()")).isEqualTo(numberSeries); + } + +} From 6c083d3a51950f03c10c289f7dab3b8870e0adbf Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 20:48:44 -0400 Subject: [PATCH 3/9] reverted change to lotto.json, added test case for functions in predicates --- .../src/test/resources/lotto.json | 1 - .../jayway/jsonpath/AggregationMapReduce.java | 23 ------ .../internal/function/numeric/Min.java | 2 +- .../internal/token/FunctionPathToken.java | 12 ++- .../jsonpath/internal/token/PathToken.java | 7 -- .../jsonpath/functions/BaseFunctionTest.java | 7 ++ .../functions/JSONEntityFunctionTest.java | 73 +++++++++++++++++++ .../functions/NumericFunctionTest.java | 4 +- 8 files changed, 92 insertions(+), 37 deletions(-) delete mode 100644 json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java diff --git a/json-path-assert/src/test/resources/lotto.json b/json-path-assert/src/test/resources/lotto.json index eb4e4691..4ba13d0d 100644 --- a/json-path-assert/src/test/resources/lotto.json +++ b/json-path-assert/src/test/resources/lotto.json @@ -1,6 +1,5 @@ { "lotto": { - "maxAverage": 100, "lottoId": 5, "winning-numbers": [ 2, diff --git a/json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java b/json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java deleted file mode 100644 index f147eb57..00000000 --- a/json-path/src/main/java/com/jayway/jsonpath/AggregationMapReduce.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.jayway.jsonpath; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -/** - * Defines a pattern for taking a collection of input streams and executing the same path operation across all files - * map / reducing the results as they come in to provide an aggregation function on top of the - * - * Created by matt@mjgreenwood.net on 6/26/15. - */ -public class AggregationMapReduce { - - public static void main(String args[]) throws IOException { - ReadContext ctx = JsonPath.parse(new File("/home/mattg/dev/JsonPath/json-path-assert/src/test/resources/lotto.json")); - List numbers = ctx.read("$.lotto.winners..numbers.%sum()"); - - Object value = ctx.read("$.lotto.winners.[?(@.winnerId > $.lotto.winners.%length())].numbers.%avg()"); - System.out.println(numbers); - System.out.println(value); - } -} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java index d626d48e..3c57e5f2 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java @@ -10,7 +10,7 @@ public class Min extends AbstractAggregation { @Override protected void next(Number value) { - if (min < value.doubleValue()) { + if (min > value.doubleValue()) { min = value.doubleValue(); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java index 7765e292..5008b230 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java @@ -34,17 +34,23 @@ public class FunctionPathToken extends PathToken { @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { Function function = FunctionFactory.newFunction(functionName); - ctx.addResult(currentPath, parent, function.invoke(currentPath, parent, model, ctx)); + Object result = function.invoke(currentPath, parent, model, ctx); + ctx.addResult(currentPath, parent, result); } + /** + * 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. + * + * @return + */ @Override boolean isTokenDefinite() { - return false; + return true; } @Override String getPathFragment() { return pathFragment; } - } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java index bfd31564..5b6da174 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java @@ -73,9 +73,6 @@ public abstract class PathToken { PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, property) : PathRef.NO_OP; if (isLeaf()) { ctx.addResult(evalPath, pathRef, propertyVal); -// } else if (isFunction()) { -// Function function = FunctionFactory.newFunction(next.getPathFragment()); -// next.invoke(function, evalPath, pathRef, propertyVal, ctx); } else { next().evaluate(evalPath, pathRef, propertyVal, ctx); @@ -153,10 +150,6 @@ public abstract class PathToken { return next == null; } -// boolean isFunction() { -// return null != next && next.getPathFragment().startsWith("['%"); -// } - boolean isRoot() { return prev == null; } diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java index 100be413..0d50808f 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java @@ -2,6 +2,9 @@ package com.jayway.jsonpath.functions; import com.jayway.jsonpath.Configuration; +import java.io.IOException; +import java.util.Scanner; + import static com.jayway.jsonpath.JsonPath.using; import static org.assertj.core.api.Assertions.assertThat; @@ -33,4 +36,8 @@ public class BaseFunctionTest { protected void verifyMathFunction(String pathExpr, Object expectedValue) { verifyFunction(pathExpr, NUMBER_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/functions/JSONEntityFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java index 254d2e9f..2bcf5179 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java @@ -1,7 +1,10 @@ package com.jayway.jsonpath.functions; +import net.minidev.json.JSONArray; import org.junit.Test; +import java.io.IOException; + /** * Verifies methods that are helper implementations of functions for manipulating JSON entities, i.e. * length, etc. @@ -9,6 +12,42 @@ import org.junit.Test; * Created by mattg on 6/27/15. */ public class JSONEntityFunctionTest extends BaseFunctionTest { + + private static final String BATCH_JSON = "{\n" + + " \"batches\": {\n" + + " \"minBatchSize\": 10,\n" + + " \"results\": [\n" + + " {\n" + + " \"productId\": 23,\n" + + " \"values\": [\n" + + " 2,\n" + + " 45,\n" + + " 34,\n" + + " 23,\n" + + " 3,\n" + + " 5,\n" + + " 4,\n" + + " 3,\n" + + " 2,\n" + + " 1,\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"productId\": 23,\n" + + " \"values\": [\n" + + " 52,\n" + + " 3,\n" + + " 12,\n" + + " 11,\n" + + " 18,\n" + + " 22,\n" + + " 1\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + @Test public void testLengthOfTextArray() { // The length of JSONArray is an integer @@ -20,4 +59,38 @@ public class JSONEntityFunctionTest extends BaseFunctionTest { // The length of JSONArray is an integer verifyFunction("$.numbers.%length()", NUMBER_SERIES, 10); } + + /** + * The fictitious use-case/story - is we have a collection of batches with values indicating some quality metric. + * We want to determine the average of the values for only the batch's values where the number of items in the batch + * is greater than the min batch size which is encoded in the JSON document. + * + * We use the length function in the predicate to determine the number of values in each batch and then for those + * batches where the count is greater than min we calculate the average batch value. + * + * Its completely contrived example, however, this test exercises functions within predicates. + */ + @Test + public void testPredicateWithFunctionCallSingleMatch() { + String path = "$.batches.results[?(@.values.%length() >= $.batches.minBatchSize)].values.%avg()"; + + // Its an array because in some use-cases the min size might match more than one batch and thus we'll get + // the average out for each collection + JSONArray values = new JSONArray(); + values.add(12.2d); + verifyFunction(path, BATCH_JSON, values); + } + + @Test + public void testPredicateWithFunctionCallTwoMatches() { + String path = "$.batches.results[?(@.values.%length() >= 3)].values.%avg()"; + + // Its an array because in some use-cases the min size might match more than one batch and thus we'll get + // the average out for each collection + JSONArray values = new JSONArray(); + values.add(12.2d); + values.add(17d); + verifyFunction(path, BATCH_JSON, values); + } + } diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java index fcfa394b..4f00bda9 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java @@ -25,7 +25,7 @@ public class NumericFunctionTest extends BaseFunctionTest { @Test public void testAverageOfDoubles() { - verifyMathFunction("$.numbers.%average()", (10d * (10d + 1d)) / 2d); + verifyMathFunction("$.numbers.%avg()", 5.5); } @Test @@ -45,7 +45,7 @@ public class NumericFunctionTest extends BaseFunctionTest { @Test public void testStdDevOfDouble() { - verifyMathFunction("$.numbers.%stddev()", 1d); + verifyMathFunction("$.numbers.%stddev()", 2.8722813232690143d); } /** From d38ec66af203936bd65568b32aa272d69abdac4d Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 21:01:14 -0400 Subject: [PATCH 4/9] reverting lotto.json formatting --- .../src/test/resources/lotto.json | 39 +------------------ 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/json-path-assert/src/test/resources/lotto.json b/json-path-assert/src/test/resources/lotto.json index 4ba13d0d..94d36c8f 100644 --- a/json-path-assert/src/test/resources/lotto.json +++ b/json-path-assert/src/test/resources/lotto.json @@ -1,38 +1 @@ -{ - "lotto": { - "lottoId": 5, - "winning-numbers": [ - 2, - 45, - 34, - 23, - 7, - 5, - 3 - ], - "winners": [ - { - "winnerId": 23, - "numbers": [ - 2, - 45, - 34, - 23, - 3, - 5 - ] - }, - { - "winnerId": 54, - "numbers": [ - 52, - 3, - 12, - 11, - 18, - 22 - ] - } - ] - } -} \ No newline at end of file +{"lotto":{"lottoId":5,"winning-n umbers":[2,45,34,23,7,5,3],"winners":[{"winnerId":23,"numbers":[2,45,34,23,3,5]},{"winnerId":54,"numbers":[52,3,12,11,18,22]}]}} \ No newline at end of file From 528b97c2ec9b1a28af3a62c0b4926c62650eb68a Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 21:01:50 -0400 Subject: [PATCH 5/9] reverting lotto.json formatting --- json-path-assert/src/test/resources/lotto.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json-path-assert/src/test/resources/lotto.json b/json-path-assert/src/test/resources/lotto.json index 94d36c8f..853e62d9 100644 --- a/json-path-assert/src/test/resources/lotto.json +++ b/json-path-assert/src/test/resources/lotto.json @@ -1 +1 @@ -{"lotto":{"lottoId":5,"winning-n umbers":[2,45,34,23,7,5,3],"winners":[{"winnerId":23,"numbers":[2,45,34,23,3,5]},{"winnerId":54,"numbers":[52,3,12,11,18,22]}]}} \ No newline at end of file +{"lotto":{"lottoId":5,"winning-numbers":[2,45,34,23,7,5,3],"winners":[{"winnerId":23,"numbers":[2,45,34,23,3,5]},{"winnerId":54,"numbers":[52,3,12,11,18,22]}]}} \ No newline at end of file From a4c4cd58fa47e6b5a14f204cba36398e68c4d3db Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 21:06:19 -0400 Subject: [PATCH 6/9] Removed the need for parsing the path - its no longer the path, its now the function name --- .../internal/function/FunctionFactory.java | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java index f224d679..35104ab3 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java @@ -45,27 +45,24 @@ public class FunctionFactory { * @see #FUNCTIONS * @see Function * - * @param pathFragment - * The path fragment that is currently being processed which is believed to be the name of a function + * @param name + * The name of the function * * @return * The implementation of a function * * @throws InvalidPathException */ - public static Function newFunction(String pathFragment) throws InvalidPathException { + public static Function newFunction(String name) throws InvalidPathException { Function result = new PassthruFunction(); - if (null != pathFragment) { - String name = pathFragment.replaceAll("['%\\]\\[\\(\\)]", "").trim().toLowerCase(); - if (null != name && FUNCTIONS.containsKey(name) && Function.class.isAssignableFrom(FUNCTIONS.get(name))) { - try { - result = (Function)FUNCTIONS.get(name).newInstance(); - } catch (InstantiationException e) { - throw new InvalidPathException("Function of name: " + name + " cannot be created", e); - } catch (IllegalAccessException e) { - throw new InvalidPathException("Function of name: " + name + " cannot be created", e); - } + if (null != name && FUNCTIONS.containsKey(name) && Function.class.isAssignableFrom(FUNCTIONS.get(name))) { + try { + result = (Function)FUNCTIONS.get(name).newInstance(); + } catch (InstantiationException e) { + throw new InvalidPathException("Function of name: " + name + " cannot be created", e); + } catch (IllegalAccessException e) { + throw new InvalidPathException("Function of name: " + name + " cannot be created", e); } } return result; From 10ee2b4e547cfde2751d2c5b8e251c2bb906f74c Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 21:19:19 -0400 Subject: [PATCH 7/9] updated markdown --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 55b311cd..57166f13 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,22 @@ Operators | `[start:end]` | Array slice operator | | `[?()]` | Filter expression. Expression must evaluate to a boolean value. | + +Functions +--------- + +Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression. +The function output is dictated by the function itself. + +| Function | Description | Output | +| :------------------------ | :----------------------------------------------------------------- |-----------| +| %min() | Provides the min value of an array of numbers | Double | +| %max() | Provides the max value of an array of numbers | Double | +| %avg() | Provides the average value of an array of numbers | Double | +| %stddev() | Provides the standard deviation value of an array of numbers | Double | +| %length() | Provides the length of an array | Integer | + + Path Examples ------------- @@ -124,7 +140,7 @@ Given the json | $..book[?(@.price <= $['expensive'])] | All books in store that are not "expensive" | | $..book[?(@.author =~ /.*REES/i)] | All books matching regex (ignore case) | | $..* | Give me every thing | - +| $..book.%length() | The number of books | Reading a Document ------------------ From f40063bf02c3af6634032183650c6783983cd085 Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sun, 28 Jun 2015 13:58:46 -0400 Subject: [PATCH 8/9] added case for length of map or JSONArray of values --- .../java/com/jayway/jsonpath/internal/function/Length.java | 7 +++++++ .../jayway/jsonpath/functions/JSONEntityFunctionTest.java | 6 ++++++ 2 files changed, 13 insertions(+) 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 747b93b4..1137049c 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 @@ -5,6 +5,10 @@ import com.jayway.jsonpath.internal.EvaluationContext; import com.jayway.jsonpath.internal.PathRef; import net.minidev.json.JSONArray; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + /** * Provides the length of a JSONArray Object * @@ -18,6 +22,9 @@ public class Length implements Function { JSONArray array = (JSONArray)model; return Integer.valueOf(array.size()); } + else if (model instanceof Map) { + return Integer.valueOf(((Map) model).size()); + } return null; } } \ No newline at end of file diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java index 2bcf5179..bcd1d073 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java @@ -60,6 +60,12 @@ public class JSONEntityFunctionTest extends BaseFunctionTest { verifyFunction("$.numbers.%length()", NUMBER_SERIES, 10); } + + @Test + public void testLengthOfStructure() { + verifyFunction("$.batches.%length()", BATCH_JSON, 2); + } + /** * The fictitious use-case/story - is we have a collection of batches with values indicating some quality metric. * We want to determine the average of the values for only the batch's values where the number of items in the batch From f76d556fb639f4c379bee2e9cdfc99580b7cb251 Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sun, 28 Jun 2015 14:05:33 -0400 Subject: [PATCH 9/9] changed JSONArray -> interface Collection to catch all use-cases --- .../com/jayway/jsonpath/internal/function/Length.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 1137049c..2831d4ef 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 @@ -5,9 +5,7 @@ import com.jayway.jsonpath.internal.EvaluationContext; import com.jayway.jsonpath.internal.PathRef; import net.minidev.json.JSONArray; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; /** * Provides the length of a JSONArray Object @@ -18,9 +16,8 @@ public class Length implements Function { @Override public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { - if (model instanceof JSONArray) { - JSONArray array = (JSONArray)model; - return Integer.valueOf(array.size()); + if (model instanceof Collection) { + return Integer.valueOf(((Collection) model).size()); } else if (model instanceof Map) { return Integer.valueOf(((Map) model).size());