Skip to content

Commit

Permalink
Make JsonMergePatch more objekt oriented
Browse files Browse the repository at this point in the history
* Moves merge logic from JsonValueMerger to JsonMergePatch (tests were also moved)
* Remove Abstract class AbstractJsonMerge because no longer needed
* Use JsonMergePatch in MergeThingStrategy

Signed-off-by: Yannic Klem <Yannic.Klem@bosch.io>
  • Loading branch information
Yannic92 committed Mar 22, 2022
1 parent f4a0b56 commit 5c60113
Show file tree
Hide file tree
Showing 9 changed files with 540 additions and 505 deletions.
53 changes: 0 additions & 53 deletions json/src/main/java/org/eclipse/ditto/json/AbstractJsonMerger.java

This file was deleted.

2 changes: 1 addition & 1 deletion json/src/main/java/org/eclipse/ditto/json/JsonFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ public static JsonObject newObject(final JsonObject jsonObject1, final JsonObjec
* @since 2.0.0
*/
public static JsonValue mergeJsonValues(final JsonValue jsonValue1, final JsonValue jsonValue2) {
return JsonValueMerger.mergeJsonValues(jsonValue1, jsonValue2);
return JsonMergePatch.of(jsonValue1).applyOn(jsonValue2);
}

/**
Expand Down
160 changes: 143 additions & 17 deletions json/src/main/java/org/eclipse/ditto/json/JsonMergePatch.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,26 @@
package org.eclipse.ditto.json;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

/**
* Can be used to get the diff in form of a JSON merge Patch according to
* <a href="https://datatracker.ietf.org/doc/html/rfc7386">RFC 7386</a> between two {@link JsonValue json values}.
* This class is responsible to compute or apply a JSON merge patch according to
* <a href="https://datatracker.ietf.org/doc/html/rfc7386">RFC 7386</a> for {@link JsonValue json values}.
*
* @since 2.4.0
*/
@Immutable
public final class JsonMergePatch {

private JsonMergePatch() {
throw new AssertionError();
private final JsonValue mergePatch;

private JsonMergePatch(final JsonValue mergePatch) {
this.mergePatch = mergePatch;
}

/**
Expand All @@ -39,28 +44,24 @@ private JsonMergePatch() {
* @return a JSON merge patch according to <a href="https://datatracker.ietf.org/doc/html/rfc7386">RFC 7386</a> or empty if values are equal.
* @since 2.4.0
*/
public static Optional<JsonValue> compute(final JsonValue oldValue, final JsonValue newValue) {
public static Optional<JsonMergePatch> compute(final JsonValue oldValue, final JsonValue newValue) {
return computeForValue(oldValue, newValue).map(JsonMergePatch::of);
}

private static Optional<JsonValue> computeForValue(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);
diff = computeForObject(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 <a href="https://datatracker.ietf.org/doc/html/rfc7386">RFC 7386</a>.
*
* @param oldJsonObject the original JSON object
* @param newJsonObject the new changed JSON object
* @return a JSON merge patch according to <a href="https://datatracker.ietf.org/doc/html/rfc7386">RFC 7386</a> or empty if values are equal.
* @since 2.4.0
*/
public static Optional<JsonObject> compute(final JsonObject oldJsonObject, final JsonObject newJsonObject) {
private static Optional<JsonObject> computeForObject(final JsonObject oldJsonObject,
final JsonObject newJsonObject) {
final JsonObjectBuilder builder = JsonObject.newBuilder();
final List<JsonKey> oldKeys = oldJsonObject.getKeys();
final List<JsonKey> newKeys = newJsonObject.getKeys();
Expand All @@ -82,7 +83,7 @@ public static Optional<JsonObject> compute(final JsonObject oldJsonObject, final
final Optional<JsonValue> oldValue = oldJsonObject.getValue(key);
final Optional<JsonValue> newValue = newJsonObject.getValue(key);
if (oldValue.isPresent() && newValue.isPresent()) {
compute(oldValue.get(), newValue.get()).ifPresent(diff -> builder.set(key, diff));
computeForValue(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());
Expand All @@ -95,4 +96,129 @@ public static Optional<JsonObject> compute(final JsonObject oldJsonObject, final
return builder.isEmpty() ? Optional.empty() : Optional.of(builder.build());
}

/**
* Creates a {@link JsonMergePatch} with an patch object containing the given {@code mergePatch} at the given {@code path}.
*
* @param path The path on which the given {@code mergePatch} should be applied later.
* @param mergePatch the actual patch.
* @return the merge patch.
* @since 2.4.0
*/
public static JsonMergePatch of(final JsonPointer path, final JsonValue mergePatch) {
return new JsonMergePatch(JsonFactory.newObject(path, mergePatch));
}

/**
* Creates a {@link JsonMergePatch} with an patch object containing the given {@code mergePatch} at root level.
*
* @param mergePatch the actual patch.
* @return the merge patch.
* @since 2.4.0
*/
public static JsonMergePatch of(final JsonValue mergePatch) {
return new JsonMergePatch(mergePatch);
}

/**
* Merge 2 JSON values recursively into one. In case of conflict, the first value is more important.
*
* @param value1 the first json value to merge, overrides conflicting fields.
* @param value2 the second json value to merge.
* @return the merged json value.
*/
private static JsonValue mergeJsonValues(final JsonValue value1, final JsonValue value2) {
final JsonValue result;
if (value1.isObject() && value2.isObject()) {
result = mergeJsonObjects(value1.asObject(), value2.asObject());
} else {
if (value1.isObject()) {
result = value1.asObject().filter(field -> !field.getValue().isNull());
} else {
result = value1;
}
}

return result;
}

private static JsonObject mergeJsonObjects(final JsonObject jsonObject1, final JsonObject jsonObject2) {

if (jsonObject1.isNull() || (jsonObject1.isNull() && jsonObject2.isNull())) {
return JsonFactory.nullObject();
}

final JsonObjectBuilder builder = JsonFactory.newObjectBuilder();
// add fields of jsonObject1
jsonObject1.forEach(jsonField -> {
final JsonKey key = jsonField.getKey();
final JsonValue value1 = jsonField.getValue();
final Optional<JsonValue> maybeValue2 = jsonObject2.getValue(key);

if (value1.isNull()) {
return;
}
if (maybeValue2.isPresent()) {
builder.set(key, mergeJsonValues(value1, maybeValue2.get()));
} else {
if (value1.isObject()) {
builder.set(key, value1.asObject().filter(field -> !field.getValue().isNull()));
} else {
builder.set(jsonField);
}
}
});

// add fields of jsonObject2 not present in jsonObject1
jsonObject2.forEach(jsonField -> {
if (!jsonObject1.contains(jsonField.getKey())) {
builder.set(jsonField);
}
});

return builder.build();
}

/**
* Applies this merge patch on the given json value.
*
* @param jsonValue the json value that should be patched.
* @return the patched json value.
* @since 2.4.0
*/
public JsonValue applyOn(final JsonValue jsonValue) {
return mergeJsonValues(mergePatch, jsonValue);
}

/**
* @return the merge patch json value
* @since 2.4.0
*/
public JsonValue asJsonValue() {
return mergePatch;
}

@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final JsonMergePatch that = (JsonMergePatch) o;
return Objects.equals(mergePatch, that.mergePatch);
}

@Override
public int hashCode() {
return Objects.hash(mergePatch);
}

@Override
public String toString() {
return getClass().getSimpleName() + " [" +
"mergePatch=" + mergePatch +
"]";
}

}
22 changes: 22 additions & 0 deletions json/src/main/java/org/eclipse/ditto/json/JsonObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;

import javax.annotation.Nullable;

Expand Down Expand Up @@ -404,4 +405,25 @@ default JsonObjectBuilder toBuilder() {
*/
Optional<JsonField> getField(CharSequence key);

/**
* Filters the {@link JsonField json fields} of this object and all nested objects based on the given predicate.
*
* @param predicate The predicate that all fields should pass.
* @return the filtered JSON object.
* @since 2.4.0
*/
default JsonObject filter(final Predicate<JsonField> predicate) {
return stream()
.filter(predicate)
.map(field -> {
final JsonValue value = field.getValue();
if (value.isObject()) {
return JsonField.newInstance(field.getKey(), value.asObject().filter(predicate));
} else {
return JsonField.newInstance(field.getKey(), value);
}
})
.collect(JsonCollectors.fieldsToObject());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* Package-private function to merge 2 {@link org.eclipse.ditto.json.JsonObject}s into 1.
*/
@Immutable
final class JsonObjectMerger extends AbstractJsonMerger {
final class JsonObjectMerger {

private JsonObjectMerger() {}

Expand All @@ -35,7 +35,7 @@ private JsonObjectMerger() {}
public static JsonObject mergeJsonObjects(final JsonObject jsonObject1, final JsonObject jsonObject2) {
final JsonObjectBuilder builder = JsonFactory.newObjectBuilder();

if(jsonObject1.isNull() && jsonObject2.isNull()) {
if (jsonObject1.isNull() && jsonObject2.isNull()) {
return JsonFactory.nullObject();
}

Expand Down Expand Up @@ -64,9 +64,9 @@ public static JsonObject mergeJsonObjects(final JsonObject jsonObject1, final Js

private static JsonValue mergeJsonValues(final JsonValue value1, final JsonValue value2) {
final JsonValue result;
if (areJsonObjects(value1, value2)) {
if (value1.isObject() && value2.isObject()) {
result = mergeJsonObjects(value1.asObject(), value2.asObject());
} else if (areJsonArrays(value1, value2)) {
} else if (value1.isArray() && value2.isArray()) {
// take jsonArray from jsonObject1 - jsonArrays will not get merged
result = value1.asArray();
} else {
Expand Down
Loading

0 comments on commit 5c60113

Please sign in to comment.