Skip to content

Commit e286fc8

Browse files
KaiStapelkaistapel
andauthored
GH-139: Allow pointing to array elements via a reference field (#203)
* GH-139: Allow pointing to array elements via a reference field. Example path `/array/id=123/data` * GH-139: Update README.md * GH-139: Increase version * GH-139: Incorporate review remarks * GH-139: Clarify implementation details and limitations in the README.md --------- Co-authored-by: kaistapel <kai.stapel@zalando.de>
1 parent 4e49968 commit e286fc8

File tree

6 files changed

+236
-33
lines changed

6 files changed

+236
-33
lines changed

README.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
[![CircleCI](https://circleci.com/gh/flipkart-incubator/zjsonpatch/tree/master.svg?style=svg)](https://circleci.com/gh/flipkart-incubator/zjsonpatch/tree/master) [![Join the chat at https://gitter.im/zjsonpatch/community](https://badges.gitter.im/zjsonpatch/community.svg)](https://gitter.im/zjsonpatch/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
22

3-
# This is an implementation of [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) written in Java.
3+
# This is an implementation of [RFC 6902 JSON Patch](https://datatracker.ietf.org/doc/html/rfc6902) written in Java with extended JSON pointer.
44

55
## Description & Use-Cases
66
- Java Library to find / apply JSON Patches according to [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902).
77
- JSON Patch defines a JSON document structure for representing changes to a JSON document.
88
- It can be used to avoid sending a whole document when only a part has changed, thus reducing network bandwidth requirements if data (in JSON format) is required to send across multiple systems over network or in case of multi DC transfer.
9-
- When used in combination with the HTTP PATCH method as per [RFC 5789 HTTP PATCH](https://datatracker.ietf.org/doc/html/rfc5789), it will do partial updates for HTTP APIs in a standard way.
9+
- When used in combination with the HTTP PATCH method as per [RFC 5789 HTTP PATCH](https://datatracker.ietf.org/doc/html/rfc5789), it will do partial updates for HTTP APIs in a standard way.
10+
- Extended JSON pointer functionality (i.e. reference array elements via a key): `/array/id=123/data`
11+
- The user has to ensure that a unique field is used as a reference key. Should there be more than one array
12+
element matching the given key-value pair, the first element will be selected.
13+
- Key based referencing may be slow for large arrays. Hence, standard index based array pointers should be used for large arrays.
1014

1115

1216
### Compatible with : Java 7+ versions
@@ -17,7 +21,7 @@ Package | Class, % | Method, % | Line, % |
1721
all classes | 100% (6/ 6) | 93.6% (44/ 47) | 96.2% (332/ 345) |
1822

1923
## Complexity
20-
- To find JsonPatch : Ω(N+M) ,N and M represents number of keys in first and second json respectively / O(summation of la*lb) where la , lb represents JSON array of length la / lb of against same key in first and second JSON ,since LCS is used to find difference between 2 JSON arrays there of order of quadratic.
24+
- To find JsonPatch : Ω(N+M), N and M represents number of keys in first and second json respectively / O(summation of la*lb) where la , lb represents JSON array of length la / lb of against same key in first and second JSON ,since LCS is used to find difference between 2 JSON arrays there of order of quadratic.
2125
- To Optimize Diffs ( compact move and remove into Move ) : Ω(D) / O(D*D) where D represents number of diffs obtained before compaction into Move operation.
2226
- To Apply Diff : O(D) where D represents number of diffs
2327

@@ -79,6 +83,33 @@ Following patch will be returned:
7983
```
8084
here `"op"` specifies the operation (`"move"`), `"from"` specifies the path from where the value should be moved, and `"path"` specifies where value should be moved. The value that is moved is taken as the content at the `"from"` path.
8185

86+
### Extended JSON Pointer Example
87+
JSON
88+
```json
89+
{
90+
"a": [
91+
{
92+
"id": 1,
93+
"data": "abc"
94+
},
95+
{
96+
"id": 2,
97+
"data": "def"
98+
}
99+
]
100+
}
101+
```
102+
103+
JSON path
104+
```jsonpath
105+
/a/id=2/data
106+
```
107+
108+
Following JSON would be returned
109+
```json
110+
"def"
111+
```
112+
82113
### Apply Json Patch In-Place
83114
```xml
84115
JsonPatch.applyInPlace(JsonNode patch, JsonNode source);

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>com.flipkart.zjsonpatch</groupId>
66
<artifactId>zjsonpatch</artifactId>
7-
<version>0.4.17-SNAPSHOT</version>
7+
<version>0.5.0-SNAPSHOT</version>
88
<packaging>jar</packaging>
99

1010
<name>zjsonpatch</name>

src/main/java/com/flipkart/zjsonpatch/JsonDiff.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ private static JsonPointer updatePathWithCounters(List<Integer> counters, JsonPo
269269
int value = counters.get(i);
270270
if (value != 0) {
271271
int currValue = tokens.get(i).getIndex();
272-
tokens.set(i, new JsonPointer.RefToken(Integer.toString(currValue + value)));
272+
tokens.set(i, JsonPointer.RefToken.parse(Integer.toString(currValue + value)));
273273
}
274274
}
275275
return new JsonPointer(tokens);

src/main/java/com/flipkart/zjsonpatch/JsonPointer.java

Lines changed: 123 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import java.util.ArrayList;
77
import java.util.Arrays;
88
import java.util.List;
9+
import java.util.Objects;
910
import java.util.regex.Matcher;
1011
import java.util.regex.Pattern;
1112

@@ -82,16 +83,20 @@ public static JsonPointer parse(String path) throws IllegalArgumentException {
8283
// Escape sequences
8384
case '~':
8485
switch (path.charAt(++i)) {
85-
case '0': reftoken.append('~'); break;
86-
case '1': reftoken.append('/'); break;
86+
case '0':
87+
case '1':
88+
case '2':
89+
reftoken.append('~');
90+
reftoken.append(path.charAt(i));
91+
break;
8792
default:
8893
throw new IllegalArgumentException("Invalid escape sequence ~" + path.charAt(i) + " at index " + i);
8994
}
9095
break;
9196

9297
// New reftoken
9398
case '/':
94-
result.add(new RefToken(reftoken.toString()));
99+
result.add(RefToken.parse(reftoken.toString()));
95100
reftoken.setLength(0);
96101
break;
97102

@@ -124,7 +129,7 @@ public boolean isRoot() {
124129
*/
125130
JsonPointer append(String field) {
126131
RefToken[] newTokens = Arrays.copyOf(tokens, tokens.length + 1);
127-
newTokens[tokens.length] = new RefToken(field);
132+
newTokens[tokens.length] = new RefToken(field, null, null);
128133
return new JsonPointer(newTokens);
129134
}
130135

@@ -135,7 +140,9 @@ JsonPointer append(String field) {
135140
* @return The new {@link JsonPointer} instance.
136141
*/
137142
JsonPointer append(int index) {
138-
return append(Integer.toString(index));
143+
RefToken[] newTokens = Arrays.copyOf(tokens, tokens.length + 1);
144+
newTokens[tokens.length] = new RefToken(Integer.toString(index), index, null);
145+
return new JsonPointer(newTokens);
139146
}
140147

141148
/** Returns the number of reference tokens comprising this instance. */
@@ -226,11 +233,27 @@ public JsonNode evaluate(final JsonNode document) throws JsonPointerEvaluationEx
226233
final RefToken token = tokens[idx];
227234

228235
if (current.isArray()) {
229-
if (!token.isArrayIndex())
236+
if (token.isArrayIndex()) {
237+
if (token.getIndex() == LAST_INDEX || token.getIndex() >= current.size())
238+
error(idx, "Array index " + token + " is out of bounds", document);
239+
current = current.get(token.getIndex());
240+
} else if (token.isArrayKeyRef()) {
241+
KeyRef keyRef = token.getKeyRef();
242+
JsonNode foundArrayNode = null;
243+
for (int arrayIdx = 0; arrayIdx < current.size(); ++arrayIdx) {
244+
JsonNode arrayNode = current.get(arrayIdx);
245+
if (matches(keyRef, arrayNode)) {
246+
foundArrayNode = arrayNode;
247+
break;
248+
}
249+
}
250+
if (foundArrayNode == null) {
251+
error(idx, "Array has no matching object for key reference " + token, document);
252+
}
253+
current = foundArrayNode;
254+
} else {
230255
error(idx, "Can't reference field \"" + token.getField() + "\" on array", document);
231-
if (token.getIndex() == LAST_INDEX || token.getIndex() >= current.size())
232-
error(idx, "Array index " + token.toString() + " is out of bounds", document);
233-
current = current.get(token.getIndex());
256+
}
234257
}
235258
else if (current.isObject()) {
236259
if (!current.has(token.getField()))
@@ -244,6 +267,19 @@ else if (current.isObject()) {
244267
return current;
245268
}
246269

270+
private boolean matches(KeyRef keyRef, JsonNode arrayNode) {
271+
boolean matches = false;
272+
if (arrayNode.has(keyRef.key)) {
273+
JsonNode valueNode = arrayNode.get(keyRef.key);
274+
if (valueNode.isTextual()) {
275+
matches = Objects.equals(keyRef.value, valueNode.textValue());
276+
} else if (valueNode.isNumber() || valueNode.isBoolean()) {
277+
matches = Objects.equals(keyRef.value, valueNode.toString());
278+
}
279+
}
280+
return matches;
281+
}
282+
247283
@Override
248284
public boolean equals(Object o) {
249285
if (this == o) return true;
@@ -262,61 +298,99 @@ public int hashCode() {
262298

263299
/** Represents a single JSON Pointer reference token. */
264300
static class RefToken {
265-
private String decodedToken;
266-
transient private Integer index = null;
301+
private final String decodedToken;
302+
private final Integer index;
303+
private final KeyRef keyRef;
267304

268-
public RefToken(String decodedToken) {
305+
private RefToken(String decodedToken, Integer arrayIndex, KeyRef arrayKeyRef) {
269306
if (decodedToken == null) throw new IllegalArgumentException("Token can't be null");
270307
this.decodedToken = decodedToken;
308+
this.index = arrayIndex;
309+
this.keyRef = arrayKeyRef;
271310
}
272311

273312
private static final Pattern DECODED_TILDA_PATTERN = Pattern.compile("~0");
274313
private static final Pattern DECODED_SLASH_PATTERN = Pattern.compile("~1");
314+
private static final Pattern DECODED_EQUALS_PATTERN = Pattern.compile("~2");
275315

276316
private static String decodePath(Object object) {
277317
String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
278318
path = DECODED_SLASH_PATTERN.matcher(path).replaceAll("/");
279-
return DECODED_TILDA_PATTERN.matcher(path).replaceAll("~");
319+
path = DECODED_TILDA_PATTERN.matcher(path).replaceAll("~");
320+
return DECODED_EQUALS_PATTERN.matcher(path).replaceAll("=");
280321
}
281322

282323
private static final Pattern ENCODED_TILDA_PATTERN = Pattern.compile("~");
283324
private static final Pattern ENCODED_SLASH_PATTERN = Pattern.compile("/");
325+
private static final Pattern ENCODED_EQUALS_PATTERN = Pattern.compile("=");
284326

285327
private static String encodePath(Object object) {
286328
String path = object.toString(); // see http://tools.ietf.org/html/rfc6901#section-4
287329
path = ENCODED_TILDA_PATTERN.matcher(path).replaceAll("~0");
288-
return ENCODED_SLASH_PATTERN.matcher(path).replaceAll("~1");
330+
path = ENCODED_SLASH_PATTERN.matcher(path).replaceAll("~1");
331+
return ENCODED_EQUALS_PATTERN.matcher(path).replaceAll("~2");
289332
}
290333

291334
private static final Pattern VALID_ARRAY_IND = Pattern.compile("-|0|(?:[1-9][0-9]*)");
292335

336+
private static final Pattern VALID_ARRAY_KEY_REF = Pattern.compile("([^=]+)=([^=]+)");
337+
293338
public static RefToken parse(String rawToken) {
294339
if (rawToken == null) throw new IllegalArgumentException("Token can't be null");
295-
return new RefToken(decodePath(rawToken));
340+
341+
Integer index = null;
342+
Matcher indexMatcher = VALID_ARRAY_IND.matcher(rawToken);
343+
if (indexMatcher.matches()) {
344+
if (indexMatcher.group().equals("-")) {
345+
index = LAST_INDEX;
346+
} else {
347+
try {
348+
int validInt = Integer.parseInt(indexMatcher.group());
349+
index = validInt;
350+
} catch (NumberFormatException ignore) {}
351+
}
352+
}
353+
354+
KeyRef keyRef = null;
355+
Matcher arrayKeyRefMatcher = VALID_ARRAY_KEY_REF.matcher(rawToken);
356+
if (arrayKeyRefMatcher.matches()) {
357+
keyRef = new KeyRef(
358+
decodePath(arrayKeyRefMatcher.group(1)),
359+
decodePath(arrayKeyRefMatcher.group(2))
360+
);
361+
}
362+
return new RefToken(decodePath(rawToken), index, keyRef);
296363
}
297364

298365
public boolean isArrayIndex() {
299-
if (index != null) return true;
300-
Matcher matcher = VALID_ARRAY_IND.matcher(decodedToken);
301-
if (matcher.matches()) {
302-
index = matcher.group().equals("-") ? LAST_INDEX : Integer.parseInt(matcher.group());
303-
return true;
304-
}
305-
return false;
366+
return index != null;
367+
}
368+
369+
public boolean isArrayKeyRef() {
370+
return keyRef != null;
306371
}
307372

308373
public int getIndex() {
309-
if (!isArrayIndex()) throw new IllegalStateException("Object operation on array target");
374+
if (!isArrayIndex()) throw new IllegalStateException("Object operation on array index target");
310375
return index;
311376
}
312377

378+
public KeyRef getKeyRef() {
379+
if (!isArrayKeyRef()) throw new IllegalStateException("Object operation on array key ref target");
380+
return keyRef;
381+
}
382+
313383
public String getField() {
314384
return decodedToken;
315385
}
316386

317387
@Override
318388
public String toString() {
319-
return encodePath(decodedToken);
389+
if (isArrayKeyRef()) {
390+
return encodePath(keyRef.key) + "=" + encodePath(keyRef.value);
391+
} else {
392+
return encodePath(decodedToken);
393+
}
320394
}
321395

322396
@Override
@@ -335,6 +409,31 @@ public int hashCode() {
335409
}
336410
}
337411

412+
static class KeyRef {
413+
private String key;
414+
private String value;
415+
416+
public KeyRef(String key, String value) {
417+
this.key = key;
418+
this.value = value;
419+
}
420+
421+
@Override
422+
public boolean equals(Object o) {
423+
if (this == o) return true;
424+
if (o == null || getClass() != o.getClass()) return false;
425+
426+
KeyRef keyRef = (KeyRef) o;
427+
428+
return Objects.equals(key, keyRef.key) && Objects.equals(value, keyRef.value);
429+
}
430+
431+
@Override
432+
public int hashCode() {
433+
return Objects.hash(key, value);
434+
}
435+
}
436+
338437
/**
339438
* Represents an array index pointing past the end of the array.
340439
*

0 commit comments

Comments
 (0)