From fa878be144a07cc0414cf395725d8341c7bf8014 Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Wed, 15 Jan 2025 00:17:19 +0900
Subject: [PATCH 1/7] Expose JSON Patch operations as public API
---
.../client/armeria/ArmeriaCentralDogma.java | 5 +-
.../client/CentralDogmaRepository.java | 10 +-
.../linecorp/centraldogma/common/Change.java | 38 +++
.../jsonpatch/AddOperation.java | 35 +--
.../jsonpatch/CopyOperation.java | 32 +-
.../jsonpatch/DualPathOperation.java | 60 ++--
.../jsonpatch/JsonPatchException.java | 35 +--
.../common/jsonpatch/JsonPatchOperation.java | 293 ++++++++++++++++++
.../jsonpatch/MoveOperation.java | 32 +-
.../jsonpatch/PathValueOperation.java | 57 ++--
.../jsonpatch/RemoveIfExistsOperation.java | 22 +-
.../jsonpatch/RemoveOperation.java | 38 +--
.../jsonpatch/ReplaceOperation.java | 32 +-
.../jsonpatch/SafeReplaceOperation.java | 62 +++-
.../jsonpatch/TestAbsenceOperation.java | 29 +-
.../jsonpatch/TestOperation.java | 37 +--
.../common/jsonpatch/package-info.java | 34 ++
.../internal/jsonpatch/DiffProcessor.java | 14 +-
.../internal/jsonpatch/JsonNumEquals.java | 2 +-
.../internal/jsonpatch/JsonPatch.java | 2 +
.../jsonpatch/JsonPatchOperation.java | 163 ----------
.../internal/jsonpatch/package-info.java | 14 +-
.../JsonPatchOperationIntegrationTest.java | 287 +++++++++++++++++
.../JsonPatchOperationSerializationTest.java | 10 +
.../jsonpatch/JsonPatchOperationTest.java | 3 +
.../internal/jsonpatch/JsonPatchTest.java | 3 +
.../jsonpatch/JsonPatchTestSuite.java | 2 +
.../RemoveIfExistsOperationTest.java | 4 +-
.../jsonpatch/RemoveOperationTest.java | 4 +-
.../internal/api/HttpApiExceptionHandler.java | 3 +
.../internal/api/MetadataApiService.java | 4 +-
.../repository/git/DefaultChangesApplier.java | 3 +-
.../storage/repository/git/GitRepository.java | 10 +-
.../server/metadata/MetadataService.java | 105 ++++---
.../server/metadata/TokenTest.java | 13 +-
35 files changed, 1017 insertions(+), 480 deletions(-)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/AddOperation.java (79%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/CopyOperation.java (70%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/DualPathOperation.java (67%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/JsonPatchException.java (52%)
create mode 100644 common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/MoveOperation.java (79%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/PathValueOperation.java (72%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/RemoveIfExistsOperation.java (81%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/RemoveOperation.java (72%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/ReplaceOperation.java (66%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/SafeReplaceOperation.java (66%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/TestAbsenceOperation.java (69%)
rename common/src/main/java/com/linecorp/centraldogma/{internal => common}/jsonpatch/TestOperation.java (57%)
create mode 100644 common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
delete mode 100644 common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperation.java
create mode 100644 common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
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..49a6d02944 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,7 @@
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.common.RevisionNotFoundException;
import com.linecorp.centraldogma.common.ShuttingDownException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
import com.linecorp.centraldogma.internal.HistoryConstants;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.Util;
@@ -137,6 +138,7 @@ 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(JsonPatchException.class.getName(), JsonPatchException::new)
.build();
private final WebClient client;
@@ -153,7 +155,8 @@ public ArmeriaCentralDogma(ScheduledExecutorService blockingTaskExecutor,
@Override
public CompletableFuture whenEndpointReady() {
- return client.endpointGroup().whenReady().thenRun(() -> {});
+ return client.endpointGroup().whenReady().thenRun(() -> {
+ });
}
@Override
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 ofJsonPatch(String path, @Nullable JsonNode oldJsonNode,
JsonPatch.generate(oldJsonNode, newJsonNode, ReplaceMode.SAFE).toJson());
}
+ /**
+ * Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
+ *
+ * @param path the path of the file
+ * @param jsonPatch the patch in JSON patch format
+ */
+ static Change ofJsonPatch(String path, JsonPatchOperation jsonPatch) {
+ requireNonNull(path, "path");
+ requireNonNull(jsonPatch, "jsonPatch");
+ return new DefaultChange<>(path, ChangeType.APPLY_JSON_PATCH, jsonPatch.toJsonNode());
+ }
+
+ /**
+ * Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
+ *
+ * @param path the path of the file
+ * @param jsonPatches the list of patches in JSON patch format
+ */
+ static Change ofJsonPatch(String path, JsonPatchOperation... jsonPatches) {
+ requireNonNull(jsonPatches, "jsonPatches");
+ return ofJsonPatch(path, ImmutableList.copyOf(jsonPatches));
+ }
+
+ /**
+ * Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
+ *
+ * @param path the path of the file
+ * @param jsonPatches the list of patches in JSON patch format
+ */
+ static Change ofJsonPatch(String path, Iterable extends JsonPatchOperation> jsonPatches) {
+ requireNonNull(path, "path");
+ requireNonNull(jsonPatches, "jsonPatches");
+ return new DefaultChange<>(path, ChangeType.APPLY_JSON_PATCH,
+ JsonPatchOperation.asJsonArray(jsonPatches));
+ }
+
/**
* Returns a newly-created {@link Change} whose type is {@link ChangeType#APPLY_JSON_PATCH}.
*
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/AddOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
similarity index 79%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/AddOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
index e70b37496f..f6cf5a0a15 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/AddOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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;
@@ -79,14 +63,19 @@ public final class AddOperation extends PathValueOperation {
private static final String LAST_ARRAY_ELEMENT = "-";
+ /**
+ * Creates a new instance with the specified {@code path} and {@code value}.
+ */
@JsonCreator
- public AddOperation(@JsonProperty("path") final JsonPointer path,
- @JsonProperty("value") final JsonNode value) {
+ AddOperation(@JsonProperty("path") final JsonPointer path,
+ @JsonProperty("value") final JsonNode value) {
super("add", path, value);
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
+ final JsonPointer path = path();
if (path.toString().isEmpty()) {
return valueCopy();
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/CopyOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
similarity index 70%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/CopyOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
index 5923b671d6..736d7aa901 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/CopyOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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;
@@ -49,6 +33,9 @@
*/
public final class CopyOperation extends DualPathOperation {
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
CopyOperation(@JsonProperty("from") final JsonPointer from,
@JsonProperty("path") final JsonPointer path) {
@@ -56,12 +43,15 @@ public final class CopyOperation extends DualPathOperation {
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
+ final JsonPointer from = from();
JsonNode source = node.at(from);
if (source.isMissingNode()) {
throw new JsonPatchException("non-existent source path: " + from);
}
+ final JsonPointer path = path();
if (path.toString().isEmpty()) {
return source;
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DualPathOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java
similarity index 67%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DualPathOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java
index eaa1efd71b..174cba02b9 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DualPathOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,28 +13,13 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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;
@@ -49,7 +34,7 @@
abstract class DualPathOperation extends JsonPatchOperation {
@JsonSerialize(using = ToStringSerializer.class)
- final JsonPointer from;
+ private final JsonPointer from;
/**
* Creates a new instance.
@@ -60,15 +45,23 @@ abstract class DualPathOperation extends JsonPatchOperation {
*/
DualPathOperation(final String op, final JsonPointer from, final JsonPointer path) {
super(op, path);
- this.from = from;
+ this.from = requireNonNull(from, "from");
+ }
+
+ /**
+ * Returns the source path.
+ */
+ public JsonPointer from() {
+ return from;
}
@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.writeStringField("from", from.toString());
jgen.writeEndObject();
}
@@ -80,8 +73,25 @@ public final void serializeWithType(final JsonGenerator jgen,
serialize(jgen, provider);
}
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof DualPathOperation)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ final DualPathOperation that = (DualPathOperation) o;
+ return from.equals(that.from);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), from);
+ }
+
@Override
public final String toString() {
- return "op: " + op + "; from: \"" + from + "\"; path: \"" + path + '"';
+ return "op: " + op() + "; from: \"" + from + "\"; path: \"" + path() + '"';
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchException.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchException.java
similarity index 52%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchException.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchException.java
index aeb102d06f..4423249cfb 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchException.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,35 +13,28 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
- */
-package com.linecorp.centraldogma.internal.jsonpatch;
+package com.linecorp.centraldogma.common.jsonpatch;
-public final class JsonPatchException extends RuntimeException {
+import com.linecorp.centraldogma.common.CentralDogmaException;
+
+/**
+ * An exception raised when a JSON Patch operation fails.
+ */
+public final class JsonPatchException extends CentralDogmaException {
private static final long serialVersionUID = 4746173383862473527L;
+ /**
+ * Creates a new instance.
+ */
public JsonPatchException(final String message) {
super(message);
}
+ /**
+ * Creates a new instance with the specified {@code cause}.
+ */
public JsonPatchException(final String message, final Throwable cause) {
super(message, cause);
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
new file mode 100644
index 0000000000..1e35ad677e
--- /dev/null
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2025 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static com.fasterxml.jackson.annotation.JsonSubTypes.Type;
+import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
+import static java.util.Objects.requireNonNull;
+
+import java.util.Objects;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.JsonSerializable;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+
+import com.linecorp.centraldogma.internal.Jackson;
+
+/**
+ * Base abstract class for one JSON
+ * Patch operation. A {@link JsonPatchOperation} can be converted into a JSON patch by calling
+ * {@link #toJsonNode()}.
+ *
+ * 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:
+ *
+ *
+ * [
+ * {
+ * "op": "add",
+ * "path": "/-",
+ * "value": {
+ * "productId": 19,
+ * "name": "Duvel",
+ * "type": "beer"
+ * }
+ * }
+ * ]
+ *
+ *
+ * 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.
+ */
+@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 {
+
+ /**
+ * Creates a new JSON Patch {@code add} operation.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param value the value to add
+ */
+ public static AddOperation add(JsonPointer path, JsonNode value) {
+ return new AddOperation(path, value);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code copy} operation.
+ *
+ * @param from the source JSON Pointer
+ * @param path the destination JSON Pointer
+ */
+ public static CopyOperation copy(JsonPointer from, JsonPointer path) {
+ return new CopyOperation(from, path);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code move} operation.
+ *
+ * @param from the source JSON Pointer
+ * @param path the destination JSON Pointer
+ */
+ public static MoveOperation move(JsonPointer from, JsonPointer path) {
+ return new MoveOperation(from, 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(JsonPointer path) {
+ return new RemoveOperation(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 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 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 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 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);
+ }
+
+ /**
+ * 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 JsonPatchException 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 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;
+ }
+
+ @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 79%
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..615ccc3715 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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 +57,9 @@
*/
public final class MoveOperation extends DualPathOperation {
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
MoveOperation(@JsonProperty("from") final JsonPointer from,
@JsonProperty("path") final JsonPointer path) {
@@ -80,7 +67,10 @@ 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;
}
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 72%
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..70fed8c043 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,28 +13,13 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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 +29,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 +50,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,7 +67,7 @@ JsonNode valueCopy() {
void ensureEquivalence(JsonNode actual) {
if (!EQUIVALENCE.equivalent(actual, value)) {
- throw new JsonPatchException("mismatching value at '" + path + "': " +
+ throw new JsonPatchException("mismatching value at '" + path() + "': " +
actual + " (expected: " + value + ')');
}
}
@@ -84,9 +75,10 @@ void ensureEquivalence(JsonNode actual) {
@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 +91,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 81%
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..125cc3b7f7 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -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 72%
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..8a4189c11c 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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 +39,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 +69,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 +85,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 66%
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..13b8c92f60 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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 +37,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 66%
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..2a5f3bf6e2 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -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.
+ */
public final class SafeReplaceOperation extends JsonPatchOperation {
private static final Equivalence EQUIVALENCE = JsonNumEquals.getInstance();
@@ -39,18 +50,40 @@ public final class SafeReplaceOperation extends JsonPatchOperation {
@JsonSerialize
private final JsonNode newValue;
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
SafeReplaceOperation(@JsonProperty("path") final JsonPointer path,
@JsonProperty("oldValue") JsonNode oldValue,
@JsonProperty("value") JsonNode newValue) {
super("safeReplace", path);
+ requireNonNull(oldValue, "oldValue");
+ requireNonNull(newValue, "newValue");
this.oldValue = oldValue.deepCopy();
this.newValue = newValue.deepCopy();
}
+ /**
+ * Returns the old value to be replaced.
+ */
+ public JsonNode oldValue() {
+ return oldValue;
+ }
+
+ /**
+ * Returns the new value to replace the old value.
+ */
+ public JsonNode newValue() {
+ return newValue;
+ }
+
@Override
- JsonNode apply(JsonNode node) {
+ public JsonNode apply(JsonNode node) {
+ requireNonNull(node, "node");
final JsonNode actual = ensureExistence(node);
+
+ final JsonPointer path = path();
if (!EQUIVALENCE.equivalent(actual, oldValue)) {
throw new JsonPatchException("mismatching value at '" + path + "': " +
actual + " (expected: " + oldValue + ')');
@@ -72,8 +105,8 @@ JsonNode apply(JsonNode node) {
@Override
public void serialize(JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
- gen.writeStringField("op", op);
- gen.writeStringField("path", path.toString());
+ gen.writeStringField("op", op());
+ gen.writeStringField("path", path().toString());
gen.writeFieldName("oldValue");
gen.writeTree(oldValue);
gen.writeFieldName("value");
@@ -87,8 +120,25 @@ public void serializeWithType(JsonGenerator gen, SerializerProvider serializers,
serialize(gen, serializers);
}
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof SafeReplaceOperation)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ final SafeReplaceOperation that = (SafeReplaceOperation) o;
+ return oldValue.equals(that.oldValue) && newValue.equals(that.newValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), oldValue, newValue);
+ }
+
@Override
public String toString() {
- return "op: " + op + "; path: \"" + path + "\"; oldValue: " + oldValue + "; value: " + newValue;
+ return "op: " + op() + "; path: \"" + path() + "\"; oldValue: " + oldValue + "; value: " + newValue;
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/TestAbsenceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
similarity index 69%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/TestAbsenceOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
index 646b48daf6..0e275f691a 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/TestAbsenceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2018 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -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;
@@ -26,14 +28,27 @@
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+/**
+ * JSON Patch {@code testAbsence} operation.
+ *
+ * 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.
+ */
public final class TestAbsenceOperation extends JsonPatchOperation {
+
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
- public TestAbsenceOperation(@JsonProperty("path") final JsonPointer path) {
+ TestAbsenceOperation(@JsonProperty("path") final JsonPointer path) {
super("testAbsence", path);
}
@Override
- JsonNode apply(JsonNode node) {
+ public JsonNode apply(JsonNode node) {
+ requireNonNull(node, "node");
+ final JsonPointer path = path();
final JsonNode found = node.at(path);
if (!found.isMissingNode()) {
throw new JsonPatchException("existent path: " + path);
@@ -44,8 +59,8 @@ JsonNode apply(JsonNode node) {
@Override
public void serialize(JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
- gen.writeStringField("op", op);
- gen.writeStringField("path", path.toString());
+ gen.writeStringField("op", op());
+ gen.writeStringField("path", path().toString());
gen.writeEndObject();
}
@@ -57,6 +72,6 @@ public void serializeWithType(JsonGenerator gen, SerializerProvider serializers,
@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/TestOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java
similarity index 57%
rename from common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/TestOperation.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java
index 10fb8f0752..b61edb2534 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/TestOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 LINE Corporation
+ * Copyright 2025 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - 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;
@@ -49,19 +33,22 @@
* 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.
+ * is defined by JSON Schema itself.
*/
public final class TestOperation extends PathValueOperation {
+ /**
+ * Creates a new instance.
+ */
@JsonCreator
- public TestOperation(@JsonProperty("path") final JsonPointer path,
- @JsonProperty("value") final JsonNode value) {
+ TestOperation(@JsonProperty("path") final JsonPointer path,
+ @JsonProperty("value") final JsonNode value) {
super("test", path, value);
}
@Override
- JsonNode apply(final JsonNode node) {
+ public JsonNode apply(final JsonNode node) {
+ requireNonNull(node, "node");
ensureEquivalence(ensureExistence(node));
return node;
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
new file mode 100644
index 0000000000..4de3a53296
--- /dev/null
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2025 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Implementation of JSON Patch.
+ *
+ * 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}).
+ *
+ */
+@NonNullByDefault
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import com.linecorp.centraldogma.common.util.NonNullByDefault;
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DiffProcessor.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DiffProcessor.java
index 55af1fb877..52133fc81d 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DiffProcessor.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/DiffProcessor.java
@@ -46,6 +46,8 @@
import com.google.common.base.Equivalence;
import com.google.common.base.Predicate;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+
// TODO: cleanup
final class DiffProcessor {
@@ -75,16 +77,16 @@ public Map get() {
void valueReplaced(final JsonPointer pointer, final JsonNode oldValue, final JsonNode newValue) {
switch (replaceMode) {
case RFC6902:
- diffs.add(new ReplaceOperation(pointer, newValue));
+ diffs.add(JsonPatchOperation.replace(pointer, newValue));
break;
case SAFE:
- diffs.add(new SafeReplaceOperation(pointer, oldValue, newValue));
+ diffs.add(JsonPatchOperation.safeReplace(pointer, oldValue, newValue));
break;
}
}
void valueRemoved(final JsonPointer pointer) {
- diffs.add(new RemoveOperation(pointer));
+ diffs.add(JsonPatchOperation.remove(pointer));
}
void valueAdded(final JsonPointer pointer, final JsonNode value) {
@@ -92,10 +94,10 @@ void valueAdded(final JsonPointer pointer, final JsonNode value) {
if (value.isContainerNode()) {
// Use copy operation only for container nodes.
final JsonPointer ptr = findUnchangedValue(value);
- op = ptr != null ? new CopyOperation(ptr, pointer)
- : new AddOperation(pointer, value);
+ op = ptr != null ? JsonPatchOperation.copy(ptr, pointer)
+ : JsonPatchOperation.add(pointer, value);
} else {
- op = new AddOperation(pointer, value);
+ op = JsonPatchOperation.add(pointer, value);
}
diffs.add(op);
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonNumEquals.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonNumEquals.java
index 0bde00dd95..3342969a32 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonNumEquals.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonNumEquals.java
@@ -56,7 +56,7 @@
* equal if their mathematical value is the same. This class implements this
* kind of equality.
*/
-final class JsonNumEquals extends Equivalence {
+public final class JsonNumEquals extends Equivalence {
private static final Equivalence INSTANCE = new JsonNumEquals();
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
index b05946ccd9..9a12244f53 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
@@ -62,6 +62,8 @@
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
import com.linecorp.centraldogma.internal.Jackson;
/**
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperation.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperation.java
deleted file mode 100644
index 50e2236d59..0000000000
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperation.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/*
- * Copyright 2017 LINE Corporation
- *
- * LINE Corporation licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-/*
- * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
- *
- * This software is dual-licensed under:
- *
- * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
- * later version;
- * - the Apache Software License (ASL) version 2.0.
- *
- * The text of this file and of both licenses is available at the root of this
- * project or, if you have the jar distribution, in directory META-INF/, under
- * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
- *
- * Direct link to the sources:
- *
- * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
- * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
- */
-
-package com.linecorp.centraldogma.internal.jsonpatch;
-
-import static com.fasterxml.jackson.annotation.JsonSubTypes.Type;
-import static com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
-import static java.util.Objects.requireNonNull;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonSubTypes;
-import com.fasterxml.jackson.annotation.JsonTypeInfo;
-import com.fasterxml.jackson.core.JsonPointer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.JsonSerializable;
-import com.fasterxml.jackson.databind.node.JsonNodeFactory;
-
-import com.linecorp.centraldogma.internal.Jackson;
-
-/**
- * Base abstract class for one patch operation.
- *
- * Two more abstract classes extend this one according to the arguments of
- * the operation:
- *
- *
- * - {@link DualPathOperation} for operations taking a second pointer as
- * an argument ({@code copy} and {@code move});
- * - {@link PathValueOperation} for operations taking a value as an
- * argument ({@code add}, {@code replace} and {@code test}).
- *
- */
-@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.
- *
- * 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}).
- *
+ * Internal utility classes.
*/
@NonNullByDefault
package com.linecorp.centraldogma.internal.jsonpatch;
diff --git a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
new file mode 100644
index 0000000000..5665247601
--- /dev/null
+++ b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2025 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.linecorp.centraldogma.common.jsonpatch;
+
+import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import java.util.List;
+import java.util.concurrent.CompletionException;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.JsonPointer;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.google.common.collect.ImmutableList;
+
+import com.linecorp.centraldogma.client.CentralDogma;
+import com.linecorp.centraldogma.client.CentralDogmaRepository;
+import com.linecorp.centraldogma.common.Change;
+import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension;
+
+class JsonPatchOperationIntegrationTest {
+
+ @RegisterExtension
+ final CentralDogmaExtension dogma = new CentralDogmaExtension() {
+ @Override
+ protected void scaffold(CentralDogma client) {
+ client.createProject("foo").join();
+ client.createRepository("foo", "bar").join();
+ }
+
+ @Override
+ protected boolean runForEachTest() {
+ return true;
+ }
+ };
+
+ CentralDogmaRepository repository;
+
+ @BeforeEach
+ void setUp() {
+ repository = dogma.client().forRepo("foo", "bar");
+ }
+
+ @Test
+ void testAdd() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ final AddOperation add = JsonPatchOperation.add(JsonPointer.compile("/b"), new IntNode(2));
+ final Change change = Change.ofJsonPatch("/a.json", add);
+ repository.commit("add b", change)
+ .push()
+ .join();
+
+ final JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"a\": 1, \"b\": 2 }");
+ }
+
+ @Test
+ void testCopy() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ final CopyOperation copy = JsonPatchOperation.copy(JsonPointer.compile("/a"),
+ JsonPointer.compile("/b"));
+ final Change change = Change.ofJsonPatch("/a.json", copy);
+ repository.commit("copy a", change)
+ .push()
+ .join();
+
+ final JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"a\": 1, \"b\": 1 }");
+ }
+
+ @Test
+ void testMove() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ final MoveOperation move = JsonPatchOperation.move(JsonPointer.compile("/a"),
+ JsonPointer.compile("/b"));
+ final Change change = Change.ofJsonPatch("/a.json", move);
+ repository.commit("move a", change)
+ .push()
+ .join();
+
+ final JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"b\": 1 }");
+ }
+
+ @Test
+ void testRemove() throws JsonParseException {
+ repository.commit("add ab", Change.ofJsonUpsert("/a.json", "{ \"a\": 1, \"b\": 2 }"))
+ .push()
+ .join();
+ final RemoveOperation remove = JsonPatchOperation.remove(JsonPointer.compile("/a"));
+ final Change change = Change.ofJsonPatch("/a.json", remove);
+ repository.commit("remove a", change)
+ .push()
+ .join();
+
+ final JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"b\": 2 }");
+
+ assertThatThrownBy(() -> repository.commit("remove a again", change)
+ .push()
+ .join())
+ .isInstanceOf(CompletionException.class)
+ .hasCauseInstanceOf(JsonPatchException.class)
+ .hasMessageContaining("failed to apply JSON patch:")
+ .hasMessageContaining("path=/a.json, content=[{\"op\":\"remove\",\"path\":\"/a\"}]");
+ }
+
+ @Test
+ void removeIfExists() throws JsonParseException {
+ repository.commit("add ab", Change.ofJsonUpsert("/a.json", "{ \"a\": 1, \"b\": 2 }"))
+ .push()
+ .join();
+ final RemoveIfExistsOperation removeIfExists =
+ JsonPatchOperation.removeIfExists(JsonPointer.compile("/a"));
+ final Change change = Change.ofJsonPatch("/a.json", removeIfExists);
+ repository.commit("remove a", change)
+ .push()
+ .join();
+
+ JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"b\": 2 }");
+
+ repository.commit("remove a again", change)
+ .push()
+ .join();
+
+ jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"b\": 2 }");
+ }
+
+ @Test
+ void replace() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ final ReplaceOperation replace = JsonPatchOperation.replace(JsonPointer.compile("/a"), new IntNode(2));
+ final Change change = Change.ofJsonPatch("/a.json", replace);
+ repository.commit("replace a", change)
+ .push()
+ .join();
+
+ final JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"a\": 2 }");
+ }
+
+ @Test
+ void safeReplace() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ SafeReplaceOperation safeReplace =
+ JsonPatchOperation.safeReplace(JsonPointer.compile("/a"), new IntNode(1), new IntNode(2));
+ final Change change = Change.ofJsonPatch("/a.json", safeReplace);
+ repository.commit("safe replace a", change)
+ .push()
+ .join();
+
+ final JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
+ assertThatJson(jsonNode).isEqualTo("{ \"a\": 2 }");
+
+ safeReplace = JsonPatchOperation.safeReplace(JsonPointer.compile("/a"), new IntNode(3), new IntNode(4));
+ final Change change1 = Change.ofJsonPatch("/a.json", safeReplace);
+ assertThatThrownBy(() -> {
+ repository.commit("invalid safe replace a", change1)
+ .push()
+ .join();
+ }).isInstanceOf(CompletionException.class)
+ .hasCauseInstanceOf(JsonPatchException.class)
+ .hasMessageContaining(
+ "failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, path=/a.json, " +
+ "content=[{\"op\":\"safeReplace\",\"path\":\"/a\",\"oldValue\":3,\"value\":4}]}");
+ }
+
+ @Test
+ void test() {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ TestOperation test = JsonPatchOperation.test(JsonPointer.compile("/a"), new IntNode(1));
+ final Change change = Change.ofJsonPatch("/a.json", test);
+ repository.commit("test a", change)
+ .push()
+ .join();
+
+ test = JsonPatchOperation.test(JsonPointer.compile("/a"), new IntNode(2));
+ final Change change1 = Change.ofJsonPatch("/a.json", test);
+ assertThatThrownBy(() -> {
+ repository.commit("invalid test a", change1)
+ .push()
+ .join();
+ }).isInstanceOf(CompletionException.class)
+ .hasCauseInstanceOf(JsonPatchException.class)
+ .hasMessageContaining(
+ "failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, " +
+ "path=/a.json, content=[{\"op\":\"test\",\"path\":\"/a\",\"value\":2}]}");
+ }
+
+ @Test
+ void testAbsence() {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
+ .push()
+ .join();
+ TestAbsenceOperation testAbsence = JsonPatchOperation.testAbsence(JsonPointer.compile("/b"));
+ final Change change = Change.ofJsonPatch("/a.json", testAbsence);
+ repository.commit("test absence", change)
+ .push()
+ .join();
+
+ testAbsence = JsonPatchOperation.testAbsence(JsonPointer.compile("/a"));
+ final Change change1 = Change.ofJsonPatch("/a.json", testAbsence);
+ assertThatThrownBy(() -> {
+ repository.commit("invalid test absence", change1)
+ .push()
+ .join();
+ }).isInstanceOf(CompletionException.class)
+ .hasCauseInstanceOf(JsonPatchException.class)
+ .hasMessageContaining(
+ "failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, " +
+ "path=/a.json, content=[{\"op\":\"testAbsence\",\"path\":\"/a\"}]}");
+ }
+
+ @Test
+ void testMultipleOperations() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1, \"b\": 2 }"))
+ .push()
+ .join();
+ // { "a": 1, "b": 2 } -> { "a": 1, "b": 2, "c": 3 }
+ final AddOperation add = JsonPatchOperation.add(JsonPointer.compile("/c"), new IntNode(3));
+ // { "a": 1, "b": 2, "c": 3 } -> { "b": 2, "c": 3, "d": 1 }
+ final MoveOperation move = JsonPatchOperation.move(JsonPointer.compile("/a"),
+ JsonPointer.compile("/d"));
+ // { "b": 2, "c": 3, "d": 1 } -> { "c": 3, "d": 1 }
+ final RemoveOperation remove = JsonPatchOperation.remove(JsonPointer.compile("/b"));
+ // { "c": 3, "d": 1 } -> { "c": 4, "d": 1 }
+ final SafeReplaceOperation safeReplace = JsonPatchOperation.safeReplace(JsonPointer.compile("/c"),
+ new IntNode(3), new IntNode(4));
+
+ final Change change = Change.ofJsonPatch("/a.json",
+ ImmutableList.of(add, move, remove, safeReplace));
+
+ repository.commit("json patch operations", change)
+ .push()
+ .join();
+
+ assertThatJson(repository.file("/a.json").get().join().contentAsJson())
+ .isEqualTo("{ \"c\": 4, \"d\": 1 }");
+ }
+
+ @Test
+ void testEquality() throws JsonProcessingException {
+ final RemoveOperation remove = JsonPatchOperation.remove(JsonPointer.compile("/a"));
+ final JsonNode jsonNode = remove.toJsonNode();
+ final ObjectMapper mapper = new ObjectMapper();
+ final List jsonPatchOperations =
+ mapper.treeToValue(jsonNode, new TypeReference>() {});
+ assertThat(jsonPatchOperations.get(0)).isEqualTo(remove);
+ }
+}
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationSerializationTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationSerializationTest.java
index 668081cc2b..aa4fc3ce76 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationSerializationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationSerializationTest.java
@@ -52,6 +52,16 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
+import com.linecorp.centraldogma.common.jsonpatch.AddOperation;
+import com.linecorp.centraldogma.common.jsonpatch.CopyOperation;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+import com.linecorp.centraldogma.common.jsonpatch.MoveOperation;
+import com.linecorp.centraldogma.common.jsonpatch.RemoveIfExistsOperation;
+import com.linecorp.centraldogma.common.jsonpatch.RemoveOperation;
+import com.linecorp.centraldogma.common.jsonpatch.ReplaceOperation;
+import com.linecorp.centraldogma.common.jsonpatch.TestAbsenceOperation;
+import com.linecorp.centraldogma.common.jsonpatch.TestOperation;
+
class JsonPatchOperationSerializationTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java
index 8bffaea9cb..d105f69af5 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java
@@ -51,6 +51,9 @@
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+
class JsonPatchOperationTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java
index 261af8de7b..0feb8752a2 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java
@@ -52,6 +52,9 @@
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.google.common.collect.ImmutableList;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+
class JsonPatchTest {
private static final JsonNodeFactory FACTORY = JsonNodeFactory.instance;
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java
index 64ed246761..70c16972e9 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java
@@ -49,6 +49,8 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+
class JsonPatchTestSuite {
private static final ObjectMapper MAPPER = new ObjectMapper();
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperationTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperationTest.java
index be7a8512d9..eb17f6c6a1 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveIfExistsOperationTest.java
@@ -42,6 +42,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+
class RemoveIfExistsOperationTest {
private static final JsonPointer EMPTY_JSON_POINTER = JsonPointer.compile("");
@@ -49,7 +51,7 @@ class RemoveIfExistsOperationTest {
@Test
void removingRootReturnsMissingNode() {
final JsonNode node = JsonNodeFactory.instance.nullNode();
- final JsonPatchOperation op = new RemoveIfExistsOperation(EMPTY_JSON_POINTER);
+ final JsonPatchOperation op = JsonPatchOperation.removeIfExists(EMPTY_JSON_POINTER);
final JsonNode ret = op.apply(node);
assertThat(ret.isMissingNode()).isTrue();
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperationTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperationTest.java
index b718825b4b..8c68c7152f 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/RemoveOperationTest.java
@@ -42,6 +42,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+
class RemoveOperationTest {
private static final JsonPointer EMPTY_JSON_POINTER = JsonPointer.compile("");
@@ -49,7 +51,7 @@ class RemoveOperationTest {
@Test
void removingRootReturnsMissingNode() {
final JsonNode node = JsonNodeFactory.instance.nullNode();
- final JsonPatchOperation op = new RemoveOperation(EMPTY_JSON_POINTER);
+ final JsonPatchOperation op = JsonPatchOperation.remove(EMPTY_JSON_POINTER);
final JsonNode ret = op.apply(node);
assertThat(ret.isMissingNode()).isTrue();
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
index c388f2c034..9a9c2af0a4 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
@@ -49,6 +49,7 @@
import com.linecorp.centraldogma.common.RepositoryNotFoundException;
import com.linecorp.centraldogma.common.RevisionNotFoundException;
import com.linecorp.centraldogma.common.TooManyRequestsException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
import com.linecorp.centraldogma.server.internal.storage.RequestAlreadyTimedOutException;
import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryMetadataException;
import com.linecorp.centraldogma.server.metadata.MemberNotFoundException;
@@ -89,6 +90,8 @@ public final class HttpApiExceptionHandler implements ServerErrorHandler {
.put(RepositoryExistsException.class,
(ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
"Repository '%s' exists already.", cause.getMessage()))
+ .put(JsonPatchException.class,
+ (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause))
.put(RepositoryMetadataException.class,
(ctx, cause) -> newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, cause))
.put(RepositoryNotFoundException.class,
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java
index 74003c8d6b..9a748ce7a3 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/MetadataApiService.java
@@ -41,9 +41,9 @@
import com.linecorp.centraldogma.common.ProjectRole;
import com.linecorp.centraldogma.common.RepositoryRole;
import com.linecorp.centraldogma.common.Revision;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
+import com.linecorp.centraldogma.common.jsonpatch.ReplaceOperation;
import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
-import com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation;
-import com.linecorp.centraldogma.internal.jsonpatch.ReplaceOperation;
import com.linecorp.centraldogma.server.QuotaConfig;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.internal.api.auth.RequiresProjectRole;
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
index 86c232801e..75fde09009 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
@@ -43,6 +43,7 @@
import com.linecorp.centraldogma.common.Change;
import com.linecorp.centraldogma.common.ChangeConflictException;
import com.linecorp.centraldogma.common.Revision;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.Util;
import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
@@ -158,7 +159,7 @@ int doApply(Revision unused, DirCache dirCache,
try {
newJsonNode = JsonPatch.fromJson((JsonNode) change.content()).apply(oldJsonNode);
} catch (Exception e) {
- throw new ChangeConflictException("failed to apply JSON patch: " + change, e);
+ throw new JsonPatchException("failed to apply JSON patch: " + change, e);
}
// Apply only when the contents are really different.
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
index ec2334e510..3ff5dc3a41 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
@@ -81,6 +81,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Streams;
import com.linecorp.armeria.common.CommonPools;
import com.linecorp.armeria.common.util.Exceptions;
@@ -88,6 +89,7 @@
import com.linecorp.centraldogma.common.Author;
import com.linecorp.centraldogma.common.CentralDogmaException;
import com.linecorp.centraldogma.common.Change;
+import com.linecorp.centraldogma.common.ChangeType;
import com.linecorp.centraldogma.common.Commit;
import com.linecorp.centraldogma.common.Entry;
import com.linecorp.centraldogma.common.EntryNotFoundException;
@@ -854,8 +856,12 @@ public CompletableFuture commit(
requireNonNull(detail, "detail");
requireNonNull(markup, "markup");
requireNonNull(changes, "changes");
+
+ // JsonPatch operations its own validation for the changes so we don't need to validate them here.
+ final boolean allowEmptyCommit = Streams.stream(changes).allMatch(
+ change -> change.type() == ChangeType.APPLY_JSON_PATCH);
final CommitExecutor commitExecutor =
- new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, false);
+ new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, allowEmptyCommit);
return commit(baseRevision, commitExecutor, normBaseRevision -> {
if (!directExecution) {
return changes;
@@ -1323,7 +1329,7 @@ public void cloneTo(File newRepoDir, BiConsumer progressListen
// NB: We allow an empty commit here because an old version of Central Dogma had a bug
// which allowed the creation of an empty commit.
new CommitExecutor(newRepo, c.when(), c.author(), c.summary(),
- c.detail(), c.markup(), true)
+ c.detail(), c.markup(), true)
.execute(baseRevision, normBaseRevision -> blockingPreviewDiff(
normBaseRevision, new DefaultChangesApplier(changes)).values());
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java b/server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java
index 2ab51c74f3..7b08ec4a75 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/metadata/MetadataService.java
@@ -18,7 +18,7 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
-import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation.asJsonArray;
+import static com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation.asJsonArray;
import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchUtil.encodeSegment;
import static com.linecorp.centraldogma.server.internal.storage.project.ProjectApiManager.listProjectsWithoutInternal;
import static com.linecorp.centraldogma.server.metadata.RepositoryMetadata.DEFAULT_PROJECT_ROLES;
@@ -55,11 +55,8 @@
import com.linecorp.centraldogma.common.RepositoryExistsException;
import com.linecorp.centraldogma.common.RepositoryRole;
import com.linecorp.centraldogma.common.Revision;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
import com.linecorp.centraldogma.internal.Jackson;
-import com.linecorp.centraldogma.internal.jsonpatch.AddOperation;
-import com.linecorp.centraldogma.internal.jsonpatch.RemoveOperation;
-import com.linecorp.centraldogma.internal.jsonpatch.ReplaceOperation;
-import com.linecorp.centraldogma.internal.jsonpatch.TestAbsenceOperation;
import com.linecorp.centraldogma.server.QuotaConfig;
import com.linecorp.centraldogma.server.command.CommandExecutor;
import com.linecorp.centraldogma.server.storage.project.Project;
@@ -188,9 +185,9 @@ public CompletableFuture removeProject(Author author, String projectNa
final Change change = Change.ofJsonPatch(
METADATA_JSON,
- asJsonArray(new TestAbsenceOperation(PROJECT_REMOVAL),
- new AddOperation(PROJECT_REMOVAL,
- Jackson.valueToTree(UserAndTimestamp.of(author)))));
+ asJsonArray(JsonPatchOperation.testAbsence(PROJECT_REMOVAL),
+ JsonPatchOperation.add(PROJECT_REMOVAL,
+ Jackson.valueToTree(UserAndTimestamp.of(author)))));
return metadataRepo.push(projectName, Project.REPO_DOGMA, author,
"Remove the project: " + projectName, change);
}
@@ -203,7 +200,7 @@ public CompletableFuture restoreProject(Author author, String projectN
requireNonNull(projectName, "projectName");
final Change change =
- Change.ofJsonPatch(METADATA_JSON, new RemoveOperation(PROJECT_REMOVAL).toJsonNode());
+ Change.ofJsonPatch(METADATA_JSON, JsonPatchOperation.remove(PROJECT_REMOVAL).toJsonNode());
return metadataRepo.push(projectName, Project.REPO_DOGMA, author,
"Restore the project: " + projectName, change);
}
@@ -234,8 +231,8 @@ public CompletableFuture addMember(Author author, String projectName,
final JsonPointer path = JsonPointer.compile("/members" + encodeSegment(newMember.id()));
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- asJsonArray(new TestAbsenceOperation(path),
- new AddOperation(path, Jackson.valueToTree(newMember))));
+ asJsonArray(JsonPatchOperation.testAbsence(path),
+ JsonPatchOperation.add(path, Jackson.valueToTree(newMember))));
final String commitSummary =
"Add a member '" + newMember.id() + "' to the project '" + projectName + '\'';
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
@@ -307,8 +304,9 @@ public CompletableFuture updateMemberRole(Author author, String projec
final Change change = Change.ofJsonPatch(
METADATA_JSON,
- new ReplaceOperation(JsonPointer.compile("/members" + encodeSegment(member.id()) + "/role"),
- Jackson.valueToTree(projectRole)).toJsonNode());
+ JsonPatchOperation.replace(
+ JsonPointer.compile("/members" + encodeSegment(member.id()) + "/role"),
+ Jackson.valueToTree(projectRole)).toJsonNode());
final String commitSummary = "Updates the role of the member '" + member.id() +
"' as '" + projectRole + "' for the project '" + projectName + '\'';
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
@@ -349,9 +347,9 @@ public CompletableFuture addRepo(Author author, String projectName, St
RepositoryMetadata.of(repoName, UserAndTimestamp.of(author), projectRoles);
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- asJsonArray(new TestAbsenceOperation(path),
- new AddOperation(path,
- Jackson.valueToTree(newRepositoryMetadata))));
+ asJsonArray(JsonPatchOperation.testAbsence(path),
+ JsonPatchOperation.add(
+ path, Jackson.valueToTree(newRepositoryMetadata))));
final String commitSummary =
"Add a repo '" + newRepositoryMetadata.id() + "' to the project '" + projectName + '\'';
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change)
@@ -379,8 +377,8 @@ public CompletableFuture removeRepo(Author author, String projectName,
final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/removal");
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- asJsonArray(new TestAbsenceOperation(path),
- new AddOperation(path, Jackson.valueToTree(
+ asJsonArray(JsonPatchOperation.testAbsence(path),
+ JsonPatchOperation.add(path, Jackson.valueToTree(
UserAndTimestamp.of(author)))));
final String commitSummary =
"Remove the repo '" + repoName + "' from the project '" + projectName + '\'';
@@ -398,7 +396,7 @@ public CompletableFuture purgeRepo(Author author, String projectName,
final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName));
final Change change = Change.ofJsonPatch(METADATA_JSON,
- new RemoveOperation(path).toJsonNode());
+ JsonPatchOperation.remove(path).toJsonNode());
final String commitSummary =
"Purge the repo '" + repoName + "' from the project '" + projectName + '\'';
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
@@ -415,7 +413,7 @@ public CompletableFuture restoreRepo(Author author, String projectName
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- new RemoveOperation(JsonPointer.compile(
+ JsonPatchOperation.remove(JsonPointer.compile(
"/repos" + encodeSegment(repoName) + "/removal")).toJsonNode());
final String commitSummary =
"Restore the repo '" + repoName + "' from the project '" + projectName + '\'';
@@ -490,8 +488,9 @@ public CompletableFuture addToken(Author author, String projectName,
final JsonPointer path = JsonPointer.compile("/tokens" + encodeSegment(registration.id()));
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- asJsonArray(new TestAbsenceOperation(path),
- new AddOperation(path, Jackson.valueToTree(registration))));
+ asJsonArray(JsonPatchOperation.testAbsence(path),
+ JsonPatchOperation.add(path,
+ Jackson.valueToTree(registration))));
final String commitSummary = "Add a token '" + registration.id() +
"' to the project '" + projectName + "' with a role '" + role + '\'';
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
@@ -524,30 +523,30 @@ private CompletableFuture removeToken(String projectName, Author autho
final String commitSummary = "Remove the token '" + appId + "' from the project '" + projectName + '\'';
final ProjectMetadataTransformer transformer =
new ProjectMetadataTransformer((headRevision, projectMetadata) -> {
- final Map tokens = projectMetadata.tokens();
- final Map newTokens;
- if (tokens.get(appId) == null) {
- if (!quiet) {
- throw new TokenNotFoundException(
- "failed to find the token " + appId + " in project " + projectName);
- }
- newTokens = tokens;
- } else {
- newTokens = tokens.entrySet()
- .stream()
- .filter(entry -> !entry.getKey().equals(appId))
- .collect(toImmutableMap(Entry::getKey, Entry::getValue));
- }
+ final Map tokens = projectMetadata.tokens();
+ final Map newTokens;
+ if (tokens.get(appId) == null) {
+ if (!quiet) {
+ throw new TokenNotFoundException(
+ "failed to find the token " + appId + " in project " + projectName);
+ }
+ newTokens = tokens;
+ } else {
+ newTokens = tokens.entrySet()
+ .stream()
+ .filter(entry -> !entry.getKey().equals(appId))
+ .collect(toImmutableMap(Entry::getKey, Entry::getValue));
+ }
- final ImmutableMap newRepos =
- removeTokenFromRepositories(appId, projectMetadata);
- return new ProjectMetadata(projectMetadata.name(),
- newRepos,
- projectMetadata.members(),
- newTokens,
- projectMetadata.creation(),
- projectMetadata.removal());
- });
+ final ImmutableMap newRepos =
+ removeTokenFromRepositories(appId, projectMetadata);
+ return new ProjectMetadata(projectMetadata.name(),
+ newRepos,
+ projectMetadata.members(),
+ newTokens,
+ projectMetadata.creation(),
+ projectMetadata.removal());
+ });
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, transformer);
}
@@ -587,8 +586,8 @@ public CompletableFuture updateTokenRole(Author author, String project
final JsonPointer path = JsonPointer.compile("/tokens" + encodeSegment(registration.id()));
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- new ReplaceOperation(path, Jackson.valueToTree(registration))
- .toJsonNode());
+ JsonPatchOperation.replace(
+ path, Jackson.valueToTree(registration)).toJsonNode());
final String commitSummary = "Update the role of a token '" + token.appId() +
"' as '" + role + "' for the project '" + projectName + '\'';
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
@@ -835,7 +834,7 @@ public CompletableFuture updateWriteQuota(
final JsonPointer path = JsonPointer.compile("/repos" + encodeSegment(repoName) + "/writeQuota");
final Change change =
Change.ofJsonPatch(METADATA_JSON,
- new AddOperation(path, Jackson.valueToTree(writeQuota)).toJsonNode());
+ JsonPatchOperation.add(path, Jackson.valueToTree(writeQuota)).toJsonNode());
final String commitSummary = "Update a write quota for the repository '" + repoName + '\'';
executor.setWriteQuota(projectName, repoName, writeQuota);
return metadataRepo.push(projectName, Project.REPO_DOGMA, author, commitSummary, change);
@@ -1001,11 +1000,11 @@ public CompletableFuture createToken(Author author, String appId, Stri
final JsonPointer secretPath = JsonPointer.compile("/secrets" + encodeSegment(newTokenSecret));
final Change change =
Change.ofJsonPatch(TOKEN_JSON,
- asJsonArray(new TestAbsenceOperation(appIdPath),
- new TestAbsenceOperation(secretPath),
- new AddOperation(appIdPath, Jackson.valueToTree(newToken)),
- new AddOperation(secretPath,
- Jackson.valueToTree(newToken.id()))));
+ asJsonArray(JsonPatchOperation.testAbsence(appIdPath),
+ JsonPatchOperation.testAbsence(secretPath),
+ JsonPatchOperation.add(appIdPath, Jackson.valueToTree(newToken)),
+ JsonPatchOperation.add(secretPath,
+ Jackson.valueToTree(newToken.id()))));
return tokenRepo.push(INTERNAL_PROJECT_DOGMA, Project.REPO_DOGMA, author,
"Add a token: " + newToken.id(), change);
}
diff --git a/server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenTest.java b/server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenTest.java
index b4fed9864b..8068a8f71e 100644
--- a/server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenTest.java
+++ b/server/src/test/java/com/linecorp/centraldogma/server/metadata/TokenTest.java
@@ -15,7 +15,7 @@
*/
package com.linecorp.centraldogma.server.metadata;
-import static com.linecorp.centraldogma.internal.jsonpatch.JsonPatchOperation.asJsonArray;
+import static com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation.asJsonArray;
import static com.linecorp.centraldogma.server.metadata.MetadataService.TOKEN_JSON;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,9 +38,8 @@
import com.linecorp.centraldogma.common.Author;
import com.linecorp.centraldogma.common.Change;
import com.linecorp.centraldogma.common.Revision;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
import com.linecorp.centraldogma.internal.Jackson;
-import com.linecorp.centraldogma.internal.jsonpatch.AddOperation;
-import com.linecorp.centraldogma.internal.jsonpatch.TestAbsenceOperation;
import com.linecorp.centraldogma.server.internal.api.sysadmin.TokenLevelRequest;
import com.linecorp.centraldogma.server.internal.api.sysadmin.TokenService;
import com.linecorp.centraldogma.server.storage.project.InternalProjectInitializer;
@@ -77,10 +76,10 @@ static void setUp() throws JsonParseException {
final JsonPointer secretPath = JsonPointer.compile("/secrets/" + APP_SECRET);
final Change change = Change.ofJsonPatch(
TOKEN_JSON,
- asJsonArray(new TestAbsenceOperation(appIdPath),
- new TestAbsenceOperation(secretPath),
- new AddOperation(appIdPath, Jackson.readTree(tokenJson(true))),
- new AddOperation(secretPath, Jackson.valueToTree(APP_ID))));
+ asJsonArray(JsonPatchOperation.testAbsence(appIdPath),
+ JsonPatchOperation.testAbsence(secretPath),
+ JsonPatchOperation.add(appIdPath, Jackson.readTree(tokenJson(true))),
+ JsonPatchOperation.add(secretPath, Jackson.valueToTree(APP_ID))));
dogmaRepository.commit(Revision.HEAD, System.currentTimeMillis(), AUTHOR,
"Add the legacy token", change).join();
From 820316f53497f4f9a83256be721eeb0e77ad13fd Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Fri, 17 Jan 2025 16:41:32 +0900
Subject: [PATCH 2/7] polish
---
.../client/armeria/ArmeriaCentralDogma.java | 6 ++-
.../common/TextPatchConflictException.java | 40 +++++++++++++++++++
.../common/jsonpatch/AddOperation.java | 6 +--
.../common/jsonpatch/CopyOperation.java | 2 +-
...n.java => JsonPatchConflictException.java} | 9 +++--
.../common/jsonpatch/JsonPatchOperation.java | 10 ++---
.../common/jsonpatch/MoveOperation.java | 2 +-
.../common/jsonpatch/PathValueOperation.java | 4 +-
.../jsonpatch/SafeReplaceOperation.java | 4 +-
.../jsonpatch/TestAbsenceOperation.java | 2 +-
.../internal/jsonpatch/JsonPatch.java | 6 +--
.../JsonPatchOperationIntegrationTest.java | 8 ++--
.../jsonpatch/JsonPatchOperationTest.java | 4 +-
.../internal/jsonpatch/JsonPatchTest.java | 6 +--
.../jsonpatch/JsonPatchTestSuite.java | 4 +-
.../internal/api/HttpApiExceptionHandler.java | 7 +++-
.../repository/git/DefaultChangesApplier.java | 15 +++++--
.../storage/repository/git/GitRepository.java | 15 +++++--
.../repository/git/GitRepositoryTest.java | 29 +++++++++-----
19 files changed, 126 insertions(+), 53 deletions(-)
create mode 100644 common/src/main/java/com/linecorp/centraldogma/common/TextPatchConflictException.java
rename common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/{JsonPatchException.java => JsonPatchConflictException.java} (74%)
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 49a6d02944..022661ead7 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,7 +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.jsonpatch.JsonPatchException;
+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;
@@ -138,7 +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(JsonPatchException.class.getName(), JsonPatchException::new)
+ .put(JsonPatchConflictException.class.getName(), JsonPatchConflictException::new)
+ .put(TextPatchConflictException.class.getName(), TextPatchConflictException::new)
.build();
private final WebClient client;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/TextPatchConflictException.java b/common/src/main/java/com/linecorp/centraldogma/common/TextPatchConflictException.java
new file mode 100644
index 0000000000..cca896e2e5
--- /dev/null
+++ b/common/src/main/java/com/linecorp/centraldogma/common/TextPatchConflictException.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2025 LINE Corporation
+ *
+ * LINE Corporation licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+package com.linecorp.centraldogma.common;
+
+/**
+ * A {@link CentralDogmaException} that is raised when attempted to apply a text patch which cannot be applied
+ * without a conflict.
+ */
+public final class TextPatchConflictException extends ChangeConflictException {
+ private static final long serialVersionUID = -6150468151945332532L;
+
+ /**
+ * Creates a new instance.
+ */
+ public TextPatchConflictException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new instance with the specified {@code cause}.
+ */
+ public TextPatchConflictException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
index f6cf5a0a15..6338940070 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
@@ -99,12 +99,12 @@ static JsonNode addToArray(final JsonPointer path, final JsonNode node, final Js
try {
index = Integer.parseInt(rawToken);
} catch (NumberFormatException ignored) {
- throw new JsonPatchException("not an index: " + rawToken + " (expected: a non-negative integer)");
+ throw new JsonPatchConflictException("not an index: " + rawToken + " (expected: a non-negative integer)");
}
if (index < 0 || index > size) {
- throw new JsonPatchException("index out of bounds: " + index +
- " (expected: >= 0 && <= " + size + ')');
+ throw new JsonPatchConflictException("index out of bounds: " + index +
+ " (expected: >= 0 && <= " + size + ')');
}
target.insert(index, value);
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
index 736d7aa901..d9ea5a5b03 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
@@ -48,7 +48,7 @@ public JsonNode apply(final JsonNode node) {
final JsonPointer from = from();
JsonNode source = node.at(from);
if (source.isMissingNode()) {
- throw new JsonPatchException("non-existent source path: " + from);
+ throw new JsonPatchConflictException("non-existent source path: " + from);
}
final JsonPointer path = path();
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchException.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchConflictException.java
similarity index 74%
rename from common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchException.java
rename to common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchConflictException.java
index 4423249cfb..1ae4a5faa0 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchException.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchConflictException.java
@@ -17,25 +17,26 @@
package com.linecorp.centraldogma.common.jsonpatch;
import com.linecorp.centraldogma.common.CentralDogmaException;
+import com.linecorp.centraldogma.common.ChangeConflictException;
/**
- * An exception raised when a JSON Patch operation fails.
+ * A {@link CentralDogmaException} raised when a JSON Patch operation fails.
*/
-public final class JsonPatchException extends CentralDogmaException {
+public final class JsonPatchConflictException extends ChangeConflictException {
private static final long serialVersionUID = 4746173383862473527L;
/**
* Creates a new instance.
*/
- public JsonPatchException(final String message) {
+ public JsonPatchConflictException(String message) {
super(message);
}
/**
* Creates a new instance with the specified {@code cause}.
*/
- public JsonPatchException(final String message, final Throwable cause) {
+ public JsonPatchConflictException(String message, Throwable cause) {
super(message, cause);
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
index 1e35ad677e..d8ee15c32a 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
@@ -230,7 +230,7 @@ public final JsonPointer path() {
*
* @param node the value to patch
* @return the patched value
- * @throws JsonPatchException operation failed to apply to this value
+ * @throws JsonPatchConflictException operation failed to apply to this value
*/
public abstract JsonNode apply(JsonNode node);
@@ -244,7 +244,7 @@ public JsonNode toJsonNode() {
JsonNode ensureExistence(JsonNode node) {
final JsonNode found = node.at(path);
if (found.isMissingNode()) {
- throw new JsonPatchException("non-existent path: " + path);
+ throw new JsonPatchConflictException("non-existent path: " + path);
}
return found;
}
@@ -265,11 +265,11 @@ private static JsonNode ensureParent(JsonNode node, JsonPointer path, String typ
final JsonPointer parentPath = path.head();
final JsonNode parentNode = node.at(parentPath);
if (parentNode.isMissingNode()) {
- throw new JsonPatchException("non-existent " + typeName + " parent: " + parentPath);
+ throw new JsonPatchConflictException("non-existent " + typeName + " parent: " + parentPath);
}
if (!parentNode.isContainerNode()) {
- throw new JsonPatchException(typeName + " parent is not a container: " + parentPath +
- " (" + parentNode.getNodeType() + ')');
+ throw new JsonPatchConflictException(typeName + " parent is not a container: " + parentPath +
+ " (" + parentNode.getNodeType() + ')');
}
return parentNode;
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
index 615ccc3715..421770c0b9 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
@@ -75,7 +75,7 @@ public JsonNode apply(final JsonNode node) {
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/common/jsonpatch/PathValueOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
index 70fed8c043..cb3ab39f6e 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
@@ -67,8 +67,8 @@ 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 + ')');
}
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
index 2a5f3bf6e2..4641968534 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
@@ -85,8 +85,8 @@ public JsonNode apply(JsonNode node) {
final JsonPointer path = path();
if (!EQUIVALENCE.equivalent(actual, oldValue)) {
- throw new JsonPatchException("mismatching value at '" + path + "': " +
- actual + " (expected: " + oldValue + ')');
+ throw new JsonPatchConflictException("mismatching value at '" + path + "': " +
+ actual + " (expected: " + oldValue + ')');
}
final JsonNode replacement = newValue.deepCopy();
if (path.toString().isEmpty()) {
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
index 0e275f691a..2c199c7ae8 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
@@ -51,7 +51,7 @@ public JsonNode apply(JsonNode node) {
final JsonPointer path = path();
final JsonNode found = node.at(path);
if (!found.isMissingNode()) {
- throw new JsonPatchException("existent path: " + path);
+ throw new JsonPatchConflictException("existent path: " + path);
}
return node;
}
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
index 9a12244f53..fb7ec3a0c2 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
@@ -62,7 +62,7 @@
import com.google.common.collect.Iterators;
import com.google.common.collect.Sets;
-import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
import com.linecorp.centraldogma.internal.Jackson;
@@ -141,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);
}
}
@@ -341,7 +341,7 @@ public List operations() {
*
* @param node the value to apply the patch to
* @return the patched JSON value
- * @throws JsonPatchException failed to apply patch
+ * @throws JsonPatchConflictException failed to apply patch
* @throws NullPointerException input is null
*/
public JsonNode apply(final JsonNode node) {
diff --git a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
index 5665247601..bbd7d235fb 100644
--- a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
@@ -129,7 +129,7 @@ void testRemove() throws JsonParseException {
.push()
.join())
.isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(JsonPatchException.class)
+ .hasCauseInstanceOf(JsonPatchConflictException.class)
.hasMessageContaining("failed to apply JSON patch:")
.hasMessageContaining("path=/a.json, content=[{\"op\":\"remove\",\"path\":\"/a\"}]");
}
@@ -194,7 +194,7 @@ void safeReplace() throws JsonParseException {
.push()
.join();
}).isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(JsonPatchException.class)
+ .hasCauseInstanceOf(JsonPatchConflictException.class)
.hasMessageContaining(
"failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, path=/a.json, " +
"content=[{\"op\":\"safeReplace\",\"path\":\"/a\",\"oldValue\":3,\"value\":4}]}");
@@ -218,7 +218,7 @@ void test() {
.push()
.join();
}).isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(JsonPatchException.class)
+ .hasCauseInstanceOf(JsonPatchConflictException.class)
.hasMessageContaining(
"failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, " +
"path=/a.json, content=[{\"op\":\"test\",\"path\":\"/a\",\"value\":2}]}");
@@ -242,7 +242,7 @@ void testAbsence() {
.push()
.join();
}).isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(JsonPatchException.class)
+ .hasCauseInstanceOf(JsonPatchConflictException.class)
.hasMessageContaining(
"failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, " +
"path=/a.json, content=[{\"op\":\"testAbsence\",\"path\":\"/a\"}]}");
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java
index d105f69af5..fbb6c35624 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchOperationTest.java
@@ -51,7 +51,7 @@
import com.google.common.base.Equivalence;
import com.google.common.collect.ImmutableList;
-import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
class JsonPatchOperationTest {
@@ -79,7 +79,7 @@ void errorsAreCorrectlyReported(JsonNode patch, JsonNode node, String message) t
final JsonPatchOperation op = READER.readValue(patch);
assertThatThrownBy(() -> op.apply(node))
- .isInstanceOf(JsonPatchException.class)
+ .isInstanceOf(JsonPatchConflictException.class)
.hasMessage(message);
}
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java
index 0feb8752a2..552b2a4f04 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTest.java
@@ -52,7 +52,7 @@
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.google.common.collect.ImmutableList;
-import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.common.jsonpatch.JsonPatchOperation;
class JsonPatchTest {
@@ -99,12 +99,12 @@ void operationsAreCalledInOrder() {
void whenOneOperationFailsNextOperationIsNotCalled() {
final String message = "foo";
when(op1.apply(any(JsonNode.class)))
- .thenThrow(new JsonPatchException(message));
+ .thenThrow(new JsonPatchConflictException(message));
final JsonPatch patch = new JsonPatch(ImmutableList.of(op1, op2));
assertThatThrownBy(() -> patch.apply(FACTORY.nullNode()))
- .isInstanceOf(JsonPatchException.class)
+ .isInstanceOf(JsonPatchConflictException.class)
.hasMessage(message);
verifyNoMoreInteractions(op2);
diff --git a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java
index 70c16972e9..92d4741f11 100644
--- a/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java
+++ b/common/src/test/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatchTestSuite.java
@@ -49,7 +49,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.ImmutableList;
-import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
class JsonPatchTestSuite {
@@ -64,7 +64,7 @@ void testSuite(JsonNode source, JsonPatch patch, JsonNode expected, boolean vali
assertThat((Object) actual).isEqualTo(expected);
} else {
assertThatThrownBy(() -> patch.apply(source))
- .isInstanceOf(JsonPatchException.class);
+ .isInstanceOf(JsonPatchConflictException.class);
}
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
index 9a9c2af0a4..31fcde1ef3 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/api/HttpApiExceptionHandler.java
@@ -48,8 +48,9 @@
import com.linecorp.centraldogma.common.RepositoryExistsException;
import com.linecorp.centraldogma.common.RepositoryNotFoundException;
import com.linecorp.centraldogma.common.RevisionNotFoundException;
+import com.linecorp.centraldogma.common.TextPatchConflictException;
import com.linecorp.centraldogma.common.TooManyRequestsException;
-import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.server.internal.storage.RequestAlreadyTimedOutException;
import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryMetadataException;
import com.linecorp.centraldogma.server.metadata.MemberNotFoundException;
@@ -90,7 +91,9 @@ public final class HttpApiExceptionHandler implements ServerErrorHandler {
.put(RepositoryExistsException.class,
(ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
"Repository '%s' exists already.", cause.getMessage()))
- .put(JsonPatchException.class,
+ .put(JsonPatchConflictException.class,
+ (ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause))
+ .put(TextPatchConflictException.class,
(ctx, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause))
.put(RepositoryMetadataException.class,
(ctx, cause) -> newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, cause))
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
index 75fde09009..af04a7add1 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
@@ -43,7 +43,8 @@
import com.linecorp.centraldogma.common.Change;
import com.linecorp.centraldogma.common.ChangeConflictException;
import com.linecorp.centraldogma.common.Revision;
-import com.linecorp.centraldogma.common.jsonpatch.JsonPatchException;
+import com.linecorp.centraldogma.common.TextPatchConflictException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.Util;
import com.linecorp.centraldogma.internal.jsonpatch.JsonPatch;
@@ -159,7 +160,11 @@ int doApply(Revision unused, DirCache dirCache,
try {
newJsonNode = JsonPatch.fromJson((JsonNode) change.content()).apply(oldJsonNode);
} catch (Exception e) {
- throw new JsonPatchException("failed to apply JSON patch: " + change, e);
+ if (e instanceof JsonPatchConflictException) {
+ throw (JsonPatchConflictException) e;
+ } else {
+ throw new JsonPatchConflictException("failed to apply JSON patch: " + change, e);
+ }
}
// Apply only when the contents are really different.
@@ -196,7 +201,11 @@ int doApply(Revision unused, DirCache dirCache,
newText = joiner.toString();
}
} catch (Exception e) {
- throw new ChangeConflictException("failed to apply text patch: " + change, e);
+ String message = "failed to apply text patch: " + change;
+ if (e.getMessage() != null) {
+ message += " (reason: " + e.getMessage() + ')';
+ }
+ throw new TextPatchConflictException(message, e);
}
// Apply only when the contents are really different.
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
index 3ff5dc3a41..87d2b6a59e 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
@@ -81,6 +81,7 @@
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Streams;
import com.linecorp.armeria.common.CommonPools;
@@ -857,9 +858,7 @@ public CompletableFuture commit(
requireNonNull(markup, "markup");
requireNonNull(changes, "changes");
- // JsonPatch operations its own validation for the changes so we don't need to validate them here.
- final boolean allowEmptyCommit = Streams.stream(changes).allMatch(
- change -> change.type() == ChangeType.APPLY_JSON_PATCH);
+ final boolean allowEmptyCommit = shouldAllowEmptyCommit(changes);
final CommitExecutor commitExecutor =
new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, allowEmptyCommit);
return commit(baseRevision, commitExecutor, normBaseRevision -> {
@@ -870,6 +869,16 @@ public CompletableFuture commit(
});
}
+ private boolean shouldAllowEmptyCommit(Iterable> changes) {
+ // allMatch returns true if the stream is empty.
+ if (Iterables.isEmpty(changes)) {
+ return false;
+ }
+ // JsonPatch operations its own validation for the changes so we don't need to validate them here.
+ return Streams.stream(changes)
+ .allMatch(change -> change.type() == ChangeType.APPLY_JSON_PATCH);
+ }
+
@Override
public CompletableFuture commit(Revision baseRevision, long commitTimeMillis, Author author,
String summary, String detail, Markup markup,
diff --git a/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java b/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java
index b2c49c8ca8..6767296c33 100644
--- a/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java
+++ b/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java
@@ -76,6 +76,8 @@
import com.linecorp.centraldogma.common.RedundantChangeException;
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.common.RevisionNotFoundException;
+import com.linecorp.centraldogma.common.TextPatchConflictException;
+import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.internal.Util;
import com.linecorp.centraldogma.server.internal.JGitUtil;
import com.linecorp.centraldogma.server.storage.StorageException;
@@ -241,20 +243,20 @@ void testJsonPatch_safeReplace(TestInfo testInfo) {
assertThatThrownBy(
() -> repo.commit(HEAD, 0L, Author.UNKNOWN, SUMMARY, nextChange).join())
.isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(ChangeConflictException.class);
+ .hasCauseInstanceOf(JsonPatchConflictException.class);
}
@Test
void testJsonPatch() {
- testPatch(jsonPatches, jsonUpserts);
+ testPatch(jsonPatches, jsonUpserts, true);
}
@Test
void testTextPatch() {
- testPatch(textPatches, textUpserts);
+ testPatch(textPatches, textUpserts, false);
}
- private static void testPatch(Change>[] patches, Change>[] upserts) {
+ private static void testPatch(Change>[] patches, Change>[] upserts, boolean jsonPatch) {
final String path = patches[0].path();
for (int i = 0; i < NUM_ITERATIONS; i++) {
assert path.equals(patches[i].path());
@@ -267,7 +269,8 @@ private static void testPatch(Change>[] patches, Change>[] upserts) {
assertThatThrownBy(
() -> repo.commit(HEAD, 0L, Author.UNKNOWN, SUMMARY, patches[finalJ]).join())
.isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(ChangeConflictException.class);
+ .hasCauseExactlyInstanceOf(jsonPatch ? JsonPatchConflictException.class :
+ TextPatchConflictException.class);
}
// Ensure that the failed commit does not change the revision.
@@ -558,7 +561,7 @@ void testMultipleChangesWithConflict() {
assertThatThrownBy(() -> repo
.commit(HEAD, 0L, Author.UNKNOWN, SUMMARY, jsonUpserts[0], jsonPatches[2]).join())
.isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(ChangeConflictException.class);
+ .hasCauseInstanceOf(JsonPatchConflictException.class);
}
/**
@@ -599,7 +602,7 @@ void testPreviewDiff() {
// Invalid patch
assertThatThrownBy(() -> repo.previewDiff(HEAD, jsonPatches[1]).join())
.isInstanceOf(CompletionException.class)
- .hasCauseInstanceOf(ChangeConflictException.class);
+ .hasCauseInstanceOf(JsonPatchConflictException.class);
// Invalid removal
assertThatThrownBy(() -> repo.previewDiff(HEAD, Change.ofRemoval(jsonPaths[0])).join())
@@ -662,7 +665,9 @@ void testDiff_add() {
// PATCH_TO_UPSERT diff option.
final Map> diffUpsert = repo.diff(
- prevRevision, currRevision, Repository.ALL_PATH, DiffResultType.PATCH_TO_UPSERT)
+ prevRevision, currRevision,
+ Repository.ALL_PATH,
+ DiffResultType.PATCH_TO_UPSERT)
.join();
assertThat(diffUpsert).hasSize(2)
.containsEntry(jsonPath, jsonUpsert)
@@ -766,13 +771,17 @@ void testDiff_modify() {
// PATCH_TO_UPSERT diff option.
final Map> diffUpsert = repo.diff(
- prevRevision, currRevision, Repository.ALL_PATH, DiffResultType.PATCH_TO_UPSERT)
+ prevRevision, currRevision,
+ Repository.ALL_PATH,
+ DiffResultType.PATCH_TO_UPSERT)
.join();
assertThat(diffUpsert).hasSize(2)
.containsEntry(jsonNodePath, expectedJsonUpsert)
.containsEntry(textNodePath, expectedTextUpsert);
final Change> jsonQueryUpsert = repo.diff(
- prevRevision, currRevision, Query.ofJson(jsonNodePath), DiffResultType.PATCH_TO_UPSERT)
+ prevRevision, currRevision,
+ Query.ofJson(jsonNodePath),
+ DiffResultType.PATCH_TO_UPSERT)
.join();
assertThat(jsonQueryUpsert).isEqualTo(expectedJsonUpsert);
final Change> textQueryUpsert = repo.diff(prevRevision, currRevision, Query.ofText(textNodePath),
From c709af4856b4bb478dd0a4e80f713786439b0463 Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Fri, 17 Jan 2025 18:41:47 +0900
Subject: [PATCH 3/7] fix tests
---
.../common/jsonpatch/package-info.java | 8 +--
.../JsonPatchOperationIntegrationTest.java | 51 ++++++++++---------
2 files changed, 29 insertions(+), 30 deletions(-)
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
index 4de3a53296..89dbb24f73 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
@@ -15,18 +15,12 @@
*/
/**
- * Implementation of JSON Patch.
+ * Implementation of JSON Patch.
*
* 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}).
- *
*/
@NonNullByDefault
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
index bbd7d235fb..7f1b9f2339 100644
--- a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
@@ -20,7 +20,6 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import java.util.List;
import java.util.concurrent.CompletionException;
import org.junit.jupiter.api.BeforeEach;
@@ -30,15 +29,14 @@
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonPointer;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.IntNode;
import com.google.common.collect.ImmutableList;
import com.linecorp.centraldogma.client.CentralDogma;
import com.linecorp.centraldogma.client.CentralDogmaRepository;
import com.linecorp.centraldogma.common.Change;
+import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension;
class JsonPatchOperationIntegrationTest {
@@ -65,7 +63,7 @@ void setUp() {
}
@Test
- void testAdd() throws JsonParseException {
+ void add() throws JsonParseException {
repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
.push()
.join();
@@ -80,7 +78,7 @@ void testAdd() throws JsonParseException {
}
@Test
- void testCopy() throws JsonParseException {
+ void copy() throws JsonParseException {
repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1 }"))
.push()
.join();
@@ -112,7 +110,7 @@ void testMove() throws JsonParseException {
}
@Test
- void testRemove() throws JsonParseException {
+ void remove() throws JsonParseException {
repository.commit("add ab", Change.ofJsonUpsert("/a.json", "{ \"a\": 1, \"b\": 2 }"))
.push()
.join();
@@ -130,8 +128,7 @@ void testRemove() throws JsonParseException {
.join())
.isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(JsonPatchConflictException.class)
- .hasMessageContaining("failed to apply JSON patch:")
- .hasMessageContaining("path=/a.json, content=[{\"op\":\"remove\",\"path\":\"/a\"}]");
+ .hasMessageContaining("non-existent path: /a");
}
@Test
@@ -195,9 +192,7 @@ void safeReplace() throws JsonParseException {
.join();
}).isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(JsonPatchConflictException.class)
- .hasMessageContaining(
- "failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, path=/a.json, " +
- "content=[{\"op\":\"safeReplace\",\"path\":\"/a\",\"oldValue\":3,\"value\":4}]}");
+ .hasMessageContaining("mismatching value at '/a': 2 (expected: 3)");
}
@Test
@@ -219,9 +214,7 @@ void test() {
.join();
}).isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(JsonPatchConflictException.class)
- .hasMessageContaining(
- "failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, " +
- "path=/a.json, content=[{\"op\":\"test\",\"path\":\"/a\",\"value\":2}]}");
+ .hasMessageContaining("mismatching value at '/a': 1 (expected: 2)");
}
@Test
@@ -243,9 +236,7 @@ void testAbsence() {
.join();
}).isInstanceOf(CompletionException.class)
.hasCauseInstanceOf(JsonPatchConflictException.class)
- .hasMessageContaining(
- "failed to apply JSON patch: Change{type=APPLY_JSON_PATCH, " +
- "path=/a.json, content=[{\"op\":\"testAbsence\",\"path\":\"/a\"}]}");
+ .hasMessageContaining("existent path: /a");
}
@Test
@@ -277,11 +268,25 @@ void testMultipleOperations() throws JsonParseException {
@Test
void testEquality() throws JsonProcessingException {
- final RemoveOperation remove = JsonPatchOperation.remove(JsonPointer.compile("/a"));
- final JsonNode jsonNode = remove.toJsonNode();
- final ObjectMapper mapper = new ObjectMapper();
- final List jsonPatchOperations =
- mapper.treeToValue(jsonNode, new TypeReference>() {});
- assertThat(jsonPatchOperations.get(0)).isEqualTo(remove);
+ ensureSerdesEquality(JsonPatchOperation.add(JsonPointer.compile("/a"), new IntNode(1)), AddOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.copy(JsonPointer.compile("/a"), JsonPointer.compile("/b")),
+ CopyOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.move(JsonPointer.compile("/a"), JsonPointer.compile("/b")),
+ MoveOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.remove(JsonPointer.compile("/a")), RemoveOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.removeIfExists(JsonPointer.compile("/a")), RemoveIfExistsOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.replace(JsonPointer.compile("/a"), new IntNode(1)), ReplaceOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.safeReplace(JsonPointer.compile("/a"), new IntNode(1), new IntNode(2)),
+ SafeReplaceOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.test(JsonPointer.compile("/a"), new IntNode(1)), TestOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.testAbsence(JsonPointer.compile("/a")), TestAbsenceOperation.class);
+ }
+
+ private static void ensureSerdesEquality(T operation, Class clazz)
+ throws JsonProcessingException {
+ final String json = Jackson.writeValueAsString(operation);
+ final JsonNode jsonNode = Jackson.readTree(json);
+ final T deserialized = Jackson.convertValue(jsonNode, clazz);
+ assertThat(deserialized).isEqualTo(operation);
}
}
From 7c28e785012e61ddc47bc27a3298f41905399d7b Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Fri, 17 Jan 2025 18:46:54 +0900
Subject: [PATCH 4/7] lint
---
.../client/armeria/ArmeriaCentralDogma.java | 3 +--
.../common/jsonpatch/AddOperation.java | 3 ++-
.../JsonPatchOperationIntegrationTest.java | 20 ++++++++++++-------
.../storage/repository/git/GitRepository.java | 20 +++++++++----------
.../repository/git/GitRepositoryTest.java | 4 ++--
5 files changed, 28 insertions(+), 22 deletions(-)
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 022661ead7..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
@@ -157,8 +157,7 @@ public ArmeriaCentralDogma(ScheduledExecutorService blockingTaskExecutor,
@Override
public CompletableFuture whenEndpointReady() {
- return client.endpointGroup().whenReady().thenRun(() -> {
- });
+ return client.endpointGroup().whenReady().thenRun(() -> {});
}
@Override
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
index 6338940070..6ab2eba06e 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
@@ -99,7 +99,8 @@ static JsonNode addToArray(final JsonPointer path, final JsonNode node, final Js
try {
index = Integer.parseInt(rawToken);
} catch (NumberFormatException ignored) {
- throw new JsonPatchConflictException("not an index: " + rawToken + " (expected: a non-negative integer)");
+ throw new JsonPatchConflictException(
+ "not an index: " + rawToken + " (expected: a non-negative integer)");
}
if (index < 0 || index > size) {
diff --git a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
index 7f1b9f2339..f6ae064ef5 100644
--- a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
@@ -268,18 +268,24 @@ void testMultipleOperations() throws JsonParseException {
@Test
void testEquality() throws JsonProcessingException {
- ensureSerdesEquality(JsonPatchOperation.add(JsonPointer.compile("/a"), new IntNode(1)), AddOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.add(JsonPointer.compile("/a"), new IntNode(1)),
+ AddOperation.class);
ensureSerdesEquality(JsonPatchOperation.copy(JsonPointer.compile("/a"), JsonPointer.compile("/b")),
CopyOperation.class);
ensureSerdesEquality(JsonPatchOperation.move(JsonPointer.compile("/a"), JsonPointer.compile("/b")),
MoveOperation.class);
ensureSerdesEquality(JsonPatchOperation.remove(JsonPointer.compile("/a")), RemoveOperation.class);
- ensureSerdesEquality(JsonPatchOperation.removeIfExists(JsonPointer.compile("/a")), RemoveIfExistsOperation.class);
- ensureSerdesEquality(JsonPatchOperation.replace(JsonPointer.compile("/a"), new IntNode(1)), ReplaceOperation.class);
- ensureSerdesEquality(JsonPatchOperation.safeReplace(JsonPointer.compile("/a"), new IntNode(1), new IntNode(2)),
- SafeReplaceOperation.class);
- ensureSerdesEquality(JsonPatchOperation.test(JsonPointer.compile("/a"), new IntNode(1)), TestOperation.class);
- ensureSerdesEquality(JsonPatchOperation.testAbsence(JsonPointer.compile("/a")), TestAbsenceOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.removeIfExists(JsonPointer.compile("/a")),
+ RemoveIfExistsOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.replace(JsonPointer.compile("/a"), new IntNode(1)),
+ ReplaceOperation.class);
+ ensureSerdesEquality(
+ JsonPatchOperation.safeReplace(JsonPointer.compile("/a"), new IntNode(1), new IntNode(2)),
+ SafeReplaceOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.test(JsonPointer.compile("/a"), new IntNode(1)),
+ TestOperation.class);
+ ensureSerdesEquality(JsonPatchOperation.testAbsence(JsonPointer.compile("/a")),
+ TestAbsenceOperation.class);
}
private static void ensureSerdesEquality(T operation, Class clazz)
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
index 87d2b6a59e..426fd80b91 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
@@ -847,6 +847,16 @@ private static void putChange(Map> changeMap, String path, Cha
assert oldChange == null;
}
+ private static boolean shouldAllowEmptyCommit(Iterable> changes) {
+ // allMatch returns true if the stream is empty.
+ if (Iterables.isEmpty(changes)) {
+ return false;
+ }
+ // JsonPatch operations its own validation for the changes so we don't need to validate them here.
+ return Streams.stream(changes)
+ .allMatch(change -> change.type() == ChangeType.APPLY_JSON_PATCH);
+ }
+
@Override
public CompletableFuture commit(
Revision baseRevision, long commitTimeMillis, Author author, String summary,
@@ -869,16 +879,6 @@ public CompletableFuture commit(
});
}
- private boolean shouldAllowEmptyCommit(Iterable> changes) {
- // allMatch returns true if the stream is empty.
- if (Iterables.isEmpty(changes)) {
- return false;
- }
- // JsonPatch operations its own validation for the changes so we don't need to validate them here.
- return Streams.stream(changes)
- .allMatch(change -> change.type() == ChangeType.APPLY_JSON_PATCH);
- }
-
@Override
public CompletableFuture commit(Revision baseRevision, long commitTimeMillis, Author author,
String summary, String detail, Markup markup,
diff --git a/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java b/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java
index 6767296c33..39fd3bf549 100644
--- a/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java
+++ b/server/src/test/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepositoryTest.java
@@ -269,8 +269,8 @@ private static void testPatch(Change>[] patches, Change>[] upserts, boolean
assertThatThrownBy(
() -> repo.commit(HEAD, 0L, Author.UNKNOWN, SUMMARY, patches[finalJ]).join())
.isInstanceOf(CompletionException.class)
- .hasCauseExactlyInstanceOf(jsonPatch ? JsonPatchConflictException.class :
- TextPatchConflictException.class);
+ .hasCauseExactlyInstanceOf(jsonPatch ? JsonPatchConflictException.class
+ : TextPatchConflictException.class);
}
// Ensure that the failed commit does not change the revision.
From f50f4920b447ed69eabf67350728cfad8a1c02f0 Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Fri, 17 Jan 2025 18:54:17 +0900
Subject: [PATCH 5/7] copyright
---
.../common/jsonpatch/AddOperation.java | 20 ++++++++++++++++++-
.../common/jsonpatch/CopyOperation.java | 20 ++++++++++++++++++-
.../common/jsonpatch/DualPathOperation.java | 20 ++++++++++++++++++-
.../common/jsonpatch/JsonPatchOperation.java | 20 ++++++++++++++++++-
.../common/jsonpatch/MoveOperation.java | 20 ++++++++++++++++++-
.../common/jsonpatch/PathValueOperation.java | 20 ++++++++++++++++++-
.../jsonpatch/RemoveIfExistsOperation.java | 2 +-
.../common/jsonpatch/RemoveOperation.java | 20 ++++++++++++++++++-
.../common/jsonpatch/ReplaceOperation.java | 20 ++++++++++++++++++-
.../jsonpatch/SafeReplaceOperation.java | 2 +-
.../jsonpatch/TestAbsenceOperation.java | 2 +-
.../common/jsonpatch/TestOperation.java | 20 ++++++++++++++++++-
12 files changed, 174 insertions(+), 12 deletions(-)
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
index 6ab2eba06e..a47b51f06f 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/AddOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
index d9ea5a5b03..e1d7c3cbf8 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/CopyOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java
index 174cba02b9..f1779a5121 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/DualPathOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
index d8ee15c32a..829bc4a907 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
index 421770c0b9..4bdc6f0a98 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/MoveOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
index cb3ab39f6e..b7fccf5c37 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/PathValueOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java
index 125cc3b7f7..0e6a33046c 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveIfExistsOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2018 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java
index 8a4189c11c..8eb98e1c9a 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/RemoveOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java
index 13b8c92f60..b75ac120ba 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/ReplaceOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
index 4641968534..f9f7020798 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/SafeReplaceOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
index 2c199c7ae8..1286549502 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestAbsenceOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java
index b61edb2534..d9e9d16191 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/TestOperation.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 LINE Corporation
+ * Copyright 2017 LINE Corporation
*
* LINE Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
@@ -13,6 +13,24 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
+/*
+ * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com)
+ *
+ * This software is dual-licensed under:
+ *
+ * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any
+ * later version;
+ * - the Apache Software License (ASL) version 2.0.
+ *
+ * The text of this file and of both licenses is available at the root of this
+ * project or, if you have the jar distribution, in directory META-INF/, under
+ * the names LGPL-3.0.txt and ASL-2.0.txt respectively.
+ *
+ * Direct link to the sources:
+ *
+ * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt
+ * - ASL 2.0: https://www.apache.org/licenses/LICENSE-2.0.txt
+ */
package com.linecorp.centraldogma.common.jsonpatch;
From 1d2c74e0ee4dc66ba65f09c293662fc3c5d6989e Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Tue, 21 Jan 2025 16:19:35 +0900
Subject: [PATCH 6/7] add shortcut factory methods
---
.../common/jsonpatch/JsonPatchOperation.java | 112 +++++++++++++++++-
.../JsonPatchOperationIntegrationTest.java | 26 ++++
2 files changed, 132 insertions(+), 6 deletions(-)
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
index 829bc4a907..ed73f6f898 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
@@ -108,24 +108,54 @@ public static AddOperation add(JsonPointer path, JsonNode value) {
return new AddOperation(path, value);
}
+ /**
+ * Creates a new JSON Patch {@code add} operation.
+ *
+ * @param path the JSON Pointer for this operation
+ * @param value the value to add
+ */
+ public static AddOperation add(String path, JsonNode value) {
+ requireNonNull(path, "path");
+ return add(JsonPointer.compile(path), value);
+ }
+
+ /**
+ * Creates a new JSON Patch {@code copy} operation.
+ *
+ * @param from the source JSON Pointer
+ * @param to the destination JSON Pointer
+ */
+ public static CopyOperation copy(JsonPointer from, JsonPointer to) {
+ return new CopyOperation(from, to);
+ }
+
/**
* Creates a new JSON Patch {@code copy} operation.
*
* @param from the source JSON Pointer
- * @param path the destination JSON Pointer
+ * @param to the destination JSON Pointer
*/
- public static CopyOperation copy(JsonPointer from, JsonPointer path) {
- return new CopyOperation(from, path);
+ public static CopyOperation copy(String from, String to) {
+ requireNonNull(from, "from");
+ requireNonNull(to, "to");
+ return copy(JsonPointer.compile(from), JsonPointer.compile(to));
}
+
/**
* Creates a new JSON Patch {@code move} operation.
*
* @param from the source JSON Pointer
- * @param path the destination JSON Pointer
+ * @param to the destination JSON Pointer
*/
- public static MoveOperation move(JsonPointer from, JsonPointer path) {
- return new MoveOperation(from, path);
+ public static MoveOperation move(JsonPointer from, JsonPointer to) {
+ return new MoveOperation(from, to);
+ }
+
+ public static MoveOperation move(String from, String to) {
+ requireNonNull(from, "from");
+ requireNonNull(to, "to");
+ return move(JsonPointer.compile(from), JsonPointer.compile(to));
}
/**
@@ -139,6 +169,18 @@ 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.
*
@@ -148,6 +190,16 @@ 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.
*
@@ -158,6 +210,17 @@ 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.
*
@@ -169,6 +232,18 @@ public static SafeReplaceOperation safeReplace(JsonPointer path, JsonNode oldVal
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.
*
@@ -181,6 +256,19 @@ 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.
*
@@ -192,6 +280,18 @@ 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}.
*/
diff --git a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
index f6ae064ef5..45a62d830e 100644
--- a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
@@ -266,6 +266,32 @@ void testMultipleOperations() throws JsonParseException {
.isEqualTo("{ \"c\": 4, \"d\": 1 }");
}
+ @Test
+ void testMultipleOperationsWithStringPath() throws JsonParseException {
+ repository.commit("add a", Change.ofJsonUpsert("/a.json", "{ \"a\": 1, \"b\": 2 }"))
+ .push()
+ .join();
+ // { "a": 1, "b": 2 } -> { "a": 1, "b": 2, "c": 3 }
+ final AddOperation add = JsonPatchOperation.add("/c", new IntNode(3));
+ // { "a": 1, "b": 2, "c": 3 } -> { "b": 2, "c": 3, "d": 1 }
+ final MoveOperation move = JsonPatchOperation.move("/a", "/d");
+ // { "b": 2, "c": 3, "d": 1 } -> { "c": 3, "d": 1 }
+ final RemoveOperation remove = JsonPatchOperation.remove("/b");
+ // { "c": 3, "d": 1 } -> { "c": 4, "d": 1 }
+ final SafeReplaceOperation safeReplace =
+ JsonPatchOperation.safeReplace("/c", new IntNode(3), new IntNode(4));
+
+ final Change change = Change.ofJsonPatch("/a.json",
+ ImmutableList.of(add, move, remove, safeReplace));
+
+ repository.commit("json patch operations", change)
+ .push()
+ .join();
+
+ assertThatJson(repository.file("/a.json").get().join().contentAsJson())
+ .isEqualTo("{ \"c\": 4, \"d\": 1 }");
+ }
+
@Test
void testEquality() throws JsonProcessingException {
ensureSerdesEquality(JsonPatchOperation.add(JsonPointer.compile("/a"), new IntNode(1)),
From 5630ad252fbf809e477975bb598f163bc8bb58ae Mon Sep 17 00:00:00 2001
From: Ikhun Um
Date: Thu, 23 Jan 2025 15:43:57 +0900
Subject: [PATCH 7/7] Address comments
---
.../common/jsonpatch/JsonPatchOperation.java | 10 ++++--
.../common/jsonpatch/package-info.java | 2 +-
.../internal/jsonpatch/JsonPatch.java | 2 +-
.../JsonPatchOperationIntegrationTest.java | 21 ++++++++-----
.../repository/git/CommitExecutor.java | 29 ++++++++++++-----
.../repository/git/DefaultChangesApplier.java | 8 ++---
.../storage/repository/git/GitRepository.java | 31 ++++++++++++-------
7 files changed, 67 insertions(+), 36 deletions(-)
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
index ed73f6f898..0113acb744 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperation.java
@@ -51,11 +51,11 @@
import com.linecorp.centraldogma.internal.Jackson;
/**
- * Base abstract class for one JSON
+ * Base abstract class for one JSON
* Patch operation. A {@link JsonPatchOperation} can be converted into a JSON patch by calling
* {@link #toJsonNode()}.
*
- * 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
@@ -152,6 +152,12 @@ public static MoveOperation move(JsonPointer from, JsonPointer to) {
return new MoveOperation(from, to);
}
+ /**
+ * Creates a new JSON Patch {@code move} operation.
+ *
+ * @param from the source JSON Pointer
+ * @param to the destination JSON Pointer
+ */
public static MoveOperation move(String from, String to) {
requireNonNull(from, "from");
requireNonNull(to, "to");
diff --git a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
index 89dbb24f73..13359235d4 100644
--- a/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
+++ b/common/src/main/java/com/linecorp/centraldogma/common/jsonpatch/package-info.java
@@ -15,7 +15,7 @@
*/
/**
- * Implementation of JSON Patch.
+ * Implementation of JSON Patch.
*
*
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
diff --git a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
index fb7ec3a0c2..3035e15e8d 100644
--- a/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
+++ b/common/src/main/java/com/linecorp/centraldogma/internal/jsonpatch/JsonPatch.java
@@ -69,7 +69,7 @@
/**
* Implementation of JSON Patch.
*
- *
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
diff --git a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
index 45a62d830e..e70b7d44a3 100644
--- a/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
+++ b/common/src/test/java/com/linecorp/centraldogma/common/jsonpatch/JsonPatchOperationIntegrationTest.java
@@ -36,6 +36,8 @@
import com.linecorp.centraldogma.client.CentralDogma;
import com.linecorp.centraldogma.client.CentralDogmaRepository;
import com.linecorp.centraldogma.common.Change;
+import com.linecorp.centraldogma.common.Entry;
+import com.linecorp.centraldogma.common.PushResult;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.testing.junit.CentralDogmaExtension;
@@ -139,19 +141,22 @@ void removeIfExists() throws JsonParseException {
final RemoveIfExistsOperation removeIfExists =
JsonPatchOperation.removeIfExists(JsonPointer.compile("/a"));
final Change change = Change.ofJsonPatch("/a.json", removeIfExists);
- repository.commit("remove a", change)
- .push()
- .join();
+ final PushResult result0 = repository.commit("remove a", change)
+ .push()
+ .join();
JsonNode jsonNode = repository.file("/a.json").get().join().contentAsJson();
assertThatJson(jsonNode).isEqualTo("{ \"b\": 2 }");
- repository.commit("remove a again", change)
- .push()
- .join();
+ final PushResult result1 = repository.commit("remove a again", change)
+ .push()
+ .join();
+ // Should not increase the revision if the path is absent and the history must be the same.
+ assertThat(result1.revision()).isEqualTo(result0.revision());
- jsonNode = repository.file("/a.json").get().join().contentAsJson();
- assertThatJson(jsonNode).isEqualTo("{ \"b\": 2 }");
+ final Entry> data = repository.file("/a.json").get().join();
+ assertThat(data.revision()).isEqualTo(result0.revision());
+ assertThatJson(data.contentAsJson()).isEqualTo("{ \"b\": 2 }");
}
@Test
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/CommitExecutor.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/CommitExecutor.java
index b3bccb8833..c1390810db 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/CommitExecutor.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/CommitExecutor.java
@@ -61,17 +61,17 @@ final class CommitExecutor {
private final String summary;
private final String detail;
private final Markup markup;
- private final boolean allowEmptyCommit;
+ private final EmptyCommitPolicy emptyCommitPolicy;
CommitExecutor(GitRepository gitRepository, long commitTimeMillis, Author author,
- String summary, String detail, Markup markup, boolean allowEmptyCommit) {
+ String summary, String detail, Markup markup, EmptyCommitPolicy emptyCommitPolicy) {
this.gitRepository = gitRepository;
this.commitTimeMillis = commitTimeMillis;
this.author = author;
this.summary = summary;
this.detail = detail;
this.markup = markup;
- this.allowEmptyCommit = allowEmptyCommit;
+ this.emptyCommitPolicy = emptyCommitPolicy;
}
Author author() {
@@ -153,11 +153,18 @@ RevisionAndEntries commit(@Nullable Revision headRevision, Revision nextRevision
} else {
diffEntries = ImmutableList.of();
}
- if (!allowEmptyCommit && isEmpty) {
- throw new RedundantChangeException(
- headRevision,
- "changes did not change anything in " + gitRepository.parent().name() + '/' +
- gitRepository.name() + " at revision " + headRevision.major() + ": " + changes);
+ if (isEmpty) {
+ switch (emptyCommitPolicy) {
+ case ALLOW:
+ break;
+ case DISALLOW:
+ throw new RedundantChangeException(
+ headRevision,
+ "changes did not change anything in " + gitRepository.parent().name() + '/' +
+ gitRepository.name() + " at revision " + headRevision.major() + ": " + changes);
+ case IGNORE:
+ return new RevisionAndEntries(headRevision, diffEntries);
+ }
}
} else {
// initial commit.
@@ -211,4 +218,10 @@ static final class RevisionAndEntries {
this.diffEntries = diffEntries;
}
}
+
+ enum EmptyCommitPolicy {
+ ALLOW,
+ DISALLOW,
+ IGNORE
+ }
}
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
index af04a7add1..3931a7ce1b 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/DefaultChangesApplier.java
@@ -159,12 +159,10 @@ int doApply(Revision unused, DirCache dirCache,
final JsonNode newJsonNode;
try {
newJsonNode = JsonPatch.fromJson((JsonNode) change.content()).apply(oldJsonNode);
+ } catch (JsonPatchConflictException e) {
+ throw e;
} catch (Exception e) {
- if (e instanceof JsonPatchConflictException) {
- throw (JsonPatchConflictException) e;
- } else {
- throw new JsonPatchConflictException("failed to apply JSON patch: " + change, e);
- }
+ throw new JsonPatchConflictException("failed to apply JSON patch: " + change, e);
}
// Apply only when the contents are really different.
diff --git a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
index 426fd80b91..b66dd88125 100644
--- a/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
+++ b/server/src/main/java/com/linecorp/centraldogma/server/internal/storage/repository/git/GitRepository.java
@@ -111,6 +111,7 @@
import com.linecorp.centraldogma.server.internal.IsolatedSystemReader;
import com.linecorp.centraldogma.server.internal.JGitUtil;
import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryCache;
+import com.linecorp.centraldogma.server.internal.storage.repository.git.CommitExecutor.EmptyCommitPolicy;
import com.linecorp.centraldogma.server.storage.StorageException;
import com.linecorp.centraldogma.server.storage.project.Project;
import com.linecorp.centraldogma.server.storage.repository.DiffResultType;
@@ -251,7 +252,7 @@ class GitRepository implements Repository {
commitIdDatabase = new CommitIdDatabase(jGitRepository);
new CommitExecutor(this, creationTimeMillis, author, "Create a new repository", "",
- Markup.PLAINTEXT, true)
+ Markup.PLAINTEXT, EmptyCommitPolicy.ALLOW)
.executeInitialCommit();
headRevision = Revision.INIT;
@@ -847,14 +848,22 @@ private static void putChange(Map> changeMap, String path, Cha
assert oldChange == null;
}
- private static boolean shouldAllowEmptyCommit(Iterable> changes) {
+ private static EmptyCommitPolicy getEmptyCommitPolicy(Iterable> changes) {
// allMatch returns true if the stream is empty.
if (Iterables.isEmpty(changes)) {
- return false;
+ // Disallowed by default
+ return EmptyCommitPolicy.DISALLOW;
+ }
+ final boolean allJsonPatches =
+ Streams.stream(changes)
+ .allMatch(change -> change.type() == ChangeType.APPLY_JSON_PATCH);
+ if (allJsonPatches) {
+ // JsonPatch operations may have an empty change that should be ignored.
+ // For example, the removeIfExists operation does nothing if the target path is absent.
+ return EmptyCommitPolicy.IGNORE;
+ } else {
+ return EmptyCommitPolicy.DISALLOW;
}
- // JsonPatch operations its own validation for the changes so we don't need to validate them here.
- return Streams.stream(changes)
- .allMatch(change -> change.type() == ChangeType.APPLY_JSON_PATCH);
}
@Override
@@ -868,9 +877,9 @@ public CompletableFuture commit(
requireNonNull(markup, "markup");
requireNonNull(changes, "changes");
- final boolean allowEmptyCommit = shouldAllowEmptyCommit(changes);
+ final EmptyCommitPolicy emptyCommitPolicy = getEmptyCommitPolicy(changes);
final CommitExecutor commitExecutor =
- new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, allowEmptyCommit);
+ new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, emptyCommitPolicy);
return commit(baseRevision, commitExecutor, normBaseRevision -> {
if (!directExecution) {
return changes;
@@ -890,7 +899,7 @@ public CompletableFuture commit(Revision baseRevision, long commit
requireNonNull(markup, "markup");
requireNonNull(transformer, "transformer");
final CommitExecutor commitExecutor =
- new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, false);
+ new CommitExecutor(this, commitTimeMillis, author, summary, detail, markup, EmptyCommitPolicy.DISALLOW);
return commit(baseRevision, commitExecutor,
normBaseRevision -> blockingPreviewDiff(
normBaseRevision, new TransformingChangesApplier(transformer)).values());
@@ -1330,7 +1339,7 @@ public void cloneTo(File newRepoDir, BiConsumer progressListen
try {
new CommitExecutor(newRepo, c.when(), c.author(), c.summary(),
- c.detail(), c.markup(), false)
+ c.detail(), c.markup(), EmptyCommitPolicy.DISALLOW)
.execute(baseRevision, normBaseRevision -> blockingPreviewDiff(
normBaseRevision, new DefaultChangesApplier(changes)).values());
previousNonEmptyRevision = revision;
@@ -1338,7 +1347,7 @@ public void cloneTo(File newRepoDir, BiConsumer progressListen
// NB: We allow an empty commit here because an old version of Central Dogma had a bug
// which allowed the creation of an empty commit.
new CommitExecutor(newRepo, c.when(), c.author(), c.summary(),
- c.detail(), c.markup(), true)
+ c.detail(), c.markup(), EmptyCommitPolicy.ALLOW)
.execute(baseRevision, normBaseRevision -> blockingPreviewDiff(
normBaseRevision, new DefaultChangesApplier(changes)).values());
}