Book HomeXSLSearch this book

7.2. The document() Function

We'll start with a couple of simple examples that use the document() function. We'll assume that we have several purchase orders and that we want to combine them into a single report document. One thing we can do is create a master document that references all the purchase orders we want to include in the report. Here's what that master document might look like:

<report>
  <title>Purchase Orders</title>
  <po filename="po38292.xml"/>
  <po filename="po38293.xml"/>
  <po filename="po38294.xml"/>
  <po filename="po38295.xml"/>
</report>

We'll fill in the details of our stylesheet as we go along, but here's what the shell of our stylesheet looks like:

<xsl:template match="/">
  <xsl:for-each select="/report/po">
    <xsl:apply-templates select="document(@filename)"/>
  </xsl:for-each>
</xsl:template>

In this template, we use the filename attribute as the argument to the document() function. The simplest thing we can do is open each purchase order, then write its details to the output stream. Here's a stylesheet that does this:

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

  <xsl:output method="html" indent="no"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="/">
    <html>
      <head>
        <title><xsl:value-of select="/report/title"/></title>
      </head>
      <body>
        <xsl:for-each select="/report/po">
          <xsl:apply-templates select="document(@filename)/purchase-order"/>
        </xsl:for-each>
      </body>
    </html>
  </xsl:template>

  <xsl:template match="purchase-order">
    <h1>
      <xsl:value-of select="customer/address[@type='business']/name/title"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="customer/address[@type='business']/name/first-name"/>
      <xsl:text> </xsl:text>
      <xsl:value-of select="customer/address[@type='business']/name/last-name"/>
    </h1>
    <p>
      <xsl:text>Ordered on </xsl:text>
      <xsl:value-of select="date/@month"/>
      <xsl:text>/</xsl:text>
      <xsl:value-of select="date/@day"/>
      <xsl:text>/</xsl:text>
      <xsl:value-of select="date/@year"/>
    </p>
    <h2>Items:</h2>
    <table width="100%" border="1" cols="55% 15% 15% 15%">
      <tr bgcolor="lightgreen">
        <th>Item</th>
        <th>Quantity</th>
        <th>Price Each</th>
        <th>Total</th>
      </tr>
      <xsl:for-each select="items/item">
        <tr>
          <xsl:attribute name="bgcolor">
            <xsl:choose>
              <xsl:when test="position() mod 2">
                <xsl:text>white</xsl:text>
              </xsl:when>
              <xsl:otherwise>
                <xsl:text>lightgreen</xsl:text>
              </xsl:otherwise>
            </xsl:choose>
          </xsl:attribute>
          <td>
            <b><xsl:value-of select="name"/></b>
            <xsl:text> (part #</xsl:text>
            <xsl:value-of select="@part_no"/>
            <xsl:text>)</xsl:text>
          </td>
          <td align="center">
            <xsl:value-of select="qty"/>
          </td>
          <td align="right">
            <xsl:value-of select="price"/>
          </td>
          <td align="right">
            <xsl:choose>
              <xsl:when test="position()=1">
                <xsl:value-of select="format-number(price * qty, '$#,###.00')"/>
              </xsl:when>
              <xsl:otherwise>
                <xsl:value-of select="format-number(price * qty, '#,###.00')"/>
              </xsl:otherwise>
            </xsl:choose>
          </td>
        </tr>
      </xsl:for-each>
      <tr>
        <td colspan="3" align="right">
          <b>Total:</b>
        </td>
        <td align="right">
          <xsl:variable name="orderTotal">
            <xsl:call-template name="sumItems">
              <xsl:with-param name="index" select="'1'"/>
              <xsl:with-param name="items" select="items"/>
              <xsl:with-param name="runningTotal" select="'0'"/>
            </xsl:call-template>
          </xsl:variable>
          <xsl:value-of select="format-number($orderTotal, '$#,###.00')"/>
        </td>
      </tr>
    </table>
  </xsl:template>

  <xsl:template name="sumItems">
    <xsl:param name="index" select="'1'"/>
    <xsl:param name="items"/>
    <xsl:param name="runningTotal" select="'0'"/>
    <xsl:variable name="currentItem">
      <xsl:value-of select="$items/item[$index]/qty * 
  $items/item[$index]/price"/>
    </xsl:variable>
    <xsl:variable name="remainingItems">
      <xsl:choose>
        <xsl:when test="$index=count($items/item)">
          <xsl:text>0</xsl:text>
        </xsl:when>
        <xsl:otherwise>
          <xsl:call-template name="sumItems">
            <xsl:with-param name="index" select="$index+1"/>
            <xsl:with-param name="items" select="$items"/>
            <xsl:with-param name="runningTotal" 
              select="$runningTotal+$currentItem"/>
          </xsl:call-template>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:value-of select="$currentItem+$remainingItems"/>
  </xsl:template>

</xsl:stylesheet>

When we process our master document with this stylesheet, the results look like Figure 7-1.

Figure 7-1

Figure 7-1. Document generated from multiple input files

The most notable thing about our results is that we've been able to generate a document that contains the contents of several other documents. To keep our example short, we've only combined four purchase orders, but there's no limit (beyond the physical limits of our machine) to the number of documents we could combine. Best of all, we didn't have to modify any of the individual purchase orders to generate our report.

7.2.1. An Aside: Doing Math with Recursion

While we're here, we'll also mention the recursive technique we used to calculate the total for each purchase order. At first glance, this seems like a perfect opportunity to use the sum() function. We want to add the total of the price of each item multiplied by its quantity. We could try to invoke the sum() function like this:

<xsl:value-of select="sum(item/qty*item/price)"/>

Unfortunately, the sum() function simply takes the node-set passed to it, converts each item in the node-set to a number, then returns the sum of all of those numbers. The expression item/qty*item/price, while a perfectly valid XPath expression, isn't a valid node-set. With that in mind, we have to create a recursive <xsl:template> to do the work for us. There are a couple of techniques worth mentioning here; we'll go through them in the order we used them in our stylesheet.

7.2.1.1. Recursive design

First, we pass three parameters to the template:

items
The node-set of all <item> elements in the current <items> element.

index
The position in that node-set of the <item> we're currently processing.

runningTotal
The total of all the <item>s we've processed so far.

Our basic design is as follows:

  • Calculate the total for the current <item>. This total is the value of the <qty> element multiplied by the value of the <price> element. We store this value in the variable currentItem:

    <xsl:variable name="currentItem">
      <xsl:value-of select="$items/item[$index]/qty * 
        $items/item[$index]/price"/>
    </xsl:variable>

    Notice how the XPath expression in the select attribute uses the $items and $index parameters to find the exact items we're looking for.

  • Calculate the total for the remaining items. If this is the last item (the parameter $index is equal to the number of <item> elements), then the total for the remaining items is zero. Otherwise, the total for the remaining items is returned by calling the template again.

    When we call the template again, we increment the position of the current item:

    <xsl:with-param name="index" select="$index+1"/>

    We also update the parameter $runningTotal, which is equal to the value of the current item plus the previous value of $runningTotal:

    <xsl:with-param name="runningTotal" 
      select="$runningTotal+$currentItem"/>

This recursive design lets us generate the totals we need for our purchase order. Our approach is equivalent to invoking a function against each node in a node-set, only this approach doesn't require us to use extensions. As a result, we can use this stylesheet with any standards-compliant stylesheet processor, without having to worry about porting any extension functions or extension elements.

7.2.1.2. Generating output to initialize a variable

When we needed to set the value of the variable runningTotal, we simply called the template named sumItems. The sumItems template uses the <xsl:value-of> element to output some text; everything output by the template is concatenated to form the value of the variable runningTotal. The advantage of this technique is that it allows us to completely control the value of the variable, and it allows us to avoid converting the variable to a number until we're ready. Once the sumItems template finishes its work, we can pass the variable's value to the format-number() function to print the invoice total exactly how we want.

7.2.1.3. Using format-number() to control output

The final nicety in our stylesheet is that we use the XSLT format-number() function to display the total for the current purchase order. We've already discussed how we set the value of the variable $orderTotal to be the output of the template named sumItems; once the variable is set, we use format-number to display it with a currency sign, commas, and two decimal places:

<xsl:value-of select="format-number($order-total, '$#,###.00')"/>


Library Navigation Links

Copyright © 2002 O'Reilly & Associates. All rights reserved.