Reputation: 128
I'm currently trying to flatten a xml structure to display in a simple table. The basic problem is, that the xml contains repeating nodes on various levels - each combination of the extracted nodes should result in a separate output node.
The xml document looks like this:
<customer>
<name>Mustermann</name>
<contract>
<contract_id>C1</contract_id>
<products>
<product>
<name>Product C1.P1</name>
<price>23.12</price>
<properties>
<property>Property C1.P1.A</property>
<property>Property C1.P1.B</property>
</properties>
</product>
<product>
<name>Product C1.P2</name>
<price>2.32</price>
</product>
</products>
</contract>
<contract>
<contract_id>C2</contract_id>
<products>
<product>
<name>Product C2.P1</name>
<price>143.33</price>
</product>
<product>
<name>Product C2.P2</name>
<price>231.76</price>
<properties>
<property>Property C2.P2.A</property>
<property>Property C2.P2.B</property>
</properties>
</product>
</products>
</contract>
<contract>
<contract_id>C3</contract_id>
</contract>
</customer>
So a contract does not need to have products, a product does not need to have properties.
The result should flatten the repeating nodes and form a separate result node for each set of extracted nodes (which of course introduces some redundancy in the result).
The output should look like this:
<output>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C1</contract_id>
<product_name>Product C1.P1</product_name>
<product_price>23.12</product_price>
<product_property>Property C1.P1.A</product_property>
</data>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C1</contract_id>
<product_name>Product C1.P1</product_name>
<product_price>23.12</product_price>
<product_property>Property C1.P1.B</product_property>
</data>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C1</contract_id>
<product_name>Product C1.P2</product_name>
<product_price>2.32</product_price>
<product_property/>
</data>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C2</contract_id>
<product_name>Product C2.P1</product_name>
<product_price>143.33</product_price>
<product_property/>
</data>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C2</contract_id>
<product_name>Product C2.P2</product_name>
<product_price>231.76</product_price>
<product_property>Property C2.P2.A</product_property>
</data>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C2</contract_id>
<product_name>Product C2.P2</product_name>
<product_price>231.76</product_price>
<product_property>Property C2.P2.B</product_property>
</data>
<data>
<customer_name>Mustermann</customer_name>
<contract_id>C3</contract_id>
<product_name/>
<product_price/>
<product_property/>
</data>
</output>
I tried the following xslt stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/customer">
<output>
<xsl:for-each select="contract">
<xsl:choose>
<xsl:when test="products/product">
<xsl:for-each select="products/product">
<xsl:choose>
<xsl:when test="properties/property">
<xsl:for-each select="properties/property">
<xsl:call-template name="data">
<xsl:with-param name="customer_name" select="/customer/name"/>
<xsl:with-param name="contract_id" select="../../../../contract_id"/>
<xsl:with-param name="product_name" select="../../name"/>
<xsl:with-param name="product_price" select="../../price"/>
<xsl:with-param name="property" select="."/>
</xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="data">
<xsl:with-param name="customer_name" select="/customer/name"/>
<xsl:with-param name="contract_id" select="../../contract_id"/>
<xsl:with-param name="product_name" select="name"/>
<xsl:with-param name="product_price" select="price"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="data">
<xsl:with-param name="customer_name" select="/customer/name"/>
<xsl:with-param name="contract_id" select="contract_id"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</output>
</xsl:template>
<xsl:template name="data">
<xsl:param name="customer_name"/>
<xsl:param name="contract_id"/>
<xsl:param name="product_name"/>
<xsl:param name="product_price"/>
<xsl:param name="property"/>
<data>
<customer_name><xsl:value-of select="$customer_name"/></customer_name>
<contract_id><xsl:value-of select="$contract_id"/></contract_id>
<product_name><xsl:value-of select="$product_name"/></product_name>
<product_price><xsl:value-of select="$product_price"/></product_price>
<product_property><xsl:value-of select="$property"/></product_property>
</data>
</xsl:template>
</xsl:stylesheet>
This produces the desired result, but to me, this looks rather ugly. The naive approach to just do a nested loop (loop over contract, loop over products inside, and so on) does not work, because it does not pick up all output nodes (e.g. a contract without product is ommited).
Background: I'd like to use the xslt stylesheet in a oracle xmltable query, and the intermediate result will be mapped to a simple row/column output. Therefor each result needs to be in its own row.
Any help is appreciated!
Upvotes: 2
Views: 1158
Reputation: 116959
This produces the desired result, but to me, this looks rather ugly.
Well, it's an ugly problem. Is the result really useful for anything? I would have thought a separate table (and hence a separate stylesheet) for each level would be more practical.
Still, if that's the result you want, you may try a more elegant way to produce it:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/customer">
<output>
<xsl:apply-templates/>
</output>
</xsl:template>
<xsl:template match="contract[not(products)] | product[not(properties)] | property">
<data>
<customer_name><xsl:value-of select="ancestor::customer/name"/></customer_name>
<contract_id><xsl:value-of select="ancestor-or-self::contract/contract_id"/></contract_id>
<product_name><xsl:value-of select="ancestor-or-self::product/name"/></product_name>
<product_price><xsl:value-of select="ancestor-or-self::product/price"/></product_price>
<product_property><xsl:value-of select="self::property"/></product_property>
</data>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
Upvotes: 1