Skip to content

Commit 95b036b

Browse files
[US642] JSON Patch Query (#1)
* Add tests from spec TMF630 * Replace JsonPointer with JsonPath in Add operation * Replace JsonPointer with JsonPath in Replace operation * Replace JsonPointer with JsonPath in Remove operation * Refactor library to use String instead of JsonPointer * Format if statements * Add missing final keyword to some vals * Add comments in not active tests * Extract regex constants
1 parent 55b27b9 commit 95b036b

21 files changed

+619
-239
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ buildscript {
2121
repositories {
2222
mavenCentral()
2323
maven {
24-
url "http://repo.springsource.org/plugins-release";
24+
url "https://repo.springsource.org/plugins-release";
2525
}
2626
}
2727
dependencies {

project.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ project.ext.description = "JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386)
3232
dependencies {
3333
provided(group: "com.google.code.findbugs", name: "jsr305", version: "3.0.2");
3434
compile(group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.11.0");
35+
compile(group: 'com.jayway.jsonpath', name: 'json-path', version: '2.6.0')
3536
compile(group: "com.github.java-json-tools", name: "msg-simple", version: "1.2");
36-
3737
compile(group: "com.github.java-json-tools", name: "jackson-coreutils", version: "2.0");
3838
testCompile(group: "org.testng", name: "testng", version: "7.1.0") {
3939
exclude(group: "junit", module: "junit");

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

Lines changed: 39 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@
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;
2526
import com.fasterxml.jackson.databind.node.ArrayNode;
2627
import com.fasterxml.jackson.databind.node.ObjectNode;
2728
import com.github.fge.jackson.jsonpointer.JsonPointer;
2829
import com.github.fge.jackson.jsonpointer.ReferenceToken;
29-
import com.github.fge.jackson.jsonpointer.TokenResolver;
30-
31-
import java.util.NoSuchElementException;
30+
import com.jayway.jsonpath.DocumentContext;
31+
import com.jayway.jsonpath.JsonPath;
3232

3333

3434
/**
@@ -65,78 +65,65 @@
6565
* [ 1, 2, 3 ]
6666
* </pre>
6767
*/
68-
public final class AddOperation
69-
extends PathValueOperation
70-
{
71-
private static final ReferenceToken LAST_ARRAY_ELEMENT
72-
= ReferenceToken.fromRaw("-");
68+
public final class AddOperation extends PathValueOperation {
69+
public static final String LAST_ARRAY_ELEMENT_SYMBOL = "-";
7370

7471
@JsonCreator
75-
public AddOperation(@JsonProperty("path") final JsonPointer path,
76-
@JsonProperty("value") final JsonNode value)
77-
{
72+
public AddOperation(@JsonProperty("path") final String path,
73+
@JsonProperty("value") final JsonNode value) {
7874
super("add", path, value);
7975
}
8076

8177
@Override
82-
public JsonNode apply(final JsonNode node)
83-
throws JsonPatchException
84-
{
85-
if (path.isEmpty())
78+
public JsonNode apply(final JsonNode node) throws JsonPatchException {
79+
if (path.isEmpty()) {
8680
return value;
87-
81+
}
8882
/*
8983
* Check the parent node: it must exist and be a container (ie an array
9084
* or an object) for the add operation to work.
9185
*/
92-
final JsonNode parentNode = path.parent().path(node);
93-
if (parentNode.isMissingNode())
94-
throw new JsonPatchException(BUNDLE.getMessage(
95-
"jsonPatch.noSuchParent"));
96-
if (!parentNode.isContainerNode())
97-
throw new JsonPatchException(BUNDLE.getMessage(
98-
"jsonPatch.parentNotContainer"));
99-
return parentNode.isArray()
100-
? addToArray(path, node)
101-
: addToObject(path, node);
102-
}
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);
90+
final DocumentContext nodeContext = JsonPath.parse(node.deepCopy());
10391

104-
private JsonNode addToArray(final JsonPointer path, final JsonNode node)
105-
throws JsonPatchException
106-
{
107-
final JsonNode ret = node.deepCopy();
108-
final ArrayNode target = (ArrayNode) path.parent().get(ret);
92+
final JsonNode parentNode = nodeContext.read(jsonPath);
93+
if (parentNode == null) {
94+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchParent"));
95+
}
96+
if (!parentNode.isContainerNode()) {
97+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.parentNotContainer"));
98+
}
10999

110-
final TokenResolver<JsonNode> token = Iterables.getLast(path);
100+
return parentNode.isArray()
101+
? addToArray(nodeContext, jsonPath, newNodeName)
102+
: addToObject(nodeContext, jsonPath, newNodeName);
103+
}
111104

112-
if (token.getToken().equals(LAST_ARRAY_ELEMENT)) {
113-
target.add(value);
114-
return ret;
105+
private JsonNode addToArray(final DocumentContext node, String jsonPath, String newNodeName) throws JsonPatchException {
106+
if (newNodeName.equals(LAST_ARRAY_ELEMENT_SYMBOL)) {
107+
return node.add(jsonPath, value).read("$", JsonNode.class);
115108
}
116109

117-
final int size = target.size();
110+
final int size = node.read(jsonPath, JsonNode.class).size();
118111
final int index;
119112
try {
120-
index = Integer.parseInt(token.toString());
113+
index = Integer.parseInt(newNodeName);
121114
} catch (NumberFormatException ignored) {
122-
throw new JsonPatchException(BUNDLE.getMessage(
123-
"jsonPatch.notAnIndex"));
115+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.notAnIndex"));
124116
}
125-
126117
if (index < 0 || index > size)
127-
throw new JsonPatchException(BUNDLE.getMessage(
128-
"jsonPatch.noSuchIndex"));
118+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchIndex"));
129119

130-
target.insert(index, value);
131-
return ret;
120+
ArrayNode updatedArray = node.read(jsonPath, ArrayNode.class).insert(index, value);
121+
return "$".equals(jsonPath) ? updatedArray : node.set(jsonPath, updatedArray).read("$", JsonNode.class);
132122
}
133123

134-
private JsonNode addToObject(final JsonPointer path, final JsonNode node)
135-
{
136-
final TokenResolver<JsonNode> token = Iterables.getLast(path);
137-
final JsonNode ret = node.deepCopy();
138-
final ObjectNode target = (ObjectNode) path.parent().get(ret);
139-
target.set(token.getToken().getRaw(), value);
140-
return ret;
124+
private JsonNode addToObject(final DocumentContext node, String jsonPath, String newNodeName) {
125+
return node
126+
.put(jsonPath, newNodeName, value)
127+
.read("$", JsonNode.class);
141128
}
142129
}

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

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +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.github.fge.jackson.jsonpointer.JsonPointer;
25+
import com.jayway.jsonpath.JsonPath;
2626

2727
/**
2828
* JSON Patch {@code copy} operation
@@ -40,24 +40,20 @@
4040
*
4141
* <p>It is an error if {@code from} fails to resolve to a JSON value.</p>
4242
*/
43-
public final class CopyOperation
44-
extends DualPathOperation
45-
{
43+
public final class CopyOperation extends DualPathOperation {
44+
4645
@JsonCreator
47-
public CopyOperation(@JsonProperty("from") final JsonPointer from,
48-
@JsonProperty("path") final JsonPointer path)
49-
{
46+
public CopyOperation(@JsonProperty("from") final String from, @JsonProperty("path") final String path) {
5047
super("copy", from, path);
5148
}
5249

5350
@Override
54-
public JsonNode apply(final JsonNode node)
55-
throws JsonPatchException
56-
{
57-
final JsonNode dupData = from.path(node).deepCopy();
58-
if (dupData.isMissingNode())
59-
throw new JsonPatchException(BUNDLE.getMessage(
60-
"jsonPatch.noSuchPath"));
51+
public JsonNode apply(final JsonNode node) throws JsonPatchException {
52+
final String jsonPath = JsonPathParser.tmfStringToJsonPath(from);
53+
final JsonNode dupData = JsonPath.parse(node.deepCopy()).read(jsonPath);
54+
if (dupData == null) {
55+
throw new JsonPatchException(BUNDLE.getMessage("jsonPatch.noSuchPath"));
56+
}
6157
return new AddOperation(path, dupData).apply(node);
6258
}
6359
}

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

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,30 @@
2525
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2626
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
2727
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
28-
import com.github.fge.jackson.jsonpointer.JsonPointer;
2928

3029
import java.io.IOException;
3130

3231
/**
3332
* Base class for JSON Patch operations taking two JSON Pointers as arguments
3433
*/
35-
public abstract class DualPathOperation
36-
extends JsonPatchOperation
37-
{
34+
public abstract class DualPathOperation extends JsonPatchOperation {
3835
@JsonSerialize(using = ToStringSerializer.class)
39-
protected final JsonPointer from;
36+
protected final String from;
4037

4138
/**
4239
* Protected constructor
4340
*
44-
* @param op operation name
41+
* @param op operation name
4542
* @param from source path
4643
* @param path destination path
4744
*/
48-
protected DualPathOperation(final String op, final JsonPointer from,
49-
final JsonPointer path)
50-
{
45+
protected DualPathOperation(final String op, final String from, final String path) {
5146
super(op, path);
5247
this.from = from;
5348
}
5449

5550
@Override
56-
public final void serialize(final JsonGenerator jgen,
57-
final SerializerProvider provider)
58-
throws IOException, JsonProcessingException
59-
{
51+
public final void serialize(final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
6052
jgen.writeStartObject();
6153
jgen.writeStringField("op", op);
6254
jgen.writeStringField("path", path.toString());
@@ -65,20 +57,17 @@ public final void serialize(final JsonGenerator jgen,
6557
}
6658

6759
@Override
68-
public final void serializeWithType(final JsonGenerator jgen,
69-
final SerializerProvider provider, final TypeSerializer typeSer)
70-
throws IOException, JsonProcessingException
71-
{
60+
public final void serializeWithType(final JsonGenerator jgen, final SerializerProvider provider,
61+
final TypeSerializer typeSer) throws IOException, JsonProcessingException {
7262
serialize(jgen, provider);
7363
}
7464

75-
public final JsonPointer getFrom() {
65+
public final String getFrom() {
7666
return from;
7767
}
7868

7969
@Override
80-
public final String toString()
81-
{
70+
public final String toString() {
8271
return "op: " + op + "; from: \"" + from + "\"; path: \"" + path + '"';
8372
}
8473
}

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

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,28 @@
2727
import com.github.fge.jackson.jsonpointer.JsonPointer;
2828
import com.github.fge.msgsimple.bundle.MessageBundle;
2929
import com.github.fge.msgsimple.load.MessageBundles;
30+
import com.jayway.jsonpath.Configuration;
31+
import com.jayway.jsonpath.Option;
32+
import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider;
33+
import com.jayway.jsonpath.spi.json.JsonProvider;
34+
import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
35+
import com.jayway.jsonpath.spi.mapper.MappingProvider;
36+
37+
import java.util.EnumSet;
38+
import java.util.Set;
3039

3140
import static com.fasterxml.jackson.annotation.JsonSubTypes.*;
3241
import static com.fasterxml.jackson.annotation.JsonTypeInfo.*;
3342

3443
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "op")
3544

3645
@JsonSubTypes({
37-
@Type(name = "add", value = AddOperation.class),
38-
@Type(name = "copy", value = CopyOperation.class),
39-
@Type(name = "move", value = MoveOperation.class),
40-
@Type(name = "remove", value = RemoveOperation.class),
41-
@Type(name = "replace", value = ReplaceOperation.class),
42-
@Type(name = "test", value = TestOperation.class)
46+
@Type(name = "add", value = AddOperation.class),
47+
@Type(name = "copy", value = CopyOperation.class),
48+
@Type(name = "move", value = MoveOperation.class),
49+
@Type(name = "remove", value = RemoveOperation.class),
50+
@Type(name = "replace", value = ReplaceOperation.class),
51+
@Type(name = "test", value = TestOperation.class)
4352
})
4453

4554
/**
@@ -56,11 +65,27 @@
5665
* </ul>
5766
*/
5867
@JsonIgnoreProperties(ignoreUnknown = true)
59-
public abstract class JsonPatchOperation
60-
implements JsonSerializable
61-
{
62-
protected static final MessageBundle BUNDLE
63-
= MessageBundles.getBundle(JsonPatchMessages.class);
68+
public abstract class JsonPatchOperation implements JsonSerializable {
69+
protected static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class);
70+
71+
static {
72+
Configuration.setDefaults(new Configuration.Defaults() {
73+
@Override
74+
public JsonProvider jsonProvider() {
75+
return new JacksonJsonNodeJsonProvider();
76+
}
77+
78+
@Override
79+
public Set<Option> options() {
80+
return EnumSet.of(Option.SUPPRESS_EXCEPTIONS);
81+
}
82+
83+
@Override
84+
public MappingProvider mappingProvider() {
85+
return new JacksonMappingProvider();
86+
}
87+
});
88+
}
6489

6590
protected final String op;
6691

@@ -70,16 +95,15 @@ public abstract class JsonPatchOperation
7095
*
7196
* However, we need to serialize using .toString().
7297
*/
73-
protected final JsonPointer path;
98+
protected final String path;
7499

75100
/**
76101
* Constructor
77102
*
78-
* @param op the operation name
103+
* @param op the operation name
79104
* @param path the JSON Pointer for this operation
80105
*/
81-
protected JsonPatchOperation(final String op, final JsonPointer path)
82-
{
106+
protected JsonPatchOperation(final String op, final String path) {
83107
this.op = op;
84108
this.path = path;
85109
}
@@ -91,14 +115,13 @@ protected JsonPatchOperation(final String op, final JsonPointer path)
91115
* @return the patched value
92116
* @throws JsonPatchException operation failed to apply to this value
93117
*/
94-
public abstract JsonNode apply(final JsonNode node)
95-
throws JsonPatchException;
118+
public abstract JsonNode apply(final JsonNode node) throws JsonPatchException;
96119

97120
public final String getOp() {
98121
return op;
99122
}
100123

101-
public final JsonPointer getPath() {
124+
public final String getPath() {
102125
return path;
103126
}
104127

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.github.fge.jsonpatch;
2+
3+
public class JsonPathParser {
4+
5+
private static final String ARRAY_ELEMENT_REGEX = "\\.(\\d+)\\.";
6+
private static final String ARRAY_ELEMENT_LAST_REGEX = "\\.(\\d+)$";
7+
8+
public static String tmfStringToJsonPath(String path) {
9+
if ("/".equals(path)) {
10+
return "$";
11+
}
12+
final String jsonPath = "$" + path.replace('/', '.')
13+
.replaceAll(ARRAY_ELEMENT_REGEX, ".[$1].")
14+
.replaceAll(ARRAY_ELEMENT_LAST_REGEX, ".[$1]");
15+
return jsonPath;
16+
}
17+
}

0 commit comments

Comments
 (0)