using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using DiffMatchPatch; using Newtonsoft.Json.Linq; namespace JsonDiffPatchDotNet { public class JsonDiffPatch { private readonly Options _options; public JsonDiffPatch() : this(new Options()) { } public JsonDiffPatch(Options options) { if (options == null) { throw new ArgumentNullException(nameof(options)); } _options = options; } /// /// Diff two JSON objects. /// /// The output is a JObject that contains enough information to represent the /// delta between the two objects and to be able perform patch and reverse operations. /// /// The base JSON object /// The JSON object to compare against the base /// JSON Patch Document public JToken Diff(JToken left, JToken right) { if (left == null) left = new JValue(""); if (right == null) right = new JValue(""); if (left.Type == JTokenType.Object && right.Type == JTokenType.Object) { return ObjectDiff((JObject)left, (JObject)right); } if (_options.ArrayDiff == ArrayDiffMode.Efficient && left.Type == JTokenType.Array && right.Type == JTokenType.Array) { return ArrayDiff((JArray)left, (JArray)right); } if (_options.TextDiff == TextDiffMode.Efficient && left.Type == JTokenType.String && right.Type == JTokenType.String && (left.ToString().Length > _options.MinEfficientTextDiffLength || right.ToString().Length > _options.MinEfficientTextDiffLength)) { var dmp = new diff_match_patch(); List patches = dmp.patch_make(left.ToObject(), right.ToObject()); return patches.Any() ? new JArray(dmp.patch_toText(patches), 0, (int)DiffOperation.TextDiff) : null; } if (!JToken.DeepEquals(left, right)) { return new JArray(left, right); } return null; } /// /// Patch a JSON object /// /// Unpatched JSON object /// JSON Patch Document /// Patched JSON object /// Thrown if the patch document is invalid public JToken Patch(JToken left, JToken patch) { if (patch == null) return left; if (patch.Type == JTokenType.Object) { var patchObj = (JObject)patch; JProperty arrayDiffCanary = patchObj.Property("_t"); if (left != null && left.Type == JTokenType.Array && arrayDiffCanary != null && arrayDiffCanary.Value.Type == JTokenType.String && arrayDiffCanary.Value.ToObject() == "a") { return ArrayPatch((JArray)left, patchObj); } return ObjectPatch(left as JObject, patchObj); } if (patch.Type == JTokenType.Array) { var patchArray = (JArray)patch; if (patchArray.Count == 1) // Add { return patchArray[0]; } if (patchArray.Count == 2) // Replace { return patchArray[1]; } if (patchArray.Count == 3) // Delete, Move or TextDiff { if (patchArray[2].Type != JTokenType.Integer) throw new InvalidDataException("Invalid patch object"); int op = patchArray[2].Value(); if (op == 0) { return null; } if (op == 2) { if (left.Type != JTokenType.String) throw new InvalidDataException("Invalid patch object"); var dmp = new diff_match_patch(); List patches = dmp.patch_fromText(patchArray[0].ToObject()); if (!patches.Any()) throw new InvalidDataException("Invalid textline"); object[] result = dmp.patch_apply(patches, left.Value()); var patchResults = (bool[])result[1]; if (patchResults.Any(x => !x)) throw new InvalidDataException("Text patch failed"); string right = (string)result[0]; return right; } throw new InvalidDataException("Invalid patch object"); } throw new InvalidDataException("Invalid patch object"); } return null; } /// /// Unpatch a JSON object /// /// Patched JSON object /// JSON Patch Document /// Unpatched JSON object /// Thrown if the patch document is invalid public JToken Unpatch(JToken right, JToken patch) { if (patch == null) return right; if (patch.Type == JTokenType.Object) { var patchObj = (JObject)patch; JProperty arrayDiffCanary = patchObj.Property("_t"); if (right != null && right.Type == JTokenType.Array && arrayDiffCanary != null && arrayDiffCanary.Value.Type == JTokenType.String && arrayDiffCanary.Value.ToObject() == "a") { return ArrayUnpatch((JArray)right, patchObj); } return ObjectUnpatch(right as JObject, patchObj); } if (patch.Type == JTokenType.Array) { var patchArray = (JArray)patch; if (patchArray.Count == 1) // Add (we need to remove the property) { return null; } if (patchArray.Count == 2) // Replace { return patchArray[0]; } if (patchArray.Count == 3) // Delete, Move or TextDiff { if (patchArray[2].Type != JTokenType.Integer) throw new InvalidDataException("Invalid patch object"); int op = patchArray[2].Value(); if (op == 0) { return patchArray[0]; } if (op == 2) { if (right.Type != JTokenType.String) throw new InvalidDataException("Invalid patch object"); var dmp = new diff_match_patch(); List patches = dmp.patch_fromText(patchArray[0].ToObject()); if (!patches.Any()) throw new InvalidDataException("Invalid textline"); var unpatches = new List(); for (int i = patches.Count - 1; i >= 0; --i) { Patch p = patches[i]; var u = new Patch { length1 = p.length1, length2 = p.length2, start1 = p.start1, start2 = p.start2 }; foreach (Diff d in p.diffs) { if (d.operation == Operation.DELETE) { u.diffs.Add(new Diff(Operation.INSERT, d.text)); } else if (d.operation == Operation.INSERT) { u.diffs.Add(new Diff(Operation.DELETE, d.text)); } else { u.diffs.Add(d); } } unpatches.Add(u); } object[] result = dmp.patch_apply(unpatches, right.Value()); var unpatchResults = (bool[])result[1]; if (unpatchResults.Any(x => !x)) throw new InvalidDataException("Text patch failed"); string left = (string)result[0]; return left; } throw new InvalidDataException("Invalid patch object"); } throw new InvalidDataException("Invalid patch object"); } return null; } #region String Overrides /// /// Diff two JSON objects. /// /// The output is a JObject that contains enough information to represent the /// delta between the two objects and to be able perform patch and reverse operations. /// /// The base JSON object /// The JSON object to compare against the base /// JSON Patch Document public string Diff(string left, string right) { JToken obj = Diff(JToken.Parse(left ?? ""), JToken.Parse(right ?? "")); return obj?.ToString(); } /// /// Patch a JSON object /// /// Unpatched JSON object /// JSON Patch Document /// Patched JSON object /// Thrown if the patch document is invalid public string Patch(string left, string patch) { JToken patchedObj = Patch(JToken.Parse(left ?? ""), JToken.Parse(patch ?? "")); return patchedObj?.ToString(); } /// /// Unpatch a JSON object /// /// Patched JSON object /// JSON Patch Document /// Unpatched JSON object /// Thrown if the patch document is invalid public string Unpatch(string right, string patch) { JToken unpatchedObj = Unpatch(JToken.Parse(right ?? ""), JToken.Parse(patch ?? "")); return unpatchedObj?.ToString(); } #endregion private JObject ObjectDiff(JObject left, JObject right) { if (left == null) throw new ArgumentNullException(nameof(left)); if (right == null) throw new ArgumentNullException(nameof(right)); var diffPatch = new JObject(); // Find properties modified or deleted foreach (var lp in left.Properties()) { //Skip property if in path exclustions if (_options.ExcludePaths.Count > 0 && _options.ExcludePaths.Any(p => p.Equals(lp.Path, StringComparison.OrdinalIgnoreCase))) { continue; } JProperty rp = right.Property(lp.Name); // Property deleted if (rp == null && (_options.DiffBehaviors & DiffBehavior.IgnoreMissingProperties) == DiffBehavior.IgnoreMissingProperties) { continue; } if (rp == null) { diffPatch.Add(new JProperty(lp.Name, new JArray(lp.Value, 0, (int)DiffOperation.Deleted))); continue; } JToken d = Diff(lp.Value, rp.Value); if (d != null) { diffPatch.Add(new JProperty(lp.Name, d)); } } // Find properties that were added foreach (var rp in right.Properties()) { if (left.Property(rp.Name) != null || (_options.DiffBehaviors & DiffBehavior.IgnoreNewProperties) == DiffBehavior.IgnoreNewProperties) continue; diffPatch.Add(new JProperty(rp.Name, new JArray(rp.Value))); } if (diffPatch.Properties().Any()) return diffPatch; return null; } private JObject ArrayDiff(JArray left, JArray right) { var result = JObject.Parse(@"{ ""_t"": ""a"" }"); int commonHead = 0; int commonTail = 0; if (JToken.DeepEquals(left, right)) return null; // Find common head while (commonHead < left.Count && commonHead < right.Count && JToken.DeepEquals(left[commonHead], right[commonHead])) { commonHead++; } // Find common tail while (commonTail + commonHead < left.Count && commonTail + commonHead < right.Count && JToken.DeepEquals(left[left.Count - 1 - commonTail], right[right.Count - 1 - commonTail])) { commonTail++; } if (commonHead + commonTail == left.Count) { // Trivial case, a block (1 or more consecutive items) was added for (int index = commonHead; index < right.Count - commonTail; ++index) { result[$"{index}"] = new JArray(right[index]); } return result; } if (commonHead + commonTail == right.Count) { // Trivial case, a block (1 or more consecutive items) was removed for (int index = commonHead; index < left.Count - commonTail; ++index) { result[$"_{index}"] = new JArray(left[index], 0, (int)DiffOperation.Deleted); } return result; } // Complex Diff, find the LCS (Longest Common Subsequence) List trimmedLeft = left.ToList().GetRange(commonHead, left.Count - commonTail - commonHead); List trimmedRight = right.ToList().GetRange(commonHead, right.Count - commonTail - commonHead); Lcs lcs = Lcs.Get(trimmedLeft, trimmedRight); for (int index = commonHead; index < left.Count - commonTail; ++index) { if (lcs.Indices1.IndexOf(index - commonHead) < 0) { // Removed result[$"_{index}"] = new JArray(left[index], 0, (int)DiffOperation.Deleted); } } for (int index = commonHead; index < right.Count - commonTail; index++) { int indexRight = lcs.Indices2.IndexOf(index - commonHead); if (indexRight < 0) { // Added result[$"{index}"] = new JArray(right[index]); } else { int li = lcs.Indices1[indexRight] + commonHead; int ri = lcs.Indices2[indexRight] + commonHead; JToken diff = Diff(left[li], right[ri]); if (diff != null) { result[$"{index}"] = diff; } } } return result; } private JObject ObjectPatch(JObject obj, JObject patch) { if (obj == null) obj = new JObject(); if (patch == null) return obj; var target = (JObject)obj.DeepClone(); foreach (var diff in patch.Properties()) { JProperty property = target.Property(diff.Name); JToken patchValue = diff.Value; // We need to special case deletion when doing objects since a delete is a removal of a property // not a null assignment if (patchValue.Type == JTokenType.Array && ((JArray)patchValue).Count == 3 && patchValue[2].Value() == 0) { target.Remove(diff.Name); } else { if (property == null) { target.Add(new JProperty(diff.Name, Patch(null, patchValue))); } else { property.Value = Patch(property.Value, patchValue); } } } return target; } private JArray ArrayPatch(JArray left, JObject patch) { var toRemove = new List(); var toInsert = new List(); var toModify = new List(); foreach (JProperty op in patch.Properties()) { if (op.Name == "_t") continue; var value = op.Value as JArray; if (op.Name.StartsWith("_")) { // removed item from original array if (value != null && value.Count == 3 && (value[2].ToObject() == (int)DiffOperation.Deleted || value[2].ToObject() == (int)DiffOperation.ArrayMove)) { toRemove.Add(new JProperty(op.Name.Substring(1), op.Value)); if (value[2].ToObject() == (int)DiffOperation.ArrayMove) toInsert.Add(new JProperty(value[1].ToObject().ToString(), new JArray(left[int.Parse(op.Name.Substring(1))].DeepClone()))); } else { throw new Exception($"Only removal or move can be applied at original array indices. Context: {value}"); } } else { if (value != null && value.Count == 1) { toInsert.Add(op); } else { toModify.Add(op); } } } // remove items, in reverse order to avoid sawing our own floor toRemove.Sort((x, y) => int.Parse(x.Name).CompareTo(int.Parse(y.Name))); for (int i = toRemove.Count - 1; i >= 0; --i) { JProperty op = toRemove[i]; left.RemoveAt(int.Parse(op.Name)); } // insert items, in reverse order to avoid moving our own floor toInsert.Sort((x, y) => int.Parse(y.Name).CompareTo(int.Parse(x.Name))); for (int i = toInsert.Count - 1; i >= 0; --i) { JProperty op = toInsert[i]; left.Insert(int.Parse(op.Name), ((JArray)op.Value)[0]); } foreach (var op in toModify) { JToken p = Patch(left[int.Parse(op.Name)], op.Value); left[int.Parse(op.Name)] = p; } return left; } private JObject ObjectUnpatch(JObject obj, JObject patch) { if (obj == null) obj = new JObject(); if (patch == null) return obj; var target = (JObject)obj.DeepClone(); foreach (var diff in patch.Properties()) { JProperty property = target.Property(diff.Name); JToken patchValue = diff.Value; // We need to special case addition when doing objects since an undo add is a removal of a property // not a null assignment if (patchValue.Type == JTokenType.Array && ((JArray)patchValue).Count == 1) { target.Remove(property.Name); } else { if (property == null) { target.Add(new JProperty(diff.Name, Unpatch(null, patchValue))); } else { property.Value = Unpatch(property.Value, patchValue); } } } return target; } private JArray ArrayUnpatch(JArray right, JObject patch) { var toRemove = new List(); var toInsert = new List(); var toModify = new List(); foreach (JProperty op in patch.Properties()) { if (op.Name == "_t") continue; var value = op.Value as JArray; if (op.Name.StartsWith("_")) { // removed item from original array if (value != null && value.Count == 3 && (value[2].ToObject() == (int)DiffOperation.Deleted || value[2].ToObject() == (int)DiffOperation.ArrayMove)) { var newOp = new JProperty(value[1].ToObject().ToString(), op.Value); if (value[2].ToObject() == (int)DiffOperation.ArrayMove) { toInsert.Add(new JProperty(op.Name.Substring(1), new JArray(right[value[1].ToObject()].DeepClone()))); toRemove.Add(newOp); } else { toInsert.Add(new JProperty(op.Name.Substring(1), new JArray(value[0]))); } } else { throw new Exception($"Only removal or move can be applied at original array indices. Context: {value}"); } } else { if (value != null && value.Count == 1) { toRemove.Add(op); } else { toModify.Add(op); } } } // first modify entries foreach (var op in toModify) { JToken p = Unpatch(right[int.Parse(op.Name)], op.Value); right[int.Parse(op.Name)] = p; } // remove items, in reverse order to avoid sawing our own floor toRemove.Sort((x, y) => int.Parse(x.Name).CompareTo(int.Parse(y.Name))); for (int i = toRemove.Count - 1; i >= 0; --i) { JProperty op = toRemove[i]; right.RemoveAt(int.Parse(op.Name)); } // insert items, in reverse order to avoid moving our own floor toInsert.Sort((x, y) => int.Parse(x.Name).CompareTo(int.Parse(y.Name))); foreach (var op in toInsert) { right.Insert(int.Parse(op.Name), ((JArray)op.Value)[0]); } return right; } } }