Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose JSON Patch operations as public API #1089

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
import com.linecorp.centraldogma.common.Revision;
import com.linecorp.centraldogma.common.RevisionNotFoundException;
import com.linecorp.centraldogma.common.ShuttingDownException;
import com.linecorp.centraldogma.common.TextPatchConflictException;
import com.linecorp.centraldogma.common.jsonpatch.JsonPatchConflictException;
import com.linecorp.centraldogma.internal.HistoryConstants;
import com.linecorp.centraldogma.internal.Jackson;
import com.linecorp.centraldogma.internal.Util;
Expand Down Expand Up @@ -137,6 +139,8 @@ public final class ArmeriaCentralDogma extends AbstractCentralDogma {
.put(ReadOnlyException.class.getName(), ReadOnlyException::new)
.put(MirrorException.class.getName(), MirrorException::new)
.put(PermissionException.class.getName(), PermissionException::new)
.put(JsonPatchConflictException.class.getName(), JsonPatchConflictException::new)
.put(TextPatchConflictException.class.getName(), TextPatchConflictException::new)
.build();

private final WebClient client;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getters will be useful when logging.

return repositoryName;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -223,6 +225,42 @@ static Change<JsonNode> 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 <a href="https://tools.ietf.org/html/rfc6902">JSON patch format</a>
*/
static Change<JsonNode> 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 <a href="https://tools.ietf.org/html/rfc6902">JSON patch format</a>
*/
static Change<JsonNode> 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 <a href="https://tools.ietf.org/html/rfc6902">JSON patch format</a>
*/
static Change<JsonNode> 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}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
/*
* Copyright (c) 2014, Francis Galiegue ([email protected])
*
* 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;
Expand Down Expand Up @@ -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();
}
Expand All @@ -110,12 +99,13 @@ 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,26 +13,10 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
/*
* Copyright (c) 2014, Francis Galiegue ([email protected])
*
* 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;
Expand All @@ -49,19 +33,25 @@
*/
public final class CopyOperation extends DualPathOperation {

/**
* Creates a new instance.
*/
@JsonCreator
CopyOperation(@JsonProperty("from") final JsonPointer from,
@JsonProperty("path") final JsonPointer path) {
super("copy", from, path);
}

@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);
throw new JsonPatchConflictException("non-existent source path: " + from);
}

final JsonPointer path = path();
if (path.toString().isEmpty()) {
return source;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -13,28 +13,13 @@
* License for the specific language governing permissions and limitations
* under the License.
*/
/*
* Copyright (c) 2014, Francis Galiegue ([email protected])
*
* 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;
Expand All @@ -49,7 +34,7 @@
abstract class DualPathOperation extends JsonPatchOperation {

@JsonSerialize(using = ToStringSerializer.class)
final JsonPointer from;
private final JsonPointer from;

/**
* Creates a new instance.
Expand All @@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public JsonPointer from() {
JsonPointer from() {

because DualPathOperation is package-private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally made it public so users can read the value from subclasses.

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();
}
Expand All @@ -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() + '"';
}
}
Loading
Loading