Skip to content

Commit 02fd39d

Browse files
[US-642] implement json patch queries (#2)
* [US642] Implement filtering * [US642] Add filtering tests and query parsing * [US642] Handle booleans, integers and floating point numbers * [US642] Add some test cases * [US642] Handle nested array filter queries * [US642] Add a couple more tests for an add operation * [US642] Improve naming - fix review comments * [US642] Additional tests for add operation * [US642] Optimize imports
1 parent 95b036b commit 02fd39d

19 files changed

+1528
-64
lines changed

src/main/java/com/github/fge/jsonpatch/AddOperation.java

Lines changed: 42 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@
2222
import com.fasterxml.jackson.annotation.JsonCreator;
2323
import com.fasterxml.jackson.annotation.JsonProperty;
2424
import com.fasterxml.jackson.databind.JsonNode;
25-
import com.fasterxml.jackson.databind.ObjectMapper;
2625
import com.fasterxml.jackson.databind.node.ArrayNode;
27-
import com.fasterxml.jackson.databind.node.ObjectNode;
28-
import com.github.fge.jackson.jsonpointer.JsonPointer;
29-
import com.github.fge.jackson.jsonpointer.ReferenceToken;
3026
import com.jayway.jsonpath.DocumentContext;
3127
import com.jayway.jsonpath.JsonPath;
3228

@@ -83,23 +79,41 @@ public JsonNode apply(final JsonNode node) throws JsonPatchException {
8379
* Check the parent node: it must exist and be a container (ie an array
8480
* or an object) for the add operation to work.
8581
*/
86-
final int lastSlashIndex = path.lastIndexOf('/');
87-
final String newNodeName = path.substring(lastSlashIndex + 1);
88-
final String pathToParent = path.substring(0, lastSlashIndex);
89-
final String jsonPath = JsonPathParser.tmfStringToJsonPath(pathToParent);
82+
final String fullJsonPath = JsonPathParser.tmfStringToJsonPath(path);
83+
final int lastDotIndex = fullJsonPath.lastIndexOf('.');
84+
final String newNodeName = fullJsonPath.substring(lastDotIndex + 1)
85+
.replace("[", "").replace("]", "");
86+
final String pathToParent = fullJsonPath.substring(0, lastDotIndex);
87+
9088
final DocumentContext nodeContext = JsonPath.parse(node.deepCopy());
9189

92-
final JsonNode parentNode = nodeContext.read(jsonPath);
93-
if (parentNode == null) {
90+
final JsonNode evaluatedJsonParents = nodeContext.read(pathToParent);
91+
if (evaluatedJsonParents == null) {
9492
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchParent"));
9593
}
96-
if (!parentNode.isContainerNode()) {
94+
if (!evaluatedJsonParents.isContainerNode()) {
9795
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.parentNotContainer"));
9896
}
9997

100-
return parentNode.isArray()
101-
? addToArray(nodeContext, jsonPath, newNodeName)
102-
: addToObject(nodeContext, jsonPath, newNodeName);
98+
if (pathToParent.contains("[?(")) { // json filter result is always a list
99+
for (int i = 0; i < evaluatedJsonParents.size(); i++) {
100+
JsonNode parentNode = evaluatedJsonParents.get(i);
101+
if (!parentNode.isContainerNode()) {
102+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.parentNotContainer"));
103+
}
104+
DocumentContext containerContext = JsonPath.parse(parentNode);
105+
if (parentNode.isArray()) {
106+
addToArray(containerContext, "$", newNodeName);
107+
} else {
108+
addToObject(containerContext, "$", newNodeName);
109+
}
110+
}
111+
return nodeContext.read("$");
112+
} else {
113+
return evaluatedJsonParents.isArray()
114+
? addToArray(nodeContext, pathToParent, newNodeName)
115+
: addToObject(nodeContext, pathToParent, newNodeName);
116+
}
103117
}
104118

105119
private JsonNode addToArray(final DocumentContext node, String jsonPath, String newNodeName) throws JsonPatchException {
@@ -108,14 +122,7 @@ private JsonNode addToArray(final DocumentContext node, String jsonPath, String
108122
}
109123

110124
final int size = node.read(jsonPath, JsonNode.class).size();
111-
final int index;
112-
try {
113-
index = Integer.parseInt(newNodeName);
114-
} catch (NumberFormatException ignored) {
115-
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.notAnIndex"));
116-
}
117-
if (index < 0 || index > size)
118-
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchIndex"));
125+
final int index = verifyAndGetArrayIndex(newNodeName, size);
119126

120127
ArrayNode updatedArray = node.read(jsonPath, ArrayNode.class).insert(index, value);
121128
return "$".equals(jsonPath) ? updatedArray : node.set(jsonPath, updatedArray).read("$", JsonNode.class);
@@ -126,4 +133,17 @@ private JsonNode addToObject(final DocumentContext node, String jsonPath, String
126133
.put(jsonPath, newNodeName, value)
127134
.read("$", JsonNode.class);
128135
}
136+
137+
private int verifyAndGetArrayIndex(String stringIndex, int size) throws JsonPatchException {
138+
int index;
139+
try {
140+
index = Integer.parseInt(stringIndex);
141+
} catch (NumberFormatException ignored) {
142+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.notAnIndex"));
143+
}
144+
if (index < 0 || index > size) {
145+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchIndex"));
146+
}
147+
return index;
148+
}
129149
}

src/main/java/com/github/fge/jsonpatch/DualPathOperation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ protected DualPathOperation(final String op, final String from, final String pat
5151
public final void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
5252
jgen.writeStartObject();
5353
jgen.writeStringField("op", op);
54-
jgen.writeStringField("path", path.toString());
55-
jgen.writeStringField("from", from.toString());
54+
jgen.writeStringField("path", path);
55+
jgen.writeStringField("from", from);
5656
jgen.writeEndObject();
5757
}
5858

src/main/java/com/github/fge/jsonpatch/JsonPatchOperation.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.fasterxml.jackson.annotation.JsonTypeInfo;
2525
import com.fasterxml.jackson.databind.JsonNode;
2626
import com.fasterxml.jackson.databind.JsonSerializable;
27-
import com.github.fge.jackson.jsonpointer.JsonPointer;
2827
import com.github.fge.msgsimple.bundle.MessageBundle;
2928
import com.github.fge.msgsimple.load.MessageBundles;
3029
import com.jayway.jsonpath.Configuration;
Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
11
package com.github.fge.jsonpatch;
22

3+
import static com.github.fge.jsonpatch.JsonPatchOperation.BUNDLE;
4+
35
public class JsonPathParser {
46

57
private static final String ARRAY_ELEMENT_REGEX = "\\.(\\d+)\\.";
68
private static final String ARRAY_ELEMENT_LAST_REGEX = "\\.(\\d+)$";
79

8-
public static String tmfStringToJsonPath(String path) {
10+
public static String tmfStringToJsonPath(String path) throws JsonPatchException {
11+
if (!path.startsWith("/") && !path.isEmpty()) {
12+
return "$." + path;
13+
}
914
if ("/".equals(path)) {
1015
return "$";
1116
}
12-
final String jsonPath = "$" + path.replace('/', '.')
17+
final String[] pointerAndQuery = path
18+
.replaceAll("(\\w)\\?", "$1#THIS_IS_SPLIT_PLACEHOLDER#")
19+
.split("#THIS_IS_SPLIT_PLACEHOLDER#", -1);
20+
if (pointerAndQuery.length > 2) {
21+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.invalidPathExpression"));
22+
}
23+
24+
final String jsonPath = "$" + pointerAndQuery[0].replace('/', '.')
1325
.replaceAll(ARRAY_ELEMENT_REGEX, ".[$1].")
26+
.replaceAll(ARRAY_ELEMENT_REGEX, ".[$1].") // has to be repeated due to positive lookahead not working properly
1427
.replaceAll(ARRAY_ELEMENT_LAST_REGEX, ".[$1]");
15-
return jsonPath;
28+
final String jsonPathWithQuery = addQueryIfApplicable(jsonPath, pointerAndQuery);
29+
return jsonPathWithQuery;
30+
}
31+
32+
private static String addQueryIfApplicable(String jsonPath, String[] pointerAndQuery) {
33+
if (pointerAndQuery.length == 2) {
34+
String preparedFilter = pointerAndQuery[1]
35+
.replaceAll("]", "] empty false") // add empty false to nested array expressions
36+
.replaceAll("(\\w)=(\\w)", "$1==$2") // replace single equals with double
37+
.replaceAll("==([\\w .]+)", "=='$1'") // surround strings with single quotes
38+
.replaceFirst("\\w+", "@") // jsonpath expression should start with @ as the name of item
39+
.replaceAll("([&|])\\w+", " $1$1 @"); // replace single | and & with doubles
40+
String filterWithBooleansAndNumbers = preparedFilter
41+
.replaceAll("@([\\w.]+)=='(true|false)'", "(@$1==$2 || @$1=='$2')") // prepare a statement for boolean and boolean as string
42+
.replaceAll("@([\\w.]+)=='(\\d+)'", "(@$1==$2 || @$1=='$2')") // prepare a statement for an integer and integer as string
43+
.replaceAll("@([\\w.]+)=='(\\d+\\.\\d+)'", "(@$1==$2 || @$1=='$2')"); // prepare a statement for float and float as string
44+
return jsonPath.replaceFirst("(\\w+)", "$1[?(" + filterWithBooleansAndNumbers + ")]");
45+
} else {
46+
return jsonPath;
47+
}
1648
}
1749
}

src/main/java/com/github/fge/jsonpatch/MoveOperation.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import com.fasterxml.jackson.annotation.JsonCreator;
2323
import com.fasterxml.jackson.annotation.JsonProperty;
2424
import com.fasterxml.jackson.databind.JsonNode;
25-
import com.github.fge.jackson.jsonpointer.JsonPointer;
2625
import com.jayway.jsonpath.JsonPath;
2726

2827
/**

src/main/java/com/github/fge/jsonpatch/PathValueOperation.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import com.fasterxml.jackson.databind.SerializerProvider;
2626
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2727
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
28-
import com.github.fge.jackson.jsonpointer.JsonPointer;
2928

3029
import java.io.IOException;
3130

src/main/java/com/github/fge/jsonpatch/ReplaceOperation.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@
2222
import com.fasterxml.jackson.annotation.JsonCreator;
2323
import com.fasterxml.jackson.annotation.JsonProperty;
2424
import com.fasterxml.jackson.databind.JsonNode;
25-
import com.fasterxml.jackson.databind.node.ArrayNode;
26-
import com.fasterxml.jackson.databind.node.ObjectNode;
27-
import com.github.fge.jackson.jsonpointer.JsonPointer;
2825
import com.jayway.jsonpath.DocumentContext;
2926
import com.jayway.jsonpath.JsonPath;
3027

src/main/resources/com/github/fge/jsonpatch/messages.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@ jsonPatch.noSuchIndex=no such index in target array
2727
jsonPatch.noSuchPath=no such path in target JSON document
2828
jsonPatch.parentNotContainer=parent of path to add to is not a container
2929
jsonPatch.valueTestFailure=value differs from expectations
30+
jsonPatch.invalidPathExpression=invalid path expression
3031
mergePatch.notContainer=value is neither an object or an array (found %s)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package com.github.fge.jsonpatch;
2+
3+
import org.testng.annotations.Test;
4+
5+
import static org.testng.Assert.*;
6+
7+
public class JsonPathParserTest {
8+
9+
@Test
10+
public void shouldConvertQueryToJsonPath() throws JsonPatchException {
11+
String jsonPointerWithQuery = "/productPrice/prodPriceAlteration?productPrice.name=Regular Price";
12+
String expected = "$.productPrice[?(@.name=='Regular Price')].prodPriceAlteration";
13+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
14+
assertEquals(result, expected);
15+
}
16+
17+
@Test
18+
public void shouldConvertArrayPathToJsonPath() throws JsonPatchException {
19+
String jsonPointerWithQuery = "/2/1/-";
20+
String expected = "$.[2].[1].-";
21+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
22+
assertEquals(result, expected);
23+
}
24+
25+
@Test
26+
public void shouldConvertBooleans() throws JsonPatchException {
27+
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.valid=true&orderItem.product.relatedParty.role=customer";
28+
String expected = "$.orderItem[?((@.productOffering.valid==true || @.productOffering.valid=='true') && @.product.relatedParty.role=='customer')].quantity";
29+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
30+
assertEquals(result, expected);
31+
}
32+
33+
@Test
34+
public void shouldConvertFloatingPoint() throws JsonPatchException {
35+
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.price=1513.77&orderItem.product.relatedParty.role=customer";
36+
String expected = "$.orderItem[?((@.productOffering.price==1513.77 || @.productOffering.price=='1513.77') && @.product.relatedParty.role=='customer')].quantity";
37+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
38+
assertEquals(result, expected);
39+
}
40+
41+
@Test
42+
public void shouldConvertIntegers() throws JsonPatchException {
43+
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.id=1513&orderItem.product.relatedParty.role=customer";
44+
String expected = "$.orderItem[?((@.productOffering.id==1513 || @.productOffering.id=='1513') && @.product.relatedParty.role=='customer')].quantity";
45+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
46+
assertEquals(result, expected);
47+
}
48+
49+
@Test
50+
public void shouldConvertManyConditions() throws JsonPatchException {
51+
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.product.relatedParty.role=customer&orderItem.product.relatedParty.name=Mary";
52+
String expected = "$.orderItem[?(@.product.relatedParty.role=='customer' && @.product.relatedParty.name=='Mary')].quantity";
53+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
54+
assertEquals(result, expected);
55+
}
56+
57+
@Test
58+
public void shouldConvertNestedArrayQuery() throws JsonPatchException {
59+
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.productOffering.id=1513&orderItem.product.relatedParty[?(@.role=='customer' && @.name=='Mary')]";
60+
String expected = "$.orderItem[?((@.productOffering.id==1513 || @.productOffering.id=='1513') && @.product.relatedParty[?(@.role=='customer' && @.name=='Mary')] empty false)].quantity";
61+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
62+
assertEquals(result, expected);
63+
}
64+
65+
@Test
66+
public void shouldConvertNestedArrayQueryWhichIsNotLastStatement() throws JsonPatchException {
67+
String jsonPointerWithQuery = "/orderItem/quantity?orderItem.product.relatedParty[?(@.role=='customer' && @.name=='Mary')]&orderItem.productOffering.id=1513";
68+
String expected = "$.orderItem[?(@.product.relatedParty[?(@.role=='customer' && @.name=='Mary')] empty false && (@.productOffering.id==1513 || @.productOffering.id=='1513'))].quantity";
69+
String result = JsonPathParser.tmfStringToJsonPath(jsonPointerWithQuery);
70+
assertEquals(result, expected);
71+
}
72+
73+
@Test
74+
public void shouldConvertFilterQuery() throws JsonPatchException {
75+
String filterQuery = "note[?(@.author=='John Doe')].date";
76+
String expected = "$.note[?(@.author=='John Doe')].date";
77+
String result = JsonPathParser.tmfStringToJsonPath(filterQuery);
78+
assertEquals(result, expected);
79+
}
80+
81+
}

src/test/java/com/github/fge/jsonpatch/query/AddQueryOperationTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import com.github.fge.jsonpatch.JsonPatchOperationTest;
44

55
import java.io.IOException;
6-
// TODO extend with JsonPatchOperationTest and uncomment constructor when this test needs to be active, couldn't ignore it otherway
7-
public class AddQueryOperationTest extends Object {
6+
7+
public class AddQueryOperationTest extends JsonPatchOperationTest {
88

99
public AddQueryOperationTest() throws IOException {
10-
//super("query/add");
10+
super("query/add");
1111
}
1212
}

0 commit comments

Comments
 (0)