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:
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;
}
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
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)
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;
}
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");
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;
}