xmlxsltmergemuenchian-groupingapply-templates

XSLT 1.0 conditional merge after grouping


My input xml looks as shown below:

<items>
    <item>
        <geoDetails>
            <street>xxx</street>
            <city>yyy</city>
            <state>zzz</state>
        </geoDetails>
        <otherDetails>
            <desc>abcd111</desc>
            <comments>good item</comments>
        </otherDetails>
        <key>
            <name>item1</name>
            <id>123</id>
            <color>red</color>
        </key>
        <misc>
            <available>false</available>
            <details>geo</details>
        </misc>
    </item>
    <item>
        <otherDetails>
            <desc>efgh222</desc>
            <comments>good item</comments>
        </otherDetails>
        <key>
            <name>item2</name>
            <id>123</id>
            <color>red</color>
        </key>
        <misc>
            <available>false</available>
            <details>other</details>
        </misc>
    </item>
    <item>
        <geoDetails>
            <street>ppp</street>
            <city>qqq</city>
            <state>rrr</state>
        </geoDetails>
        <otherDetails>
            <desc>ijkl333</desc>
            <comments>best item</comments>
        </otherDetails>
        <key>
            <name>item3</name>
            <id>456</id>
            <color>blue</color>
        </key>
        <misc>
            <available>false</available>
            <details>other</details>
        </misc>
    </item>
</items>

The key for grouping 'item' nodes is concat(/items/item/key/id,/items/item/key/color) For each identified group, merge should happen with the logic as given below:

a. Extract 'geoDetails' from the 'item' where misc/details is 'geo'.

b. Extract 'otherDetails' from the 'item' where misc/details is 'other'. Only one misc/details is present in each 'item' with the value of either 'geo' or 'other'.

c. Extract the 'key' element from the first 'item' in a given group.

d. The 'misc' element should contain 'available' element as it is (this is always 'false' and hence populating this once is enough) and the 'details' element should have value based on the element 'geoDetails' or 'otherDetails' populated through the logic as given in steps a & b above. There should be 2 'details' elements if both the 'geoDetails' and 'otherDetails' are populated.

e. Any other single 'item' element that is which is not part of a group should be pushed to output as it is.

And the expected output based on above logic is as shown below:

<items>
<item>
    <geoDetails>
        <street>xxx</street>
        <city>yyy</city>
        <state>zzz</state>
    </geoDetails>
    <otherDetails>
        <desc>efgh222</desc>
        <comments>good item</comments>
    </otherDetails>
    <key>
        <name>item1</name>
        <id>123</id>
        <color>red</color>
    </key>
    <misc>
        <available>false</available>
        <details>geo</details>
        <details>other</details>
    </misc>
</item>
<item>
    <geoDetails>
        <street>ppp</street>
        <city>qqq</city>
        <state>rrr</state>
    </geoDetails>
    <otherDetails>
        <desc>ijkl333</desc>
        <comments>best item</comments>
    </otherDetails>
    <key>
        <name>item3</name>
        <id>456</id>
        <color>blue</color>
    </key>
    <misc>
        <available>false</available>
        <details>other</details>
    </misc>
</item>

I tried the transformation based on Muenchian grouping using xsl:key and apply-templates. I was able to group the 'item' elements but couldn't proceed further on how to merge these grouped elements based on the above set of conditions.

XSLT Transformation:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common" version="1.0" exclude-result-prefixes="exsl">
<xsl:key match="item" name="itemKey" use="concat(key/id,key/color)" />
<xsl:template match="/">
    <xsl:variable name="output">
        <xsl:apply-templates />
    </xsl:variable>
    <xsl:copy-of select="exsl:node-set($output)/*" />
</xsl:template>
<!-- default template -->
<xsl:template match="node( ) | @*">
    <xsl:copy>
        <xsl:apply-templates select="@*" />
        <xsl:apply-templates />
    </xsl:copy>
</xsl:template>
<xsl:template match="item[generate-id(.)=generate-id(key('itemKey',concat(key/id,key/color))[1])]">
    <xsl:copy>
        <xsl:apply-templates select="@* | key('itemKey',concat(key/id,key/color))/node()" />
    </xsl:copy>
</xsl:template>
<xsl:template match="item" />

I've gone through several related questions on Stackoverflow but couldn't adapt the merging process to my current scenario. Any help is greatly appreciated.


Solution

  • AFAICT, this satisfies your conditions (as far as I was able to understand them):

    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:key name="itemKey" match="item" use="concat(key/id, key/color)" />
    
    <xsl:template match="/items">
        <xsl:copy>
            <xsl:apply-templates select="item[generate-id()=generate-id(key('itemKey', concat(key/id, key/color))[1])]"/>
        </xsl:copy>
    </xsl:template>
    
    <xsl:template match="item">
        <xsl:variable name="curr-group" select="key('itemKey', concat(key/id, key/color))" />
        <xsl:choose>
            <!-- e. Any other single 'item' element that is which is not part of a group should be pushed to output as it is. -->
            <xsl:when test="count($curr-group)=1">
                <xsl:copy-of select="."/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:copy>
                    <!-- a. Extract 'geoDetails' from the 'item' where misc/details is 'geo'. --> 
                    <xsl:copy-of select="$curr-group[misc/details='geo']/geoDetails"/>
                    <!-- b. Extract 'otherDetails' from the 'item' where misc/details is 'other'. --> 
                    <xsl:copy-of select="$curr-group[misc/details='other']/otherDetails"/>
                    <!-- c. Extract the 'key' element from the first 'item' in a given group. -->
                    <xsl:copy-of select="key"/>
                    <!-- d. ??? --> 
                    <misc>
                        <available>false</available>
                        <xsl:copy-of select="$curr-group/misc/details"/>
                    </misc>
                </xsl:copy>
            </xsl:otherwise>
        </xsl:choose>   
    </xsl:template>
    
    </xsl:stylesheet>