예제 #1
0
        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()
            });
        }
예제 #2
0
        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);
        }
예제 #3
0
        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));
        }
예제 #4
0
        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);
        }
예제 #5
0
        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))));
        }
예제 #6
0
 public XmlNodeDiff(XmlSelector selector, List <XmlModification> mods)
 {
     Selector      = selector;
     Modifications = mods;
 }
예제 #7
0
 public TaggedXmlDoc(XmlDocument doc, XmlSelector selector)
 {
     // tag each node using XPath selector
     this.doc = doc.Clone() as XmlDocument;
     Root     = TagNodeWithXPath(this.doc, selector);
 }