All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.setl.json.patch.PatchFactory Maven / Gradle / Ivy

Go to download

An implementation of the Canonical JSON format with support for javax.json and Jackson

The newest version!
package io.setl.json.patch;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;
import jakarta.json.JsonPatch;
import jakarta.json.JsonValue;
import jakarta.json.JsonValue.ValueType;

import org.apache.commons.collections4.ListUtils;

import io.setl.json.patch.key.ArrayKey;
import io.setl.json.patch.key.Key;
import io.setl.json.patch.key.ObjectKey;

/**
 * Factory for creating patches using a Diff algorithm.
 */
public final class PatchFactory {

  /**
   * Helper class to improve the speed of the comparison of items in arrays.
   */
  static class Item {

    final int hashCode;

    final JsonValue jsonValue;


    Item(JsonValue jsonValue) {
      this.jsonValue = jsonValue;
      hashCode = jsonValue.hashCode();
    }


    @Override
    public boolean equals(Object o) {
      if (o == this) {
        return true;
      }
      if (!(o instanceof Item)) {
        return false;
      }
      Item that = (Item) o;
      return hashCode == that.hashCode && jsonValue.equals(that.jsonValue);
    }


    @Override
    public int hashCode() {
      return hashCode;
    }

  }


  /**
   * Create a JSON Patch that transforms the source into the target.
   *
   * @param source the source JSON
   * @param target the target JSON
   *
   * @return the patch
   */
  public static JsonPatch create(JsonValue source, JsonValue target) {
    return create(source, target, Collections.emptySet());
  }


  /**
   * Create a patch that transforms the source into the target.
   *
   * @param source   the source JSON
   * @param target   the target JSON
   * @param features the features used in creating the patch.
   *
   * @return the patch
   */
  public static JsonPatch create(JsonValue source, JsonValue target, Set features) {
    PatchFactory diff = new PatchFactory(features);
    diff.generateDiffs(source, target);
    return diff.patchBuilder.build();
  }


  /** The flags affecting this patch's creation. */
  private final EnumSet features;

  /** The patch operations that make up the derived patch. */
  private final PatchBuilder patchBuilder = new PatchBuilder();


  private PatchFactory(Set features) {
    this.features = (features == null || features.isEmpty()) ? EnumSet.noneOf(DiffFeatures.class) : EnumSet.copyOf(features);
  }


  private int addRemaining(Key path, JsonArray target, int pos, int targetIdx, int targetSize) {
    while (targetIdx < targetSize) {
      JsonValue jsonValue = target.get(targetIdx);
      String itemKey = new ArrayKey(path, pos).toString();
      patchBuilder.add(itemKey, jsonValue);
      pos++;
      targetIdx++;
    }
    return pos;
  }


  @SuppressWarnings({"java:S3776", "JavaNCSS", "CyclomaticComplexity"}) // Ignore cognitive complexity of LCS algorithm.
  private void compareArray(Key path, JsonArray source, JsonArray target) {
    List sourceItems = new ArrayList<>(source.size());
    for (JsonValue jsonValue : source) {
      sourceItems.add(new Item(jsonValue));
    }
    List targetItems = new ArrayList<>(target.size());
    for (JsonValue jsonValue : target) {
      targetItems.add(new Item(jsonValue));
    }
    List lcs = ListUtils.longestCommonSubsequence(sourceItems, targetItems);

    int srcIdx = 0;
    int targetIdx = 0;
    int lcsIdx = 0;
    int srcSize = source.size();
    int targetSize = target.size();
    int lcsSize = lcs.size();

    int pos = 0;
    while (lcsIdx < lcsSize) {
      Item lcsNode = lcs.get(lcsIdx);
      Item srcNode = sourceItems.get(srcIdx);
      Item targetNode = targetItems.get(targetIdx);

      if (lcsNode.equals(srcNode) && lcsNode.equals(targetNode)) {
        // These nodes are part of the LCS, simply step forward
        srcIdx++;
        targetIdx++;
        lcsIdx++;
        pos++;
      } else {
        if (lcsNode.equals(srcNode)) {
          // Source node is part of the LCS, but not target node is not, so this is an addition of the target node.
          String itemKey = new ArrayKey(path, pos).toString();
          patchBuilder.add(itemKey, targetNode.jsonValue);
          pos++;
          targetIdx++;
        } else if (lcsNode.equals(targetNode)) {
          // Target node is part of LCS, but source node is not, so this is a removal of the source node.
          String itemKey = new ArrayKey(path, pos).toString();
          if (features.contains(DiffFeatures.EMIT_TESTS)) {
            patchBuilder.test(itemKey, srcNode.jsonValue);
          }
          patchBuilder.remove(itemKey);
          srcIdx++;
        } else {
          Key itemKey = new ArrayKey(path, pos);
          //both are unequal to lcs node
          generateDiffs(itemKey, srcNode.jsonValue, targetNode.jsonValue);
          srcIdx++;
          targetIdx++;
          pos++;
        }
      }
    }

    while ((srcIdx < srcSize) && (targetIdx < targetSize)) {
      JsonValue srcNode = source.get(srcIdx);
      JsonValue targetNode = target.get(targetIdx);
      generateDiffs(new ArrayKey(path, pos), srcNode, targetNode);
      srcIdx++;
      targetIdx++;
      pos++;
    }
    pos = addRemaining(path, target, pos, targetIdx, targetSize);
    removeRemaining(path, pos, srcIdx, srcSize, source);
  }


  private void compareObjects(Key path, JsonObject source, JsonObject target) {
    TreeSet allNames = new TreeSet<>(source.keySet());
    allNames.addAll(target.keySet());
    for (String name : allNames) {
      Key child = new ObjectKey(path, name);
      if (source.containsKey(name)) {
        if (target.containsKey(name)) {
          // in both source and target, so generate diffs
          generateDiffs(child, source.get(name), target.get(name));
        } else {
          // only in source, so remove
          String childPath = child.toString();
          if (features.contains(DiffFeatures.EMIT_TESTS)) {
            patchBuilder.test(childPath, source.get(name));
          }
          patchBuilder.remove(childPath);
        }
      } else {
        // Not in source so must be in target. Hence, this is an add
        patchBuilder.add(child.toString(), target.get(name));
      }
    }
  }


  private void generateDiffs(JsonValue source, JsonValue target) {
    if (features.contains(DiffFeatures.EMIT_DIGEST)) {
      patchBuilder.digest("", source);
    }

    generateDiffs(null, source, target);

    if (features.contains(DiffFeatures.EMIT_DIGEST)) {
      patchBuilder.digest("", target);
    }
  }


  private void generateDiffs(Key path, JsonValue source, JsonValue target) {
    if (source.equals(target)) {
      // nothing to do
      return;
    }

    ValueType sourceType = source.getValueType();
    ValueType targetType = target.getValueType();

    if (sourceType == ValueType.ARRAY && targetType == ValueType.ARRAY) {
      //both are arrays
      compareArray(path, (JsonArray) source, (JsonArray) target);
    } else if (sourceType == ValueType.OBJECT && targetType == ValueType.OBJECT) {
      //both are json
      compareObjects(path, (JsonObject) source, (JsonObject) target);
    } else {
      //can be replaced
      if (features.contains(DiffFeatures.EMIT_TESTS)) {
        patchBuilder.test(path.toString(), source);
      }
      patchBuilder.replace(path.toString(), target);
    }
  }


  private void removeRemaining(Key path, int pos, int srcIdx, int srcSize, JsonArray source) {
    String itemKey = new ArrayKey(path, pos).toString();
    while (srcIdx < srcSize) {
      if (features.contains(DiffFeatures.EMIT_TESTS)) {
        patchBuilder.test(itemKey, source.get(srcIdx));
      }
      patchBuilder.remove(itemKey);
      srcIdx++;
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy