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

com.github.fge.jsonpatch.diff.JsonDiff Maven / Gradle / Ivy

Go to download

JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386) implementation in Java

There is a newer version: 1.9
Show newest version
/*
 * Copyright (c) 2013, Randy Watler 
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the Lesser GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * Lesser GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */

package com.github.fge.jsonpatch.diff;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.github.fge.jackson.JacksonUtils;
import com.github.fge.jackson.JsonNumEquals;
import com.github.fge.jackson.NodeType;
import com.github.fge.jackson.jsonpointer.JsonPointer;
import com.github.fge.jsonpatch.JsonPatch;
import com.google.common.base.Equivalence;
import com.google.common.collect.Lists;

import java.util.List;

/**
 * "Reverse" factorizing JSON Patch implementation
 *
 * 

This class only has one method, {@link #asJson(JsonNode, JsonNode)}, which * takes two JSON values as arguments and returns a patch as a {@link JsonNode}. * This generated patch can then be used in {@link * JsonPatch#fromJson(JsonNode)}.

* *

Numeric equivalence is respected. When dealing with object values, * operations are always generated in the following order: * *

    *
  • additions,
  • *
  • removals,
  • *
  • replacements.
  • *
* * Array values generate operations in the order of elements. Factorizing is * done to merge add and remove into move operations and convert duplicate * add to copy operations if values are equivalent. Test operations are not * generated.

* *

Note that due to the way {@link JsonNode} is implemented, this class is * inherently not thread safe (since {@code JsonNode} is mutable). It is * therefore the responsibility of the caller to ensure that the calling context * is safe (by ensuring, for instance, that only the diff operation has * references to the values to be diff'ed).

* * @since 1.2 * @author Randy Watler */ public final class JsonDiff { private static final JsonNodeFactory FACTORY = JacksonUtils.nodeFactory(); private static final Equivalence EQUIVALENCE = JsonNumEquals.getInstance(); private JsonDiff() { } /** * Generate a JSON patch for transforming the first node into the second * node * * @param first the node to be patched * @param second the expected result after applying the patch * @return the patch as a {@link JsonNode} */ public static JsonNode asJson(final JsonNode first, final JsonNode second) { // recursively compute node diffs final List diffs = Lists.newArrayList(); generateDiffs(diffs, JsonPointer.empty(), first, second); // factorize diffs to optimize patch operations DiffFactorizer.factorizeDiffs(diffs); // generate patch operations from node diffs final ArrayNode patch = FACTORY.arrayNode(); for (final Diff diff: diffs) patch.add(diff.asJsonPatch()); return patch; } /** * Generate differences between first and second nodes. * * @param diffs returned ordered differences * @param path path to first and second nodes * @param first first node to compare * @param second second node to compare */ private static void generateDiffs(final List diffs, final JsonPointer path, final JsonNode first, final JsonNode second) { // compare deep nodes if (EQUIVALENCE.equivalent(first, second)) return; // compare node types: if types are not the same or if not // an array or object, this is a replace operation final NodeType firstType = NodeType.getNodeType(first); final NodeType secondType = NodeType.getNodeType(second); if (firstType != secondType || !first.isContainerNode()) { diffs.add(new Diff(DiffOperation.REPLACE, path, second.deepCopy())); return; } // matching array or object nodes: recursively generate diffs // for object members or array elements if (firstType == NodeType.OBJECT) generateObjectDiffs(diffs, path, first, second); else // array generateArrayDiffs(diffs, path, first, second); } /** * Generate differences between first and second object nodes. Differences * are generated in the following order: added fields, removed fields, and * common fields differences. * * @param diffs returned ordered differences * @param path path to first and second nodes * @param first first object node to compare * @param second second object node to compare */ private static void generateObjectDiffs(final List diffs, final JsonPointer path, final JsonNode first, final JsonNode second) { // compare different objects fieldwise in predictable order; // maintaining order is cosmetic, but facilitates test construction final List inFirst = Lists.newArrayList(first.fieldNames()); final List inSecond = Lists.newArrayList(second.fieldNames()); List fields; // added fields fields = Lists.newArrayList(inSecond); fields.removeAll(inFirst); for (final String added: fields) diffs.add(new Diff(DiffOperation.ADD, path.append(added), second.get(added).deepCopy())); // removed fields fields = Lists.newArrayList(inFirst); fields.removeAll(inSecond); for (final String removed: fields) diffs.add(new Diff(DiffOperation.REMOVE, path.append(removed), first.get(removed).deepCopy())); // recursively generate diffs for fields in both objects fields = Lists.newArrayList(inFirst); fields.retainAll(inSecond); for (final String common: fields) generateDiffs(diffs, path.append(common), first.get(common), second.get(common)); } /** * Generate differences between first and second array nodes. Differences * are generated in order by comparing elements against the longest common * subsequence of elements in both arrays. * * @param diffs returned ordered differences * @param path path to first and second nodes * @param first first array node to compare * @param second second array node to compare */ private static void generateArrayDiffs(final List diffs, final JsonPointer path, final JsonNode first, final JsonNode second) { // compare array elements linearly using longest common subsequence // algorithm applied to the array elements final List lcs = LCS.getLCS(first, second); final int firstSize = first.size(); final int secondSize = second.size(); final int lcsSize = lcs.size(); int firstIndex = 0; int secondIndex = 0; int lcsIndex = 0; JsonNode firstElement; JsonNode secondElement; JsonNode lcsElement; while (firstIndex < firstSize || secondIndex < secondSize) { firstElement = first.get(firstIndex); secondElement = second.get(secondIndex); lcsElement = lcsIndex < lcsSize ? lcs.get(lcsIndex) : null; if (firstElement == null) { // appended elements diffs.add(new Diff(DiffOperation.ADD, path, firstIndex, -1, second.get(secondIndex).deepCopy())); secondIndex++; continue; } if (EQUIVALENCE.equivalent(firstElement, lcsElement)) { if (EQUIVALENCE.equivalent(firstElement, secondElement)) { // common subsequence elements firstIndex++; secondIndex++; lcsIndex++; } else { // inserted elements diffs.add(new Diff(DiffOperation.ADD, path, firstIndex, secondIndex, second.get(secondIndex).deepCopy())); secondIndex++; } } else if (secondElement != null && !EQUIVALENCE.equivalent(secondElement, lcsElement)) { // generate diffs for or replaced elements if (firstIndex == secondIndex) generateDiffs(diffs, path.append(firstIndex), firstElement, secondElement); else diffs.add(new Diff(DiffOperation.REPLACE, path, firstIndex, secondIndex, second.get(secondIndex).deepCopy())); firstIndex++; secondIndex++; } else { // removed elements diffs.add(new Diff(DiffOperation.REMOVE, path, firstIndex, secondIndex, first.get(firstIndex).deepCopy())); firstIndex++; } } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy