Create XML Nodes based on XPath?

后端 未结 12 505
囚心锁ツ
囚心锁ツ 2020-11-27 15:36

Does anyone know of an existing means of creating an XML hierarchy programatically from an XPath expression?

For example if I have an XML fragment such as:

相关标签:
12条回答
  • 2020-11-27 15:55

    I needed a XNode instead of a XmlNode implementation, and the RegEx was not working for me (because element names with . or - are not functioning)

    So this what's what worked for me:

    public static XNode createNodeFromXPath(XElement elem, string xpath)
    {
        // Create a new Regex object
        Regex r = new Regex(@"/*([a-zA-Z0-9_\.\-]+)(\[@([a-zA-Z0-9_\.\-]+)='([^']*)'\])?|/@([a-zA-Z0-9_\.\-]+)");
    
        xpath = xpath.Replace("\"", "'");
        // Find matches
        Match m = r.Match(xpath);
    
        XNode currentNode = elem;
        StringBuilder currentPath = new StringBuilder();
    
        while (m.Success)
        {
            String currentXPath = m.Groups[0].Value;    // "/configuration" or "/appSettings" or "/add"
            String elementName = m.Groups[1].Value;     // "configuration" or "appSettings" or "add"
            String filterName = m.Groups[3].Value;      // "" or "key"
            String filterValue = m.Groups[4].Value;     // "" or "name"
            String attributeName = m.Groups[5].Value;   // "" or "value"
    
            StringBuilder builder = currentPath.Append(currentXPath);
            String relativePath = builder.ToString();
            XNode newNode = (XNode)elem.XPathSelectElement(relativePath);
    
            if (newNode == null)
            {
                if (!string.IsNullOrEmpty(attributeName))
                {
                    ((XElement)currentNode).Attribute(attributeName).Value = "";
                    newNode = (XNode)elem.XPathEvaluate(relativePath);
                }
                else if (!string.IsNullOrEmpty(elementName))
                {
                    XElement newElem = new XElement(elementName);
                    if (!string.IsNullOrEmpty(filterName))
                    {
                        newElem.Add(new XAttribute(filterName, filterValue));
                    }
    
                    ((XElement)currentNode).Add(newElem);
                    newNode = newElem;
                }
                else
                {
                    throw new FormatException("The given xPath is not supported " + relativePath);
                }
            }
    
            currentNode = newNode;
            m = m.NextMatch();
        }
    
        // Assure that the node is found or created
        if (elem.XPathEvaluate(xpath) == null)
        {
            throw new FormatException("The given xPath cannot be created " + xpath);
        }
    
        return currentNode;
    }
    
    0 讨论(0)
  • 2020-11-27 15:56

    I liked Chris' version because it handled attributes in xpaths and the other solutions didn't (though it doesn't handle "text()" in the path that well which I fixed). I unfortunately had to use this in a VB app, so here's the conversion for that:

            Private Sub SplitOnce(ByVal value As String, ByVal separator As String, ByRef part1 As String, ByRef part2 As String)
            If (value IsNot Nothing) Then
                Dim idx As Integer = value.IndexOf(separator)
                If (idx >= 0) Then
                    part1 = value.Substring(0, idx)
                    part2 = value.Substring(idx + separator.Length)
                Else
                    part1 = value
                    part2 = Nothing
                End If
            Else
                part1 = ""
                part2 = Nothing
            End If
        End Sub
        Private Function createXPath(ByVal doc As XmlDocument, ByVal xpath As String) As XmlNode
            Dim node As XmlNode = doc
            Dim part As String
            For Each part In xpath.Substring(1).Split("/")
                Dim nodes As XmlNodeList = node.SelectNodes(part)
                If (nodes.Count > 1) Then
                    Throw New Exception("Xpath '" + xpath + "' was not found multiple times!")
                ElseIf (nodes.Count = 1) Then
                    node = nodes(0)
                    Continue For
                End If
    
                If (part.EndsWith("text()")) Then
                    ' treat this the same as previous node since this is really innertext
                    Exit For
                ElseIf (part.StartsWith("@")) Then
                    Dim anode As XmlAttribute = doc.CreateAttribute(part.Substring(1))
                    node.Attributes.Append(anode)
                    node = anode
                Else
                    Dim elName As String = Nothing
                    Dim attrib As String = Nothing
                    If (part.Contains("[")) Then
                        SplitOnce(part, "[", elName, attrib)
                        If (Not attrib.EndsWith("]")) Then
                            Throw New Exception("Unsupported XPath (missing ]): " + part)
                        End If
                        attrib = attrib.Substring(0, attrib.Length - 1)
                    Else
                        elName = part
                    End If
                    Dim nextnode As XmlNode = doc.CreateElement(elName)
                    node.AppendChild(nextnode)
                    node = nextnode
                    If (attrib IsNot Nothing) Then
                        If (Not attrib.StartsWith("@")) Then
                            Throw New Exception("Unsupported XPath attrib (missing @): " + part)
                        End If
                        Dim name As String = ""
                        Dim value As String = ""
                        SplitOnce(attrib.Substring(1), "='", name, value)
                        If (String.IsNullOrEmpty(value) Or Not value.EndsWith("'")) Then
                            Throw New Exception("Unsupported XPath attrib: " + part)
                        End If
                        value = value.Substring(0, value.Length - 1)
                        Dim anode As XmlAttribute = doc.CreateAttribute(name)
                        anode.Value = value
                        node.Attributes.Append(anode)
                    End If
                End If
            Next
            Return node
        End Function
    
    0 讨论(0)
  • 2020-11-27 15:59

    Here is a enhanced RegEx based on Mark Miller's code

    /([\w]+)(?:(?:[\[])(@|)([\w]+)(?:([!=<>]+)(?:(?:(?:')([^']+)(?:'))|([^']+))|)(?:[]])|)|([.]+))
    
    Group 1: Node name
    Group 2: @ (or Empty, for non attributes)
    Group 3: Attribute Key
    Group 4: Attribute Value (if string)
    Group 5: Attribute Value (if number)
    Group 6: .. (dots, one or more)
    
    0 讨论(0)
  • 2020-11-27 16:02

    This is an improved version of Christian Peeters' solution that supports namespaces in the xpath expression.

    public static XNode CreateNodeFromXPath(XElement elem, string xpath)
    {
        // Create a new Regex object
        Regex r = new Regex(@"/*([a-zA-Z0-9_\.\-\:]+)(\[@([a-zA-Z0-9_\.\-]+)='([^']*)'\])?|/@([a-zA-Z0-9_\.\-]+)");
    
        xpath = xpath.Replace("\"", "'");
        // Find matches
        Match m = r.Match(xpath);
    
        XNode currentNode = elem;
        StringBuilder currentPath = new StringBuilder();
        XPathNavigator XNav = elem.CreateNavigator();
    
        while (m.Success)
        {
            String currentXPath = m.Groups[0].Value;    // "/ns:configuration" or "/appSettings" or "/add"
            String NamespaceAndElementName = m.Groups[1].Value;     // "ns:configuration" or "appSettings" or "add"
            String filterName = m.Groups[3].Value;      // "" or "key"
            String filterValue = m.Groups[4].Value;     // "" or "name"
            String attributeName = m.Groups[5].Value;   // "" or "value"
    
            XNamespace nspace = "";
            string elementName;
            int p = NamespaceAndElementName.IndexOf(':');
            if (p >= 0)
            {
                string ns = NamespaceAndElementName.Substring(0, p);
                elementName = NamespaceAndElementName.Substring(p + 1);
                nspace = XNav.GetNamespace(ns);
            }
            else
                elementName = NamespaceAndElementName;
    
    
            StringBuilder builder = currentPath.Append(currentXPath);
            String relativePath = builder.ToString();
            XNode newNode = (XNode)elem.XPathSelectElement(relativePath, XNav);
    
            if (newNode == null)
            {
                if (!string.IsNullOrEmpty(attributeName))
                {
                    ((XElement)currentNode).Attribute(attributeName).Value = "";
                    newNode = (XNode)elem.XPathEvaluate(relativePath, XNav);
                }
                else if (!string.IsNullOrEmpty(elementName))
                {
                    XElement newElem = new XElement(nspace + elementName);
                    if (!string.IsNullOrEmpty(filterName))
                    {
                        newElem.Add(new XAttribute(filterName, filterValue));
                    }
    
                    ((XElement)currentNode).Add(newElem);
                    newNode = newElem;
                }
                else
                {
                    throw new FormatException("The given xPath is not supported " + relativePath);
                }
            }
    
            currentNode = newNode;
            m = m.NextMatch();
        }
    
        // Assure that the node is found or created
        if (elem.XPathEvaluate(xpath, XNav) == null)
        {
            throw new FormatException("The given xPath cannot be created " + xpath);
        }
    
        return currentNode;
    }
    
    0 讨论(0)
  • 2020-11-27 16:04

    Here's my quick hack that can also create attributes as long as you use a format like /configuration/appSettings/add[@key='name']/@value.

    static XmlNode createXPath(XmlDocument doc, string xpath)
    {
      XmlNode node=doc;
      foreach (string part in xpath.Substring(1).Split('/'))
      {
        XmlNodeList nodes=node.SelectNodes(part);
        if (nodes.Count>1) throw new ComponentException("Xpath '"+xpath+"' was not found multiple times!");
        else if (nodes.Count==1) { node=nodes[0]; continue; }
    
        if (part.StartsWith("@"))
        {
          var anode=doc.CreateAttribute(part.Substring(1));
          node.Attributes.Append(anode);
          node=anode;
        }
        else
        {
          string elName, attrib=null;
          if (part.Contains("["))
          {
            part.SplitOnce("[", out elName, out attrib);
            if (!attrib.EndsWith("]")) throw new ComponentException("Unsupported XPath (missing ]): "+part);
            attrib=attrib.Substring(0, attrib.Length-1);
          }
          else elName=part;
    
          XmlNode next=doc.CreateElement(elName);
          node.AppendChild(next);
          node=next;
    
          if (attrib!=null)
          {
            if (!attrib.StartsWith("@")) throw new ComponentException("Unsupported XPath attrib (missing @): "+part);
            string name, value;
            attrib.Substring(1).SplitOnce("='", out name, out value);
            if (string.IsNullOrEmpty(value) || !value.EndsWith("'")) throw new ComponentException("Unsupported XPath attrib: "+part);
            value=value.Substring(0, value.Length-1);
            var anode=doc.CreateAttribute(name);
            anode.Value=value;
            node.Attributes.Append(anode);
          }
        }
      }
      return node;
    }
    

    SplitOnce is an extension method:

    public static void SplitOnce(this string value, string separator, out string part1, out string part2)
    {
      if (value!=null)
      {
        int idx=value.IndexOf(separator);
        if (idx>=0)
        {
          part1=value.Substring(0, idx);
          part2=value.Substring(idx+separator.Length);
        }
        else
        {
          part1=value;
          part2=null;
        }
      }
      else
      {
        part1="";
        part2=null;
      }
    }
    

    Sample:

    public static void Set(XmlDocument doc, string xpath, string value)
    {
      if (doc==null) throw new ArgumentNullException("doc");
      if (string.IsNullOrEmpty(xpath)) throw new ArgumentNullException("xpath");
    
      XmlNodeList nodes=doc.SelectNodes(xpath);
      if (nodes.Count>1) throw new ComponentException("Xpath '"+xpath+"' was not found multiple times!");
      else if (nodes.Count==0) createXPath(doc, xpath).InnerText=value;
      else nodes[0].InnerText=value;
    }
    

    e.g.

    Set(doc, "/configuration/appSettings/add[@key='Server']/@value", "foobar");
    
    0 讨论(0)
  • 2020-11-27 16:05
    • For XDocument
    • Supports attribute creation

    Use

    var xDoc = new XDocument(new XElement("root",
                            new XElement("child1"),
                            new XElement("child2")));
    
    CreateElement(xDoc, "/root/child3");
    CreateElement(xDoc, "/root/child4[@year=32][@month=44]");
    CreateElement(xDoc, "/root/child4[@year=32][@month=44]/subchild1");
    CreateElement(xDoc, "/root/child4[@year=32][@month=44]/subchild1/subchild[@name='jon']");
    CreateElement(xDoc, "/root/child1");
    

    define

    public static XDocument CreateElement(XDocument document, string xpath)
    {
        if (string.IsNullOrEmpty(xpath))
            throw new InvalidOperationException("Xpath must not be empty");
    
        var xNodes = Regex.Matches(xpath, @"\/[^\/]+").Cast<Match>().Select(it => it.Value).ToList();
        if (!xNodes.Any())
            throw new InvalidOperationException("Invalid xPath");
    
        var parent = document.Root;
        var currentNodeXPath = "";
        foreach (var xNode in xNodes)
        {
            currentNodeXPath += xNode;
            var nodeName = Regex.Match(xNode, @"(?<=\/)[^\[]+").Value;
            var existingNode = parent.XPathSelectElement(currentNodeXPath);
            if (existingNode != null)
            {
                parent = existingNode;
                continue;
            }
    
            var attributeNames =
              Regex.Matches(xNode, @"(?<=@)([^=]+)\=([^]]+)")
                    .Cast<Match>()
                    .Select(it =>
                    {
                        var groups = it.Groups.Cast<Group>().ToList();
                        return new { AttributeName = groups[1].Value, AttributeValue = groups[2].Value };
                    });
    
            parent.Add(new XElement(nodeName, attributeNames.Select(it => new XAttribute(it.AttributeName, it.AttributeValue)).ToArray()));
            parent = parent.Descendants().Last();
        }
        return document;
    }
    
    0 讨论(0)
提交回复
热议问题