diff --git a/LICENSE b/LICENSE index d6456956..9972f34b 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2017 Jayway Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index c146999c..e0953842 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Given the json | <a href="http://jsonpath.herokuapp.com/?path=$.store.*" target="_blank">$.store.*</a> | All things, both books and bicycles | | <a href="http://jsonpath.herokuapp.com/?path=$.store..price" target="_blank">$.store..price</a> | The price of everything | | <a href="http://jsonpath.herokuapp.com/?path=$..book[2]" target="_blank">$..book[2]</a> | The third book | +| <a href="http://jsonpath.herokuapp.com/?path=$..book[2]" target="_blank">$..book[-2]</a> | The second to last book | | <a href="http://jsonpath.herokuapp.com/?path=$..book[0,1]" target="_blank">$..book[0,1]</a> | The first two books | | <a href="http://jsonpath.herokuapp.com/?path=$..book[:2]" target="_blank">$..book[:2]</a> | All books from index 0 (inclusive) until index 2 (exclusive) | | <a href="http://jsonpath.herokuapp.com/?path=$..book[1:2]" target="_blank">$..book[1:2]</a> | All books from index 1 (inclusive) until index 2 (exclusive) | @@ -256,6 +257,8 @@ List<Map<String, Object>> books = JsonPath.parse(json) You can use `&&` and `||` to combine multiple predicates `[?(@.price < 10 && @.category == 'fiction')]` , `[?(@.category == 'reference' || @.price > 10)]`. +You can use `!` to negate a predicate `[?(!(@.price < 10 && @.category == 'fiction'))]`. + ###Filter Predicates Predicates can be built using the Filter API as shown below: diff --git a/build.gradle b/build.gradle index 31f6bbdf..899486e2 100644 --- a/build.gradle +++ b/build.gradle @@ -12,12 +12,12 @@ buildscript { ext { libs = [ slf4jApi: 'org.slf4j:slf4j-api:1.7.25', - jsonSmart: 'net.minidev:json-smart:2.2.1', + jsonSmart: 'net.minidev:json-smart:2.3', jacksonDatabind: 'com.fasterxml.jackson.core:jackson-databind:2.6.3', gson: 'com.google.code.gson:gson:2.3.1', jettison: 'org.codehaus.jettison:jettison:1.3.7', jsonOrg: 'org.json:json:20140107', - tapestryJson: 'org.apache.tapestry:tapestry-json:5.4.0', + tapestryJson: 'org.apache.tapestry:tapestry-json:5.4.1', hamcrestCore: 'org.hamcrest:hamcrest-core:1.3', hamcrestLibrary: 'org.hamcrest:hamcrest-library:1.3', @@ -73,7 +73,7 @@ subprojects { } task wrapper(type: Wrapper) { - gradleVersion = '2.11' + gradleVersion = '3.4.1' } //Task used by Heroku for staging diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5ccda13e..17279fa9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ec299545..10eb72ab 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Feb 12 10:36:51 CET 2016 +#Fri Mar 17 10:32:10 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.11-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip diff --git a/gradlew b/gradlew index 9d82f789..4453ccea 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,26 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -85,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -150,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 72d362da..e95643d6 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -49,7 +49,6 @@ goto fail @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/json-path-assert/build.gradle b/json-path-assert/build.gradle index 0a47bd0c..f63e70bb 100644 --- a/json-path-assert/build.gradle +++ b/json-path-assert/build.gradle @@ -1,7 +1,5 @@ apply from: "$rootDir/gradle/publishMaven.gradle" -displayName = "JsonPath Assert" - description = "Assertions on Json using JsonPath" jar { diff --git a/json-path-web-test/build.gradle b/json-path-web-test/build.gradle index 7c185eb4..83eb94fc 100644 --- a/json-path-web-test/build.gradle +++ b/json-path-web-test/build.gradle @@ -1,18 +1,18 @@ apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'application' -displayName = "JsonPath Test Bench" - description = "Web app that compares different JsonPath implementations." mainClassName = 'com.jayway.jsonpath.web.boot.Main' -task createBuildInfoFile << { - def buildInfoFile = new File("$buildDir/classes/main/build-info.properties") - Properties props = new Properties() - props.setProperty('version', project.version.toString()) - props.setProperty('timestamp', project.buildTimestamp) - props.store(buildInfoFile.newWriter(), null) +task createBuildInfoFile { + doLast { + def buildInfoFile = new File("$buildDir/classes/main/build-info.properties") + Properties props = new Properties() + props.setProperty('version', project.version.toString()) + props.setProperty('timestamp', project.buildTimestamp) + props.store(buildInfoFile.newWriter(), null) + } } jar { diff --git a/json-path/build.gradle b/json-path/build.gradle index fa79910b..3aec3088 100644 --- a/json-path/build.gradle +++ b/json-path/build.gradle @@ -1,15 +1,12 @@ apply from: "$rootDir/gradle/publishMaven.gradle" - -displayName = "Json Path" - description = "Java port of Stefan Goessner JsonPath." jar { baseName 'json-path' manifest { attributes 'Implementation-Title': 'json-path', 'Implementation-Version': version - instruction 'Import-Package', 'org.json.*;resolution:=optional', 'com.google.gson.*;resolution:=optional', 'com.fasterxml.jackson.*;resolution:=optional', 'net.minidev.json.*;resolution:=optional', 'org.apache.tapestry5.json.*;resolution:=optional', '*' + instruction 'Import-Package', 'org.json.*;resolution:=optional', 'com.google.gson.*;resolution:=optional', 'com.fasterxml.jackson.*;resolution:=optional', 'net.minidev.json.*;resolution:=optional', 'org.apache.tapestry5.json.*;resolution:=optional', 'org.codehaus.jettison.*;resolution:=optional', '*' } } @@ -39,7 +36,7 @@ task distZip(type: Zip, dependsOn: assemble) { } from(project.configurations.compile) { into 'lib' - exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') } + exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') || it.file.name.contains('tapestry') } } from(project.configurations.compile) { into 'lib-optional/jackson' @@ -58,7 +55,7 @@ task distZip(type: Zip, dependsOn: assemble) { include { it.file.name.contains('json-2') } } from(project.configurations.compile) { - into 'lib-optional/jsonOrg' + into 'lib-optional/tapestry' include { it.file.name.contains('tapestry') } } } @@ -79,7 +76,7 @@ task distTar(type: Tar, dependsOn: assemble) { } from(project.configurations.compile) { into 'lib' - exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') } + exclude { it.file.name.contains('gson') || it.file.name.contains('jackson') || it.file.name.contains('json-2') || it.file.name.contains('jettison') || it.file.name.contains('tapestry') } } from(project.configurations.compile) { into 'lib-optional/jackson' @@ -98,7 +95,7 @@ task distTar(type: Tar, dependsOn: assemble) { include { it.file.name.contains('json-2') } } from(project.configurations.compile) { - into 'lib-optional/jsonOrg' + into 'lib-optional/tapestry' include { it.file.name.contains('tapestry') } } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/CharacterIndex.java b/json-path/src/main/java/com/jayway/jsonpath/internal/CharacterIndex.java index b93811c3..1aa28679 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/CharacterIndex.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/CharacterIndex.java @@ -17,14 +17,16 @@ public class CharacterIndex { private final CharSequence charSequence; private int position; + private int endPosition; public CharacterIndex(CharSequence charSequence) { this.charSequence = charSequence; this.position = 0; + this.endPosition = charSequence.length() - 1; } public int length() { - return charSequence.length(); + return endPosition + 1; } public char charAt(int idx) { @@ -39,6 +41,10 @@ public class CharacterIndex { return (charSequence.charAt(position) == c); } + public boolean lastCharIs(char c) { + return charSequence.charAt(endPosition) == c; + } + public boolean nextCharIs(char c) { return inBounds(position + 1) && (charSequence.charAt(position + 1) == c); } @@ -47,12 +53,21 @@ public class CharacterIndex { return setPosition(position + charCount); } + public int decrementEndPosition(int charCount) { + return setEndPosition(endPosition - charCount); + } + public int setPosition(int newPosition) { //position = min(newPosition, charSequence.length() - 1); position = newPosition; return position; } + private int setEndPosition(int newPosition) { + endPosition = newPosition; + return endPosition; + } + public int position(){ return position; } @@ -244,7 +259,7 @@ public class CharacterIndex { } public boolean currentIsTail() { - return position >= charSequence.length()-1; + return position >= endPosition; } public boolean hasMoreCharacters() { @@ -252,7 +267,7 @@ public class CharacterIndex { } public boolean inBounds(int idx) { - return (idx >= 0) && (idx < charSequence.length()); + return (idx >= 0) && (idx <= endPosition); } public boolean inBounds() { return inBounds(position); @@ -281,9 +296,22 @@ public class CharacterIndex { } public CharacterIndex skipBlanks() { - while (inBounds() && currentChar() == SPACE){ + while (inBounds() && position < endPosition && currentChar() == SPACE){ incrementPosition(1); } return this; } + + private CharacterIndex skipBlanksAtEnd() { + while (inBounds() && position < endPosition && lastCharIs(SPACE)){ + decrementEndPosition(1); + } + return this; + } + + public CharacterIndex trim() { + skipBlanks(); + skipBlanksAtEnd(); + return this; + } } \ No newline at end of file diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/JsonContext.java b/json-path/src/main/java/com/jayway/jsonpath/internal/JsonContext.java index 17ce9bce..50b84a08 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/JsonContext.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/JsonContext.java @@ -289,7 +289,7 @@ public class JsonContext implements ParseContext, DocumentContext { return this; } - private final class LimitingEvaluationListener implements EvaluationListener { + private final static class LimitingEvaluationListener implements EvaluationListener { final int limit; private LimitingEvaluationListener(int limit) { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java index 7126b278..67b512e4 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java @@ -54,20 +54,22 @@ public class FilterCompiler { } private FilterCompiler(String filterString) { - filterString = filterString.trim(); - if (!filterString.startsWith("[") || !filterString.endsWith("]")) { + filter = new CharacterIndex(filterString); + filter.trim(); + if (!filter.currentCharIs('[') || !filter.lastCharIs(']')) { throw new InvalidPathException("Filter must start with '[' and end with ']'. " + filterString); } - filterString = filterString.substring(1, filterString.length() - 1).trim(); - if (!filterString.startsWith("?")) { + filter.incrementPosition(1); + filter.decrementEndPosition(1); + filter.trim(); + if (!filter.currentCharIs('?')) { throw new InvalidPathException("Filter must start with '[?' and end with ']'. " + filterString); } - filterString = filterString.substring(1).trim(); - if (!filterString.startsWith("(") || !filterString.endsWith(")")) { + filter.incrementPosition(1); + filter.trim(); + if (!filter.currentCharIs('(') || !filter.lastCharIs(')')) { throw new InvalidPathException("Filter must start with '[?(' and end with ')]'. " + filterString); } - - filter = new CharacterIndex(filterString); } public Predicate compile() { @@ -120,7 +122,7 @@ public class FilterCompiler { /* * LogicalOR = LogicalAND { '||' LogicalAND } * LogicalAND = LogicalANDOperand { '&&' LogicalANDOperand } - * LogicalANDOperand = RelationalExpression | '(' LogicalOR ')' + * LogicalANDOperand = RelationalExpression | '(' LogicalOR ')' | '!' LogicalANDOperand * RelationalExpression = Value [ RelationalOperator Value ] */ @@ -164,6 +166,19 @@ public class FilterCompiler { } private ExpressionNode readLogicalANDOperand() { + int savepoint = filter.skipBlanks().position(); + if (filter.skipBlanks().currentCharIs(NOT)) { + filter.readSignificantChar(NOT); + switch (filter.skipBlanks().currentChar()) { + case DOC_CONTEXT: + case EVAL_CONTEXT: + filter.setPosition(savepoint); + break; + default: + final ExpressionNode op = readLogicalANDOperand(); + return LogicalExpressionNode.createLogicalNot(op); + } + } if (filter.skipBlanks().currentCharIs(OPEN_PARENTHESIS)) { filter.readSignificantChar(OPEN_PARENTHESIS); final ExpressionNode op = readLogicalOR(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java index 3a5d9605..57cb3884 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalExpressionNode.java @@ -10,6 +10,10 @@ public class LogicalExpressionNode extends ExpressionNode { protected List<ExpressionNode> chain = new ArrayList<ExpressionNode>(); private final LogicalOperator operator; + public static ExpressionNode createLogicalNot(ExpressionNode op) { + return new LogicalExpressionNode(op, LogicalOperator.NOT, null); + } + public static LogicalExpressionNode createLogicalOr(ExpressionNode left,ExpressionNode right){ return new LogicalExpressionNode(left, LogicalOperator.OR, right); } @@ -68,13 +72,17 @@ public class LogicalExpressionNode extends ExpressionNode { } } return false; - } else { + } else if (operator == LogicalOperator.AND) { for (ExpressionNode expression : chain) { if(!expression.apply(ctx)){ return false; } } return true; + } else { + ExpressionNode expression = chain.get(0); + return !expression.apply(ctx); } } + } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalOperator.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalOperator.java index 376e2ada..809a8560 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalOperator.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/LogicalOperator.java @@ -5,6 +5,7 @@ import com.jayway.jsonpath.InvalidPathException; public enum LogicalOperator { AND("&&"), + NOT("!"), OR("||"); private final String operatorString; @@ -24,6 +25,7 @@ public enum LogicalOperator { public static LogicalOperator fromString(String operatorString){ if(AND.operatorString.equals(operatorString)) return AND; + else if(NOT.operatorString.equals(operatorString)) return NOT; else if(OR.operatorString.equals(operatorString)) return OR; else throw new InvalidPathException("Failed to parse operator " + operatorString); } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java index 7506358a..3f3914e4 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/ValueNode.java @@ -154,7 +154,7 @@ public abstract class ValueNode { if ((c0 == '[' && c1 == ']') || (c0 == '{' && c1 == '}')){ try { Configuration.defaultConfiguration().jsonProvider().parse(str); - return false; + return true; } catch(Exception e){ return false; } @@ -805,9 +805,9 @@ public abstract class ValueNode { } public ValueNode evaluate(Predicate.PredicateContext ctx) { - Configuration c = Configuration.builder().jsonProvider(ctx.configuration().jsonProvider()).options(Option.REQUIRE_PROPERTIES).build(); if (isExistsCheck()) { try { + Configuration c = Configuration.builder().jsonProvider(ctx.configuration().jsonProvider()).options(Option.REQUIRE_PROPERTIES).build(); Object result = path.evaluate(ctx.item(), ctx.root(), c).getValue(false); return result == JsonProvider.UNDEFINED ? ValueNode.FALSE : ValueNode.TRUE; } catch (PathNotFoundException e) { @@ -842,4 +842,4 @@ public abstract class ValueNode { } -} \ No newline at end of file +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java index 8fde93a9..cbfa0253 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/ArrayIndexOperation.java @@ -42,7 +42,7 @@ public class ArrayIndexOperation { //check valid chars for (int i = 0; i < operation.length(); i++) { char c = operation.charAt(i); - if (!isDigit(c) && c != ',' && c != ' ') { + if (!isDigit(c) && c != ',' && c != ' ' && c != '-') { throw new InvalidPathException("Failed to parse ArrayIndexOperation: " + operation); } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java index 3814419e..3fc7e487 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/EvaluationContextImpl.java @@ -163,7 +163,7 @@ public class EvaluationContextImpl implements EvaluationContext { return res; } - private class FoundResultImpl implements EvaluationListener.FoundResult { + private static class FoundResultImpl implements EvaluationListener.FoundResult { private final int index; private final String path; diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java index f83e953a..36d6b822 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathCompiler.java @@ -45,9 +45,13 @@ public class PathCompiler { private final LinkedList<Predicate> filterStack; private final CharacterIndex path; - private PathCompiler(String path, LinkedList<Predicate> filterStack) { + private PathCompiler(String path, LinkedList<Predicate> filterStack){ + this(new CharacterIndex(path), filterStack); + } + + private PathCompiler(CharacterIndex path, LinkedList<Predicate> filterStack){ this.filterStack = filterStack; - this.path = new CharacterIndex(path); + this.path = path; } private Path compile() { @@ -57,16 +61,18 @@ public class PathCompiler { public static Path compile(String path, final Predicate... filters) { try { - path = path.trim(); + CharacterIndex ci = new CharacterIndex(path); + ci.trim(); - if(!(path.charAt(0) == DOC_CONTEXT) && !(path.charAt(0) == EVAL_CONTEXT)){ - path = "$." + path; + if(!( ci.charAt(0) == DOC_CONTEXT) && !( ci.charAt(0) == EVAL_CONTEXT)){ + ci = new CharacterIndex("$." + path); + ci.trim(); } - if(path.endsWith(".")){ + if(ci.lastCharIs('.')){ fail("Path must not end with a '.' or '..'"); } - LinkedList filterStack = new LinkedList<Predicate>(asList(filters)); - Path p = new PathCompiler(path.trim(), filterStack).compile(); + LinkedList<Predicate> filterStack = new LinkedList<Predicate>(asList(filters)); + Path p = new PathCompiler(ci, filterStack).compile(); return p; } catch (Exception e) { InvalidPathException ipe; @@ -103,7 +109,6 @@ public class PathCompiler { } RootPathToken pathToken = PathTokenFactory.createRootPathToken(path.currentChar()); - PathTokenAppender appender = pathToken.getPathTokenAppender(); if (path.currentIsTail()) { return pathToken; @@ -115,6 +120,7 @@ public class PathCompiler { fail("Illegal character at position " + path.position() + " expected '.' or '["); } + PathTokenAppender appender = pathToken.getPathTokenAppender(); readNextToken(appender); return pathToken; @@ -261,7 +267,6 @@ public class PathCompiler { * an array. */ private List<Parameter> parseFunctionParameters(String funcName) { - PathToken currentToken; ParamType type = null; // Parenthesis starts at 1 since we're marking the start of a function call, the close paren will denote the diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java index dc22dda1..e4d23604 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/path/PathToken.java @@ -125,8 +125,9 @@ public abstract class PathToken { protected void handleArrayIndex(int index, String currentPath, Object model, EvaluationContextImpl ctx) { String evalPath = Utils.concat(currentPath, "[", String.valueOf(index), "]"); PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, index) : PathRef.NO_OP; + int effectiveIndex = index < 0 ? ctx.jsonProvider().length(model) + index : index; try { - Object evalHit = ctx.jsonProvider().getArrayIndex(model, index); + Object evalHit = ctx.jsonProvider().getArrayIndex(model, effectiveIndex); if (isLeaf()) { ctx.addResult(evalPath, pathRef, evalHit); } else { diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java index 7a2636a8..0828b295 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/json/GsonJsonProvider.java @@ -14,28 +14,25 @@ */ package com.jayway.jsonpath.spi.json; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.UnsupportedEncodingException; - -import java.math.BigDecimal; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; - import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; -import com.google.gson.internal.LazilyParsedNumber; - import com.jayway.jsonpath.InvalidJsonException; import com.jayway.jsonpath.JsonPathException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + public class GsonJsonProvider extends AbstractJsonProvider { private static final JsonParser PARSER = new JsonParser(); @@ -86,25 +83,38 @@ public class GsonJsonProvider extends AbstractJsonProvider { return o; } + private static boolean isPrimitiveNumber(final Number n) { + return n instanceof Integer || + n instanceof Double || + n instanceof Long || + n instanceof BigDecimal || + n instanceof BigInteger; + } + private static Number unwrapNumber(final Number n) { Number unwrapped; - if (n instanceof LazilyParsedNumber) { - LazilyParsedNumber lpn = (LazilyParsedNumber) n; - BigDecimal bigDecimal = new BigDecimal(lpn.toString()); + if (!isPrimitiveNumber(n)) { + BigDecimal bigDecimal = new BigDecimal(n.toString()); if (bigDecimal.scale() <= 0) { if (bigDecimal.compareTo(new BigDecimal(Integer.MAX_VALUE)) <= 0) { unwrapped = bigDecimal.intValue(); - } else { + } else if (bigDecimal.compareTo(new BigDecimal(Long.MAX_VALUE)) <= 0){ unwrapped = bigDecimal.longValue(); + } else { + unwrapped = bigDecimal; } } else { - unwrapped = bigDecimal.doubleValue(); + final double doubleValue = bigDecimal.doubleValue(); + if (BigDecimal.valueOf(doubleValue).compareTo(bigDecimal) != 0) { + unwrapped = bigDecimal; + } else { + unwrapped = doubleValue; + } } } else { unwrapped = n; } - return unwrapped; } diff --git a/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonOrgJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonOrgJsonProvider.java index a5b77e88..0c5f169e 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonOrgJsonProvider.java +++ b/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonOrgJsonProvider.java @@ -1,6 +1,6 @@ package com.jayway.jsonpath.spi.json; -import com.google.gson.JsonObject; +import org.json.JSONObject; import com.jayway.jsonpath.InvalidJsonException; import com.jayway.jsonpath.JsonPathException; import org.json.JSONArray; @@ -62,7 +62,7 @@ public class JsonOrgJsonProvider extends AbstractJsonProvider { @Override public Object createMap() { - return new JsonObject(); + return new JSONObject(); } @Override @@ -96,8 +96,8 @@ public class JsonOrgJsonProvider extends AbstractJsonProvider { public Object getMapValue(Object obj, String key) { try { JSONObject jsonObject = toJsonObject(obj); - Object o = jsonObject.get(key); - if (!jsonObject.has(key)) { + Object o = jsonObject.opt(key); + if (o == null) { return UNDEFINED; } else { return unwrap(o); 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 58493a35..a9c02e76 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/FilterTest.java @@ -89,6 +89,13 @@ public class FilterTest extends BaseTest { assertThat(filter(where("string-key").eq(null)).apply(createPredicateContext(json))).isEqualTo(false); } + @Test + public void arr_eq_evals() { + assertThat(filter(where("arr-empty").eq("[]")).apply(createPredicateContext(json))).isEqualTo(true); + assertThat(filter(where("int-arr").eq("[0,1,2,3,4]")).apply(createPredicateContext(json))).isEqualTo(true); + assertThat(filter(where("int-arr").eq("[0,1,2,3]")).apply(createPredicateContext(json))).isEqualTo(false); + assertThat(filter(where("int-arr").eq("[0,1,2,3,4,5]")).apply(createPredicateContext(json))).isEqualTo(false); + } //---------------------------------------------------------------------------- // // NE diff --git a/json-path/src/test/java/com/jayway/jsonpath/GsonJsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/GsonJsonProviderTest.java index 8f3ec333..1fc85bd9 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/GsonJsonProviderTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/GsonJsonProviderTest.java @@ -7,6 +7,8 @@ import com.jayway.jsonpath.spi.mapper.MappingException; import org.junit.Test; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.List; import static com.jayway.jsonpath.JsonPath.using; @@ -69,6 +71,65 @@ public class GsonJsonProviderTest extends BaseTest { assertThat(val).isEqualTo(node.getAsLong()); } + @Test + public void doubles_are_unwrapped() { + final String json = "{double-property = 56.78}"; + + JsonElement node = using(GSON_CONFIGURATION).parse(json).read("$.double-property"); + Double val = using(GSON_CONFIGURATION).parse(json).read("$.double-property", Double.class); + + assertThat(val).isEqualTo(56.78); + assertThat(val).isEqualTo(node.getAsDouble()); + } + + @Test + public void bigdecimals_are_unwrapped() { + final BigDecimal bd = BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.valueOf(10.5)); + final String json = "{bd-property = " + bd.toString() + "}"; + + JsonElement node = using(GSON_CONFIGURATION).parse(json).read("$.bd-property"); + BigDecimal val = using(GSON_CONFIGURATION).parse(json).read("$.bd-property", BigDecimal.class); + + assertThat(val).isEqualTo(bd); + assertThat(val).isEqualTo(node.getAsBigDecimal()); + } + + @Test + public void small_bigdecimals_are_unwrapped() { + final BigDecimal bd = BigDecimal.valueOf(10.5); + final String json = "{bd-property = " + bd.toString() + "}"; + + JsonElement node = using(GSON_CONFIGURATION).parse(json).read("$.bd-property"); + BigDecimal val = using(GSON_CONFIGURATION).parse(json).read("$.bd-property", BigDecimal.class); + + assertThat(val).isEqualTo(bd); + assertThat(val).isEqualTo(node.getAsBigDecimal()); + } + + @Test + public void bigintegers_are_unwrapped() { + final BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.TEN); + final String json = "{bi-property = " + bi.toString() + "}"; + + JsonElement node = using(GSON_CONFIGURATION).parse(json).read("$.bi-property"); + BigInteger val = using(GSON_CONFIGURATION).parse(json).read("$.bi-property", BigInteger.class); + + assertThat(val).isEqualTo(bi); + assertThat(val).isEqualTo(node.getAsBigInteger()); + } + + @Test + public void small_bigintegers_are_unwrapped() { + final BigInteger bi = BigInteger.valueOf(Long.MAX_VALUE); + final String json = "{bi-property = " + bi.toString() + "}"; + + JsonElement node = using(GSON_CONFIGURATION).parse(json).read("$.bi-property"); + BigInteger val = using(GSON_CONFIGURATION).parse(json).read("$.bi-property", BigInteger.class); + + assertThat(val).isEqualTo(bi); + assertThat(val).isEqualTo(node.getAsBigInteger()); + } + @Test public void int_to_long_mapping() { assertThat(using(GSON_CONFIGURATION).parse("{\"val\": 1}").read("val", Long.class)).isEqualTo(1L); diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonOrgJsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/JsonOrgJsonProviderTest.java index d24e30d1..59ba665c 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/JsonOrgJsonProviderTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonOrgJsonProviderTest.java @@ -44,4 +44,12 @@ public class JsonOrgJsonProviderTest extends BaseTest { assertThat(books.size()).isEqualTo(4); } + + @Test + public void read_books_with_isbn() { + + JSONArray books = using(JSON_ORG_CONFIGURATION).parse(JSON_DOCUMENT).read("$..book[?(@.isbn)]"); + + assertThat(books.length()).isEqualTo(2); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/ProviderInTest.java b/json-path/src/test/java/com/jayway/jsonpath/ProviderInTest.java new file mode 100644 index 00000000..0508539c --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/ProviderInTest.java @@ -0,0 +1,128 @@ +package com.jayway.jsonpath; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.google.gson.JsonArray; +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.JsonOrgJsonProvider; +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.JsonOrgMappingProvider; +import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider; +import org.assertj.core.util.Lists; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class ProviderInTest { + private final String JSON = "[{\"foo\": \"bar\"}, {\"foo\": \"baz\"}]"; + private final String EQUALS_FILTER = "$.[?(@.foo == %s)].foo"; + private final String IN_FILTER = "$.[?(@.foo in [%s])].foo"; + private final String DOUBLE_QUOTES = "\"bar\""; + private final String DOUBLE_QUOTES_EQUALS_FILTER = String.format(EQUALS_FILTER, DOUBLE_QUOTES); + private final String DOUBLE_QUOTES_IN_FILTER = String.format(IN_FILTER, DOUBLE_QUOTES); + private final String SINGLE_QUOTES = "'bar'"; + private final String SINGLE_QUOTES_EQUALS_FILTER = String.format(EQUALS_FILTER, SINGLE_QUOTES); + private final String SINGLE_QUOTES_IN_FILTER = String.format(IN_FILTER, SINGLE_QUOTES); + + + @Rule + public final ExpectedException exception = ExpectedException.none(); + + @Test + public void testJsonPathQuotesJackson() throws Exception { + final Configuration jackson = Configuration.builder().jsonProvider(new JacksonJsonProvider()).mappingProvider(new JacksonMappingProvider()).build(); + final DocumentContext ctx = JsonPath.using(jackson).parse(JSON); + + final List<String> doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER); + assertEquals(Lists.newArrayList("bar"), doubleQuoteEqualsResult); + + final List<String> singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER); + assertEquals(doubleQuoteEqualsResult, singleQuoteEqualsResult); + + final List<String> doubleQuoteInResult = ctx.read(DOUBLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult, doubleQuoteEqualsResult); + + exception.expect(InvalidJsonException.class); + ctx.read(SINGLE_QUOTES_IN_FILTER); + } + + + @Test + public void testJsonPathQuotesJacksonJsonNode() throws Exception { + final Configuration jacksonJsonNode = Configuration.builder().jsonProvider(new JacksonJsonNodeJsonProvider()).mappingProvider(new JacksonMappingProvider()).build(); + final DocumentContext ctx = JsonPath.using(jacksonJsonNode).parse(JSON); + + final ArrayNode doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER); + assertEquals("bar", doubleQuoteEqualsResult.get(0).asText()); + + final ArrayNode singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER); + assertEquals(doubleQuoteEqualsResult, singleQuoteEqualsResult); + + final ArrayNode doubleQuoteInResult = ctx.read(DOUBLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult, doubleQuoteEqualsResult); + + exception.expect(InvalidJsonException.class); + ctx.read(SINGLE_QUOTES_IN_FILTER); + } + + @Test + public void testJsonPathQuotesGson() throws Exception { + final Configuration gson = Configuration.builder().jsonProvider(new GsonJsonProvider()).mappingProvider(new GsonMappingProvider()).build(); + final DocumentContext ctx = JsonPath.using(gson).parse(JSON); + + final JsonArray doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER); + assertEquals("bar", doubleQuoteEqualsResult.get(0).getAsString()); + + final JsonArray singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER); + assertEquals(doubleQuoteEqualsResult, singleQuoteEqualsResult); + + final JsonArray doubleQuoteInResult = ctx.read(DOUBLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult, doubleQuoteEqualsResult); + + final JsonArray singleQuoteInResult = ctx.read(SINGLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult, singleQuoteInResult); + } + + @Test + public void testJsonPathQuotesJsonOrg() throws Exception { + final Configuration jsonOrg = Configuration.builder().jsonProvider(new JsonOrgJsonProvider()).mappingProvider(new JsonOrgMappingProvider()).build(); + final DocumentContext ctx = JsonPath.using(jsonOrg).parse(JSON); + + final org.json.JSONArray doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER); + assertEquals("bar", doubleQuoteEqualsResult.get(0)); + + final org.json.JSONArray singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER); + assertEquals(doubleQuoteEqualsResult.get(0), singleQuoteEqualsResult.get(0)); + + final org.json.JSONArray doubleQuoteInResult = ctx.read(DOUBLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult.get(0), doubleQuoteEqualsResult.get(0)); + + final org.json.JSONArray singleQuoteInResult = ctx.read(SINGLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult.get(0), singleQuoteInResult.get(0)); + } + + @Test + public void testJsonPathQuotesJsonSmart() throws Exception { + final Configuration jsonSmart = Configuration.builder().jsonProvider(new JsonSmartJsonProvider()).mappingProvider(new JsonSmartMappingProvider()).build(); + final DocumentContext ctx = JsonPath.using(jsonSmart).parse(JSON); + + final net.minidev.json.JSONArray doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER); + assertEquals("bar", doubleQuoteEqualsResult.get(0)); + + final net.minidev.json.JSONArray singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER); + assertEquals(doubleQuoteEqualsResult, singleQuoteEqualsResult); + + final net.minidev.json.JSONArray doubleQuoteInResult = ctx.read(DOUBLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult, doubleQuoteEqualsResult); + + final net.minidev.json.JSONArray singleQuoteInResult = ctx.read(SINGLE_QUOTES_IN_FILTER); + assertEquals(doubleQuoteInResult, singleQuoteInResult); + } +} \ No newline at end of file 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 93d3f8b2..04ffa00f 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 @@ -999,4 +999,24 @@ public class IssuesTest extends BaseTest { assertThat(objectNode.get("can delete").isNull()); assertThat(objectNode.get("can't delete").isNull()); } + + @Test + public void issue_309(){ + + String json = "{\n" + + "\"jsonArr\": [\n" + + " {\n" + + " \"name\":\"nOne\"\n" + + " },\n" + + " {\n" + + " \"name\":\"nTwo\"\n" + + " }\n" + + " ]\n" + + "}"; + + DocumentContext doc = JsonPath.parse(json).set("$.jsonArr[1].name", "Jayway"); + + assertThat(doc.read("$.jsonArr[0].name")).isEqualTo("nOne"); + assertThat(doc.read("$.jsonArr[1].name")).isEqualTo("Jayway"); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayIndexFilterTest.java b/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayIndexFilterTest.java index 434775af..eaada166 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayIndexFilterTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/old/internal/ArrayIndexFilterTest.java @@ -1,6 +1,9 @@ package com.jayway.jsonpath.old.internal; import com.jayway.jsonpath.JsonPath; + +import org.junit.Assert; + import org.hamcrest.Matchers; import org.junit.Test; @@ -43,4 +46,10 @@ public class ArrayIndexFilterTest { assertThat(result, Matchers.contains(1, 3, 5)); } + @Test + public void can_access_items_from_end_with_negative_index() { + int result = JsonPath.parse(JSON).read("$[-3]"); + Assert.assertEquals(8, result); + } + }