49

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:

<feed>
    <entry>
        <data></data>
        <content></content>
    </entry>
</feed>

Given the XPath expression /feed/entry/content/@source I would have:

<feed>
    <entry>
        <data></data>
        <content @source=""></content>
    </entry>
</feed>

I realize this is possible using XSLT but due to the dynamic nature of what I'm trying to accomplish a fixed transformation won't work.

I am working in C# but if someone has a solution using some other language please chime in.

Thanks for the help!

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
Fred Strauss
  • 1,435
  • 2
  • 13
  • 21
  • 1
    What do you mean by "a fixed transformation won't work"? – Dimitre Novatchev Feb 03 '09 at 19:51
  • The end goal is I am trying to map values in a database to varying locations within an XML document. In cases where the value does not exist in the database I do not want to create the relevant hierarchy. – Fred Strauss Feb 04 '09 at 15:33

12 Answers12

48

In the example you present the only thing being created is the attribute ...

XmlElement element = (XmlElement)doc.SelectSingleNode("/feed/entry/content");
if (element != null)
    element.SetAttribute("source", "");

If what you really want is to be able to create the hierarchy where it doesn't exist then you could your own simple xpath parser. I don't know about keeping the attribute in the xpath though. I'd rather cast the node as an element and tack on a .SetAttribute as I've done here:


static private XmlNode makeXPath(XmlDocument doc, string xpath)
{
    return makeXPath(doc, doc as XmlNode, xpath);
}

static private XmlNode makeXPath(XmlDocument doc, XmlNode parent, string xpath)
{
    // grab the next node name in the xpath; or return parent if empty
    string[] partsOfXPath = xpath.Trim('/').Split('/');
    string nextNodeInXPath = partsOfXPath.First();
    if (string.IsNullOrEmpty(nextNodeInXPath))
        return parent;

    // get or create the node from the name
    XmlNode node = parent.SelectSingleNode(nextNodeInXPath);
    if (node == null)
        node = parent.AppendChild(doc.CreateElement(nextNodeInXPath));

    // rejoin the remainder of the array as an xpath expression and recurse
    string rest = String.Join("/", partsOfXPath.Skip(1).ToArray());
    return makeXPath(doc, node, rest);
}

static void Main(string[] args)
{
    XmlDocument doc = new XmlDocument();
    doc.LoadXml("<feed />");

    makeXPath(doc, "/feed/entry/data");
    XmlElement contentElement = (XmlElement)makeXPath(doc, "/feed/entry/content");
    contentElement.SetAttribute("source", "");

    Console.WriteLine(doc.OuterXml);
}
xcud
  • 14,422
  • 3
  • 33
  • 29
  • Thanks for responding. I was hoping not to have to "roll my own" XPath parser but it may in fact come to that. – Fred Strauss Feb 04 '09 at 15:32
  • 1
    Thanks, I had the need for this and you just saved me an hour or two of writing my own implementation. – Chris Miller Nov 16 '09 at 20:20
  • 1
    @xcud: Same as for Chris! Thanks a lot! – Marc Feb 01 '10 at 10:14
  • 2
    useful function but would be a lot more useful if you could pass in proper XPath syntax such as nodea/nodeb[1]/nodec :-) – Gordon Thompson Feb 16 '10 at 21:48
  • thanks, this came in very handy for a very basic xpath syntax – Steven Pardo Aug 25 '10 at 17:18
  • @GordonThompson - Try this. It also strips any namespace qualifiers: private static XmlNode MakeXPath(XmlDocument doc, XmlNode parent, string xpath) { string[] partsOfXPath = xpath.Trim('/').Split('/'); foreach (string nodeInXPath in partsOfXPath.Select(p => p.split(':').Last().Split('[').First())) // Remove the namespace and array indexer (i.e. "ns1:MyNode[1]") { parent = parent.SelectSingleNode(nodeInXPath) ?? parent.AppendChild(doc.CreateElement(nodeInXPath)); } return parent; } – Dan Sorak Jun 23 '20 at 17:10
15

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");
bluish
  • 26,356
  • 27
  • 122
  • 180
laktak
  • 57,064
  • 17
  • 134
  • 164
9

One problem with this idea is that xpath "destroys" information.

There are an infinite number of xml trees that can match many xpaths. Now in some cases, like the example you give, there is an obvious minimal xml tree which matches your xpath, where you have a predicate that uses "=".

But for example if the predicate uses not equal, or any other arithmetic operator other than equal, an infinite number of possibilities exist. You could try to choose a "canonical" xml tree which requires, say, the fewest bits to represent.

Suppose for example you had xpath /feed/entry/content[@source > 0]. Now any xml tree of the appropriate structure in which node content had an attribute source whose value was > 0 would match, but there are an infinite number of numbers greater than zero. By choosing the "minimal" value, presumably 1, you could attempt to canonicalize your xml.

Xpath predicates can contain pretty arbitrary arithmetic expressions, so the general solution to this is quite difficult, if not impossible. You could imagine a huge equation in there, and it would have to be solved in reverse to come up with values that would match the equation; but since there can be an infinite number of matching values (as long as it's really an inequality not an equation), a canonical solution would need to be found.

Many expressions of other forms also destroy information. For example, an operator like "or" always destroys information. If you know that (X or Y) == 1, you don't know if X is 1, Y is 1, or both of them is 1; all you know for sure is that one of them is 1! Therefore if you have an expression using OR, you cannot tell which of the nodes or values that are inputs to the OR should be 1 (you can make an arbitrary choice and set both 1, as that will satisfy the expression for sure, as will the two choices in which only one of them is 1).

Now suppose there are several expressions in the xpath which refer to the same set of values. You then end up with a system of simultaneous equations or inequalities that can be virtually impossible to solve. Again, if you restrict the allowable xpath to a small subset of its full power, you can solve this problem. I suspect the fully general case is similar to the Turing halting problem, however; in this case, given an arbitrary program (the xpath), figure out a set of consistent data that matches the program, and is in some sense minimal.

Mikael Svenson
  • 39,181
  • 7
  • 73
  • 79
  • +1 for a great rationale as to why such a feature isn't common in XML systems already. [A constraint solver](http://en.wikipedia.org/wiki/Constraint_satisfaction) could be used to help with the "system of simultaneous equations or inequalities" bit, but that's a pretty hefty solution, and might take a lot of processing power (I believe it is NP-Complete). – Merlyn Morgan-Graham Dec 03 '11 at 18:51
5

Here is my version. Hope this also would help someone.

    public static void Main(string[] args)
    {

        XmlDocument doc = new XmlDocument();
        XmlNode rootNode = GenerateXPathXmlElements(doc, "/RootNode/FirstChild/SecondChild/ThirdChild");

        Console.Write(rootNode.OuterXml);

    }

    private static XmlDocument GenerateXPathXmlElements(XmlDocument xmlDocument, string xpath)
    {
        XmlNode parentNode = xmlDocument;

        if (xmlDocument != null && !string.IsNullOrEmpty(xpath))
        {
            string[] partsOfXPath = xpath.Split('/');


            string xPathSoFar = string.Empty;

            foreach (string xPathElement in partsOfXPath)
            {
                if(string.IsNullOrEmpty(xPathElement))
                    continue;

                xPathSoFar += "/" + xPathElement.Trim();

                XmlNode childNode = xmlDocument.SelectSingleNode(xPathSoFar);
                if(childNode == null)
                {
                    childNode = xmlDocument.CreateElement(xPathElement);
                }

                parentNode.AppendChild(childNode);

                parentNode = childNode;
            }
        }

        return xmlDocument;
    }
Eranga Dissanayaka
  • 1,930
  • 3
  • 24
  • 27
5

The C# version of Mark Miller's Java solution

    /// <summary>
    /// Makes the X path. Use a format like //configuration/appSettings/add[@key='name']/@value
    /// </summary>
    /// <param name="doc">The doc.</param>
    /// <param name="xpath">The xpath.</param>
    /// <returns></returns>
    public static XmlNode createNodeFromXPath(XmlDocument doc, string xpath)
    {
        // Create a new Regex object
        Regex r = new Regex(@"/+([\w]+)(\[@([\w]+)='([^']*)'\])?|/@([\w]+)");

        // Find matches
        Match m = r.Match(xpath);

        XmlNode currentNode = doc.FirstChild;
        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();
            XmlNode newNode = doc.SelectSingleNode(relativePath);

            if (newNode == null)
            {
                if (!string.IsNullOrEmpty(attributeName))
                {
                    ((XmlElement)currentNode).SetAttribute(attributeName, "");
                    newNode = doc.SelectSingleNode(relativePath);
                }
                else if (!string.IsNullOrEmpty(elementName))
                {
                    XmlElement element = doc.CreateElement(elementName);
                    if (!string.IsNullOrEmpty(filterName))
                    {
                        element.SetAttribute(filterName, filterValue);
                    }

                    currentNode.AppendChild(element);
                    newNode = element;
                }
                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 (doc.SelectSingleNode(xpath) == null)
        {
            throw new FormatException("The given xPath cannot be created " + xpath);
        }

        return currentNode;
    }
Michael
  • 8,362
  • 6
  • 61
  • 88
Ben
  • 181
  • 3
  • 1
  • For recognizing dots in "configuration/userSettings/TransFlow.Properties.Settings" there should be small fix: Regex r = new Regex(@"/+([\w.]+)(\[@([\w.]+)='([^']*)'\])?|/@([\w.]+)"); – Jerzy Gebler Nov 07 '22 at 14:21
  • Jerzy, should that be: @"/+([\w.]+)(\\[@([\w.]+)='([^']*)'\\])?|/@([\w.]+)") ? – Doug Royer Mar 02 '23 at 00:21
2

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;
}
bluish
  • 26,356
  • 27
  • 122
  • 180
2

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;
}
Michael
  • 8,362
  • 6
  • 61
  • 88
Jan Lauridsen
  • 586
  • 4
  • 9
2
  • 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;
}
tchelidze
  • 8,050
  • 1
  • 29
  • 49
1

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)
Michael
  • 8,362
  • 6
  • 61
  • 88
1

If the XPath string is processed from back to front, its easier to process non rooted XPaths eg. //a/b/c... It should support Gordon's XPath syntax too although I have not tried...

static private XmlNode makeXPath(XmlDocument doc, string xpath)
{
    string[] partsOfXPath = xpath.Split('/');
    XmlNode node = null;
    for (int xpathPos = partsOfXPath.Length; xpathPos > 0; xpathPos--)
    {
        string subXpath = string.Join("/", partsOfXPath, 0, xpathPos);
        node = doc.SelectSingleNode(subXpath);
        if (node != null)
        {
            // append new descendants
            for (int newXpathPos = xpathPos; newXpathPos < partsOfXPath.Length; newXpathPos++)
            {
                node = node.AppendChild(doc.CreateElement(partsOfXPath[newXpathPos]));
            }
            break;
        }
    }

    return node;
}
ilen
  • 69
  • 8
0

I know this is a really old thread ... but I have just been trying the same thing and came up with the following regex which is not perfect but I find more generic

/+([\w]+)(\[@([\w]+)='([^']*)'\])?|/@([\w]+)

The string /configuration/appSettings/add[@key='name']/@value

should be parsed to

Found 14 match(es):

start=0, end=14 Group(0) = /configuration Group(1) = configuration Group(2) = null Group(3) = null Group(4) = null Group(5) = null

start=14, end=26 Group(0) = /appSettings Group(1) = appSettings Group(2) = null Group(3) = null Group(4) = null Group(5) = null

start=26, end=43 Group(0) = /add[@key='name'] Group(1) = add Group(2) = [@key='name'] Group(3) = key Group(4) = name Group(5) = null

start=43, end=50 Group(0) = /@value Group(1) = null Group(2) = null Group(3) = null Group(4) = null Group(5) = value


Which means we have

Group(0) = Ignored Group(1) = The element name Group(2) = Ignored Group(3) = Filter attribute name Group(4) = Filter attribute value

Here is a java method which can use the pattern

public static Node createNodeFromXPath(Document doc, String expression) throws XPathExpressionException {
StringBuilder currentPath = new StringBuilder();
Matcher matcher = xpathParserPattern.matcher(expression);

Node currentNode = doc.getFirstChild();

while (matcher.find()) {
    String currentXPath = matcher.group(0);
    String elementName = matcher.group(1);
    String filterName = matcher.group(3);
    String filterValue = matcher.group(4);
    String attributeName = matcher.group(5);

    StringBuilder builder = currentPath.append(currentXPath);
    String relativePath = builder.toString();
    Node newNode = selectSingleNode(doc, relativePath);

    if (newNode == null) {
        if (attributeName != null) {
            ((Element) currentNode).setAttribute(attributeName, "");
            newNode = selectSingleNode(doc, relativePath);

        } else if (elementName != null) {
            Element element = doc.createElement(elementName);
            if (filterName != null) {
                element.setAttribute(filterName, filterValue);
            }
            currentNode.appendChild(element);
            newNode = element;

        } else {
            throw new UnsupportedOperationException("The given xPath is not supported " + relativePath);
        }
    }

    currentNode = newNode;
}

if (selectSingleNode(doc, expression) == null) {
    throw new IllegalArgumentException("The given xPath cannot be created " + expression);
}

return currentNode;

}

Mark Miller
  • 143
  • 7
-1

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
kenyee
  • 2,309
  • 1
  • 24
  • 32