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 5a7a6c85..e0953842 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Given the json | $.store.* | All things, both books and bicycles | | $.store..price | The price of everything | | $..book[2] | The third book | +| $..book[-2] | The second to last book | | $..book[0,1] | The first two books | | $..book[:2] | All books from index 0 (inclusive) until index 2 (exclusive) | | $..book[1:2] | All books from index 1 (inclusive) until index 2 (exclusive) | diff --git a/build.gradle b/build.gradle index fffe227a..fec4e271 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { ext { libs = [ slf4jApi: 'org.slf4j:slf4j-api:1.7.16', - 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', diff --git a/json-path/build.gradle b/json-path/build.gradle index c86e35f0..812eac64 100644 --- a/json-path/build.gradle +++ b/json-path/build.gradle @@ -11,10 +11,7 @@ jar { } dependencies { - compile (libs.jsonSmart){ - // see https://github.com/jayway/JsonPath/issues/228, https://github.com/netplex/json-smart-v2/issues/20 - exclude group: 'org.ow2.asm', module: 'asm' - } + compile libs.jsonSmart compile libs.slf4jApi compile libs.jacksonDatabind, optional compile libs.gson, optional 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/filter/FilterCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/filter/FilterCompiler.java index e5eb39a3..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() { 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 6a8a4a8e..08d257c4 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 @@ -158,7 +158,7 @@ public abstract class ValueNode { if ((c0 == '[' && c1 == ']') || (c0 == '{' && c1 == '}')){ try { new JSONParser(JSONParser.MODE_PERMISSIVE).parse(str); - return false; + return true; } catch(Exception e){ return false; } 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/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 filterStack; private final CharacterIndex path; - private PathCompiler(String path, LinkedList filterStack) { + private PathCompiler(String path, LinkedList filterStack){ + this(new CharacterIndex(path), filterStack); + } + + private PathCompiler(CharacterIndex path, LinkedList 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(asList(filters)); - Path p = new PathCompiler(path.trim(), filterStack).compile(); + LinkedList filterStack = new LinkedList(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 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/JsonOrgJsonProvider.java b/json-path/src/main/java/com/jayway/jsonpath/spi/json/JsonOrgJsonProvider.java index 6760f869..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 @@ -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/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 doubleQuoteEqualsResult = ctx.read(DOUBLE_QUOTES_EQUALS_FILTER); + assertEquals(Lists.newArrayList("bar"), doubleQuoteEqualsResult); + + final List singleQuoteEqualsResult = ctx.read(SINGLE_QUOTES_EQUALS_FILTER); + assertEquals(doubleQuoteEqualsResult, singleQuoteEqualsResult); + + final List 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); + } + }