Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Merge two XML files and add missing tags and attributes

I have two XML files with same base format, but some of the tags and attributes in Master.XML aren't contained in the Child.XML.

I need to Merge the XML files into new one XML file with missing tags and attributes present.

If the values in the Master.XML and Child.XML differs, then values from Child.XML should be used.

I tried using Union and Concat with nodes, But it's not working.

  master.DescendantNodes().Union(child.DescendantNodes());  

Any suggestion will be helpful.

Master.XML

<SysConfig IsRuntime="False" BarcodeEnabled="false" version="1.2.0.0">
    <DbPath>C:\Agilent_i1000\ICPT_DB.sqlite</DbPath> 
 <CardDiagonsticsDelayTime>10</CardDiagonsticsDelayTime>   
    <ScreenSpecs NameID="CoreID" XrelativeID="X" YrelativeID="Y">
        <ScreenSpec Name="MainCtrlPanel" Xrelative="0" Yrelative="0" ></ScreenSpec>
        <ScreenSpec Name="1" Xrelative="75" Yrelative="0"  NotToUse="1"></ScreenSpec>
        <ScreenSpec Name="2" Xrelative="75" Yrelative="25"  NotToUse="1"></ScreenSpec>        
    </ScreenSpecs>  
</SysConfig>

Child.XML

<SysConfig IsRuntime="False" BarcodeEnabled="false" version="1.2.0.0">
<CardDiagonsticsDelayTime>20</CardDiagonsticsDelayTime>   
       <ScreenSpecs NameID="CoreID" XrelativeID="X" YrelativeID="Y">
        <ScreenSpec Name="MainCtrlPanel" Xrelative="0" Yrelative="0" ></ScreenSpec>
        <ScreenSpec Name="1" Xrelative="100" Yrelative="0" ></ScreenSpec>
        <ScreenSpec Name="2" Xrelative="75" Yrelative="25"></ScreenSpec> 
        <ScreenSpec Name="3" Xrelative="175" Yrelative="25"></ScreenSpec>        
    </ScreenSpecs>  
</SysConfig>

Expected Output

    <SysConfig IsRuntime="False" BarcodeEnabled="false" version="1.2.0.0">
    <DbPath>C:\Agilent_i1000\ICPT_DB.sqlite</DbPath> 
    <CardDiagonsticsDelayTime>20</CardDiagonsticsDelayTime>   
           <ScreenSpecs NameID="CoreID" XrelativeID="X" YrelativeID="Y">
            <ScreenSpec Name="MainCtrlPanel" Xrelative="0" Yrelative="0" ></ScreenSpec>
            <ScreenSpec Name="1" Xrelative="100" Yrelative="0" NotToUse="1" ></ScreenSpec>
            <ScreenSpec Name="2" Xrelative="75" Yrelative="25" NotToUse="1"></ScreenSpec>  
            <ScreenSpec Name="3" Xrelative="175" Yrelative="25">
</ScreenSpec>      
        </ScreenSpecs>  
    </SysConfig>
like image 239
Justin CI Avatar asked Mar 26 '18 03:03

Justin CI


People also ask

Can we merge two XML files?

To use this, create a new XSLT file (File > New > XSLT Stylesheet and place in it the stylesheet above. Save the file as "merge. xsl". You should also add the files (or folder) to an Oxygen project (Project view) and create a scenario of the "XML transformation with XSLT" type for one XML file.

What is XML merge?

The xmlmerge command collects XML snippets scattered to multiple files and merges them into one big XML tree. This tool is used by the build process of AqBanking to merge HBCI segment definitions from several files into one big XML file.


1 Answers

Here are a set of utility classes that are capable of merging two XML documents. The whole thing is quite generic but you may have to modify it to suit other specific needs.

Here is the code that handles your case:

    var result = new XmlXPathDocument();
    result.Load("master.xml");

    var child = new XmlXPathDocument();
    child.Load("child.xml");

    // here we tell the class that "Name" is a discriminant attribute.
    // two nodes at the same level with the same discriminant attributes are considered the same, so will be merged
    // if we don't do this, they will considered different and they will be added if the set of their attributes is different
    child.AddDiscriminantAttribute("Name", string.Empty);
    child.InjectXml(result);

    result.Save("output.xml");

The classes derive from standard XmlDocument class hierarchy. They use automatically computed XPATH expression to be able to merge documents node by node.

public class XmlXPathDocument : XmlDocument
{
    public const string XmlNamespaceUri = "http://www.w3.org/2000/xmlns/";
    public const string XmlNamespacePrefix = "xmlns";

    internal List<Tuple<string, string>> _discriminantAttributes = new List<Tuple<string, string>>();

    public XmlXPathDocument() => Construct();
    public XmlXPathDocument(XmlNameTable nameTable) : base(nameTable) => Construct();
    public XmlXPathDocument(XmlImplementation implementation) : base(implementation) => Construct();

    protected virtual void Construct() => XPathNamespaceManager = new XmlNamespaceManager(new NameTable());

    public virtual XmlNamespaceManager XPathNamespaceManager { get; private set; }

    public override XmlElement CreateElement(string prefix, string localName, string namespaceURI) => new XmlXPathElement(prefix, localName, namespaceURI, this);

    public override XmlCDataSection CreateCDataSection(string data) => new XmlXPathCDataSection(data, this);

    public override XmlText CreateTextNode(string text) => new XmlXPathText(text, this);

    public virtual void AddDiscriminantAttribute(string name, string namespaceURI)
    {
        if (name == null)
            throw new ArgumentNullException(nameof(name));

        _discriminantAttributes.Add(new Tuple<string, string>(name, namespaceURI));
    }

    public virtual bool IsDiscriminant(XmlAttribute attribute)
    {
        if (attribute == null)
            throw new ArgumentNullException(nameof(attribute));

        foreach (var pair in _discriminantAttributes)
        {
            string ns = Nullify(attribute.NamespaceURI);
            string dns = Nullify(pair.Item2);
            if (ns == dns && pair.Item1 == attribute.LocalName)
                return true;
        }
        return false;
    }

    private static string Nullify(string text)
    {
        if (text == null)
            return null;

        text = text.Trim();
        if (text.Length == 0)
            return null;

        return text;
    }

    internal string GetPrefix(string namespaceURI)
    {
        if (string.IsNullOrEmpty(namespaceURI))
            return null;

        string prefix = XPathNamespaceManager.LookupPrefix(namespaceURI);
        if (!string.IsNullOrEmpty(prefix))
        {
            XPathNamespaceManager.AddNamespace(prefix, namespaceURI);
            return prefix;
        }

        string newPrefix;
        int index = 0;
        do
        {
            newPrefix = "ns" + index;
            if (XPathNamespaceManager.LookupNamespace(newPrefix) == null)
                break;

            index++;
        }
        while (true);
        XPathNamespaceManager.AddNamespace(newPrefix, namespaceURI);
        return newPrefix;
    }

    private static bool IsNamespaceAttribute(XmlAttribute attribute)
    {
        if (attribute == null)
            return false;

        return attribute.NamespaceURI == XmlNamespaceUri && attribute.Prefix == XmlNamespacePrefix;
    }

    private static IEnumerable<XmlAttribute> GetAttributes(IXmlXPathNode node)
    {
        var xe = node as XmlElement;
        if (xe == null)
            yield break;

        foreach (XmlAttribute att in xe.Attributes)
        {
            yield return att;
        }
    }

    private static XmlAttribute GetAttribute(IXmlXPathNode node, string name) => node is XmlElement xe ? xe.Attributes[name] : null;
    private static XmlAttribute GetAttribute(IXmlXPathNode node, string localName, string ns) => node is XmlElement xe ? xe.Attributes[localName, ns] : null;

    public virtual bool InjectXml(XmlDocument target)
    {
        if (target == null)
            throw new ArgumentNullException(nameof(target));

        if (DocumentElement == null)
            return false;

        bool changed = false;
        foreach (XmlNode node in SelectNodes("//node()"))
        {
            var xelement = node as IXmlXPathNode;
            if (xelement == null)
                continue;

            if (string.IsNullOrEmpty(xelement.XPathExpression))
                continue;

            XmlNode other = target.SelectSingleNode(xelement.XPathExpression, XPathNamespaceManager);
            if (other != null)
            {
                if (other is XmlElement otherElement)
                {
                    foreach (XmlAttribute att in GetAttributes(xelement))
                    {
                        if (IsNamespaceAttribute(att))
                            continue;

                        if (otherElement.Attributes[att.LocalName, att.NamespaceURI]?.Value != att.Value)
                        {
                            otherElement.SetAttribute(att.LocalName, att.NamespaceURI, att.Value);
                            changed = true;
                        }
                    }
                    continue;
                }
            }

            if (node is XmlXPathElement element)
            {
                XmlElement parent = EnsureTargetParent(xelement, target, out changed);
                XmlElement targetElement = target.CreateElement(element.LocalName, element.NamespaceURI);
                changed = true;
                if (parent == null)
                {
                    target.AppendChild(targetElement);
                }
                else
                {
                    parent.AppendChild(targetElement);
                }

                foreach (XmlAttribute att in GetAttributes(xelement))
                {
                    if (IsNamespaceAttribute(att))
                        continue;

                    targetElement.SetAttribute(att.LocalName, att.NamespaceURI, att.Value);
                }
                continue;
            }

            if (node is XmlXPathCDataSection cdata)
            {
                XmlElement parent = EnsureTargetParent(xelement, target, out changed);
                var targetCData = target.CreateCDataSection(cdata.Value);
                changed = true;
                if (parent == null)
                {
                    target.AppendChild(targetCData);
                    AppendNextTexts(node, targetCData, target);
                }
                else
                {
                    if (parent.ChildNodes.Count == 1 && parent.ChildNodes[0] is XmlCharacterData)
                    {
                        parent.RemoveChild(parent.ChildNodes[0]);
                    }
                    parent.AppendChild(targetCData);
                    AppendNextTexts(node, targetCData, parent);
                }
                continue;
            }

            if (node is XmlXPathText text)
            {
                XmlElement parent = EnsureTargetParent(xelement, target, out changed);
                var targetText = target.CreateTextNode(text.Value);
                changed = true;
                if (parent == null)
                {
                    target.AppendChild(targetText);
                    AppendNextTexts(node, targetText, target);
                }
                else
                {
                    if (parent.ChildNodes.Count == 1 && parent.ChildNodes[0] is XmlCharacterData)
                    {
                        parent.RemoveChild(parent.ChildNodes[0]);
                    }
                    parent.AppendChild(targetText);
                    AppendNextTexts(node, targetText, parent);
                }
                continue;
            }
        }
        return changed;
    }

    private static void AppendNextTexts(XmlNode textNode, XmlNode targetTextNode, XmlNode parent)
    {
        do
        {
            if (textNode.NextSibling is XmlText text)
            {
                var newText = targetTextNode.OwnerDocument.CreateTextNode(text.Value);
                parent.AppendChild(newText);
            }
            else
            {
                var cdata = textNode.NextSibling as XmlCDataSection;
                if (cdata == null)
                    break;

                var newCData = targetTextNode.OwnerDocument.CreateCDataSection(cdata.Value);
                parent.AppendChild(newCData);
            }
            textNode = textNode.NextSibling;
        }
        while (true);
    }

    private static XmlElement EnsureTargetParent(IXmlXPathNode element, XmlDocument target, out bool changed)
    {
        changed = false;
        if (element.ParentNode is XmlXPathElement parent)
        {
            if (string.IsNullOrEmpty(parent.XPathExpression))
                return null;

            if (target.SelectSingleNode(parent.XPathExpression, element.OwnerDocument.XPathNamespaceManager) is XmlElement targetElement)
                return targetElement;

            var parentElement = EnsureTargetParent(parent, target, out changed);
            targetElement = target.CreateElement(parent.LocalName, parent.NamespaceURI);
            parentElement.AppendChild(targetElement);
            changed = true;
            return targetElement;
        }
        return target.DocumentElement;
    }
}

public class XmlXPathElement : XmlElement, IXmlXPathNode
{
    private Lazy<string> _xPathExpression;

    public XmlXPathElement(string prefix, string localName, string namespaceURI, XmlXPathDocument doc) : base(prefix, localName, namespaceURI, doc)
    {
        _xPathExpression = new Lazy<string>(() => GetXPathExpression());
    }

    public new XmlXPathDocument OwnerDocument => (XmlXPathDocument)base.OwnerDocument;
    public virtual string XPathExpression => _xPathExpression.Value;

    private static string GetAttEscapedValue(string value)
    {
        if (value.IndexOf('\'') >= 0)
            return "=\"" + value.Replace("\"", "&quot;") + "\"";

        return "='" + value + "'";
    }

    private string GetDiscriminantAttributeXPath()
    {
        foreach (var att in OwnerDocument._discriminantAttributes)
        {
            XmlAttribute disc;
            if (string.IsNullOrEmpty(att.Item2))
            {
                disc = GetAttributeNode(att.Item1);
            }
            else
            {
                disc = GetAttributeNode(att.Item1, att.Item2);
            }

            if (disc != null)
            {
                string newPrefix = OwnerDocument.GetPrefix(NamespaceURI);
                string name = Name + "[@" + disc.Name + GetAttEscapedValue(disc.Value) + "]";
                if (newPrefix != null)
                {
                    name = newPrefix + ":" + name;
                }
                return name;
            }
        }
        return null;
    }

    private string GetAttributesXPath()
    {
        if (Attributes.Count == 0)
            return null;

        var sb = new StringBuilder();
        foreach (XmlAttribute att in Attributes)
        {
            if (sb.Length > 0)
            {
                sb.Append(" and ");
            }

            sb.Append("@");
            sb.Append(att.Name);
            sb.Append(GetAttEscapedValue(att.Value));
            OwnerDocument.GetPrefix(att.NamespaceURI);
        }

        var text = sb.ToString().Trim();
        if (text.Length == 0)
            return null;

        return "[" + text + "]";
    }

    private string GetXPath(XmlNodeList parentNodes)
    {
        string discriminant = GetDiscriminantAttributeXPath();
        if (discriminant != null)
            return discriminant;

        string name = Name;
        string newPrefix = OwnerDocument.GetPrefix(NamespaceURI);
        if (newPrefix != null)
        {
            name = newPrefix + ":" + LocalName;
        }

        if (parentNodes.Count == 1)
            return name;

        var sameName = new List<XmlElement>();
        foreach (XmlNode node in parentNodes)
        {
            if (node.NodeType != XmlNodeType.Element)
                continue;

            if (node.Name == Name)
            {
                sameName.Add((XmlElement)node);
            }
        }

        if (sameName.Count == 1)
            return name;

        string byIndex = null;
        var sameAtts = new List<XmlElement>();
        for (int i = 0; i < sameName.Count; i++)
        {
            if (sameName[i] == this)
            {
                byIndex = name + "[" + (i + 1) + "]";
                continue;
            }

            bool same = true;
            foreach (XmlAttribute att in Attributes)
            {
                XmlAttribute sameAtt = sameName[i].Attributes[att.LocalName, att.NamespaceURI];
                if (sameAtt == null || string.Compare(sameAtt.Value, att.Value, StringComparison.OrdinalIgnoreCase) != 0)
                {
                    same = false;
                    break;
                }
            }

            if (same)
            {
                sameAtts.Add(sameName[i]);
            }
        }

        if (sameAtts.Count == 0)
            return name + GetAttributesXPath();

        return byIndex;
    }

    private string GetXPathExpression()
    {
        if (ParentNode == null)
        {
            string name = Name;
            string newPrefix = OwnerDocument.GetPrefix(NamespaceURI);
            if (newPrefix != null)
            {
                name = newPrefix + ":" + name;
            }
            return name;
        }

        string expr = GetXPath(ParentNode.ChildNodes);
        if (ParentNode is XmlXPathElement parent)
        {
            expr = parent.XPathExpression + "/" + expr;
        }

        if (ParentNode.NodeType == XmlNodeType.Document)
        {
            expr = "/" + expr;
        }
        return expr;
    }
}

public class XmlXPathText : XmlText, IXmlXPathNode
{
    private Lazy<string> _xPathExpression;

    public XmlXPathText(string data, XmlXPathDocument doc) : base(data, doc)
    {
        _xPathExpression = new Lazy<string>(() => GetTextXPathExpression(this));
    }

    public new XmlXPathDocument OwnerDocument => (XmlXPathDocument)base.OwnerDocument;
    public virtual string XPathExpression => _xPathExpression.Value;

    internal static string GetTextXPathExpression(XmlNode node)
    {
        if (node.ParentNode is IXmlXPathNode element)
            return element.XPathExpression + "/text()";

        return null;
    }
}

public class XmlXPathCDataSection : XmlCDataSection, IXmlXPathNode
{
    private Lazy<string> _xPathExpression;

    public XmlXPathCDataSection(string data, XmlXPathDocument doc) : base(data, doc)
    {
        _xPathExpression = new Lazy<string>(() => XmlXPathText.GetTextXPathExpression(this));
    }

    public new XmlXPathDocument OwnerDocument => (XmlXPathDocument)base.OwnerDocument;
    public virtual string XPathExpression => _xPathExpression.Value;
}

public interface IXmlXPathNode
{
    string XPathExpression { get; }
    XmlNode ParentNode { get; }
    XmlXPathDocument OwnerDocument { get; }
}
like image 122
Simon Mourier Avatar answered Oct 09 '22 04:10

Simon Mourier