Java tutorial
/* * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) * * 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 * * <p>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}.</p> * * <p>Note: there is <b>no guarantee</b> about the usability of the generated * patch for any other source/target combination than the one used to generate * the patch.</p> * * <p>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.</p> * * <p>You can obtain a diff either as a {@link JsonPatch} directly or, for * backwards compatibility, as a {@link JsonNode}.</p> * * @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<JsonNode> 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<JsonPointer, JsonNode> 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<String> firstFields = Sets.newTreeSet(Sets.newHashSet(source.fieldNames())); final Set<String> 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<JsonPointer, JsonNode> getUnchangedValues(final JsonNode source, final JsonNode target) { final Map<JsonPointer, JsonNode> ret = Maps.newHashMap(); computeUnchanged(ret, JsonPointer.empty(), source, target); return ret; } private static void computeUnchanged(final Map<JsonPointer, JsonNode> 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<JsonPointer, JsonNode> ret, final JsonPointer pointer, final JsonNode source, final JsonNode target) { final Iterator<String> 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<JsonPointer, JsonNode> 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)); } }