page 121 po filename=po38293.xml
po filename=po38294.xml po filename=po38295.xml
report
Well fill in the details of our stylesheet as we go along, but heres what the shell of our stylesheet looks like:
xsl:template match= xsl:for-each select=reportpo
xsl:apply-templates select=documentfilename 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. Heres a stylesheet that does this:
?xml version=1.0?-- xsl:stylesheet version=1.0 xmlns:xsl=http:www.w3.org1999XSLTransform
xsl:output method=html indent=no xsl:strip-space elements=
xsl:template match= html
head titlexsl:value-of select=reporttitletitle
head body
xsl:for-each select=reportpo xsl:apply-templates select=documentfilenamepurchase-order
xsl:for-each body
html xsl:template
xsl:template match=purchase-order h1
xsl:value-of select=customeraddress[type=business]nametitle xsl:text xsl:text
xsl:value-of select=customeraddress[type=business]namefirst-name xsl:text xsl:text
xsl:value-of select=customeraddress[type=business]namelast-name h1
p xsl:textOrdered on xsl:text
xsl:value-of select=datemonth xsl:textxsl:text
xsl:value-of select=dateday xsl:textxsl:text
xsl:value-of select=dateyear p
h2Items:h2 table width=100 border=1 cols=55 15 15 15
tr bgcolor=lightgreen thItemth
thQuantityth thPrice Eachth
thTotalth tr
xsl:for-each select=itemsitem tr
xsl:attribute name=bgcolor xsl:choose
xsl:when test=position mod 2 xsl:textwhitexsl:text
xsl:when xsl:otherwise
xsl:textlightgreenxsl:text xsl:otherwise
page 122 xsl:choose
xsl:attribute td
bxsl:value-of select=nameb xsl:text part xsl:text
xsl:value-of select=part_no xsl:textxsl: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-numberprice qty, ,.00 xsl:when
xsl:otherwise xsl:value-of select=format-numberprice qty, ,.00
xsl:otherwise xsl:choose
td tr
xsl:for-each tr
td colspan=3 align=right bTotal: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-numberorderTotal, ,.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=itemsitem[index]qty
itemsitem[index]price xsl:variable
xsl:variable name=remainingItems xsl:choose
xsl:when test=index=countitemsitem xsl:text0xsl: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
page 123
When we process our master document with this stylesheet, the results look like Figure 7-1
.
Figure 7-1. Document generated from multiple input files
The most notable thing about our results is that weve been able to generate a document that contains the contents of several other documents. To keep our example short, weve only
combined four purchase orders, but theres no limit beyond the physical limits of our machine to the number of documents we could combine. Best of all, we didnt have to
modify any of the individual purchase orders to generate our report.
7.2.1 An Aside: Doing Math with Recursion
While were here, well 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=sumitemqtyitemprice
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
itemqtyitemprice
, while a perfectly valid XPath expression, isnt 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; well go through them in the order we used them
in our stylesheet.
page 124
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
were currently processing.
runningTotal
The total of all the
item
s weve 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=itemsitem[index]qty
itemsitem[index]price xsl:variable
Notice how the XPath expression in the
select
attribute uses the
items
and
index
parameters to find the exact items were 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
doesnt 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 were
ready. Once the
sumItems
template finishes its work, we can pass the variables value to the
format-number
function to print the invoice total exactly how we want.
page 125
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. Weve 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-numberorder-total, ,.00
7.3 Invoking the document Function
In our previous stylesheet, we used the
document
function to select some number of nodes from the original source document our list of purchase orders, then open those files. There
are a number of ways to invoke the
document
function; well discuss them briefly here. The most common way to use the
document
function is as we just did. We use an XPath expression to describe a node-set; the
document
function takes each node in the node-set, converts it to a string, then uses that string as a URI. So, when we passed a node-set
containing the
filename
attributes in the list of purchase orders, each one is used as a URI. If those URIs are relative references i.e., they dont begin with a protocol like
http
, the base URI of the stylesheet is used as the base URI for the reference.
If the
document
function has two arguments, the second must be a node-set. The first argument is processed as just described, with the difference that the base URI of the first
node in the node-set is used as the base URI for any relative URIs. That combination isnt used often, but its there if you need it.
You can also pass a string or any other XPath datatype to the
document
function. If we wanted to open a particular resource, we could simply pass the name of the resource:
documenthttp:www.ibm.compricelist.xml
This action would open this particular resource and process it. Be aware that XSLT processors are required to return an empty node-set if a resource cant be found, but they
arent required to signal an error. XSLT processors also dont have to support any particular protocols
http
,
ftp
, etc.; you have to check the documentation of your XSLT processor to see what protocols are and arent supported.
Every node in the XPath source tree is associated with a base URI. When using the
document
function, the base URI is important for resolving references to various resources typically specified with
relative links in a file opened with the
document
function. If a given node is an element or processing instruction node, and that
node occurs in an external entity, then the base URI for that node is the base URI of the external entity. If an element or processing
instruction node does not occur in an external entity, then its base URI is the base URI of the document in which it appears. The base
URI of a document node is the base URI of the document itself, and the base URI of an attribute, comment, namespace, or text node is the
base URI of that nodes parent.
page 126
A special case occurs when you pass an empty string to the
document
function. As weve discussed the various combinations of arguments that can be passed to the function, weve
gone over the rules for resolving URIs. When we call
document
, the XSLT processor parses the current stylesheet and returns a single node, the root node of the stylesheet itself.
This technique is very useful for processing lookup tables in a stylesheet, something well discuss later in this chapter.
7.4 More Sophisticated Techniques
Up to now, weve written a simple XML document that contains references to other XML documents, then we created a stylesheet that combines all those referenced XML documents
into a single output document. Thats all well and good, but well probably want to do more advanced things. For example, it might be useful to generate a document that lists all items
ordered by all the customers. It might be useful to sort all the purchase orders by the state to which they were shipped, by the last name of the customer, or to group them by the state to
which they were shipped. Well go through some of these scenarios to illustrate the design challenges we face when generating documents from multiple input files.
7.4.1 The document Function and Sorting
Our first challenge will be to generate a listing of all purchase orders and sort them by state. This isnt terribly difficult; well simply use the
xsl:sort
element in conjunction with the
document
function. Heres the heart of our new stylesheet:
body h3Selected Purchase Orders - iSorted by stateih3
xsl:for-each select=documentreportpofilenamepurchase-ordercustomeraddressstate
xsl:sort select=. xsl:apply-templates select=ancestor::purchase-order
xsl:for-each body
Figure 7-2. Another document generated from multiple input files
page 127
What makes this process slightly challenging is the fact that were sorting on one thing the value of the
state
element, then invoking
xsl:apply-templates
against the
purchase- order
ancestor of the
state
element. We simply used the
ancestor::
axis to do this. Figure 7-2
shows our output document, sorted by the value of the
state
element in each purchase order.
7.4.2 Implementing Lookup Tables
We mentioned earlier that calling the
document
function with an empty string enabled us to access the nodes in the stylesheet itself. We can use this behavior to implement a lookup
table. As an example, well create a lookup table that replaces an abbreviation such as
ME
with
Maine
. We can then use the value from the lookup table as the sort key. More attentive readers might have noticed in our previous example that although the abbreviation
MA
does indeed sort before the abbreviation
ME
, a sorted list of the state names themselves would put
Maine
abbreviation
ME
before
Massachusetts
abbreviation
MA
. First, well create our lookup table. Well use the fact that a stylesheet can have any element
as a top-level element, provided that element is namespace-qualified to distinguish it from the
xsl:
namespace reserved for stylesheets. Heres the namespace prefix definition and part of the lookup table that uses it:
?xml version=1.0? xsl:stylesheet version=1.0 xmlns:xsl=http:www.w3.org1999XSLTransform
xmlns:states=http:new.usps.comcgi-binuspsbvscriptscontent.jsp?D=10090 states:name abbrev=ALAlabamastates:name
states:name abbrev=ALAlabamastates:name states:name abbrev=AKAlaskastates:name
states:name abbrev=ASAmerican Samoastates:name -- Most state abbreviations removed to keep this listing brief... --
states:name abbrev=MEMainestates:name states:name abbrev=MHMarshall Islandsstates:name
states:name abbrev=MDMarylandstates:name states:name abbrev=MAMassachusettsstates:name
The namespace mapped to the
states
prefix is the URL for the official list of state abbreviations from the United States Postal Service.
To look up values in our table, well use the
document
function to return the root node of our stylesheet, then well look for a
states:name
element with a
abbrev
attribute that matches the value of the current
state
element in the purchase order were currently processing. Heres the somewhat convoluted syntax that performs this magic:
body h3Selected Purchase Orders - iSorted by stateih3
xsl:for-each select=documentreportpofilenamepurchase-ordercustomeraddressstate
xsl:sort select=documentstates:name[abbrev=current] xsl:apply-templates select=ancestor::purchase-order
xsl:for-each body
Notice that we use the
document
function twice; once to open the document referred to by the
filename
element, and once to open the stylesheet itself. We also need to discuss the XPath expression in the
select
attribute of the
xsl:sort
element. There are four significant parts to this expression:
document
Returns the root node of the current stylesheet.