Search code examples
xmltemplatesparameter-passingxslt-1.0java-6

XSLT 1.0 - Running a template on the result of another template


I wonder how it is done by XSLT 1.0 a transformation that passes the result of one template as an input for another within a single .xslt file:

Raw XML Input > call-template(1) > re-formatted XML > call-template(2) on the re-formatted XML

Case:

I want to write a template to re-arrange an xml so that the attributes become elements; and then run another template on the resulting xml of the first one to remove the duplicates. I can have two xsl files and pass the result of the first transformation to the second. However, I want to do it in one xslt.

The raw input xml is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <resource>
        <properties>
            <property name="name" value="Foo"/>
            <property name="service" value="Bar"/>
            <property name="version" value="1"/>
        </properties>
    </resource>
    <resource>
        <properties>
            <property name="name" value="Foo"/>
            <property name="service" value="Bar"/>
            <property name="version" value="2"/>
        </properties>
    </resource>
    <resource>
        <properties>
            <property name="name" value="AnotherFoo"/>
            <property name="service" value="AnotherBar"/>
            <property name="version" value="1"/>
        </properties>a
    </resource>
</resources>

When I apply the following xslt:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"
    exclude-result-prefixes="msxsl">

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

    <xsl:template name="attributes-to-elements" match="/resources">
        <xsl:call-template name="get-resources" />
    </xsl:template>

    <xsl:template name="get-resources">
        <xsl:text>&#xa;</xsl:text>
        <products>
            <xsl:for-each select="resource">
                <xsl:call-template name="convert-attributes-to-elements" />
            </xsl:for-each>
        </products>
    </xsl:template>

    <xsl:template name="convert-attributes-to-elements">
        <product>
            <name>
                <xsl:value-of select="properties/property[@name='name']/@value" />
            </name>
            <service>
                <xsl:value-of select="properties/property[@name='service']/@value" />
            </service>
        </product>
    </xsl:template>
</xsl:stylesheet>

I am able to re-format the xml without the versions and get the output which does not contain versions and the attributes has become elements:

<?xml version="1.0" encoding="UTF-8"?>
<products>
    <product>
        <name>Foo</name>
        <service>Bar</service>
    </product>
    <product>
        <name>Foo</name>
        <service>Bar</service>
    </product>
    <product>
        <name>AnotherFoo</name>
        <service>AnotherBar</service>
    </product>
</products>

Now, the thing I want is to pass this modified xml to some template as the input xml and remove the duplicates. Finally, I want to get an xml like:

<?xml version="1.0" encoding="UTF-8"?>
<products>
    <product>
        <name>Foo</name>
        <service>Bar</service>
    </product>
    <product>
        <name>AnotherFoo</name>
        <service>AnotherBar</service>
    </product>
</products>

Solution

  • I would use a mode to separate processing steps, and of course in XSLT 1.0 you also need an extension function like exsl:node-set or msxsl:node-set to be able to further process a result tree fragment created in another template:

    <?xml version="1.0" encoding="utf-8"?>
    <xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt"
        xmlns:exsl="http://exslt.org/common"
        exclude-result-prefixes="msxsl exsl">
    
        <xsl:output method="xml" indent="yes" />
    
        <xsl:key name="group" match="product" use="concat(name, '|', service)"/>
    
        <xsl:template match="@* | node()" mode="step2">
          <xsl:copy>
            <xsl:apply-templates select="@* | node()" mode="step2"/>
          </xsl:copy>
        </xsl:template>
    
        <xsl:template match="product[not(generate-id() = generate-id(key('group', concat(name, '|', service))[1]))]" mode="step2"/>
    
        <xsl:template name="attributes-to-elements" match="/resources">
            <xsl:variable name="step1-rtf">
              <xsl:call-template name="get-resources" />
            </xsl:variable>
            <xsl:apply-templates select="exsl:node-set($step1-rtf)/*" mode="step2"/>
        </xsl:template>
    
        <xsl:template name="get-resources">
            <xsl:text>&#xa;</xsl:text>
            <products>
                <xsl:for-each select="resource">
                    <xsl:call-template name="convert-attributes-to-elements" />
                </xsl:for-each>
            </products>
        </xsl:template>
    
        <xsl:template name="convert-attributes-to-elements">
            <product>
                <name>
                    <xsl:value-of select="properties/property[@name='name']/@value" />
                </name>
                <service>
                    <xsl:value-of select="properties/property[@name='service']/@value" />
                </service>
            </product>
        </xsl:template>
    </xsl:stylesheet>
    

    You will need to check whether your XSLT processor supports exsl:node-set, MSXML needs msxsl:node-set instead.