Create Xml Nodes Based on Xpath

c# Generate XML document based on Xpath

XPath isn't really intended to generate documents but to access em.

Another (probably better) approach would be to serialize it.
But, since the question requires the fields of xml to be variable, serialization isn't a viable way. i STRONGLY advise you to change this requirement and you'll see why in the following example:

using System;

namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
using (var writer = new System.IO.StreamWriter(@"C:\Users\luigi.trabacchin\Desktop\asd.xml"))
{
var doc = new System.Xml.XmlDocument();
var root = doc.CreateElement("Root");
doc.AppendChild(root);
for (var i = 0; i <= 1; i++)
{
var dataNode = doc.CreateElement("Data");
root.AppendChild(dataNode);
{
var node = doc.CreateElement("Name");
dataNode.AppendChild(node);
var text = doc.CreateTextNode($"Name {i}");
node.AppendChild(text);
}
{
var node = doc.CreateElement("Surname");
dataNode.AppendChild(node);
var text = doc.CreateTextNode($"Surname {i}");
node.AppendChild(text);
}
{
var node = doc.CreateElement("Gender");
dataNode.AppendChild(node);
var text = doc.CreateTextNode(i %2 == 0 ? "M" : "F");
node.AppendChild(text);
}
}

doc.Save(writer);
}
Console.WriteLine("Hello World!");
}
}
}

It quickly becomes very tedious and hard to work with.
Still i hope this answer your question.
Next time, maybe, state all the requirements directly in the question!

Create XML document from XPaths

XPath purpose is to query xml documents, not to create them, so I don't think what you are trying to achieve is possible this way.

Maybe you can get inspiration there :

Create XML Nodes based on XPath?

Creating XML nodes from XPATH and merging that to existing XML

What the code does, is, it first creates some XML fragment from that VPop variable and the result from that, for your given sample data, is simply <data<txCurr>MYD</txCurr</data>, i.e. a single data element with a single txtCurr element. The next step then merges the XML fragment with the input fragment, based on the XPaths the XPath 3.1 path function gives. So the information that you might have wanted the second data element is already gone after that first step, somehow it expects you to ensure your "input" paths specify and therefore create two data elements (e.g. <xsl:variable name="vPop" as="element()*"><item path="/data[1]"/><item path="/data[2]/txCurr">MYD</item></xsl:variable>), otherwise the whole approach can't work.

Or the first step would need to be rewritten not only to break up and create elements based on names but also to try to infer which indices are left out/missing and also create them, something that is (even more) complex than the current approach; here is a basic, and admittedly, currently rather convoluted approach:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
exclude-result-prefixes="#all"
xmlns:mf="http://example.com/mf"
expand-text="yes">

<xsl:variable name="vPop" as="element()*">
<item path="/data[2]/txCurr">MYD</item>
</xsl:variable>

<xsl:variable name="new-nodes">
<xsl:sequence select="mf:generate-nodes($vPop ! map:entry(@path!string(), string(.)))"/>
</xsl:variable>

<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>

<xsl:template match="/" name="xsl:initial-template">
<xsl:sequence select="mf:merge($main-input/*, $new-nodes/*)"/>
<xsl:comment>Run with {system-property('xsl:product-name')} {system-property('xsl:product-version')} {system-property('Q{http://saxon.sf.net/}platform')}</xsl:comment>
</xsl:template>

<xsl:function name="mf:merge" as="node()*">
<xsl:param name="node1" as="node()*"/>
<xsl:param name="node2" as="node()*"/>
<xsl:for-each-group select="$node1, $node2" group-by="path()">
<xsl:copy>
<xsl:sequence select="mf:merge(@*, current-group()[2]/@*)"/>
<xsl:sequence select="mf:merge(node(), current-group()[2]/node())"/>
</xsl:copy>
</xsl:for-each-group>
</xsl:function>

<xsl:output method="xml" indent="yes" />

<xsl:mode on-no-match="shallow-copy"/>

<xsl:param name="main-input" as="document-node()" select="parse-xml-fragment($main-fragment)"/>

<xsl:param name="main-fragment" as="xs:string"><![CDATA[<header>
<identifier>12345</identifier>
</header>
<data>
<txCtry>SG</txCtry>
</data>
<data>
<txCtry>TH</txCtry>
</data>
<data>
<txCtry>MY</txCtry>
</data>]]></xsl:param>

<xsl:function name="mf:generate-nodes" as="node()*" visibility="public">
<xsl:param name="xpath-values" as="map(xs:string, item()*)*"/>
<xsl:for-each-group select="$xpath-values" group-adjacent="
let $first-step := replace(map:keys(.), '^/?([^/]+)(.*$)', '$1'),
$exp := replace($first-step, '\[[0-9]+\]$', '')
return
$exp">
<xsl:choose>
<xsl:when test="current-grouping-key() = ''">
<xsl:choose>
<xsl:when test="?* instance of node()+">
<xsl:sequence select="?*"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="?*"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:iterate select="
1 to max(current-group() !
(let $key := map:keys(.),
$first-step := replace($key, '^/?([^/]+)(.*$)', '$1'),
$pos := if (not(ends-with($first-step, ']'))) then
1
else
replace($first-step, '^[^\[]+(\[([0-9]+)\])$', '$2') ! xs:integer(.)
return
$pos))">
<xsl:variable name="exp" select="current-grouping-key()"/>
<xsl:variable name="step" as="xs:string*" select="
if (. eq 1) then
current-grouping-key()
else
(), current-grouping-key() || '[' || . || ']'"/>
<xsl:variable name="current-grouping-steps"
select="current-group()[map:keys(.) ! tokenize(., '/')[normalize-space()][1][. = $step]]"/>
<xsl:choose>
<xsl:when test="not($exp = '') and not(exists($current-grouping-steps))">
<xsl:choose>
<xsl:when test="starts-with($exp, 'comment()')">
<xsl:comment/>
</xsl:when>
<xsl:when test="starts-with($exp, 'processing-instruction(')">
<xsl:processing-instruction name="{replace($exp, '^processing-instruction\(([''"]?)([^''"]+)["'']?\)$', '$2')}"/>
</xsl:when>
<xsl:when test="starts-with($exp, '@')">
<xsl:attribute name="{substring($exp, 2)}"/>
</xsl:when>
<xsl:when test="$exp">
<xsl:element name="{$exp}"/>
</xsl:when>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:for-each-group select="$current-grouping-steps"
group-by="replace(map:keys(.), '^/?([^/]+)(.*$)', '$1')">
<xsl:variable name="name" as="xs:string"
select="replace(current-grouping-key(), '\[[0-9]+\]$', '')"/>
<xsl:choose>
<xsl:when test="starts-with($name, 'comment()')">
<xsl:comment select="?*"/>
</xsl:when>
<xsl:when
test="starts-with($name, 'processing-instruction(')">
<xsl:processing-instruction name="{replace($name, '^processing-instruction\(([''"]?)([^''"]+)["'']?\)$', '$2')}" select="?*"/>
</xsl:when>
<xsl:when test="starts-with($name, '@')">
<xsl:attribute name="{substring($name, 2)}" select="?*"
/>
</xsl:when>
<xsl:when test="$name">
<xsl:element name="{$name}">
<xsl:sequence
select="mf:generate-nodes(current-group() ! map:entry(map:keys(.) ! replace(., '^/?[^/]+(.*)', '$1'), ?*))"
/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="?* instance of node()+">
<xsl:sequence select="?*"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="?*"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:otherwise>
</xsl:choose>
</xsl:iterate>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>

Add nodes in XML based on Xpath and also add multiple similar tags in same parent

As I understand your code, when you want to create the second Official node, the makeXPath function will find an existing Official node in the first for loop.
node = doc.SelectSingleNode(subXpath) will return this existing node, and you will then set the innerText without creating a new one.
I believe you should check for the existence of the full path before the first for loop and create the sibling then.

Something like

public XmlNode makeXPath(XmlDocument doc, string xpath, string innertext)
{
string[] partsOfXPath = xpath.Split('/');
XmlNode node = null;
if (doc.SelectSingleNode(xpath) != null) {
//get the parent
node = doc.SelectSingleNode(string.Join("/", partsOfXPath, 0, partsOfXPath.Length-1));
node = node.AppendChild(doc.CreateElement(xpath));
node.InnerText = innertext.TrimStart(' ');
}
else {
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;
}
}
node.InnerText = innertext.TrimStart(' ');
}
return node;
}

Create XML nodes using XPath

Is it possible to create XML nodes using XPath?

No, it isn't. XPath can only select existing nodes.

If you want to create new nodes, you need XSLT or XQuery.

But even then, creating a node that satisfies a given path like /a/b/c/foo[5] is non-trivial, and not something that the languages can do automatically.

How to Generate an XML File from a set of XPath Expressions?

This problem has an easy solution if one builds upon the solution of the previous problem:

<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>

<xsl:key name="kNSFor" match="namespace" use="@of"/>
<xsl:variable name="vStylesheet" select="document('')"/>

<xsl:variable name="vPop" as="element()*">
<item path="/create/article/@type">richtext</item>
<item path="/create/article/@lang">en-us</item>
<item path="/create/article[1]/id">1</item>
<item path="/create/article[1]/description">bar</item>
<item path="/create/article[1]/name[1]">foo</item>
<item path="/create/article[1]/price[1]/amount">00.00</item>
<item path="/create/article[1]/price[1]/currency">USD</item>
<item path="/create/article[1]/price[2]/amount">11.11</item>
<item path="/create/article[1]/price[2]/currency">AUD</item>
<item path="/create/article[2]/id">2</item>
<item path="/create/article[2]/description">some name</item>
<item path="/create/article[2]/name[1]">some description</item>
<item path="/create/article[2]/price[1]/amount">00.01</item>
<item path="/create/article[2]/price[1]/currency">USD</item>

<namespace of="create" prefix="ns1:"
url="http://predic8.com/wsdl/material/ArticleService/1/"/>
<namespace of="article" prefix="ns1:"
url="xmlns:ns1='http://predic8.com/material/1/"/>
<namespace of="@lang" prefix="xml:"
url="http://www.w3.org/XML/1998/namespace"/>
<namespace of="price" prefix="ns1:"
url="xmlns:ns1='http://predic8.com/material/1/"/>
<namespace of="id" prefix="ns1:"
url="xmlns:ns1='http://predic8.com/material/1/"/>
</xsl:variable>

<xsl:template match="/">
<xsl:sequence select="my:subTree($vPop/@path/concat(.,'/',string(..)))"/>
</xsl:template>

<xsl:function name="my:subTree" as="node()*">
<xsl:param name="pPaths" as="xs:string*"/>

<xsl:for-each-group select="$pPaths" group-adjacent=
"substring-before(substring-after(concat(., '/'), '/'), '/')">
<xsl:if test="current-grouping-key()">
<xsl:choose>
<xsl:when test=
"substring-after(current-group()[1], current-grouping-key())">

<xsl:variable name="vLocal-name" select=
"substring-before(concat(current-grouping-key(), '['), '[')"/>

<xsl:variable name="vNamespace"
select="key('kNSFor', $vLocal-name, $vStylesheet)"/>

<xsl:choose>
<xsl:when test="starts-with($vLocal-name, '@')">
<xsl:attribute name=
"{$vNamespace/@prefix}{substring($vLocal-name,2)}"
namespace="{$vNamespace/@url}">
<xsl:value-of select=
"substring(
substring-after(current-group(), current-grouping-key()),
2
)"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{$vNamespace/@prefix}{$vLocal-name}"
namespace="{$vNamespace/@url}">

<xsl:sequence select=
"my:subTree(for $s in current-group()
return
concat('/',substring-after(substring($s, 2),'/'))
)
"/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="current-grouping-key()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>

When this transformation is applied on any XML document (not used), the wanted, correct result is produced:

<ns1:create xmlns:ns1="http://predic8.com/wsdl/material/ArticleService/1/">
<ns1:article xmlns:ns1="xmlns:ns1='http://predic8.com/material/1/" type="richtext"
xml:lang="en-us"/>
<ns1:article xmlns:ns1="xmlns:ns1='http://predic8.com/material/1/">
<ns1:id>1</ns1:id>
<description>bar</description>
<name>foo</name>
<ns1:price>
<amount>00.00</amount>
<currency>USD</currency>
</ns1:price>
<ns1:price>
<amount>11.11</amount>
<currency>AUD</currency>
</ns1:price>
</ns1:article>
<ns1:article xmlns:ns1="xmlns:ns1='http://predic8.com/material/1/">
<ns1:id>2</ns1:id>
<description>some name</description>
<name>some description</name>
<ns1:price>
<amount>00.01</amount>
<currency>USD</currency>
</ns1:price>
</ns1:article>
</ns1:create>

Explanation:

  1. A reasonable assumption is made that throughout the generated document any two elements with the same local-name() belong to the same namespace -- this covers the predominant majority of real-world XML documents.

  2. The namespace specifications follow the path specifications. A nsmespace specification has the form: <namespace of="target element's local-name" prefix="wanted prefix" url="namespace-uri"/>

  3. Before generating an element with xsl:element, the appropriate namespace specification is selected using an index created by an xsl:key. From this namespace specification the values of its prefix and url attributes are used in specifying in the xsl:element instruction the values of the full element name and the element's namespace-uri.



Related Topics



Leave a reply



Submit