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 + diff --git a/README.md b/README.md index 55b311cd..9ce24c31 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. | + + Path Examples ------------- @@ -123,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. 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..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,13 +445,12 @@ 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; } @@ -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/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/JsonPath.java b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java index f423e2d0..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,11 +169,17 @@ 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){ return (T)path.evaluate(jsonObject, jsonObject, configuration).getPath(); } else { @@ -644,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; diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java b/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java index 4a2f9cfc..e2c074c7 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java @@ -16,7 +16,7 @@ package com.jayway.jsonpath.internal; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.internal.token.EvaluationContextImpl; -import com.jayway.jsonpath.internal.token.PathToken; +import com.jayway.jsonpath.internal.token.RootPathToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,12 +24,12 @@ public class CompiledPath implements Path { private static final Logger logger = LoggerFactory.getLogger(CompiledPath.class); - private final PathToken root; + private final RootPathToken root; private final boolean isRootPath; - public CompiledPath(PathToken root, boolean isRootPath) { + public CompiledPath(RootPathToken root, boolean isRootPath) { this.root = root; this.isRootPath = isRootPath; } @@ -64,6 +64,11 @@ public class CompiledPath implements Path { return root.isPathDefinite(); } + @Override + public boolean isFunctionPath() { + return root.isFunctionPath(); + } + @Override public String toString() { return root.toString(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java b/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java index b87b7b55..3d99b1b1 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java @@ -49,6 +49,12 @@ public interface Path { */ boolean isDefinite(); + /** + * + * @return true id this path is a function + */ + boolean isFunctionPath(); + /** * * @return true id this path is starts with '$' and false if the path starts with '@' 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 9bdb3892..a0760fc2 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 @@ -5,11 +5,10 @@ import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Predicate; import com.jayway.jsonpath.internal.token.ArrayIndexOperation; import com.jayway.jsonpath.internal.token.ArraySliceOperation; +import com.jayway.jsonpath.internal.token.FunctionPathToken; 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 +21,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 = '['; @@ -39,6 +36,7 @@ public class PathCompiler { private static final char MINUS = '-'; private static final char ESCAPE = '\\'; private static final char TICK = '\''; + private static final char FUNCTION = '%'; private static final Cache cache = new Cache(200); @@ -131,12 +129,42 @@ public class PathCompiler { case WILDCARD: return readWildCardToken(appender) || fail("Could not parse token at position " + path.position); + case FUNCTION: + return readFunctionToken(appender) || + fail("Could not parse token at position " + path.position); default: return readPropertyToken(appender) || fail("Could not parse token at position " + path.position); } } + // + // $function() + // + private boolean readFunctionToken(PathTokenAppender appender) { + if (path.currentCharIs(OPEN_SQUARE_BRACKET) || path.currentCharIs(WILDCARD) || path.currentCharIs(PERIOD) || path.currentCharIs(SPACE)) { + return false; + } + int startPosition = path.position; + int readPosition = startPosition; + int endPosition = 0; + while (path.inBounds(readPosition)) { + char c = path.charAt(readPosition); + if (c == OPEN_BRACKET && path.nextSignificantCharIs(readPosition, CLOSE_BRACKET)) { + endPosition = path.indexOfNextSignificantChar(readPosition, CLOSE_BRACKET); + break; + } + readPosition++; + } + path.setPosition(endPosition); + + String function = path.subSequence(startPosition, endPosition + 1).toString(); + + appender.appendPathToken(PathTokenFactory.createFunctionPathToken(function)); + + return path.currentIsTail(); + } + // // . // @@ -372,10 +400,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) { @@ -571,6 +596,3 @@ public class PathCompiler { } } } - - - 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..35104ab3 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java @@ -0,0 +1,71 @@ +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 name + * The name of the function + * + * @return + * The implementation of a function + * + * @throws InvalidPathException + */ + public static Function newFunction(String name) throws InvalidPathException { + Function result = new PassthruFunction(); + + 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..12188833 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java @@ -0,0 +1,26 @@ +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; + +import java.util.*; + +/** + * 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(ctx.configuration().jsonProvider().isArray(model)){ + return ctx.configuration().jsonProvider().length(model); + } else if(ctx.configuration().jsonProvider().isMap(model)){ + return ctx.configuration().jsonProvider().length(model); + } + 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..d7bb97c1 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java @@ -0,0 +1,55 @@ +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(ctx.configuration().jsonProvider().isArray(model)){ + + Iterable objects = ctx.configuration().jsonProvider().toIterable(model); + for (Object obj : objects) { +// Object unwraped = ctx.configuration().jsonProvider().unwrap(obj); +// if (unwraped instanceof Number) { +// Number value = (Number) unwraped; +// next(value); +// } + if (obj instanceof Number) { + Number value = (Number) obj; + 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..3c57e5f2 --- /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..0a83d8a8 --- /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..68d15fcc --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java @@ -0,0 +1,56 @@ +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); + 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 + public boolean isTokenDefinite() { + return true; + } + + @Override + public 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 e71768a0..781f23d2 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; @@ -59,7 +65,6 @@ public abstract class PathToken { } else { throw new PathNotFoundException("No results for path: " + evalPath); } - } } else { if (! (isUpstreamDefinite() && isTokenDefinite()) && @@ -77,7 +82,8 @@ public abstract class PathToken { PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, property) : PathRef.NO_OP; if (isLeaf()) { ctx.addResult(evalPath, pathRef, propertyVal); - } else { + } + else { next().evaluate(evalPath, pathRef, propertyVal, ctx); } } else { @@ -204,6 +210,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); public abstract boolean isTokenDefinite(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java index 0dd53bb5..b363fcb0 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java @@ -44,4 +44,8 @@ public class PathTokenFactory { public static PathToken createPredicatePathToken(Predicate predicate) { return new PredicatePathToken(predicate); } + + public static PathToken createFunctionPathToken(String function) { + return new FunctionPathToken((function)); + } } 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 83cc7705..24918f3e 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 @@ -64,7 +64,12 @@ public class PropertyPathToken extends PathToken { if (! isUpstreamDefinite()) { return; } else { - throw new PathNotFoundException("Property " + getPathFragment() + " not found in path " + currentPath); + String m = model == null ? "null" : model.getClass().getName(); + + throw new PathNotFoundException(String.format( + "Expected to find an object with property %s in path %s but found '%s'. " + + "This is not a json object according to the JsonProvider: '%s'.", + getPathFragment(), currentPath, m, ctx.configuration().jsonProvider().getClass().getName())); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java index 9495aa64..60f204ef 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java @@ -73,5 +73,7 @@ public class RootPathToken extends PathToken { return true; } - + public boolean isFunctionPath() { + return (tail instanceof FunctionPathToken); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java b/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java index ababb1e0..b387f43a 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java @@ -4,9 +4,11 @@ import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.spi.json.GsonJsonProvider; import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider; import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import com.jayway.jsonpath.internal.token.PredicateContextImpl; +import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider; import java.util.HashMap; @@ -30,7 +32,11 @@ public class BaseTest { .jsonProvider(new JacksonJsonNodeJsonProvider()) .build(); - public static final Configuration JSON_SMART_CONFIGURATION = Configuration.defaultConfiguration(); + public static final Configuration JSON_SMART_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JsonSmartMappingProvider()) + .jsonProvider(new JsonSmartJsonProvider()) + .build(); public static final String JSON_BOOK_DOCUMENT = "{ " + diff --git a/json-path/src/test/java/com/jayway/jsonpath/Configurations.java b/json-path/src/test/java/com/jayway/jsonpath/Configurations.java new file mode 100644 index 00000000..b0d01a57 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/Configurations.java @@ -0,0 +1,56 @@ +package com.jayway.jsonpath; + +import com.jayway.jsonpath.spi.json.GsonJsonProvider; +import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider; +import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider; + +import java.util.Arrays; + +public class Configurations { + + public static final Configuration GSON_CONFIGURATION = Configuration + .builder() + .mappingProvider(new GsonMappingProvider()) + .jsonProvider(new GsonJsonProvider()) + .build(); + + public static final Configuration JACKSON_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonProvider()) + .build(); + + public static final Configuration JACKSON_JSON_NODE_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonNodeJsonProvider()) + .build(); + + public static final Configuration JSON_SMART_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JsonSmartMappingProvider()) + .jsonProvider(new JsonSmartJsonProvider()) + .build(); + + public static Iterable configurations() { + return Arrays.asList( + JSON_SMART_CONFIGURATION + ,GSON_CONFIGURATION + ,JACKSON_CONFIGURATION + ,JACKSON_JSON_NODE_CONFIGURATION + ); + } + + + public static Iterable objectMappingConfigurations() { + return Arrays.asList( + GSON_CONFIGURATION + ,JACKSON_CONFIGURATION + ,JACKSON_JSON_NODE_CONFIGURATION + ); + } +} 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/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/JsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java index 9c80e56b..2949fcaa 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java @@ -1,6 +1,8 @@ package com.jayway.jsonpath; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.io.IOException; import java.util.List; @@ -8,84 +10,38 @@ import java.util.List; import static com.jayway.jsonpath.JsonPath.using; import static org.assertj.core.api.Assertions.assertThat; +@RunWith(Parameterized.class) public class JsonProviderTest extends BaseTest { - private static final String JSON = - "[" + - "{\n" + - " \"foo\" : \"foo0\",\n" + - " \"bar\" : 0,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"prop\" : \"yepp0\"}" + - "}," + - "{\n" + - " \"foo\" : \"foo1\",\n" + - " \"bar\" : 1,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"prop\" : \"yepp1\"}" + - "}," + - "{\n" + - " \"foo\" : \"foo2\",\n" + - " \"bar\" : 2,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"prop\" : \"yepp2\"}" + - "}" + - "]"; + private final Configuration conf; - @Test - public void strings_are_unwrapped() { - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); - assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); + public JsonProviderTest(Configuration conf) { + this.conf = conf; } - @Test - public void integers_are_unwrapped() { - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); + @Parameterized.Parameters + public static Iterable configurations() { + return Configurations.configurations(); } @Test - public void ints_are_unwrapped() { - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); + public void strings_are_unwrapped() { + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); } @Test - public void list_of_numbers() { - - TypeRef> typeRef = new TypeRef>() {}; - - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + public void integers_are_unwrapped() { + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); } @Test - public void test_type_ref() throws IOException { - TypeRef>> typeRef = new TypeRef>>() {}; - - assertThat(using(JACKSON_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); - assertThat(using(GSON_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + public void ints_are_unwrapped() { + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); } - public static class FooBarBaz { - public T gen; - public String foo; - public Long bar; - public boolean baz; - } - public static class Sub { - public String prop; - } + } diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTestObjectMapping.java b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTestObjectMapping.java new file mode 100644 index 00000000..b0028c03 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTestObjectMapping.java @@ -0,0 +1,75 @@ +package com.jayway.jsonpath; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.IOException; +import java.util.List; + +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(Parameterized.class) +public class JsonProviderTestObjectMapping extends BaseTest { + + private static final String JSON = + "[" + + "{\n" + + " \"foo\" : \"foo0\",\n" + + " \"bar\" : 0,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp0\"}" + + "}," + + "{\n" + + " \"foo\" : \"foo1\",\n" + + " \"bar\" : 1,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp1\"}" + + "}," + + "{\n" + + " \"foo\" : \"foo2\",\n" + + " \"bar\" : 2,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp2\"}" + + "}" + + "]"; + + private final Configuration conf; + + public JsonProviderTestObjectMapping(Configuration conf) { + this.conf = conf; + } + + @Parameterized.Parameters + public static Iterable configurations() { + return Configurations.objectMappingConfigurations(); + } + + @Test + public void list_of_numbers() { + + TypeRef> typeRef = new TypeRef>() {}; + + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + } + + @Test + public void test_type_ref() throws IOException { + TypeRef>> typeRef = new TypeRef>>() {}; + + assertThat(using(conf).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + } + + + public static class FooBarBaz { + public T gen; + public String foo; + public Long bar; + public boolean baz; + } + + public static class Sub { + public String prop; + } +} 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..fb2eaed8 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java @@ -0,0 +1,45 @@ +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; + +/** + * 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(Configuration conf, String pathExpr, String json, Object expectedValue) { + Object result = using(conf).parse(json).read(pathExpr); + assertThat(result).isEqualTo(expectedValue); + } + + protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) { + verifyFunction(conf, 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 new file mode 100644 index 00000000..f37ce18d --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java @@ -0,0 +1,105 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.Configurations; +import net.minidev.json.JSONArray; +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 { + + + 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" + + "}"; + + private Configuration conf = Configurations.JSON_SMART_CONFIGURATION; + + @Test + public void testLengthOfTextArray() { + // The length of JSONArray is an integer + System.out.println(TEXT_SERIES); + verifyFunction(conf, "$['text'].%length()", TEXT_SERIES, 6); + } + @Test + public void testLengthOfNumberArray() { + // The length of JSONArray is an integer + verifyFunction(conf, "$.numbers.%length()", NUMBER_SERIES, 10); + } + + + @Test + public void testLengthOfStructure() { + verifyFunction(conf, "$.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 + * 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(conf, 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(conf, 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 new file mode 100644 index 00000000..5baad6c6 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java @@ -0,0 +1,90 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.Configurations; +import com.jayway.jsonpath.JsonPath; +import net.minidev.json.JSONArray; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +import static com.jayway.jsonpath.Configurations.*; +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.runners.Parameterized.Parameters; + +/** + * 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. + */ +@RunWith(Parameterized.class) +public class NumericFunctionTest extends BaseFunctionTest { + + private static final Logger logger = LoggerFactory.getLogger(NumericFunctionTest.class); + + private Configuration conf = Configurations.GSON_CONFIGURATION; + + public NumericFunctionTest(Configuration conf) { + logger.debug("Testing with configuration {}", conf.getClass().getName()); + this.conf = conf; + } + + @Parameters + public static Iterable configurations() { + return Configurations.configurations(); + } + + + @Test + public void testAverageOfDoubles() { + verifyMathFunction(conf, "$.numbers.%avg()", 5.5); + } + + @Test + public void testSumOfDouble() { + verifyMathFunction(conf, "$.numbers.%sum()", (10d * (10d + 1d)) / 2d); + } + + @Test + public void testMaxOfDouble() { + verifyMathFunction(conf, "$.numbers.%max()", 10d); + } + + @Test + public void testMinOfDouble() { + verifyMathFunction(conf, "$.numbers.%min()", 1d); + } + + @Test + public void testStdDevOfDouble() { + verifyMathFunction(conf, "$.numbers.%stddev()", 2.8722813232690143d); + } + + /** + * Expect that for an invalid function name we'll get back the original input to the function + */ +// @Test +// @Ignore +// public void testInvalidFunctionNameNegative() { +// 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); +// } + +} 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 c491e6b2..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; @@ -33,6 +34,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 +177,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 +261,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 +336,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 +383,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 +391,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 +418,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 +428,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 +451,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 +507,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 +569,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 +602,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 +616,7 @@ public class IssuesTest extends BaseTest { } Thread.sleep(2000); - Assertions.assertThat(cache.size()).isEqualTo(200); + assertThat(cache.size()).isEqualTo(200); } @Test @@ -657,12 +659,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 +692,7 @@ public class IssuesTest extends BaseTest { List categories = context.read("$..category", List.class); - Assertions.assertThat(categories).containsOnly("fiction"); + assertThat(categories).containsOnly("fiction"); } @@ -736,10 +738,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){ @@ -765,4 +767,124 @@ public class IssuesTest extends BaseTest { } }; } + + @Test + public void issue_131() { + + String json = "[\n" + + " {\n" + + " \"foo\": \"1\"\n" + + " },\n" + + " {\n" + + " \"foo\": null\n" + + " },\n" + + " {\n" + + " \"xxx\": null\n" + + " }\n" + + "]"; + + List> result = JsonPath.read(json, "$[?(@.foo)]"); + + 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"); + } + + + @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); + } + + + //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"); + } + + @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); + } + + @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); + + } }