From 89e71a93535bf2bdaaaf2e215b4ab8d3494b183c Mon Sep 17 00:00:00 2001 From: Matt Greenwood Date: Sat, 27 Jun 2015 19:59:29 -0400 Subject: [PATCH 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 08/22] 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 09/22] 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()); From e1d5329a8cd129115cb4d5b84c445e626fbb61c0 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Sun, 11 Oct 2015 16:59:57 +0200 Subject: [PATCH 10/22] Improved test for Filter serialization. --- .../java/com/jayway/jsonpath/Criteria.java | 3 +- .../java/com/jayway/jsonpath/FilterTest.java | 140 ++++++++++++------ .../com/jayway/jsonpath/old/IssuesTest.java | 12 ++ 3 files changed, 108 insertions(+), 47 deletions(-) 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 e0399d55..8a3f2982 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -451,7 +451,6 @@ public class Criteria implements Predicate { } else { return (value == null); } - } catch (PathNotFoundException e) { return !exists; } @@ -841,7 +840,7 @@ public class Criteria implements Predicate { @Override public String toString() { - return getClass().getSimpleName() + " " + value; + return value; } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java index edb91c17..547345b6 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java @@ -11,8 +11,6 @@ import java.util.regex.Pattern; import static com.jayway.jsonpath.Criteria.where; import static com.jayway.jsonpath.Filter.filter; import static com.jayway.jsonpath.Filter.parse; -import static java.lang.System.out; -import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; public class FilterTest extends BaseTest { @@ -473,74 +471,116 @@ public class FilterTest extends BaseTest { @Test public void a_gte_filter_can_be_serialized() { - System.out.println(filter(where("a").gte(1)).toString()); + String filter = filter(where("a").gte(1)).toString(); + String parsed = parse("[?(@['a'] >= 1)]").toString(); - assertThat(filter(where("a").gte(1)).toString()).isEqualTo(parse("[?(@['a'] >= 1)]").toString()); + assertThat(filter).isEqualTo(parse(parsed).toString()); } @Test public void a_lte_filter_can_be_serialized() { - assertThat(filter(where("a").lte(1)).toString()).isEqualTo("[?(@['a'] <= 1)]"); + + String filter = filter(where("a").lte(1)).toString(); + String parsed = parse("[?(@['a'] <= 1)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_eq_filter_can_be_serialized() { - assertThat(filter(where("a").eq(1)).toString()).isEqualTo("[?(@['a'] == 1)]"); + + String filter = filter(where("a").eq(1)).toString(); + String parsed = parse("[?(@['a'] == 1)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_ne_filter_can_be_serialized() { - assertThat(filter(where("a").ne(1)).toString()).isEqualTo("[?(@['a'] != 1)]"); + + String filter = filter(where("a").ne(1)).toString(); + String parsed = parse("[?(@['a'] != 1)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_lt_filter_can_be_serialized() { - assertThat(filter(where("a").lt(1)).toString()).isEqualTo("[?(@['a'] < 1)]"); + + String filter = filter(where("a").lt(1)).toString(); + String parsed = parse("[?(@['a'] < 1)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_gt_filter_can_be_serialized() { - assertThat(filter(where("a").gt(1)).toString()).isEqualTo("[?(@['a'] > 1)]"); - } - @Test - public void a_regex_filter_can_be_serialized() { - assertThat(filter(where("a").regex(Pattern.compile("/.*?/i"))).toString()).isEqualTo("[?(@['a'] =~ /.*?/i)]"); + String filter = filter(where("a").gt(1)).toString(); + String parsed = parse("[?(@['a'] > 1)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_nin_filter_can_be_serialized() { - assertThat(filter(where("a").nin(1)).toString()).isEqualTo("[?(@['a'] ¦NIN¦ [1])]"); + String filter = filter(where("a").nin(1)).toString(); + String parsed = parse("[?(@['a'] ¦NIN¦ [1])]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_in_filter_can_be_serialized() { - assertThat(filter(where("a").in("a")).toString()).isEqualTo("[?(@['a'] ¦IN¦ ['a'])]"); + + String filter = filter(where("a").in("a")).toString(); + String parsed = parse("[?(@['a'] ¦IN¦ ['a'])]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_contains_filter_can_be_serialized() { - assertThat(filter(where("a").contains("a")).toString()).isEqualTo("[?(@['a'] ¦CONTAINS¦ 'a')]"); + String filter = filter(where("a").contains("a")).toString(); + String parsed = parse("[?(@['a'] ¦CONTAINS¦ 'a')]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_all_filter_can_be_serialized() { - assertThat(filter(where("a").all("a", "b")).toString()).isEqualTo("[?(@['a'] ¦ALL¦ ['a','b'])]"); + + String filter = filter(where("a").all("a", "b")).toString(); + String parsed = parse("[?(@['a'] ¦ALL¦ ['a','b'])]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_size_filter_can_be_serialized() { - assertThat(filter(where("a").size(5)).toString()).isEqualTo("[?(@['a'] ¦SIZE¦ 5)]"); + + String filter = filter(where("a").size(5)).toString(); + String parsed = parse("[?(@['a'] ¦SIZE¦ 5)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_exists_filter_can_be_serialized() { - assertThat(filter(where("a").exists(true)).toString()).isEqualTo("[?(@['a'])]"); + + String filter = filter(where("a").exists(true)).toString(); + String parsed = parse("[?(@['a'])]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_not_exists_filter_can_be_serialized() { - assertThat(filter(where("a").exists(false)).toString()).isEqualTo("[?(!@['a'])]"); + + String filter = filter(where("a").exists(false)).toString(); + String parsed = parse("[?(!@['a'])]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test @@ -557,22 +597,43 @@ public class FilterTest extends BaseTest { @Test public void a_not_empty_filter_can_be_serialized() { - assertThat(filter(where("a").notEmpty()).toString()).isEqualTo("[?(@['a'] ¦NOT_EMPTY¦)]"); + + String filter = filter(where("a").notEmpty()).toString(); + String parsed = parse("[?(@['a'] ¦NOT_EMPTY¦)]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void and_filter_can_be_serialized() { - assertThat(filter(where("a").eq(1).and("b").eq(2)).toString()).isEqualTo("[?(@['a'] == 1 && @['b'] == 2)]"); + + String filter = filter(where("a").eq(1).and("b").eq(2)).toString(); + String parsed = parse("[?(@['b'] == 2 && @['a'] == 1)]").toString(); //FIXME: criteria are reversed + + assertThat(filter).isEqualTo(parsed); } @Test public void in_string_filter_can_be_serialized() { - assertThat(filter(where("a").in("1","2")).toString()).isEqualTo("[?(@['a'] ¦IN¦ ['1','2'])]"); + + String filter = filter(where("a").in("1","2")).toString(); + String parsed = parse("[?(@['a'] ¦IN¦ ['1','2'])]").toString(); + + assertThat(filter).isEqualTo(parsed); } @Test public void a_deep_path_filter_can_be_serialized() { - assertThat(filter(where("a.b.c").in("1","2")).toString()).isEqualTo("[?(@['a']['b']['c'] ¦IN¦ ['1','2'])]"); + + String filter = filter(where("a.b.c").in("1", "2")).toString(); + String parsed = parse("[?(@['a']['b']['c'] ¦IN¦ ['1','2'])]").toString(); + + assertThat(filter).isEqualTo(parsed); + } + + @Test + public void a_regex_filter_can_be_serialized() { + assertThat(filter(where("a").regex(Pattern.compile("/.*?/i"))).toString()).isEqualTo("[?(@['a'] =~ /.*?/i)]"); } @Test @@ -585,9 +646,13 @@ public class FilterTest extends BaseTest { Filter a = filter(where("a").eq(1)); Filter b = filter(where("b").eq(2)); - Filter c = a.and(b); + Filter c = b.and(a); + + String filter = c.toString(); + String parsed = parse("[?(@['a'] == 1 && @['b'] == 2)]").toString(); - assertThat(c.toString()).isEqualTo("[?(@['a'] == 1 && @['b'] == 2)]"); + + assertThat(filter).isEqualTo(parsed); } @Test @@ -595,26 +660,11 @@ public class FilterTest extends BaseTest { Filter a = filter(where("a").eq(1)); Filter b = filter(where("b").eq(2)); - Filter c = a.or(b); - - assertThat(c.toString()).isEqualTo("[?(@['a'] == 1 || @['b'] == 2)]"); - } - - - @Test - public void a_____() { - // :2 - // 1:2 - // -2: - //2: - - + Filter c = b.or(a); - out.println(asList(":2".split(":"))); //[, 2] - out.println(asList("1:2".split(":"))); //[1, 2] - out.println(asList("-2:".split(":"))); //[-2] - out.println(asList("2:".split(":"))); //[2] - out.println(asList(":2".split(":")).get(0).equals("")); //true + String filter = c.toString(); + String parsed = parse("[?(@['a'] == 1 || @['b'] == 2)]").toString(); + assertThat(filter).isEqualTo(parsed); } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index c491e6b2..3336ee28 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -765,4 +765,16 @@ public class IssuesTest extends BaseTest { } }; } + + @Test + public void issue_131() { + + String json = "[1, 2, {\"d\": { \"random\":null, \"date\": \"1234\"} , \"l\": \"filler\"}]"; + //String json = "[1, 2, {\"d\": { \"random\":1, \"date\": \"1234\"} , \"l\": \"filler\"}]"; + + Object read = JsonPath.read(json, "$[2]['d'][?(@.random)]['date']"); + + System.out.println(read); + + } } From 562baa14370afe7f7088fbbefdd5162e0c30b265 Mon Sep 17 00:00:00 2001 From: Archimedes Trajano Date: Tue, 13 Oct 2015 02:21:37 -0400 Subject: [PATCH 11/22] Used new Travis infrastructure --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index dff5f3a5..0f4ef7c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,3 @@ language: java +sudo: false + From 87d7942b6302c5dac696a6d78d047508e686e688 Mon Sep 17 00:00:00 2001 From: Archimedes Trajano Date: Tue, 13 Oct 2015 01:08:25 -0400 Subject: [PATCH 12/22] removed unused logger --- .../java/com/jayway/jsonpath/internal/PathCompiler.java | 7 ------- 1 file changed, 7 deletions(-) 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 f9056d78..78e6a65f 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 @@ -8,8 +8,6 @@ import com.jayway.jsonpath.internal.token.ArraySliceOperation; import com.jayway.jsonpath.internal.token.PathTokenAppender; import com.jayway.jsonpath.internal.token.PathTokenFactory; import com.jayway.jsonpath.internal.token.RootPathToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; @@ -22,8 +20,6 @@ import static java.util.Arrays.asList; public class PathCompiler { - private static final Logger logger = LoggerFactory.getLogger(PathCompiler.class); - private static final char DOC_CONTEXT = '$'; private static final char EVAL_CONTEXT = '@'; private static final char OPEN_SQUARE_BRACKET = '['; @@ -562,6 +558,3 @@ public class PathCompiler { } } } - - - From 18ddbe90b4d81d7f4401bd4998e1632ba3f9fa18 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Wed, 14 Oct 2015 20:41:22 +0200 Subject: [PATCH 13/22] Checking if Node Exists - Bracket Notation Syntax #131 --- .../java/com/jayway/jsonpath/Criteria.java | 6 +- .../jsonpath/internal/token/PathToken.java | 1 - .../com/jayway/jsonpath/InlineFilterTest.java | 11 ++- .../com/jayway/jsonpath/old/FilterTest.java | 6 +- .../com/jayway/jsonpath/old/IssuesTest.java | 96 +++++++++++++------ 5 files changed, 78 insertions(+), 42 deletions(-) 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 8a3f2982..99148649 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/Criteria.java +++ b/json-path/src/main/java/com/jayway/jsonpath/Criteria.java @@ -445,11 +445,11 @@ public class Criteria implements Predicate { boolean exists = ((Boolean) right); try { Configuration c = Configuration.builder().jsonProvider(ctx.configuration().jsonProvider()).options(Option.REQUIRE_PROPERTIES).build(); - Object value = ((Path) left).evaluate(ctx.item(), ctx.root(), c).getValue(); + Object value = ((Path) left).evaluate(ctx.item(), ctx.root(), c).getValue(false); if (exists) { - return (value != null); + return (value != JsonProvider.UNDEFINED); } else { - return (value == null); + return (value == JsonProvider.UNDEFINED); } } catch (PathNotFoundException e) { return !exists; 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 122d94da..40760143 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 @@ -53,7 +53,6 @@ public abstract class PathToken { } else { throw new PathNotFoundException("No results for path: " + evalPath); } - } } else { if(!isUpstreamDefinite() && diff --git a/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java index f56c0d02..a9371f9e 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/InlineFilterTest.java @@ -152,10 +152,15 @@ public class InlineFilterTest extends BaseTest { ints.add(3); - List notNull = JsonPath.parse(ints).read("$[?(@)]"); - assertThat(notNull).containsExactly(0,1,2,3); + List hits = JsonPath.parse(ints).read("$[?(@)]"); + assertThat(hits).containsExactly(0,1,null,2,3); + + hits = JsonPath.parse(ints).read("$[?(@ != null)]"); + assertThat(hits).containsExactly(0,1,2,3); List isNull = JsonPath.parse(ints).read("$[?(!@)]"); - assertThat(isNull).containsExactly(new Integer[]{null}); + assertThat(isNull).containsExactly(new Integer[]{}); + assertThat(isNull).containsExactly(new Integer[]{}); } + } diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/FilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/FilterTest.java index 07d38646..a541dc41 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/FilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/FilterTest.java @@ -2,9 +2,7 @@ package com.jayway.jsonpath.old; import com.jayway.jsonpath.BaseTest; import com.jayway.jsonpath.Configuration; -import com.jayway.jsonpath.Criteria; import com.jayway.jsonpath.Filter; -import com.jayway.jsonpath.InvalidCriteriaException; import com.jayway.jsonpath.JsonPath; import com.jayway.jsonpath.Predicate; import com.jayway.jsonpath.spi.json.JsonProvider; @@ -201,8 +199,8 @@ public class FilterTest extends BaseTest { assertTrue(filter(where("foo").exists(true)).apply(createPredicateContext(check))); assertFalse(filter(where("foo").exists(false)).apply(createPredicateContext(check))); - assertTrue(filter(where("foo_null").exists(false)).apply(createPredicateContext(check))); - assertFalse(filter(where("foo_null").exists(true)).apply(createPredicateContext(check))); + assertTrue(filter(where("foo_null").exists(true)).apply(createPredicateContext(check))); + assertFalse(filter(where("foo_null").exists(false)).apply(createPredicateContext(check))); assertTrue(filter(where("bar").exists(false)).apply(createPredicateContext(check))); assertFalse(filter(where("bar").exists(true)).apply(createPredicateContext(check))); diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index 3336ee28..be8a3f53 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -17,7 +17,6 @@ import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; import com.jayway.jsonpath.spi.mapper.MappingException; import net.minidev.json.JSONAware; import net.minidev.json.parser.JSONParser; -import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; import org.junit.Test; @@ -33,6 +32,7 @@ import static com.jayway.jsonpath.Filter.filter; import static com.jayway.jsonpath.JsonPath.read; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; @@ -175,7 +175,7 @@ public class IssuesTest extends BaseTest { List result = read(json, "$[?(@.compatible == true)].sku"); - Assertions.assertThat(result).containsExactly("SKU-005", "SKU-003"); + assertThat(result).containsExactly("SKU-005", "SKU-003"); } @@ -259,7 +259,7 @@ public class IssuesTest extends BaseTest { public void issue_22b() throws Exception { String json = "{\"a\":[{\"b\":1,\"c\":2},{\"b\":5,\"c\":2}]}"; List res = JsonPath.using(Configuration.defaultConfiguration().setOptions(Option.DEFAULT_PATH_LEAF_TO_NULL)).parse(json).read("a[?(@.b==5)].d"); - Assertions.assertThat(res).hasSize(1).containsNull(); + assertThat(res).hasSize(1).containsNull(); } @Test(expected = PathNotFoundException.class) @@ -334,8 +334,8 @@ public class IssuesTest extends BaseTest { List> result = read(json, "$.store.book[?(@.author.age == 36)]"); - Assertions.assertThat(result).hasSize(1); - Assertions.assertThat(result.get(0)).containsEntry("title", "Sayings of the Century"); + assertThat(result).hasSize(1); + assertThat(result.get(0)).containsEntry("title", "Sayings of the Century"); } @Test @@ -381,7 +381,7 @@ public class IssuesTest extends BaseTest { List> result = read(json, "$.list[?(@.name == 'My (String)')]"); - Assertions.assertThat(result).containsExactly(Collections.singletonMap("name", "My (String)")); + assertThat(result).containsExactly(Collections.singletonMap("name", "My (String)")); } @Test @@ -389,9 +389,9 @@ public class IssuesTest extends BaseTest { String json = "{\"test\":null}"; - Assertions.assertThat(read(json, "test")).isNull(); + assertThat(read(json, "test")).isNull(); - Assertions.assertThat(JsonPath.using(Configuration.defaultConfiguration().setOptions(Option.SUPPRESS_EXCEPTIONS)).parse(json).read("nonExistingProperty")).isNull(); + assertThat(JsonPath.using(Configuration.defaultConfiguration().setOptions(Option.SUPPRESS_EXCEPTIONS)).parse(json).read("nonExistingProperty")).isNull(); try { read(json, "nonExistingProperty"); @@ -416,7 +416,7 @@ public class IssuesTest extends BaseTest { public void issue_45() { String json = "{\"rootkey\":{\"sub.key\":\"value\"}}"; - Assertions.assertThat(read(json, "rootkey['sub.key']")).isEqualTo("value"); + assertThat(read(json, "rootkey['sub.key']")).isEqualTo("value"); } @Test @@ -426,14 +426,14 @@ public class IssuesTest extends BaseTest { String json = "{\"a\": {}}"; Configuration configuration = Configuration.defaultConfiguration().setOptions(Option.SUPPRESS_EXCEPTIONS); - Assertions.assertThat(JsonPath.using(configuration).parse(json).read("a.x")).isNull(); + assertThat(JsonPath.using(configuration).parse(json).read("a.x")).isNull(); try { read(json, "a.x"); failBecauseExceptionWasNotThrown(PathNotFoundException.class); } catch (PathNotFoundException e) { - Assertions.assertThat(e).hasMessage("No results for path: $['a']['x']"); + assertThat(e).hasMessage("No results for path: $['a']['x']"); } } @@ -449,7 +449,7 @@ public class IssuesTest extends BaseTest { List result = JsonPath.read(json, "$.a.*.b.*.c"); - Assertions.assertThat(result).containsExactly("foo"); + assertThat(result).containsExactly("foo"); } @@ -505,7 +505,7 @@ public class IssuesTest extends BaseTest { List problems = JsonPath.read(json, "$..narratives[?(@.lastRule==true)].message"); - Assertions.assertThat(problems).containsExactly("Chain does not have a discovery event. Possible it was cut by the date that was picked", "No start transcoding events found"); + assertThat(problems).containsExactly("Chain does not have a discovery event. Possible it was cut by the date that was picked", "No start transcoding events found"); } //http://stackoverflow.com/questions/28596324/jsonpath-filtering-api @@ -567,7 +567,7 @@ public class IssuesTest extends BaseTest { List result = JsonPath.read(json, "$.logs[?(@.message == 'it\\'s here')].message"); - Assertions.assertThat(result).containsExactly("it's here"); + assertThat(result).containsExactly("it's here"); } @Test @@ -600,7 +600,7 @@ public class IssuesTest extends BaseTest { List res = JsonPath.read(json, "$.c.*.url[2]"); - Assertions.assertThat(res).containsExactly("url5"); + assertThat(res).containsExactly("url5"); } @Test @@ -614,7 +614,7 @@ public class IssuesTest extends BaseTest { } Thread.sleep(2000); - Assertions.assertThat(cache.size()).isEqualTo(200); + assertThat(cache.size()).isEqualTo(200); } @Test @@ -657,12 +657,12 @@ public class IssuesTest extends BaseTest { cache.get("6"); - Assertions.assertThat(cache.getSilent("6")).isNotNull(); - Assertions.assertThat(cache.getSilent("5")).isNotNull(); - Assertions.assertThat(cache.getSilent("4")).isNotNull(); - Assertions.assertThat(cache.getSilent("3")).isNotNull(); - Assertions.assertThat(cache.getSilent("2")).isNotNull(); - Assertions.assertThat(cache.getSilent("1")).isNull(); + assertThat(cache.getSilent("6")).isNotNull(); + assertThat(cache.getSilent("5")).isNotNull(); + assertThat(cache.getSilent("4")).isNotNull(); + assertThat(cache.getSilent("3")).isNotNull(); + assertThat(cache.getSilent("2")).isNotNull(); + assertThat(cache.getSilent("1")).isNull(); } @Test @@ -690,7 +690,7 @@ public class IssuesTest extends BaseTest { List categories = context.read("$..category", List.class); - Assertions.assertThat(categories).containsOnly("fiction"); + assertThat(categories).containsOnly("fiction"); } @@ -736,10 +736,10 @@ public class IssuesTest extends BaseTest { Filter parsed = Filter.parse(filterAsString); - Assertions.assertThat(orig.apply(createPredicateContext(match))).isTrue(); - Assertions.assertThat(parsed.apply(createPredicateContext(match))).isTrue(); - Assertions.assertThat(orig.apply(createPredicateContext(noMatch))).isFalse(); - Assertions.assertThat(parsed.apply(createPredicateContext(noMatch))).isFalse(); + assertThat(orig.apply(createPredicateContext(match))).isTrue(); + assertThat(parsed.apply(createPredicateContext(match))).isTrue(); + assertThat(orig.apply(createPredicateContext(noMatch))).isFalse(); + assertThat(parsed.apply(createPredicateContext(noMatch))).isFalse(); } private PredicateContext createPredicateContext(final Map map){ @@ -769,12 +769,46 @@ public class IssuesTest extends BaseTest { @Test public void issue_131() { - String json = "[1, 2, {\"d\": { \"random\":null, \"date\": \"1234\"} , \"l\": \"filler\"}]"; - //String json = "[1, 2, {\"d\": { \"random\":1, \"date\": \"1234\"} , \"l\": \"filler\"}]"; + String json = "[\n" + + " {\n" + + " \"foo\": \"1\"\n" + + " },\n" + + " {\n" + + " \"foo\": null\n" + + " },\n" + + " {\n" + + " \"xxx\": null\n" + + " }\n" + + "]"; - Object read = JsonPath.read(json, "$[2]['d'][?(@.random)]['date']"); + List> result = JsonPath.read(json, "$[?(@.foo)]"); - System.out.println(read); + assertThat(result).extracting("foo").containsExactly("1", null); + } + + + @Test + public void issue_131_2() { + + String json = "[\n" + + " {\n" + + " \"foo\": { \"bar\" : \"0\"}\n" + + " },\n" + + " {\n" + + " \"foo\": null\n" + + " },\n" + + " {\n" + + " \"xxx\": null\n" + + " }\n" + + "]"; + + List result = JsonPath.read(json, "$[?(@.foo != null)].foo.bar"); + + assertThat(result).containsExactly("0"); + + + result = JsonPath.read(json, "$[?(@.foo.bar)].foo.bar"); + assertThat(result).containsExactly("0"); } } From 25c11da57e1c710082e168cb4d18bddb68e23796 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Wed, 14 Oct 2015 20:46:26 +0200 Subject: [PATCH 14/22] Checking if Node Exists - Bracket Notation Syntax #131 --- .../com/jayway/jsonpath/old/IssuesTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index be8a3f53..e988ad73 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -811,4 +811,24 @@ public class IssuesTest extends BaseTest { assertThat(result).containsExactly("0"); } + + + @Test + public void issue_131_3() { + String json = "[\n" + + " 1,\n" + + " 2,\n" + + " {\n" + + " \"d\": {\n" + + " \"random\": null,\n" + + " \"date\": 1234\n" + + " },\n" + + " \"l\": \"filler\"\n" + + " }\n" + + "]"; + + List result = JsonPath.read(json, "$[2]['d'][?(@.random)]['date']"); + + assertThat(result).containsExactly(1234); + } } From 2b66f23dd015f8270c60982bb005f0265bd09932 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Wed, 14 Oct 2015 21:48:49 +0200 Subject: [PATCH 15/22] Using square bracket literal in path. --- .../com/jayway/jsonpath/internal/PathCompiler.java | 5 +---- .../java/com/jayway/jsonpath/old/IssuesTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) 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 78e6a65f..88b6a107 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 @@ -368,10 +368,7 @@ public class PathCompiler { while (path.inBounds(readPosition)) { char c = path.charAt(readPosition); - if (c == CLOSE_SQUARE_BRACKET) { - if (inProperty) { - throw new InvalidPathException("Expected property to be closed at position " + readPosition); - } + if (c == CLOSE_SQUARE_BRACKET && !inProperty) { break; } else if (c == TICK) { if (inProperty) { diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index e988ad73..cc3e046c 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -17,6 +17,7 @@ import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; import com.jayway.jsonpath.spi.mapper.MappingException; import net.minidev.json.JSONAware; import net.minidev.json.parser.JSONParser; +import org.assertj.core.api.Assertions; import org.hamcrest.Matchers; import org.junit.Test; @@ -831,4 +832,16 @@ public class IssuesTest extends BaseTest { assertThat(result).containsExactly(1234); } + + + //https://groups.google.com/forum/#!topic/jsonpath/Ojv8XF6LgqM + @Test + public void using_square_bracket_literal_path() { + + String json = "{ \"valid key[@num = 2]\" : \"value\" }"; + + String result = JsonPath.read(json, "$['valid key[@num = 2]']"); + + Assertions.assertThat(result).isEqualTo("value"); + } } From f836244feaaf2ca08aef00908c227e04dcc868ce Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Wed, 14 Oct 2015 21:59:56 +0200 Subject: [PATCH 16/22] How to match "(left instanceof Number && right instanceof Number)" in safeCompare #90 --- .../com/jayway/jsonpath/old/IssuesTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index cc3e046c..b1844c82 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -844,4 +844,35 @@ public class IssuesTest extends BaseTest { Assertions.assertThat(result).isEqualTo("value"); } + + @Test + public void issue_90() { + + String json = "{\n" + + " \"store\": {\n" + + " \"book\": [\n" + + " {\n" + + " \"price\": \"120\"\n" + + " },\n" + + " {\n" + + " \"price\": 8.95\n" + + " },\n" + + " {\n" + + " \"price\": 12.99\n" + + " },\n" + + " {\n" + + " \"price\": 8.99\n" + + " },\n" + + " {\n" + + " \"price\": 22.99\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"expensive\": 10\n" + + "}"; + + List numbers = JsonPath.read(json, "$.store.book[?(@.price <= 90)].price"); + + assertThat(numbers).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + } } From cc653b54b2acade267f595d7523471a11250da06 Mon Sep 17 00:00:00 2001 From: kallestenflo Date: Thu, 15 Oct 2015 22:14:12 +0200 Subject: [PATCH 17/22] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 57166f13..2b904a38 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ Operators | `[?()]` | Filter expression. Expression must evaluate to a boolean value. | -Functions +Functions (not released yet) --------- Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression. From 9dc14002aba8ffc4938c7bbf548b317a190c1f8d Mon Sep 17 00:00:00 2001 From: kallestenflo Date: Thu, 15 Oct 2015 23:24:46 +0200 Subject: [PATCH 18/22] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2b904a38..ac1ffefc 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Operators | `[start:end]` | Array slice operator | | `[?()]` | Filter expression. Expression must evaluate to a boolean value. | - + Path Examples ------------- From 3de2ae0dbf1ff46889f02efafc1fce6d779b1382 Mon Sep 17 00:00:00 2001 From: kallestenflo Date: Sat, 17 Oct 2015 15:20:41 +0200 Subject: [PATCH 19/22] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ac1ffefc..9ce24c31 100644 --- a/README.md +++ b/README.md @@ -139,9 +139,10 @@ Given the json | $.store.book[?(@.price < 10)] | All books in store cheaper than 10 | | $..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 | +| $..* | Give me every thing + Reading a Document ------------------ The simplest most straight forward way to use JsonPath is via the static read API. From e3e29444cf30e0cf336ef4004e877fa05274fdc0 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Sat, 17 Oct 2015 21:27:02 +0200 Subject: [PATCH 20/22] Handle invalid options when using functions. --- .../src/main/java/com/jayway/jsonpath/JsonPath.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java index 1ea61c5b..2c1a7905 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; +import static com.jayway.jsonpath.Option.ALWAYS_RETURN_LIST; +import static com.jayway.jsonpath.Option.AS_PATH_LIST; import static com.jayway.jsonpath.internal.Utils.*; /** @@ -167,12 +169,15 @@ public class JsonPath { */ @SuppressWarnings("unchecked") public T read(Object jsonObject, Configuration configuration) { - boolean optAsPathList = configuration.containsOption(Option.AS_PATH_LIST); + boolean optAsPathList = configuration.containsOption(AS_PATH_LIST); boolean optAlwaysReturnList = configuration.containsOption(Option.ALWAYS_RETURN_LIST); boolean optSuppressExceptions = configuration.containsOption(Option.SUPPRESS_EXCEPTIONS); try { if(path.isFunctionPath()){ + if(optAsPathList || optAlwaysReturnList){ + throw new JsonPathException("Options " + AS_PATH_LIST + " and " + ALWAYS_RETURN_LIST + " are not allowed when using path functions!"); + } return path.evaluate(jsonObject, jsonObject, configuration).getValue(true); } if(optAsPathList){ @@ -647,7 +652,7 @@ public class JsonPath { } private T resultByConfiguration(Object jsonObject, Configuration configuration, EvaluationContext evaluationContext) { - if(configuration.containsOption(Option.AS_PATH_LIST)){ + if(configuration.containsOption(AS_PATH_LIST)){ return (T)evaluationContext.getPathList(); } else { return (T) jsonObject; From 8647a607da8628b900c4229bee4a259925928560 Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Sat, 17 Oct 2015 21:43:40 +0200 Subject: [PATCH 21/22] Incorrect error message for JsonPath.read(Object) #89 --- .../jsonpath/internal/token/PropertyPathToken.java | 3 ++- .../java/com/jayway/jsonpath/old/IssuesTest.java | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java index a5851d8c..b6e3f52b 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java @@ -38,7 +38,8 @@ class PropertyPathToken extends PathToken { @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { if (!ctx.jsonProvider().isMap(model)) { - throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath); + //throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath); + throw new PathNotFoundException("Expected to find an object with property " + getPathFragment() + " but found '" + model.getClass().getName() + "'. This is not a json object according to the JsonProvider: '" + ctx.configuration().jsonProvider().getClass().getName() + "'."); } handleObjectProperty(currentPath, model, ctx, properties); diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java index b1844c82..5ae4a2de 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/IssuesTest.java @@ -1,5 +1,6 @@ package com.jayway.jsonpath.old; +import com.google.gson.JsonObject; import com.jayway.jsonpath.BaseTest; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.DocumentContext; @@ -875,4 +876,15 @@ public class IssuesTest extends BaseTest { assertThat(numbers).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); } + + @Test(expected = PathNotFoundException.class) + public void github_89() { + + com.google.gson.JsonObject json = new JsonObject(); + json.addProperty("foo", "bar"); + + JsonPath path = JsonPath.compile("$.foo"); + String object = path.read(json); + + } } From 6353b20ad426ab904441ff1841cb6b8f9b01391f Mon Sep 17 00:00:00 2001 From: Kalle Stenflo Date: Sat, 17 Oct 2015 21:48:52 +0200 Subject: [PATCH 22/22] Fixed null issue. --- .../jayway/jsonpath/internal/token/PropertyPathToken.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java index b6e3f52b..52dcc0ed 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PropertyPathToken.java @@ -38,8 +38,10 @@ class PropertyPathToken extends PathToken { @Override public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { if (!ctx.jsonProvider().isMap(model)) { - //throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath); - throw new PathNotFoundException("Expected to find an object with property " + getPathFragment() + " but found '" + model.getClass().getName() + "'. This is not a json object according to the JsonProvider: '" + ctx.configuration().jsonProvider().getClass().getName() + "'."); + + String m = model == null ? "null" : model.getClass().getName(); + + throw new PathNotFoundException("Expected to find an object with property " + getPathFragment() + " but found '" + m + "'. This is not a json object according to the JsonProvider: '" + ctx.configuration().jsonProvider().getClass().getName() + "'."); } handleObjectProperty(currentPath, model, ctx, properties);