diff --git a/Src/JsonDiffPatchDotNet/JsonDiffPatch.cs b/Src/JsonDiffPatchDotNet/JsonDiffPatch.cs index 19d0dda..645d496 100644 --- a/Src/JsonDiffPatchDotNet/JsonDiffPatch.cs +++ b/Src/JsonDiffPatchDotNet/JsonDiffPatch.cs @@ -1,672 +1,672 @@ -using System; -using System.Collections.Generic; +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) +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) + } + + 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 (_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()) + } + + 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 && (_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 (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; - } - } -} + 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; + } + } +} diff --git a/Src/JsonDiffPatchDotNet/JsonDiffPatchDotNet.csproj b/Src/JsonDiffPatchDotNet/JsonDiffPatchDotNet.csproj index b68d95b..f36aa10 100644 --- a/Src/JsonDiffPatchDotNet/JsonDiffPatchDotNet.csproj +++ b/Src/JsonDiffPatchDotNet/JsonDiffPatchDotNet.csproj @@ -5,8 +5,8 @@ latest JsonDiffPatchDotNet JsonDiffPatchDotNet - 2.2.0.0 - 2.2.0.0 + 2.3.0.0 + 2.3.0.0 true https://github.com/wbish/jsondiffpatch.net git @@ -22,7 +22,7 @@ JsonDiffPatchDotNet.snk 2.12 true - 2.2.0.0 + 2.3.0.0