Similar Build XML by XPATH expressions question can be found in Java End:
As we know, there are many tools that can generate XML from XSD file, and I already have below NOT-SO-ROBUST code to build XML by XPATH expressions.
However, it can not handle complex scenarios. Combining the information from XSD, ideally, we should be able to handle any scenarios, and build XML by XPATH expressions.
I'm wondering, whether someone already did that?
It is not duplicate question to this topic. Below code already can handle simple node creation. I need emphasize that, there are many different xml styles, if we just focus on xml itself, without XSD information, the output is unpredictable, it could work, or just got stuck because of exception.
XmlNode node=xmlDoc.SelectSingleNode(xpath,nsmgr);
if(node==null){
node=makeXPath(xmlDoc,nsmgr, xpath);
}
if(node is XmlAttribute){
XmlAttribute formId = (XmlAttribute) node;
if (formId != null)
{
formId.Value = value1; // Set to new value.
}
}
else if (node is XmlElement){
XmlElement formData = (XmlElement)node;
if (formData != null)
{
formData.InnerText=value1; // Set to new value.
}
}
.......
.......
.......
.......
.......
.......
static private XmlNode makeXPath(XmlDocument doc,XmlNamespaceManager nsmgr, string xpath)
{
return makeXPath(doc, nsmgr,doc as XmlNode, xpath);
}
static private XmlNode makeXPath(XmlDocument doc, XmlNamespaceManager nsmgr,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;
XmlNode node = parent.SelectSingleNode(nextNodeInXPath,nsmgr);
if (node == null){
string nextNodeInXPath_new=new Regex(#"\[\d+\]").Replace(nextNodeInXPath,"");
node = parent.AppendChild(doc.CreateElement(nextNodeInXPath_new));
}
string rest = String.Join("/", partsOfXPath.Skip(1).ToArray());
return makeXPath(doc, nsmgr,node, rest);
}
Related
I'm using XPath to read elements from an XML document. Specifically I want to return the values of any element which is the child of a specified element (here the specified element is <SceneryType> and these elements have single-digit values. So I want to return all of the children of <SceneryType> 1 for example.
Here is the XML:
<MissionObjectives>
<Theme themeName="Gothic">
<SceneryType>
1
<Objective>
Do a river thing.
</Objective>
<Objective>
Get all men to the other side of the river.
</Objective>
</SceneryType>
<SceneryType>
2
<Objective>
Climb some trees!
</Objective>
<Objective>
Shoot the tree!
</Objective>
</SceneryType>
</Theme>
I've tried various ways of getting these child elements, but I can't figure it out. My //objective part of the expression just returns everything from the root it seems, but the iterator isn't running which seems odd, shouldn't it loop through every element if the expression is returning a nodelist of all the elements?
XPathDocument missionDoc = new XPathDocument(objectivesPath + "MissionObjectives" + chosenTheme + ".xml");
XPathNavigator nav = missionDoc.CreateNavigator();
foreach (Scenery scenery in world.currentWorld)
{
int sceneryType = scenery.type;
XPathExpression expr = nav.Compile($"MissionObjectives/Theme/SceneryType[text()='{sceneryType}']//Objective");
XPathNodeIterator iterator = nav.Select(expr);
while (iterator.MoveNext())
{
XPathNavigator nav2 = iterator.Current.Clone();
compatibleObjectivesList.Add(nav2.Value);
}
}
I've tried looking through Stack Overflow for similar questions but I can't seem to find anything which applies to XPath. I can't use LINQ to XML for this. Any idea how I can return all the values of the various 'Objective' nodes?
Cheers for any help!
its much simpler to use the XDocument:
var doc = XDocument.Load(objectivesPath + "MissionObjectives" + chosenTheme + ".xml");
to get all of the first SceneryType child nodes:
var node = doc.XPathSelectElement("//MissionObjectives/Theme/SceneryType[1]");
to get the second objective node:
var node = doc.XPathSelectElement("//MissionObjectives/Theme/SceneryType/Objective[2]");
more xpath samples
For one, your xml data has carriage returns, line feeds, and white spaces in the search element's text node. Keep in mind, that an XML node can be an element, attribute, or text (among other node types). The solution below is a bit on the "long-handed" side and perhaps a little "hacky", but it should work. I wasn't certain if you wanted the child element text data or the entire child element, but I return just the child text node data (without carriage returns and line feeds). Also, while this solution DOES NOT use LINQ to XML in the strictest sense, it does use one LINQ expression.
private List<string> getSceneryTypeObjectiveTextList(string xml, int sceneryTypeId, string xpath = "/MissionObjectives/Theme/SceneryType")
{
List<string> result = null;
XmlDocument doc = null;
XmlNodeList sceneryTypeNodes = null;
try
{
doc = new XmlDocument();
doc.LoadXml(xml);
sceneryTypeNodes = doc.SelectNodes(xpath);
if (sceneryTypeNodes != null)
{
if (sceneryTypeNodes.Count > 0)
{
foreach (XmlNode sceneryTypeNode in sceneryTypeNodes)
{
if (sceneryTypeNode.HasChildNodes)
{
var textNode = from XmlNode n in sceneryTypeNode.ChildNodes
where (n.NodeType == XmlNodeType.Text && n.Value.Replace("\r", "").Replace("\n", "").Replace(" ", "") == sceneryTypeId.ToString())
select n;
if (textNode.Count() > 0)
{
XmlNodeList objectiveNodes = sceneryTypeNode.SelectNodes("Objective");
if (objectiveNodes != null)
{
result = new List<string>(objectiveNodes.Count);
foreach (XmlNode objectiveNode in objectiveNodes)
{
result.Add(objectiveNode.InnerText.Replace("\r", "").Replace("\n", "").Trim());
}
// Could break out of the iteration, here, if we know that SceneryType is always unique (i.e. - no duplicates in Element text node)
}
}
}
}
}
}
}
catch (Exception ex)
{
// Handle error
}
finally
{
}
return result;
}
private sampleCall(string filePath, int sceneryTypeId)
{
List<string> compatibleObjectivesList = null;
try
{
compatibleObjectivesList = getSceneryTypeObjectiveTextList(File.ReadAllText(filePath), sceneryTypeId);
}
catch (Exception ex)
{
// Handle error
}
finally
{
}
}
I am working in an older piece of code so I do not have the option to use XDocument.
I am trying to replace one XmlNode with another but from some reason XmlDocument.ReplaceChild() complains that: The node to be removed is not a child of this node.
I don't understand why I am getting this error because I am getting reference to the XmlDocument from the element to be replaced.
private XmlNode ReplaceWithSpan(XmlNode node)
{
if (XmlNodeType.Element == node.NodeType)
{
XmlDocument ownerDocument = node.OwnerDocument;
XmlNode spanNode = ownerDocument.CreateElement("span");
for (int i = 0; i < node.Attributes.Count; i++)
{
XmlAttribute attribute = node.Attributes[i];
AddAttribute(spanNode, attribute.Name, attribute.Value);
}
ownerDocument.ReplaceChild(spanNode, node); //this doesn't work
return spanNode;
}
throw new InvalidCastException(string.Format("Could not replace custom edit node of type {0}.", node.NodeType.ToString()));
}
Can someone where I've gone wrong?
Update
Got it figured out. The problem is that the old node is not a direct child of the owner document. The following change works:
node.ParentNode.ReplaceChild(spanNode, node);
return spanNode;
I am querying a xml document using the XPathSelectElement method.
if the node does not exist I would like to insert a node with that path in the same document. The parent nodes should also be created if they do not exist. Is there an easy way to do this without looping through the parents checking if they exist? (Add a new node using XPath)
No, there is not... this is no different than if you were looking for a Directory on a File System, and had to ensure that all of the parent directories were there to.
Example:
if (Directory.Exists(#":c:\test1\test2\blah blah\blah blah2")) ...
It's true that the Directory.CreateDirectory method will create all parents that need to be there to have the child show up, but there is no equivalent in XML (using .NET classes, including LINQ-to-XML).
You'll have to loop through each one manually. I suggest you make a helper method called "EnsureNodeExists" that does that for you :)
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);
}
This is a bit late, but for the next poor soul to come here and find no help here's the static method I ended up writing. It's not flawless, but should handle a variety of use cases. Essentially I just go from the bottom of the XPath and keep cutting off sections until I find a match, then go back down the path and create what's missing. It will fail if you use a path like /root/test//someNode if someNode doesn't exist, but for a path ../someNode//foo/bar/zoot, if ../someNode//foo exists it should work fine.
public static XElement GetOrCreateNodeAtXPath(XDocument doc, string key)
{
//you don't really need this, but it makes throwing errors easier when you're 10 levels deep in the recursion
return GetOrCreateNodeAtXPath(doc, key, key);
}
private static XElement GetOrCreateNodeAtXPath(XDocument doc, string key, string originalKey)
{
var node = doc.XPathSelectElement(key);
if (node != null)
return node;
if (!key.Contains('/')) //we've reached the root, and couldn't find anything
throw new Exception($"Could not create node at path {originalKey}, no part of path matches document");
var slashIndex = key.LastIndexOf('/');
var newKey = key.Substring(0, slashIndex);
var newNodeName = key.Substring(slashIndex + 1);
var parentNode = GetOrCreateNodeAtXPath(doc, newKey, originalKey);
var childNode = new XElement(newNodeName);
parentNode.Add(childNode);
return childNode;
}
You may wanna swap XDocument with XElement or XNode or something in the signature, I was just dealign with docs so I used docs.
I am a beginner to XML and XPath in C#. Here is an example of my XML doc:
<root>
<folder1>
...
<folderN>
...
<nodeMustExist>...
<nodeToBeUpdated>some value</nodeToBeUpdated>
....
</root>
What I need is to update the value of nodeToBeUdpated if the node exists or add this node after the nodeMustExist if nodeToBeUpdated is not there. The prototype of the function is something like this:
void UpdateNode(
xmlDocument xml,
string nodeMustExist,
string nodeToBeUpdte,
string newVal
)
{
/*
search for XMLNode with name = nodeToBeUpdate in xml
to XmlNodeToBeUpdated (XmlNode type?)
if (xmlNodeToBeUpdated != null)
{
xmlNodeToBeUpdated.value(?) = newVal;
}
else
{
search for nodeMustExist in xml to xmlNodeMustExist obj
if ( xmlNodeMustExist != null )
{
add xmlNodeToBeUpdated as next node
xmlNodeToBeUpdte.value = newVal;
}
}
*/
}
Maybe there are other better and simplified way to do this. Any advice?
By the way, if nodeToBeUpdated appears more than once in other places, I just want to update the first one.
This is to update all nodes in folder:
public void UpdateNodes(XmlDocument doc, string newVal)
{
XmlNodeList folderNodes = doc.SelectNodes("folder");
if (folderNodes.Count > 0)
foreach (XmlNode folderNode in folderNodes)
{
XmlNode updateNode = folderNode.SelectSingleNode("nodeToBeUpdated");
XmlNode mustExistNode = folderNode.SelectSingleNode("nodeMustExist"); ;
if (updateNode != null)
{
updateNode.InnerText = newVal;
}
else if (mustExistNode != null)
{
XmlNode node = folderNode.OwnerDocument.CreateNode(XmlNodeType.Element, "nodeToBeUpdated", null);
node.InnerText = newVal;
folderNode.AppendChild(node);
}
}
}
If you want to update a particular node, you cannot pass string nodeToBeUpdte, but you will have to pass the XmlNode of the XmlDocument.
I have omitted the passing of node names in the function since nodes names are unlikely to change and can be hardcoded. However, you can pass these to the functions and use the strings instead of hardcoded node names.
The XPath expression that selects all instances of <nodeToBeUpdated> would be this:
/root/folder[nodeMustExist]/nodeToBeUpdated
or, in a more generic form:
/root/folder[*[name() = 'nodeMustExist']]/*[name() = 'nodeToBeUpdated']
suitable for:
void UpdateNode(xmlDocument xml,
string nodeMustExist,
string nodeToBeUpdte,
string newVal)
{
string xPath = "/root/folder[*[name() = '{0}']]/*[name() = '{1}']";
xPath = String.Format(xPath, nodeMustExist, nodeToBeUpdte);
foreach (XmlNode n in xml.SelectNodes(xPath))
{
n.Value = newVal;
}
}
Have a look at the SelectSingleNode method MSDN Doc
your xpath wants to be something like "//YourNodeNameHere" ;
once you have found that node you can then traverse back up the tree to get to the 'nodeMustExist' node:
XmlNode nodeMustExistNode = yourNode.Parent["nodeMustExist];
The following code should find the appropriate project tag and remove it from the XmlDocument, however when I test it, it says:
The node to be removed is not a child of this node.
Does anyone know the proper way to do this?
public void DeleteProject (string projectName)
{
string ccConfigPath = ConfigurationManager.AppSettings["ConfigPath"];
XmlDocument configDoc = new XmlDocument();
configDoc.Load(ccConfigPath);
XmlNodeList projectNodes = configDoc.GetElementsByTagName("project");
for (int i = 0; i < projectNodes.Count; i++)
{
if (projectNodes[i].Attributes["name"] != null)
{
if (projectName == projectNodes[i].Attributes["name"].InnerText)
{
configDoc.RemoveChild(projectNodes[i]);
configDoc.Save(ccConfigPath);
}
}
}
}
UPDATE
Fixed. I did two things:
XmlNode project = configDoc.SelectSingleNode("//project[#name='" + projectName + "']");
Replaced the For loop with an XPath query, which wasn't for fixing it, just because it was a better approach.
The actual fix was:
project.ParentNode.RemoveChild(project);
Thanks Pat and Chuck for this suggestion.
Instead of
configDoc.RemoveChild(projectNodes[i]);
try
projectNodes[i].parentNode.RemoveChild(projectNodes[i]);
try
configDoc.DocumentElement.RemoveChild(projectNodes[i]);
Looks like you need to select the parent node of projectNodes[i] before calling RemoveChild.
When you get sufficiently annoyed by writing it the long way (for me that was fairly soon) you can use a helper extension method provided below. Yay new technology!
public static class Extensions {
...
public static XmlNode RemoveFromParent(this XmlNode node) {
return (node == null) ? null : node.ParentNode.RemoveChild(node);
}
}
...
//some_long_node_expression.parentNode.RemoveChild(some_long_node_expression);
some_long_node_expression.RemoveFromParent();
Is it possible that the project nodes aren't child nodes, but grandchildren or lower? GetElementsByTagName will give you elements from anywhere in the child element tree, IIRC.
It would be handy to see a sample of the XML file you're processing but my guess would be that you have something like this
<Root>
<Blah>
<project>...</project>
</Blah>
</Root>
The error message seems to be because you're trying to remove <project> from the grandparent rather than the direct parent of the project node