From 68478c27723ab68a18ad1a0cd7a91c39f2371f03 Mon Sep 17 00:00:00 2001 From: Yannic Klem Date: Mon, 21 Mar 2022 18:08:26 +0100 Subject: [PATCH] Add class to calculate a JSON merge patch between to JSON values Signed-off-by: Yannic Klem --- .../eclipse/ditto/json/JsonMergePatch.java | 90 +++++++++++++++++ .../ditto/json/JsonMergePatchTest.java | 96 +++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 json/src/main/java/org/eclipse/ditto/json/JsonMergePatch.java create mode 100644 json/src/test/java/org/eclipse/ditto/json/JsonMergePatchTest.java diff --git a/json/src/main/java/org/eclipse/ditto/json/JsonMergePatch.java b/json/src/main/java/org/eclipse/ditto/json/JsonMergePatch.java new file mode 100644 index 0000000000..1b56b93dc1 --- /dev/null +++ b/json/src/main/java/org/eclipse/ditto/json/JsonMergePatch.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.json; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +/** + * Can be used to get the diff in form of a JSON merge Patch according to + * RFC 7386 between two {@link JsonValue json values}. + */ +public final class JsonMergePatch { + + /** + * This method computes the change from the given {@code oldValue} to the given {@code newValue}. + * The result is a JSON merge patch according to RFC 7386. + * + * @param oldValue the original value + * @param newValue the new changed value + * @return a JSON merge patch according to RFC 7386 or empty if values are equal. + */ + public static Optional compute(final JsonValue oldValue, final JsonValue newValue) { + @Nullable final JsonValue diff; + if (oldValue.equals(newValue)) { + diff = null; + } else if (oldValue.isObject() && newValue.isObject()) { + diff = compute(oldValue.asObject(), newValue.asObject()).orElse(null); + } else { + diff = newValue; + } + return Optional.ofNullable(diff); + } + + /** + * This method computes the change from the given {@code oldValue} to the given {@code newValue}. + * The result is a JSON merge patch according to RFC 7386. + * + * @param oldJsonObject the original JSON object + * @param newJsonObject the new changed JSON object + * @return a JSON merge patch according to RFC 7386 or empty if values are equal. + */ + public static Optional compute(final JsonObject oldJsonObject, final JsonObject newJsonObject) { + final JsonObjectBuilder builder = JsonObject.newBuilder(); + final List oldKeys = oldJsonObject.getKeys(); + final List newKeys = newJsonObject.getKeys(); + + final List addedKeys = newKeys.stream() + .filter(key -> !oldKeys.contains(key)) + .collect(Collectors.toList()); + addedKeys.forEach(key -> newJsonObject.getValue(key).ifPresent(value -> builder.set(key, value))); + + final List deletedKeys = oldKeys.stream() + .filter(key -> !newKeys.contains(key)) + .collect(Collectors.toList()); + deletedKeys.forEach(key -> builder.set(key, JsonValue.nullLiteral())); + + final List keptKeys = oldKeys.stream() + .filter(newKeys::contains) + .collect(Collectors.toList()); + keptKeys.forEach(key -> { + final Optional oldValue = oldJsonObject.getValue(key); + final Optional newValue = newJsonObject.getValue(key); + if (oldValue.isPresent() && newValue.isPresent()) { + compute(oldValue.get(), newValue.get()).ifPresent(diff -> builder.set(key, diff)); + } else if (oldValue.isPresent()) { + // Should never happen because deleted keys were handled before + builder.set(key, JsonValue.nullLiteral()); + } else if (newValue.isPresent()) { + // Should never happen because added keys were handled before + builder.set(key, newValue.get()); + } + }); + + return builder.isEmpty() ? Optional.empty() : Optional.of(builder.build()); + } + +} diff --git a/json/src/test/java/org/eclipse/ditto/json/JsonMergePatchTest.java b/json/src/test/java/org/eclipse/ditto/json/JsonMergePatchTest.java new file mode 100644 index 0000000000..2f134f3dd2 --- /dev/null +++ b/json/src/test/java/org/eclipse/ditto/json/JsonMergePatchTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.ditto.json; + + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public final class JsonMergePatchTest { + + @Test + public void computesDiffForSingleValue() { + final JsonObject oldValue = JsonObject.newBuilder() + .set("Test", "Foo") + .build(); + + final JsonObject newValue = JsonObject.newBuilder() + .set("Test", "Bar") + .build(); + + assertThat(JsonMergePatch.compute(oldValue, newValue)).contains(newValue); + } + + @Test + public void computesDiffForSingleValueOutOfMultipleValues() { + final JsonObject oldValue = JsonObject.newBuilder() + .set("Test", "Foo") + .set("Bum", "Lux") + .build(); + + final JsonObject newValue = JsonObject.newBuilder() + .set("Test", "Bar") + .set("Bum", "Lux") + .build(); + + final JsonObject expected = JsonObject.newBuilder() + .set("Test", "Bar") + .build(); + + assertThat(JsonMergePatch.compute(oldValue, newValue)).contains(expected); + } + + @Test + public void computesDiffForMultipleValues() { + final JsonObject oldValue = JsonObject.newBuilder() + .set("Test", "Foo") + .set("Bum", "Lux") + .build(); + + final JsonObject newValue = JsonObject.newBuilder() + .set("Test", "Bar") + .set("Bum", "Luxes") + .build(); + + final JsonObject expected = newValue; + + assertThat(JsonMergePatch.compute(oldValue, newValue)).contains(expected); + } + + @Test + public void computesDiffForNested() { + final JsonObject oldValue = JsonObject.newBuilder() + .set("nested", JsonObject.newBuilder() + .set("Test", "Foo") + .set("Bum", "Lux") + .build()) + .build(); + + final JsonObject newValue = JsonObject.newBuilder() + .set("nested", JsonObject.newBuilder() + .set("Test", "Bar") + .set("Bum", "Lux") + .build()) + .build(); + + final JsonObject expected = JsonObject.newBuilder() + .set("nested", JsonObject.newBuilder() + .set("Test", "Bar") + .build()) + .build();; + + assertThat(JsonMergePatch.compute(oldValue, newValue)).contains(expected); + } + +}