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

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

The newest version!
/*
 * 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: http://www.apache.org/licenses/LICENSE-2.0.txt
 */

package com.github.fge.jsonpatch.diff;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.github.fge.jsonpatch.JsonPatchMessages;
import com.github.fge.msgsimple.bundle.MessageBundle;
import com.github.fge.msgsimple.load.MessageBundles;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Equivalence;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import javax.annotation.ParametersAreNonnullByDefault;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * JSON "diff" implementation
 *
 * 

This class generates a JSON Patch (as in, an RFC 6902 JSON Patch) given * two JSON values as inputs. The patch can be obtained directly as a {@link * JsonPatch} or as a {@link JsonNode}.

* *

Note: there is no guarantee about the usability of the generated * patch for any other source/target combination than the one used to generate * the patch.

* *

This class always performs operations in the following order: removals, * additions and replacements. It then factors removal/addition pairs into * move operations, or copy operations if a common element exists, at the same * {@link JsonPointer pointer}, in both the source and destination.

* *

You can obtain a diff either as a {@link JsonPatch} directly or, for * backwards compatibility, as a {@link JsonNode}.

* * @since 1.2 */ @ParametersAreNonnullByDefault public final class JsonDiff { private static final MessageBundle BUNDLE = MessageBundles.getBundle(JsonPatchMessages.class); private static final ObjectMapper MAPPER = JacksonUtils.newMapper(); private static final Equivalence EQUIVALENCE = JsonNumEquals.getInstance(); private JsonDiff() { } /** * Generate a JSON patch for transforming the source node into the target * node * * @param source the node to be patched * @param target the expected result after applying the patch * @return the patch as a {@link JsonPatch} * * @since 1.9 */ public static JsonPatch asJsonPatch(final JsonNode source, final JsonNode target) { BUNDLE.checkNotNull(source, "common.nullArgument"); BUNDLE.checkNotNull(target, "common.nullArgument"); final Map unchanged = getUnchangedValues(source, target); final DiffProcessor processor = new DiffProcessor(unchanged); generateDiffs(processor, JsonPointer.empty(), source, target); return processor.getPatch(); } /** * Generate a JSON patch for transforming the source node into the target * node * * @param source the node to be patched * @param target the expected result after applying the patch * @return the patch as a {@link JsonNode} */ public static JsonNode asJson(final JsonNode source, final JsonNode target) { final String s; try { s = MAPPER.writeValueAsString(asJsonPatch(source, target)); return MAPPER.readTree(s); } catch (IOException e) { throw new RuntimeException("cannot generate JSON diff", e); } } private static void generateDiffs(final DiffProcessor processor, final JsonPointer pointer, final JsonNode source, final JsonNode target) { if (EQUIVALENCE.equivalent(source, target)) return; final NodeType firstType = NodeType.getNodeType(source); final NodeType secondType = NodeType.getNodeType(target); /* * Node types differ: generate a replacement operation. */ if (firstType != secondType) { processor.valueReplaced(pointer, source, target); return; } /* * If we reach this point, it means that both nodes are the same type, * but are not equivalent. * * If this is not a container, generate a replace operation. */ if (!source.isContainerNode()) { processor.valueReplaced(pointer, source, target); return; } /* * If we reach this point, both nodes are either objects or arrays; * delegate. */ if (firstType == NodeType.OBJECT) generateObjectDiffs(processor, pointer, (ObjectNode) source, (ObjectNode) target); else // array generateArrayDiffs(processor, pointer, (ArrayNode) source, (ArrayNode) target); } private static void generateObjectDiffs(final DiffProcessor processor, final JsonPointer pointer, final ObjectNode source, final ObjectNode target) { final Set firstFields = Sets.newTreeSet(Sets.newHashSet(source.fieldNames())); final Set secondFields = Sets.newTreeSet(Sets.newHashSet(target.fieldNames())); for (final String field: Sets.difference(firstFields, secondFields)) processor.valueRemoved(pointer.append(field), source.get(field)); for (final String field: Sets.difference(secondFields, firstFields)) processor.valueAdded(pointer.append(field), target.get(field)); for (final String field: Sets.intersection(firstFields, secondFields)) generateDiffs(processor, pointer.append(field), source.get(field), target.get(field)); } private static void generateArrayDiffs(final DiffProcessor processor, final JsonPointer pointer, final ArrayNode source, final ArrayNode target) { final int firstSize = source.size(); final int secondSize = target.size(); final int size = Math.min(firstSize, secondSize); /* * Source array is larger; in this case, elements are removed from the * target; the index of removal is always the original arrays's length. */ for (int index = size; index < firstSize; index++) processor.valueRemoved(pointer.append(size), source.get(index)); for (int index = 0; index < size; index++) generateDiffs(processor, pointer.append(index), source.get(index), target.get(index)); // Deal with the destination array being larger... for (int index = size; index < secondSize; index++) processor.valueAdded(pointer.append("-"), target.get(index)); } @VisibleForTesting static Map getUnchangedValues(final JsonNode source, final JsonNode target) { final Map ret = Maps.newHashMap(); computeUnchanged(ret, JsonPointer.empty(), source, target); return ret; } private static void computeUnchanged(final Map ret, final JsonPointer pointer, final JsonNode first, final JsonNode second) { if (EQUIVALENCE.equivalent(first, second)) { ret.put(pointer, second); return; } final NodeType firstType = NodeType.getNodeType(first); final NodeType secondType = NodeType.getNodeType(second); if (firstType != secondType) return; // nothing in common // We know they are both the same type, so... switch (firstType) { case OBJECT: computeObject(ret, pointer, first, second); break; case ARRAY: computeArray(ret, pointer, first, second); default: /* nothing */ } } private static void computeObject(final Map ret, final JsonPointer pointer, final JsonNode source, final JsonNode target) { final Iterator firstFields = source.fieldNames(); String name; while (firstFields.hasNext()) { name = firstFields.next(); if (!target.has(name)) continue; computeUnchanged(ret, pointer.append(name), source.get(name), target.get(name)); } } private static void computeArray(final Map ret, final JsonPointer pointer, final JsonNode source, final JsonNode target) { final int size = Math.min(source.size(), target.size()); for (int i = 0; i < size; i++) computeUnchanged(ret, pointer.append(i), source.get(i), target.get(i)); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy