diff --git a/client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java b/client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java
index 4e0888651d..d7743bb6d5 100644
--- a/client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java
+++ b/client/java-armeria/src/main/java/com/linecorp/centraldogma/internal/client/armeria/ArmeriaCentralDogma.java
@@ -105,6 +105,8 @@
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.common.RevisionNotFoundException;
import com.linecorp.centraldogma.common.ShuttingDownException;
+import com.linecorp.centraldogma.common.TextPatchConflictException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.internal.HistoryConstants;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.Util;
@@ -137,6 +139,8 @@ public final class ArmeriaCentralDogma extends AbstractCentralDogma {
.put(ReadOnlyException.class.getName(), ReadOnlyException::new)
.put(MirrorException.class.getName(), MirrorException::new)
.put(PermissionException.class.getName(), PermissionException::new)
+ .put(JsonPatchConflictException.class.getName(), JsonPatchConflictException::new)
+ .put(TextPatchConflictException.class.getName(), TextPatchConflictException::new)
.build();
private final WebClient client;
diff --git a/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java b/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java
index 2fd4683b14..b891541c54 100644
--- a/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java
+++ b/client/java/src/main/java/com/linecorp/centraldogma/client/CentralDogmaRepository.java
@@ -55,11 +55,17 @@ CentralDogma centralDogma() {
return centralDogma;
}
- String projectName() {
+ /**
+ * Returns the name of the project.
+ */
+ public String projectName() {
return projectName;
}
- String repositoryName() {
+ /**
+ * Returns the name of the repository.
+ */
+ public String repositoryName() {
return repositoryName;
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/Change.java b/common/src/main/java/com/linecorp/centraldogma/common/Change.java
index 06c57432f9..30aceaad5c 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/Change.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/Change.java
@@ -36,7 +36,9 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.google.common.collect.ImmutableList;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.Util;
import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
@@ -223,6 +225,42 @@ static Change JSON
+ * Patch, as its name implies, is an IETF draft describing a mechanism to
+ * apply a patch to any JSON value. This implementation covers all operations
+ * according to the specification; however, there are some subtle differences
+ * with regards to some operations which are covered in these operations'
+ * respective documentation. An example of a JSON Patch is as follows: This patch contains a single operation which adds an item at the end of
+ * an array. A JSON Patch can contain more than one operation; in this case, all
+ * operations are applied to the input JSON value in their order of appearance,
+ * until all operations are applied or an error condition is encountered. Note that this operation will throw an exception if the path does not exist.
+ *
+ * @param path the JSON Pointer to remove
+ */
+ public static RemoveOperation remove(JsonPointer path) {
+ return new RemoveOperation(path);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code remove} operation.
+ *
+ * Note that this operation will throw an exception if the path does not exist.
+ *
+ * @param path the JSON Pointer to remove
+ */
+ public static RemoveOperation remove(String path) {
+ requireNonNull(path, "path");
+ return remove(JsonPointer.compile(path));
+ }
+
+ /**
+ * Creates a new JSON Patch {@code removeIfExists} operation.
+ *
+ * @param path the JSON Pointer to remove if it exists
+ */
+ public static RemoveIfExistsOperation removeIfExists(JsonPointer path) {
+ return new RemoveIfExistsOperation(path);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code removeIfExists} operation.
+ *
+ * @param path the JSON Pointer to remove if it exists
+ */
+ public static RemoveIfExistsOperation removeIfExists(String path) {
+ requireNonNull(path, "path");
+ return removeIfExists(JsonPointer.compile(path));
+ }
+
+ /**
+ * Creates a new JSON Patch {@code replace} operation.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param value the new value to replace the existing value
+ */
+ public static ReplaceOperation replace(JsonPointer path, JsonNode value) {
+ return new ReplaceOperation(path, value);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code replace} operation.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param value the new value to replace the existing value
+ */
+ public static ReplaceOperation replace(String path, JsonNode value) {
+ requireNonNull(path, "path");
+ return replace(JsonPointer.compile(path), value);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code safeReplace} operation.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param oldValue the old value to replace
+ * @param newValue the new value to replace the old value
+ */
+ public static SafeReplaceOperation safeReplace(JsonPointer path, JsonNode oldValue, JsonNode newValue) {
+ return new SafeReplaceOperation(path, oldValue, newValue);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code safeReplace} operation.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param oldValue the old value to replace
+ * @param newValue the new value to replace the old value
+ */
+ public static SafeReplaceOperation safeReplace(String path, JsonNode oldValue, JsonNode newValue) {
+ requireNonNull(path, "path");
+ return safeReplace(JsonPointer.compile(path), oldValue, newValue);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code test} operation.
+ *
+ * This operation will throw an exception if the value at the path does not match the expected value.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param value the value to test
+ */
+ public static TestOperation test(JsonPointer path, JsonNode value) {
+ return new TestOperation(path, value);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code test} operation.
+ *
+ * This operation will throw an exception if the value at the path does not match the expected value.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param value the value to test
+ */
+ public static TestOperation test(String path, JsonNode value) {
+ requireNonNull(path, "path");
+ return test(JsonPointer.compile(path), value);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code testAbsent} operation.
+ *
+ * This operation will throw an exception if the value at the path exists.
+ *
+ * @param path the JSON Pointer for this operation
+ */
+ public static TestAbsenceOperation testAbsence(JsonPointer path) {
+ return new TestAbsenceOperation(path);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code testAbsent} operation.
+ *
+ * This operation will throw an exception if the value at the path exists.
+ *
+ * @param path the JSON Pointer for this operation
+ */
+ public static TestAbsenceOperation testAbsence(String path) {
+ requireNonNull(path, "path");
+ return testAbsence(JsonPointer.compile(path));
+ }
+
+ /**
+ * Converts {@link JsonPatchOperation}s to an array of {@link JsonNode}.
+ */
+ public static JsonNode asJsonArray(JsonPatchOperation... jsonPatchOperations) {
+ requireNonNull(jsonPatchOperations, "jsonPatchOperations");
+ return Jackson.valueToTree(jsonPatchOperations);
+ }
+
+ /**
+ * Converts {@link JsonPatchOperation}s to an array of {@link JsonNode}.
+ */
+ public static JsonNode asJsonArray(Iterable extends JsonPatchOperation> jsonPatchOperations) {
+ requireNonNull(jsonPatchOperations, "jsonPatchOperations");
+ return Jackson.valueToTree(jsonPatchOperations);
+ }
+
+ private final String op;
+
+ /*
+ * Note: no need for a custom deserializer, Jackson will try and find a
+ * constructor with a single string argument and use it.
+ *
+ * However, we need to serialize using .toString().
+ */
+ private final JsonPointer path;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param op the operation name
+ * @param path the JSON Pointer for this operation
+ */
+ JsonPatchOperation(final String op, final JsonPointer path) {
+ this.op = requireNonNull(op, "op");
+ this.path = requireNonNull(path, "path");
+ }
+
+ /**
+ * Returns the operation name.
+ */
+ public final String op() {
+ return op;
+ }
+
+ /**
+ * Returns the JSON Pointer for this operation.
+ */
+ public final JsonPointer path() {
+ return path;
+ }
+
+ /**
+ * Applies this operation to a JSON value.
+ *
+ * @param node the value to patch
+ * @return the patched value
+ * @throws JsonPatchConflictException operation failed to apply to this value
+ */
+ public abstract JsonNode apply(JsonNode node);
+
+ /**
+ * Converts this {@link JsonPatchOperation} to a {@link JsonNode}.
+ */
+ public JsonNode toJsonNode() {
+ return JsonNodeFactory.instance.arrayNode().add(Jackson.valueToTree(this));
+ }
+
+ JsonNode ensureExistence(JsonNode node) {
+ final JsonNode found = node.at(path);
+ if (found.isMissingNode()) {
+ throw new JsonPatchConflictException("non-existent path: " + path);
+ }
+ return found;
+ }
+
+ static JsonNode ensureSourceParent(JsonNode node, JsonPointer path) {
+ return ensureParent(node, path, "source");
+ }
+
+ static JsonNode ensureTargetParent(JsonNode node, JsonPointer path) {
+ return ensureParent(node, path, "target");
+ }
+
+ private static JsonNode ensureParent(JsonNode node, JsonPointer path, String typeName) {
+ /*
+ * Check the parent node: it must exist and be a container (ie an array
+ * or an object) for the add operation to work.
+ */
+ final JsonPointer parentPath = path.head();
+ final JsonNode parentNode = node.at(parentPath);
+ if (parentNode.isMissingNode()) {
+ throw new JsonPatchConflictException("non-existent " + typeName + " parent: " + parentPath);
+ }
+ if (!parentNode.isContainerNode()) {
+ throw new JsonPatchConflictException(typeName + " parent is not a container: " + parentPath +
+ " (" + parentNode.getNodeType() + ')');
+ }
+ return parentNode;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof JsonPatchOperation)) {
+ return false;
+ }
+ final JsonPatchOperation that = (JsonPatchOperation) o;
+ return op.equals(that.op) && path.equals(that.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(op, path);
+ }
+
+ @Override
+ public abstract String toString();
+}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/MoveOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
similarity index 89%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/MoveOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
index ce4670265d..4bdc6f0a98 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/MoveOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
@@ -32,7 +32,9 @@
* - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
*/
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -73,6 +75,9 @@
*/
public final class MoveOperation extends DualPathOperation {
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
MoveOperation(@JsonProperty("from") final JsonPointer from,
@JsonProperty("path") final JsonPointer path) {
@@ -80,12 +85,15 @@ public final class MoveOperation extends DualPathOperation {
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
+ final JsonPointer from = from();
+ final JsonPointer path = path();
if (from.equals(path)) {
return node;
}
if (node.at(from).isMissingNode()) {
- throw new JsonPatchException("non-existent source path: " + from);
+ throw new JsonPatchConflictException("non-existent source path: " + from);
}
final JsonNode sourceParent = ensureSourceParent(node, from);
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/PathValueOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
similarity index 74%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/PathValueOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
index fbb1a16bf7..b7fccf5c37 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/PathValueOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
@@ -32,9 +32,12 @@
* - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
*/
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static java.util.Objects.requireNonNull;
import java.io.IOException;
+import java.util.Objects;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonPointer;
@@ -44,6 +47,8 @@
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.google.common.base.Equivalence;
+import com.linecorp.centraldogma.internal.jsonpatch.JsonNumEquals;
+
/**
* Base class for patch operations taking a value in addition to a path.
*/
@@ -63,9 +68,13 @@ abstract class PathValueOperation extends JsonPatchOperation {
*/
PathValueOperation(final String op, final JsonPointer path, final JsonNode value) {
super(op, path);
+ requireNonNull(value, "value");
this.value = value.deepCopy();
}
+ /**
+ * Returns the JSON value.
+ */
public JsonNode value() {
return value;
}
@@ -76,17 +85,18 @@ JsonNode valueCopy() {
void ensureEquivalence(JsonNode actual) {
if (!EQUIVALENCE.equivalent(actual, value)) {
- throw new JsonPatchException("mismatching value at '" + path + "': " +
- actual + " (expected: " + value + ')');
+ throw new JsonPatchConflictException("mismatching value at '" + path() + "': " +
+ actual + " (expected: " + value + ')');
}
}
@Override
public final void serialize(final JsonGenerator jgen,
final SerializerProvider provider) throws IOException {
+ requireNonNull(jgen, "jgen");
jgen.writeStartObject();
- jgen.writeStringField("op", op);
- jgen.writeStringField("path", path.toString());
+ jgen.writeStringField("op", op());
+ jgen.writeStringField("path", path().toString());
jgen.writeFieldName("value");
jgen.writeTree(value);
jgen.writeEndObject();
@@ -99,8 +109,25 @@ public final void serializeWithType(final JsonGenerator jgen,
serialize(jgen, provider);
}
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof PathValueOperation)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ final PathValueOperation that = (PathValueOperation) o;
+ return value.equals(that.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), value);
+ }
+
@Override
public final String toString() {
- return "op: " + op + "; path: \"" + path + "\"; value: " + value;
+ return "op: " + op() + "; path: \"" + path() + "\"; value: " + value;
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java
similarity index 82%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java
index 7ef9189c70..0e6a33046c 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java
@@ -14,7 +14,9 @@
* under the License.
*/
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static java.util.Objects.requireNonNull;
import java.io.IOException;
@@ -37,13 +39,18 @@
*/
public final class RemoveIfExistsOperation extends JsonPatchOperation {
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
- public RemoveIfExistsOperation(@JsonProperty("path") final JsonPointer path) {
+ RemoveIfExistsOperation(@JsonProperty("path") final JsonPointer path) {
super("removeIfExists", path);
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
+ final JsonPointer path = path();
if (path.toString().isEmpty()) {
return MissingNode.getInstance();
}
@@ -66,9 +73,10 @@ JsonNode apply(final JsonNode node) {
@Override
public void serialize(final JsonGenerator jgen,
final SerializerProvider provider) throws IOException {
+ requireNonNull(jgen, "jgen");
jgen.writeStartObject();
- jgen.writeStringField("op", "removeIfExists");
- jgen.writeStringField("path", path.toString());
+ jgen.writeStringField("op", op());
+ jgen.writeStringField("path", path().toString());
jgen.writeEndObject();
}
@@ -81,6 +89,6 @@ public void serializeWithType(final JsonGenerator jgen,
@Override
public String toString() {
- return "op: " + op + "; path: \"" + path + '"';
+ return "op: " + op() + "; path: \"" + path() + '"';
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java
similarity index 86%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java
index 6ba0e3142b..8eb98e1c9a 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java
@@ -32,7 +32,9 @@
* - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
*/
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static java.util.Objects.requireNonNull;
import java.io.IOException;
@@ -55,13 +57,18 @@
*/
public final class RemoveOperation extends JsonPatchOperation {
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
- public RemoveOperation(@JsonProperty("path") final JsonPointer path) {
+ RemoveOperation(@JsonProperty("path") final JsonPointer path) {
super("remove", path);
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
+ final JsonPointer path = path();
if (path.toString().isEmpty()) {
return MissingNode.getInstance();
}
@@ -80,9 +87,10 @@ JsonNode apply(final JsonNode node) {
@Override
public void serialize(final JsonGenerator jgen,
final SerializerProvider provider) throws IOException {
+ requireNonNull(jgen, "jgen");
jgen.writeStartObject();
jgen.writeStringField("op", "remove");
- jgen.writeStringField("path", path.toString());
+ jgen.writeStringField("path", path().toString());
jgen.writeEndObject();
}
@@ -95,6 +103,6 @@ public void serializeWithType(final JsonGenerator jgen,
@Override
public String toString() {
- return "op: " + op + "; path: \"" + path + '"';
+ return "op: " + op() + "; path: \"" + path() + '"';
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/ReplaceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java
similarity index 87%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/ReplaceOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java
index 0c77940c3d..b75ac120ba 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/ReplaceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java
@@ -32,7 +32,9 @@
* - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
*/
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -53,15 +55,17 @@
public final class ReplaceOperation extends PathValueOperation {
@JsonCreator
- public ReplaceOperation(@JsonProperty("path") final JsonPointer path,
- @JsonProperty("value") final JsonNode value) {
+ ReplaceOperation(@JsonProperty("path") final JsonPointer path,
+ @JsonProperty("value") final JsonNode value) {
super("replace", path, value);
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
ensureExistence(node);
+ final JsonPointer path = path();
final JsonNode replacement = valueCopy();
if (path.toString().isEmpty()) {
return replacement;
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/SafeReplaceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
similarity index 63%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/SafeReplaceOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
index 56ed5d3f50..f9f7020798 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/SafeReplaceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
@@ -14,9 +14,12 @@
* under the License.
*/
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static java.util.Objects.requireNonNull;
import java.io.IOException;
+import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -30,6 +33,14 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Equivalence;
+import com.linecorp.centraldogma.internal.jsonpatch.JsonNumEquals;
+
+/**
+ * JSON Patch {@code safeReplace} operation.
+ *
+ * This operation is similar to {@link ReplaceOperation}, but it throws an error if the path does not have
+ * the expected value. For this operation, {@code path} points to the value to test absence. It is an error condition if {@code path} points to an actual JSON value. It is an error if no value exists at the given path. Also note that equality as defined by JSON Patch is exactly the same as it
- * is defined by JSON Schema itself. As such, this operation reuses {@link
- * JsonNumEquals} for testing equality. As its name implies, JSON Patch is a mechanism designed to modify JSON
+ * documents. It consists of a series of operations to apply in order to the
+ * source JSON document until all operations are applied or an error has been
+ * encountered.
+ * [
+ * {
+ * "op": "add",
+ * "path": "/-",
+ * "value": {
+ * "productId": 19,
+ * "name": "Duvel",
+ * "type": "beer"
+ * }
+ * }
+ * ]
+ *
+ *
+ *
JSON
+ * JSON
* Patch, as its name implies, is an IETF draft describing a mechanism to
* apply a patch to any JSON value. This implementation covers all operations
* according to the specification; however, there are some subtle differences
@@ -139,7 +141,7 @@ public static JsonPatch fromJson(final JsonNode node) throws IOException {
try {
return Jackson.treeToValue(node, JsonPatch.class);
} catch (JsonMappingException e) {
- throw new JsonPatchException("invalid JSON patch", e);
+ throw new JsonPatchConflictException("invalid JSON patch", e);
}
}
@@ -339,7 +341,7 @@ public List Two more abstract classes extend this one according to the arguments of
- * the operation: As its name implies, JSON Patch is a mechanism designed to modify JSON
- * documents. It consists of a series of operations to apply in order to the
- * source JSON document until all operations are applied or an error has been
- * encountered. The main class is {@link com.linecorp.centraldogma.internal.jsonpatch.JsonPatch}. Note that at this moment, the only way to build a patch is from a JSON
- * representation (as a {@link com.fasterxml.jackson.databind.JsonNode}).
- *
- */
-@JsonTypeInfo(use = Id.NAME, property = "op")
-@JsonSubTypes({
- @Type(name = "add", value = AddOperation.class),
- @Type(name = "copy", value = CopyOperation.class),
- @Type(name = "move", value = MoveOperation.class),
- @Type(name = "remove", value = RemoveOperation.class),
- @Type(name = "removeIfExists", value = RemoveIfExistsOperation.class),
- @Type(name = "replace", value = ReplaceOperation.class),
- @Type(name = "safeReplace", value = SafeReplaceOperation.class),
- @Type(name = "test", value = TestOperation.class),
- @Type(name = "testAbsence", value = TestAbsenceOperation.class)
-})
-@JsonIgnoreProperties(ignoreUnknown = true)
-public abstract class JsonPatchOperation implements JsonSerializable {
-
- /**
- * Converts {@link JsonPatchOperation}s to an array of {@link JsonNode}.
- */
- public static JsonNode asJsonArray(JsonPatchOperation... jsonPatchOperations) {
- requireNonNull(jsonPatchOperations, "jsonPatchOperations");
- return Jackson.valueToTree(jsonPatchOperations);
- }
-
- final String op;
-
- /*
- * Note: no need for a custom deserializer, Jackson will try and find a
- * constructor with a single string argument and use it.
- *
- * However, we need to serialize using .toString().
- */
- final JsonPointer path;
-
- /**
- * Creates a new instance.
- *
- * @param op the operation name
- * @param path the JSON Pointer for this operation
- */
- JsonPatchOperation(final String op, final JsonPointer path) {
- this.op = op;
- this.path = path;
- }
-
- public JsonPointer path() {
- return path;
- }
-
- /**
- * Applies this operation to a JSON value.
- *
- * @param node the value to patch
- * @return the patched value
- * @throws JsonPatchException operation failed to apply to this value
- */
- abstract JsonNode apply(JsonNode node);
-
- @Override
- public abstract String toString();
-
- /**
- * Converts this {@link JsonPatchOperation} to a {@link JsonNode}.
- */
- public JsonNode toJsonNode() {
- return JsonNodeFactory.instance.arrayNode().add(Jackson.valueToTree(this));
- }
-
- JsonNode ensureExistence(JsonNode node) {
- final JsonNode found = node.at(path);
- if (found.isMissingNode()) {
- throw new JsonPatchException("non-existent path: " + path);
- }
- return found;
- }
-
- static JsonNode ensureSourceParent(JsonNode node, JsonPointer path) {
- return ensureParent(node, path, "source");
- }
-
- static JsonNode ensureTargetParent(JsonNode node, JsonPointer path) {
- return ensureParent(node, path, "target");
- }
-
- private static JsonNode ensureParent(JsonNode node, JsonPointer path, String typeName) {
- /*
- * Check the parent node: it must exist and be a container (ie an array
- * or an object) for the add operation to work.
- */
- final JsonPointer parentPath = path.head();
- final JsonNode parentNode = node.at(parentPath);
- if (parentNode.isMissingNode()) {
- throw new JsonPatchException("non-existent " + typeName + " parent: " + parentPath);
- }
- if (!parentNode.isContainerNode()) {
- throw new JsonPatchException(typeName + " parent is not a container: " + parentPath +
- " (" + parentNode.getNodeType() + ')');
- }
- return parentNode;
- }
-}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/package-info.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/package-info.java
index 3a440b3ae5..d06019d29c 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/package-info.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/package-info.java
@@ -31,20 +31,8 @@
* - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
* - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
*/
-
/**
- * Implementation of JSON Patch.
- *
- *