Search code examples
xmlxsltxslt-1.0xalan

Select distinct nodes and total amount from xml using xslt 1.0


I have XML, which should be transformed by XSLT 1.0. XML folder "Fields" defines names order for every "Row" element. So, MaterialCode in every "Row" folder has first position,StorageMatCode is second and "Amount" is third. I need remove all "MaterialCode" duplicates, but put all "Amounts" in one. Input xml:

<Response xmlns="http://www.sample.ru/sample/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Header>
    <ObjectType>StorageMats</ObjectType>
    <Version>1.0.0</Version>
    <Fields>
        <Field type="decimal">MaterialCode</Field>
        <Field type="decimal">StorageMatCode</Field>
        <Field type="decimal">Amount</Field>
    </Fields>
</Header>
<Body>
    <Row>
        <FieldValue>475625947</FieldValue>
        <FieldValue>456789</FieldValue>
        <FieldValue>1000</FieldValue>
    </Row>
    <Row>
        <FieldValue>804685387</FieldValue>
        <FieldValue>273456</FieldValue>
        <FieldValue>3047</FieldValue>
    </Row>
    <Row>
        <FieldValue>973681347</FieldValue>
        <FieldValue>578357</FieldValue>
        <FieldValue>2037</FieldValue>
    </Row>
    <Row>
        <FieldValue>804685387</FieldValue>
        <FieldValue>273456</FieldValue>
        <FieldValue>5000</FieldValue>
    </Row>
</Body>
</Response>

I want to get this XML:

  <?xml version="1.0" encoding="UTF-8"?>
<BDStorageMats xmlns="http://www.sample.ru/sample/BDStorageMats/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <BDStorageMat>
        <MaterialCode>475625947</MaterialCode>
        <Amount>1000</Amount>
    </BDStorageMat>
    <BDStorageMat>
        <MaterialCode>804685387</MaterialCode>
        <Amount>8047</Amount>
    </BDStorageMat>
    <BDStorageMat>
        <MaterialCode>973681347</MaterialCode>
        <Amount>2037</Amount>
    </BDStorageMat>
</BDStorageMats>

I've created this XSLT:

<xsl:stylesheet version="1.0" xmlns="http://www.sample.ru/sample/BDStorageMats/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" indent="yes" method="xml" version="1.0"/>
<xsl:key match="*[local-name()= 'Response']/*[local-name()= 'Body']/*[local-name()= 'Row']" name="codeDistinct" use="*[local-name()= 'FieldValue'][count(*[local-name()= 'Response']/*[local-name()= 'Header']/*[local-name()= 'Fields']/*[local-name()= 'Field'][.='MaterialCode']/preceding-sibling::*)+1]"/>
<xsl:template match="/">
    <BDStorageMats>
        <xsl:variable name="amountPosition" select="count(*[local-name()= 'Response']/*[local-name()= 'Header']/*[local-name()= 'Fields']/*[local-name()= 'Field'][.='Amount']/preceding-sibling::*)+1"/>
        <xsl:variable name="materialCodePosition" select="count(*[local-name()= 'Response']/*[local-name()= 'Header']/*[local-name()= 'Fields']/*[local-name()= 'Field'][.='MaterialCode']/preceding-sibling::*)+1"/>
        <xsl:for-each select="*[local-name()= 'Response']/*[local-name()= 'Body']/*[local-name()= 'Row'][generate-id() = generate-id(key('codeDistinct', *[local-name()= 'FieldValue'][count(*[local-name()= 'Response']/*[local-name()= 'Header']/*[local-name()= 'Fields']/*[local-name()= 'Field'][.='MaterialCode']/preceding-sibling::*)+1]))[1]]">
            <xsl:variable name="keyGroup" select="key('codeDistinct', *[local-name()= 'FieldValue'][count(*[local-name()= 'Response']/*[local-name()= 'Header']/*[local-name()= 'Fields']/*[local-name()= 'Field'][.='MaterialCode']/preceding-sibling::*)+1])"/>
            <BDStorageMat>
                <MaterialCode>
                    <xsl:value-of select="(*[local-name()= 'FieldValue'])[$materialCodePosition]"/>
                </MaterialCode>
                <Amount>
                    <xsl:value-of select="sum($keyGroup/(*[local-name()= 'FieldValue'])[$amountPosition])"/>
                </Amount>
            </BDStorageMat>
        </xsl:for-each>
    </BDStorageMats>
</xsl:template>
</xsl:stylesheet>

And it works fine in Altova, but my system uses Apache Xalan Processor for XSLT and it refuses this line from XSLT:

<Amount>
   <xsl:value-of select="sum($keyGroup/(*[local-name()= 'FieldValue']) [$amountPosition])"/>
</Amount>

Is there any other way to do what i want via XSLT 1.0?


Solution

  • If you're using Xalan (or another processor that supports the EXSLT set:distinct() extension function), you could do:

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:ns1="http://www.sample.ru/sample/"
    xmlns:set="http://exslt.org/sets"
    exclude-result-prefixes="ns1 set">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <!-- identity transform -->
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    
    <xsl:key name="row" match="ns1:Row" use="ns1:FieldValue[1]" />
    
    <xsl:template match="/ns1:Response">
        <BDStorageMats xmlns="http://www.sample.ru/sample/BDStorageMats/1.0">
            <xsl:for-each select="set:distinct(ns1:Body/ns1:Row/ns1:FieldValue[1])">
                <BDStorageMat>
                    <MaterialCode>
                        <xsl:value-of select="." />
                    </MaterialCode>
                    <Amount>
                        <xsl:value-of select="sum(key('row', .)/ns1:FieldValue[3])" />
                    </Amount>
                </BDStorageMat>
            </xsl:for-each>
        </BDStorageMats>
    </xsl:template>
    
    </xsl:stylesheet>
    

    Note the use of a prefix to select the nodes in the input XML correctly.