Reputation: 348
I am developing a app with C#
and trying to complete a XML
that I have got from a JSON
. And for the XML
to be valid for my app, I need to group the elements with the same name under a father element.
For example, I got this XML
<root>
<row>
<id>0001</id>
<type>credit</type>
<investment>1000</investment>
<ppr>0.83</ppr>
<candidate>
<id>5001</id>
<name>Hugo</name>
</candidate>
<candidate>
<id>5002</id>
<name>Jack</name>
</candidate>
<candidate>
<id>5005</id>
<name>Kate</name>
</candidate>
</row>
And I need to group all the elements with the name candidate, under a father node candidates, like this
<root>
<row>
<id>0001</id>
<type>credit</type>
<investment>1000</investment>
<ppr>0.83</ppr>
<candidates>
<candidate>
<id>5001</id>
<name>Hugo</name>
</candidate>
<candidate>
<id>5002</id>
<name>Jack</name>
</candidate>
<candidate>
<id>5005</id>
<name>Kate</name>
</candidate>
</candidates>
</row>
But here is my problem: I don't know the names that I can receive from the JSON
. So I need to do this comparison and complete the XML without knowing the "candidate" node name. I need this for any name that I can receive.
Also in this example the XML
only has 2 levels, but it can have any number of levels. I can iterate over the XML
without problem with this function:
public void findAllNodes(XmlNode node)
{
Console.WriteLine(node.Name);
foreach (XmlNode n in node.ChildNodes)
findAllNodes(n);
}
How can I make the comparison and group the nodes?
Upvotes: 0
Views: 102
Reputation: 163342
Here's an XSLT 2.0 solution:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:template match="*[*]">
<xsl:copy>
<xsl:for-each-group select="*" group-adjacent="node-name(.)">
<xsl:choose>
<xsl:when test="count(current-group()) > 1">
<xsl:element name="{name()}s" namespace="{namespace-uri()}">
<xsl:apply-templates select="current-group()"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match="*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row>
<id>0001</id>
<type>credit</type>
<investment>1000</investment>
<ppr>0.83</ppr>
<candidates>
<candidate>
<id>5001</id>
<name>Hugo</name>
</candidate>
<candidate>
<id>5002</id>
<name>Jack</name>
</candidate>
<candidate>
<id>5005</id>
<name>Kate</name>
</candidate>
</candidates>
</row>
</root>
Limitations
It doesn't handle mixed content (elements with children plus text content)
It drops attributes (easily fixed)
Upvotes: 0
Reputation: 26213
A fairly naive implementation could use LINQ to group elements by name and add a parent element for those that have more than 1 item in a group. This would be recursive, so child elements of an element were grouped until the tree was exhausted.
The naive-ness is that such a solution would break if there were mixed content elements, and it would group elements that weren't siblings (basically, both issues will result in things ending up in the wrong order). It should give you a good start, and could be enough for your purposes.
private static IEnumerable<XElement> GroupElements(IEnumerable<XElement> elements)
{
var elementsByName = elements.GroupBy(x => x.Name);
foreach (var grouping in elementsByName)
{
var transformed = grouping.Select(e =>
new XElement(e.Name,
GroupElements(e.Elements()),
e.Attributes(),
e.Nodes().OfType<XText>()));
if (grouping.Count() == 1)
{
yield return transformed.Single();
}
else
{
var groupName = grouping.Key + "s";
yield return new XElement(groupName, transformed);
}
}
}
You can use this by parsing/loading your existing XML and then transforming the root elements and creating a new document from those:
var original = XDocument.Parse(xml);
var grouped = new XDocument(GroupElements(original.Elements()));
See this fiddle for a working demo.
Upvotes: 2