public static XmlDiff Load(string path) { var doc = new XmlDocument(); doc.Load(path); var patch = doc.DocumentElement; var selector = patch.SelectSingleNode("Selectors/Selector"); var modifications = patch.SelectNodes("Modifications/*"); return(new XmlDiff { Selector = XmlSelector.Deserialize(selector), Modifications = modifications.Cast <XmlNode>().Select(n => XmlModification.Deserialize(n)).ToList() }); }
private TaggedXmlElement TagNodeWithXPath(XmlNode parent, XmlSelector selector) { var node = parent.SelectSingleNode(selector.XPath) as XmlElement; if (node != null) { var attribute = doc.CreateAttribute(IdAttribute); attribute.Value = selector.NodeId.ToString(); node.Attributes.Append(attribute); var childNodes = selector.Children.Select(s => TagNodeWithXPath(node, s)) .Where(n => n != null).ToList(); return(new TaggedXmlElement(selector.NodeId, node, childNodes)); } return(null); }
public XmlNodeDiff Diff(string xpath, XmlElement modified) { var mods = new List <XmlModification>(); var modifiedChildElements = new List <XmlElement>(); if (modified != null) { // determine whether element has been renamed if (Name != modified.Name) { // element has been renamed mods.Add(new RenameElementModification(Id, modified.Name)); } // compare attributes mods.AddRange(CompareAttributes(modified)); // compare element value modifiedChildElements = modified.ChildNodes.Cast <XmlNode>().Where(n => n is XmlElement) .Select(n => n as XmlElement).ToList(); if (modifiedChildElements.Count == 0 && ElementValue != modified.InnerText) { mods.Add(new ModifyElementValueModification(Id, modified.InnerText)); } else if (modifiedChildElements.Count > 0 && Elements.Count == 0) { mods.AddRange(modifiedChildElements.Select(e => new InsertElementModification(Id, e, -1))); } } if (Elements.Count == 0 || modified == null) { return(new XmlNodeDiff(new XmlSelector(xpath, Id), mods)); } // if all element names are unique, just specify the element name var groupedOriginalElements = Elements.GroupBy(e => e.Name); if (groupedOriginalElements.All(g => g.Count() == 1) && TryMapModifiedElements(modified, e => e.Name, out var mapped, out var childMods)) { mods.AddRange(childMods); return(new XmlNodeDiff(new XmlSelector(xpath, Id, mapped), mods)); } // if all elements have a common 'primary key' attribute, specify the element name + attribute if (groupedOriginalElements.Count() == 1 && TryGetKeyAttribute(groupedOriginalElements.First().ToList(), out var pkAttribute) && TryMapModifiedElements(modified, e => $"{e.Name}[@{pkAttribute}='{e.Attributes[pkAttribute]}']", out mapped, out childMods)) { mods.AddRange(childMods); return(new XmlNodeDiff(new XmlSelector(xpath, Id, mapped), mods)); } // last resort - just specify the index // if we're selecting by index, the number of original and modified elements needs to be equal var elementsToSerialize = Elements; if (modifiedChildElements.Count > Elements.Count) { // if there are more children in the modified version, we need to add items to match var lastNodeId = Elements.Last().Id; mods.AddRange(modifiedChildElements.Skip(Elements.Count).Select( e => new InsertElementModification(Id, e, lastNodeId))); } else { // if there are less children in the modified version, we need to remove items to match mods.AddRange(Elements.Skip(modifiedChildElements.Count).Select( e => new RemoveElementModification(e.Id))); elementsToSerialize = Elements.Take(modifiedChildElements.Count).ToList(); } var children = (from i in Enumerable.Range(0, Elements.Count) let child = Elements[i] let mappedChild = modifiedChildElements[i] select child.Diff($"*[{i + 1}]", mappedChild)).ToList(); mods.AddRange(children.SelectMany(c => c.Modifications)); var selector = new XmlSelector(xpath, Id, children.Select(c => c.Selector)); return(new XmlNodeDiff(selector, mods)); }
private bool TryMapModifiedElements(XmlElement modified, Func <TaggedXmlElement, string> getChildXPath, out List <XmlSelector> mappedSelectors, out List <XmlModification> modifications) { modifications = new List <XmlModification>(); var mappedArray = new XmlSelector[Elements.Count]; var mappedToOriginalId = new Dictionary <XmlElement, int>(); for (int i = 0; i < Elements.Count; i++) { var element = Elements[i]; // get the XPath for this element and try to locate it in the modified doc var xpath = getChildXPath(element); var mapped = modified.SelectNodes(xpath).Cast <XmlNode>() .Select(n => n as XmlElement).Where(n => n != null).ToList(); if (mapped.Count == 1) { // if we found exactly one match, this XPath is a valid mapping var mappedNode = mapped.First(); var diff = element.Diff(xpath, mappedNode); modifications.AddRange(diff.Modifications); mappedArray[i] = diff.Selector; mappedToOriginalId.Add(mappedNode, element.Id); } else if (mapped.Count > 2) { // we found more than one match - this XPath is ambiguous mappedSelectors = null; modifications = null; return(false); } // if we didn't find a match, the element was either removed or renamed } // identify additions, removals and renames var modifiedChildElements = modified.ChildNodes.Cast <XmlNode>().Where(n => n is XmlElement) .Select(n => n as XmlElement).ToList(); int lastNodeId = -1; for (int a = 0; a < modifiedChildElements.Count; a++) { var addition = modifiedChildElements[a]; if (mappedToOriginalId.TryGetValue(addition, out var mappedNodeId)) { // store the node ID of the original node so we know where to insert additions lastNodeId = mappedNodeId; } else { // identify whether this element was added or renamed bool wasRenamed = false; for (int r = 0; r < mappedArray.Length; r++) { if (mappedArray[r] == null) { var removal = Elements[r]; var diff = removal.Diff(getChildXPath(removal), addition); if (diff.Modifications.All( m => m.Type == XmlModificationType.RenameElement && m.NodeId == removal.Id)) { // the 'removed' element was actually renamed to the 'addition' modifications.AddRange(diff.Modifications); mappedArray[r] = diff.Selector; mappedToOriginalId.Add(addition, removal.Id); wasRenamed = true; break; } } } if (!wasRenamed) { // insert a new element modifications.Add(new InsertElementModification(Id, addition, lastNodeId)); } } } // add removal modifications for any elements that were removed for (int i = 0; i < mappedArray.Length; i++) { if (mappedArray[i] == null) { // remove the element var removal = Elements[i]; var diff = removal.Diff(getChildXPath(removal), null); modifications.AddRange(diff.Modifications); mappedArray[i] = diff.Selector; modifications.Add(new RemoveElementModification(removal.Id)); } } mappedSelectors = mappedArray.ToList(); return(true); }
public static XmlSelector Deserialize(XmlNode node) { var xpath = node.SelectSingleNode("@XPath")?.Value; if (string.IsNullOrWhiteSpace(xpath) || !int.TryParse(node.SelectSingleNode("@NodeId")?.Value ?? string.Empty, out var id)) { throw new Exception($"Unable to locate XPath or NodeId attributes on '{node.Name}' node."); } return(new XmlSelector(xpath, id, node.SelectNodes("Selector").Cast <XmlNode>().Select(n => XmlSelector.Deserialize(n)))); }
public XmlNodeDiff(XmlSelector selector, List <XmlModification> mods) { Selector = selector; Modifications = mods; }
public TaggedXmlDoc(XmlDocument doc, XmlSelector selector) { // tag each node using XPath selector this.doc = doc.Clone() as XmlDocument; Root = TagNodeWithXPath(this.doc, selector); }