Reputation: 29739
I need to transform an XML file with XSLT, and this task is kinda tricky.
I have attributes with the name attr_1000_a
whereas the number and the suffix are dynamic, so that attr_2000_b
is valid too.
Further, there are <row>
elements which combine related data. I need to transform them so that equally numbered attributes (i.e. attr_1000_a
and attr_1000_b
) are put into the same element.
Let me give you an example. Following input XML:
<root>
<row id="1">
<foo attr_1000_a="true">1</foo>
<foo attr_1000_b="true">2</foo>
<foo attr_1000_c="true">3</foo>
</row>
<row id="2">
<foo attr_1000_a="true" attr_1000_b="true" attr_1000_c="true">10</foo>
<foo attr_2000_a="true" attr_2000_b="true" attr_2000_c="true">20</foo>
</row>
<row id="3">
<foo attr_1000_a="true" attr_2000_a="true" attr_3000_a="true">100</foo>
<foo attr_1000_b="true" attr_2000_b="true" attr_3000_b="true">200</foo>
<foo attr_1000_c="true" attr_2000_c="true" attr_3000_c="true">300</foo>
</row>
</root>
You can see that the attributes can be combined in several ways, which makes the transformation difficult. Each attribute is unique in each <row>
but can be located in any <foo>
element. Also, each <foo>
can have an arbitrary number of attributes.
The desired result:
<result>
<row id="1">
<field attr="1000">
<a>1</a>
<b>2</b>
<c>3</c>
</field>
</row>
<row id="2">
<field attr="1000">
<a>10</a>
<b>10</b>
<c>10</c>
</field>
<field attr="2000">
<a>20</a>
<b>20</b>
<c>20</c>
</field>
</row>
<row id="3">
<field attr="1000">
<a>100</a>
<b>200</b>
<c>300</c>
</field>
<field attr="2000">
<a>100</a>
<b>200</b>
<c>300</c>
</field>
<field attr="3000">
<a>100</a>
<b>200</b>
<c>300</c>
</field>
</row>
</result>
I think i have to somehow get a list of all numbers in a row (for example 1000, 2000 and 3000) and then iterate through all elements that have such an attriute.
How can i do this using XSLT? Is this even possible?
Upvotes: 1
Views: 1496
Reputation: 823
Quick and Dirty, this xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output indent="yes"/>
<xsl:template match="root">
<result>
<xsl:apply-templates select="@*|node()"/>
</result>
</xsl:template>
<xsl:template match="foo/@*">
<xsl:element name="{substring-after(local-name(),'000_')}">
<xsl:value-of select=".."/>
</xsl:element>
</xsl:template>
<xsl:template match="row">
<row id="{@id}">
<xsl:for-each-group select="foo/@*" group-by="substring(local-name(),1,9)">
<field attr="{substring-after(current-grouping-key(),'attr_')}">
<xsl:apply-templates select="current-group()"/>
</field>
</xsl:for-each-group>
</row>
</xsl:template>
<xsl:template match="foo">
<xsl:apply-templates select="@*"/>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
applied to this input
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row id="1">
<foo attr_1000_a="true">1</foo>
<foo attr_1000_b="true">2</foo>
<foo attr_1000_c="true">3</foo>
</row>
<row id="2">
<foo attr_1000_a="true" attr_1000_b="true" attr_1000_c="true">10</foo>
<foo attr_2000_a="true" attr_2000_b="true" attr_2000_c="true">20</foo>
</row>
<row id="3">
<foo attr_1000_a="true" attr_2000_a="true" attr_3000_a="true">100</foo>
<foo attr_1000_b="true" attr_2000_b="true" attr_3000_b="true">200</foo>
<foo attr_1000_c="true" attr_2000_c="true" attr_3000_c="true">300</foo>
</row>
</root>
yields this result
<?xml version="1.0" encoding="UTF-8"?>
<result>
<row id="1">
<field attr="1000">
<a>1</a>
<b>2</b>
<c>3</c>
</field>
</row>
<row id="2">
<field attr="1000">
<a>10</a>
<b>10</b>
<c>10</c>
</field>
<field attr="2000">
<a>20</a>
<b>20</b>
<c>20</c>
</field>
</row>
<row id="3">
<field attr="1000">
<a>100</a>
<b>200</b>
<c>300</c>
</field>
<field attr="2000">
<a>100</a>
<b>200</b>
<c>300</c>
</field>
<field attr="3000">
<a>100</a>
<b>200</b>
<c>300</c>
</field>
</row>
</result>
the magic is in
<xsl:element name="{substring-after(local-name(),'000_')}">
<xsl:value-of select=".."/>
</xsl:element>
this creates the a/b/c elements with a dynamic name and travels up one node to get the value from the parent node (we're currently in the attribute).
and in
<xsl:for-each-group select="foo/@*" group-by="substring(local-name(),1,9)">
<field attr="{substring-after(current-grouping-key(),'attr_')}">
<xsl:apply-templates select="current-group()"/>
</field>
</xsl:for-each-group>
which regroups all attributes (foo/@*
) using part of their name (substring(local-name(),1,9)
). The first are subsequently available as current-group()
, the latter as current-grouping-key()
, as you can see.
Upvotes: 3
Reputation: 167696
Here is a sample stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:key name="k1"
match="row/foo/@*"
use="concat(generate-id(../..), '|', substring-before(substring-after(local-name(), '_'), '_'))"/>
<xsl:template match="root">
<result>
<xsl:apply-templates/>
</result>
</xsl:template>
<xsl:template match="row">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:apply-templates select="foo/@*[generate-id() = generate-id(key('k1', concat(generate-id(../..), '|', substring-before(substring-after(local-name(), '_'), '_')))[1])]" mode="field"/>
</xsl:copy>
</xsl:template>
<xsl:template match="foo/@*" mode="field">
<field attr="{substring-before(substring-after(local-name(), '_'), '_')}">
<xsl:apply-templates select="key('k1', concat(generate-id(../..), '|', substring-before(substring-after(local-name(), '_'), '_')))"/>
</field>
</xsl:template>
<xsl:template match="foo/@*">
<xsl:element name="{substring-after(substring-after(local-name(), '_'), '_')}">
<xsl:value-of select=".."/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
Upvotes: 3