Search code examples
xmlxsltxsl-fomuenchian-grouping

How to count elements returned from applied template using XSL-FO (and Apache FOP)


I would like to do something similar to this but using XSL-FO and Apache FOP.

I have xml input like this (exactly like in the linked question):

<Results>
    <Result ID="0">
        <SerialNumber>3333</SerialNumber>
        <Status>Fail</Status>
        <Date>21</Date>
    </Result>
    <Result ID="1">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>34</Date>
    </Result>
    <Result ID="2">
        <SerialNumber>1111</SerialNumber>
        <Status>Pass</Status>
        <Date>67</Date>
    </Result>
    <Result ID="3">
        <SerialNumber>2222</SerialNumber>
        <Status>Fail</Status>
        <Date>40</Date>
    </Result>
    <Result ID="4">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>55</Date>
    </Result>
    <Result ID="5">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>88</Date>
    </Result>
    <Result ID="6">
        <SerialNumber>2222</SerialNumber>
        <Status>Fail</Status>
        <Date>22</Date>
    </Result>
    <Result ID="7">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>86</Date>
    </Result>
    <Result ID="8">
        <SerialNumber>3333</SerialNumber>
        <Status>Pass</Status>
        <Date>99</Date>
    </Result>
</Results>

I would like to create XSL file which will generate XSL-FO to generate PDF (using Apache FOP) in which I will display the following text:

Total Quantity: 3
Passed: 1
Failed: 2

Those numbers are:

  • Total Quantity - number of unique serial numbers (in this case: 1111, 2222 and 3333),
  • Passed - number of passed results but counting only the latest result (the highest Date) per unique serial number (in this case only 3333 SerialNumber with Date 99),
  • Failed - number of failed results but counting only the latest result (the highest Date) per unique serial number (in this case Date 88 for 1111 and Date 40 for 2222).

In another words I need to count number of results only for the latest Date per SerialNumber. Results are not sorted.

I tried solution suggested by michael.hor257k (which works when I use just xslt to generate html in my browser):

<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:key name="result-by-sn" match="Result" use="SerialNumber" />

<xsl:template match="/Results">
    <xsl:variable name="temp">
        <xsl:for-each select="Result[count(. | key('result-by-sn', SerialNumber)[1]) = 1]">
            <xsl:for-each select="key('result-by-sn', SerialNumber)">
                <xsl:sort select="Date" order="descending"/>
                <xsl:if test="position()=1 and Status='Fail'">x</xsl:if>
            </xsl:for-each>
        </xsl:for-each>
    </xsl:variable>
    <output>
        <xsl:value-of select="string-length($temp)"/>
    </output>
</xsl:template>

</xsl:stylesheet>

But Apache FOP returns Unknown formatting object "{}output" encountered error. How to deal with this error and display my results summary?


EDIT:

Here is my current xsl file:

<?xml version="1.0" encoding="UTF-8"?><xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">

<!-- KEY FOR FINDING UUT RESULTS -->
<xsl:key name="result-by-sn" match="Results/Result" use="SerialNumber"/>

    <xsl:template match="/">
        <fo:root>
            <fo:layout-master-set>
                <fo:simple-page-master master-name="my_page" margin="0.5in">
                    <fo:region-body/>
                </fo:simple-page-master>
            </fo:layout-master-set>
            <fo:page-sequence master-reference="my_page">
                <fo:flow flow-name="xsl-region-body">
                    <fo:block>Total Quantity: <xsl:value-of select="count(Results/Result[generate-id() = generate-id(key('result-by-sn', SerialNumber)[1])])"/></fo:block>
                    <fo:block>Passed: <!--<xsl:apply-templates select="Results" mode="count"><xsl:with-param name="status" select="'Pass'"/></xsl:apply-templates>--></fo:block>
                    <fo:block>Failed: <!--<xsl:apply-templates select="Results" mode="count"><xsl:with-param name="status" select="'Fail'"/></xsl:apply-templates>--></fo:block>
                </fo:flow>
            </fo:page-sequence>
        </fo:root>
    </xsl:template>

<!-- TEMPLATE TO COUNT RESULTS -->
<!--<xsl:template match="Results" mode="count">
    <xsl:param name="status" select="'Pass'"/>
    <xsl:variable name="temp">
        <xsl:for-each select="Result[generate-id()=generate-id(key('result-by-sn', SerialNumber)[1])]">
            <xsl:for-each select="key('result-by-sn', SerialNumber)">
                <xsl:sort select="Date" order="descending"/>
                <xsl:if test="position() = 1 and  Status = $status">x</xsl:if>
            </xsl:for-each>
        </xsl:for-each>
    </xsl:variable>
    <output>
        <xsl:value-of select="string-length($temp)"/>
    </output>
</xsl:template>-->

</xsl:stylesheet>

Solution

  • So, assuming you want your XSL transformation to produce a result of:

    <?xml version="1.0" encoding="UTF-8"?>
    <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
      <fo:layout-master-set>
        <fo:simple-page-master master-name="my_page" margin="0.5in">
          <fo:region-body/>
        </fo:simple-page-master>
      </fo:layout-master-set>
      <fo:page-sequence master-reference="my_page">
        <fo:flow flow-name="xsl-region-body">
          <fo:block>Total Quantity: 3</fo:block>
          <fo:block>Passed: 1</fo:block>
          <fo:block>Failed: 2</fo:block>
        </fo:flow>
      </fo:page-sequence>
    </fo:root>
    

    you can use the following stylesheet:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:fo="http://www.w3.org/1999/XSL/Format">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="result-by-sn" match="Result" use="SerialNumber" />
    
    <xsl:template match="/Results">
        <!-- determine counts -->
        <xsl:variable name="distinct-results" select="Result[count(. | key('result-by-sn', SerialNumber)[1]) = 1]" />
        <xsl:variable name="count-distinct" select="count($distinct-results)" />
        <xsl:variable name="fails">
            <xsl:for-each select="$distinct-results">
                <xsl:for-each select="key('result-by-sn', SerialNumber)">
                    <xsl:sort select="Date" order="descending"/>
                    <xsl:if test="position()=1 and Status='Fail'">F</xsl:if>
                </xsl:for-each>
            </xsl:for-each>
        </xsl:variable>
        <xsl:variable name="count-fails" select="string-length($fails)" />
        <!-- output -->
        <fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
            <fo:layout-master-set>
                <fo:simple-page-master master-name="my_page" margin="0.5in">
                    <fo:region-body/>
                </fo:simple-page-master>
            </fo:layout-master-set>
            <fo:page-sequence master-reference="my_page">
                <fo:flow flow-name="xsl-region-body">
                    <fo:block>
                        <xsl:text>Total Quantity: </xsl:text>
                        <xsl:value-of select="$count-distinct"/>
                    </fo:block>
                    <fo:block>
                        <xsl:text>Passed: </xsl:text>
                        <xsl:value-of select="$count-distinct - $count-fails"/>
                    </fo:block>
                    <fo:block>
                        <xsl:text>Failed: </xsl:text>
                        <xsl:value-of select="$count-fails"/>
                    </fo:block>
                </fo:flow>
            </fo:page-sequence>
        </fo:root>
    </xsl:template>
    
    </xsl:stylesheet>