More Servlet Programming for Teams
Style Sheet Changes and Advanced Techniques
By Benoît Marchal
Send comments and questions about
this article to View Source.
In my article
Servlet
Programming for Teams: How Java Programmers and HTML Designers Can Collaborate
Using XML, I introduced a technique for cleanly separating the presentation
aspect from the application logic in Java servlets - a separation that matches
the division of work between HTML designers and Java developers. The technique
is based on XML (Extensible Markup Language) and XSL (XML Stylesheet Language).
This article expands on that one, introducing recent changes to XSL and some
advanced techniques for style
sheets. I'll show you how to use XSL to create compelling web pages.
I'll assume you've installed
the
software introduced in the earlier article. Although it's not strictly
required, I'd advise you to read that article if you haven't done so
already.
XSL UPDATE
XML and XSL were developed by the W3C (World Wide Web
Consortium), the
organization that brought us HTML and the web. XML was formally approved in
1998, whereas XSL was only recently approved (in late November).
While developing XSL, the W3C regularly amended the specifications, sometimes making changes
that weren't
compatible with older versions. This means that you need to revisit your style sheets
and upgrade them as necessary. The good news is that most
changes have been minor.
This article
integrates the changes made to the XSL standard since I wrote my last article, mainly by
using the following new format for the xsl:stylesheet element:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/TR/REC-html40">
The changes to this element are minimal: the value of the
xmlns:xsl attribute has changed, result-ns has been removed,
and a new attribute, version, has been introduced. If you download the
latest version of XT from James Clark's
site, you'll need to update your existing style sheets, because XT no
longer recognizes the old format.
Also, contrary to what I noted in my earlier article, the
xsl:output element is now part of XSL.
A DIFFERENT APPROACH FOR STYLE SHEETS
In my earlier article, I presented style sheets that looked a lot like a
mail merge, with special fields being replaced by values. Those style sheets
consisted essentially of HTML documents containing xsl:value-of
elements where values from the XML document were to be inserted. Here I'll use
a different approach to writing style sheets, based on the
xsl:apply-templates element. Listing 1 demonstrates this approach,
with a style sheet for the product list created in the earlier article. You can
copy this new style sheet into the document directory (along with the other
style sheets) and select it with the xsl parameter in the URL. For
example:
http://localhost/productlist?xsl=/simple_productlist_en.xsl.
Listing 1
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/TR/REC-html40">
<xsl:output method="html"/>
<xsl:template match="/">
<HTML>
<HEAD>
<TITLE>Shop</TITLE>
</HEAD>
<BODY>
<P>Select a product:</P>
<xsl:apply-templates/>
<FORM ACTION=/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fmarchal_xml2%2F%26quot%3Bshoppingcart%26quot%3B%26gt%3B&y=1999
<INPUT TYPE="SUBMIT" VALUE="Shopping cart"/>
</FORM>
EN | <A HREF=/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fmarchal_xml2%2F%26quot%3Bproductlist%3Fxsl%3D%2Fproductlist_fr.xsl%26quot%3B%26gt%3BFR%26lt%3B%2FA%26gt%3B&y=1999
</BODY>
</HTML>
</xsl:template>
<xsl:template match="product">
<FORM ACTION=/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fmarchal_xml2%2F%26quot%3Bshoppingcart%26quot%3B&y=1999 METHOD="POST">
<INPUT TYPE="HIDDEN" NAME="add">
<xsl:attribute name="value">
<xsl:value-of select="@id"/>
</xsl:attribute>
</INPUT>
<xsl:apply-templates/>
<INPUT TYPE="SUBMIT" VALUE="Buy"/>
</FORM>
</xsl:template>
<xsl:template match="name">
<B><xsl:apply-templates/></B><BR/>
</xsl:template>
<xsl:template match="description[@xml:lang='EN']">
<xsl:apply-templates/><BR/>
</xsl:template>
<xsl:template match="description[not(@xml:lang='EN')]"/>
<xsl:template match="price">
<I><xsl:apply-templates/></I><BR/>
</xsl:template>
<xsl:template match="image">
<IMG>
<xsl:attribute name="SRC"><xsl:apply-templates/></xsl:attribute>
</IMG><BR/>
</xsl:template>
</xsl:stylesheet>
If you compare this style sheet with the ones in the earlier article,
you'll notice that it has several xsl:template elements. Each of these
elements will match an element in the XML document; for example,
<xsl:template match="name"> matches the name
element.
When the XSL processor encounters an element that matches a template, it
replaces the element with the content of the template. Thus, when the processor
encounters a name element, it replaces it with
<B><xsl:apply-templates/></B><BR/>
The xsl:apply-templates element is essentially a recursive call
to the style sheet: when the XSL processor encounters
xsl:apply-templates, it goes one level down in the source document and
applies the style sheet again.
Following the Processor
As an illustration, let's follow the XSL processor when it formats the
document in Listing 2. This is the list of products, in XML, generated by the
servlet written in the earlier article.
Listing 2
<?xml version="1.0" encoding="ISO-8859-1"?>
<products>
<product id="0">
<name>WhizBang Ultra Word Processor</name>
<description xml:lang="EN">More words per minute than the competition.</description>
<description xml:lang="FR">Plus de mots à la minute que la concurrence.</description>
<image>wordprocessor.jpg</image>
<price>$799.99</price>
</product>
<product id="1">
<name>Super WhizBang Calculator</name>
<description xml:lang="EN">Cheap and reliable with power saving.</description>
<description xml:lang="FR">Economique et fiable avec économie d'énerge.</description>
<image>calculator.jpg</image>
<price>$5.99</price>
</product>
<product id="2">
<name>WhizBang Safest Safe</name>
<description xml:lang="EN">Choose the authentic WhizBang Safest Safe.</description>
<description xml:lang="FR">Exigez l'original!</description>
<image>safe.jpg</image>
<price>$1,999.00</price>
</product>
</products>
Here's what happens:
-
The processor selects the <xsl:template
match="/"> template in the style sheet (Listing 1), because it matches the root of the XML
document. (XML paths in style sheets work a lot like paths in a file system,
and / is the root of the document.) Since the processor first matches
the root, that template is the ideal place to create the HTML element
as well as the HEAD and BODY elements.
The template contains the xsl:apply-templates element to
force the processor to move one level down in the document and recursively call
the style sheet.
-
The processor is positioned on the products element, so it
looks for a template that matches products. The style sheet doesn't
define any templates for products, so the processor needs to match
against a built-in template. The built-in template isn't defined in the style
sheet but is predefined by the processor, and it looks like this (where
* means any element and | means "or"):
<xsl:template match="* | /">
<xsl:apply-templates/>
</xsl:template>
The built-in template exists solely to call the style sheet
recursively. It doesn't create HTML code, but it does make sure the processor
doesn't stop dead in its tracks. The xsl:apply-templates element in
this template moves the processor one level down.
-
The processor is positioned on the first product element;
it searches the style sheet for a template that matches product and
finds one. The product template creates an HTML form. You should
recognize the familiar xsl:value-of element for inserting the value of
the id attribute. This template also contains an
xsl:apply-templates element, which continues the recursive processing.
-
The processor is positioned on the name element, and it
searches for the corresponding template. The template for name creates
a bold HTML element and recursively calls the style sheet through
xsl:apply-templates.
-
The processor is positioned on the text content of the name
element (WhizBang Ultra Word Processor). The style sheet defines no template for
text, so the processor turns to another built-in template, which simply copies
the text into the HTML file:
<xsl:template match="text()">
<xsl:value-of select="."/>
</xsl:template>
The processor has reached a leaf in the document, so it goes one
level up in the source document and applies the style sheet again.
-
The processor is positioned on the description element.
There are two templates for description; a test in square brackets
selects which element should match:
- description[@xml:lang='EN'] matches the English
descriptions.
- description[not(@xml:lang='EN')] matches the other
(non-English) descriptions. Note that this template doesn't contain
xsl:apply-templates, so the processor doesn't go further in this
branch. This is how we filter out the non-English descriptions.
And so on. You've probably recognized the algorithm for a deep-first
search: the processor walks through the XML document; at each step, it selects
a template in the style sheet and applies it.
Comparing the Two Approaches
The major difference between this style sheet and the ones used in the
earlier article is that those were built around the resulting HTML document,
whereas this style sheet is built around the input XML document.
The earlier style sheets were similar to the mail merge feature of word
processors - again, because special fields are replaced by actual values. In
contrast, this style sheet specifies how to format each element in an XML
document.
In practice, you'll find that the earlier style sheets are ideal for
prototypes because they're so close to the final HTML document. However,
because it's organized by element, the type of style sheet used in this article
is easier to maintain. In particular, changes to the presentation of one
element do not affect other elements.
MORE FUN WITH XSL
Listing 3 is an advanced style sheet for the product list that
demonstrates several new XSL features. You apply this style sheet using the
xsl parameter, as we've seen before.
Listing 3
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/TR/REC-html40">
<xsl:output method="html"/>
<xsl:template match="/">
<HTML>
<HEAD>
<TITLE>Shop</TITLE>
</HEAD>
<BODY>
<P>Quick access:</P>
<xsl:apply-templates mode="toc"/>
<P>Select a product:</P>
<xsl:apply-templates/>
<FORM ACTION=/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fmarchal_xml2%2F%26quot%3Bshoppingcart%26quot%3B%26gt%3B&y=1999
<INPUT TYPE="SUBMIT" VALUE="Shopping cart"/>
</FORM>
EN | <A HREF=/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fmarchal_xml2%2F%26quot%3Bproductlist%3Fxsl%3D%2Fproductlist_fr.xsl%26quot%3B%26gt%3BFR%26lt%3B%2FA%26gt%3B&y=1999
</BODY>
</HTML>
</xsl:template>
<xsl:template match="product" mode="toc">
<xsl:apply-templates select="name" mode="toc"/>
<!-- -1 is to account for the "newline" text node -->
<xsl:if test="not(position()=last()-1)"> | </xsl:if>
</xsl:template>
<xsl:template match="name" mode="toc">
<A>
<xsl:attribute name="HREF">
<xsl:text>#</xsl:text>
<xsl:value-of select="generate-id(..)"/>
</xsl:attribute>
<xsl:apply-templates/>
</A>
</xsl:template>
<xsl:template match="product">
<FORM ACTION=/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fmarchal_xml2%2F%26quot%3Bshoppingcart%26quot%3B&y=1999 METHOD="POST">
<INPUT TYPE="HIDDEN" NAME="add">
<xsl:attribute name="value">
<xsl:apply-templates select="@id"/>
</xsl:attribute>
</INPUT>
<xsl:apply-templates select="image"/>
<xsl:apply-templates select="name"/><xsl:text> </xsl:text>
<xsl:apply-templates select="price"/><BR/>
<xsl:apply-templates select="description"/><BR/>
<INPUT TYPE="SUBMIT" VALUE="Buy"/>
<BR CLEAR="LEFT"/>
</FORM><BR/>
</xsl:template>
<xsl:template match="name">
<B><xsl:apply-templates/></B>
</xsl:template>
<xsl:template match="description[not(@xml:lang='EN')]"/>
<xsl:template match="image">
<A>
<xsl:attribute name="NAME">
<xsl:value-of select="generate-id(..)"/>
</xsl:attribute>
<IMG ALIGN="LEFT">
<xsl:attribute name="SRC">
<xsl:apply-templates/>
</xsl:attribute>
</IMG>
</A>
</xsl:template>
</xsl:stylesheet>
Attributes
Notice that some xsl:apply-templates and xsl:template
elements have a new attribute named mode. Modes are used for walking
through the XML document several times while applying different templates.
<xsl:apply-templates mode="toc"/> is like a regular
xsl:apply-templates element except that the processor applies only
templates that have the corresponding mode attribute. This is useful
for generating a "table of contents" (in this case,
a list of hyperlinks to all the products).
In addition, the xsl:apply-templates element can take a
select attribute, causing the processor to limit itself to elements
whose names match the select parameter. The select attribute
is commonly used to reorganize elements. In the product template, for
example, it's used to move the image element before the name
element (and price before description).
Elements
This style sheet uses two new elements:
- xsl:text, for delimiting text
- xsl:if, for testing a condition
In most cases, to insert text into the HTML document you simply type it
in the style sheet. However, because newline characters and spaces are also
used to improve the readability of the style sheet, you might not always get
the results you want. You can use the xsl:text
element to unambiguously delimit text. Here's how it's used in this style sheet:
name, xsl:text is used to separate the
# character from the newline character and spaces that have been added for
readability. If xsl:text had not been used here,
the spaces preceding the # character would have been included in the
HTML document.
In the template for products, xsl:text is used to
insert a single space between the name and the price. Otherwise, the
processor would have ignored the space, incorrectly assuming it was meant for
readability, and the name and price would have been run together.
The
xsl:if element is used to conditionally generate the
output. Its test attribute is a condition that's evaluated; if the
condition is true, the content of the xsl:if element is
inserted in the result, otherwise nothing is produced.
Functions
XSL supports several functions. We've already seen the text()
and not() functions: text() matches the text content of an
element; not() returns true if its argument was
false, and vice versa. This style sheet also uses the
position() and last() functions to test for the last product:
position() is the position of the current element or text in the
context of its parent (for example, 1 if it's the first product element or
text in the products element, or 2 if the second),
and last() is the position of the last such
element or text.
You might wonder why
position() is tested against last()-1. Notice that in
the XML document in Listing 2, each element is on a different line - that is,
there's a newline character after each element. The newline
character is counted as text by the position() and last() functions.
Therefore last() points to a newline character, not a product
element; that's why 1 has to be subtracted from it.
The style sheet also uses the
generate-id() function, which
returns a unique identifier for an element. This function is particularly
useful when you're creating links within the document.
YOUR TURN
This article and my earlier one have introduced you to some practical
applications of XML and XSL. As you gain familiarity with XML and XSL, you'll
find that they're very flexible and can solve many problems.
I couldn't cover all the elements and functions of XSL, but I hope I've
given you the tools and willingness to learn more. If you're interested, I
invite you to read a good tutorial on XML - and, if you'll forgive the
shameless plug, I'd recommend my own book,
XML by Example.
I'm very interested in hearing about your experiences with XML and XSL.
Please write to me and tell me
about them.
View Source wants your feedback!
Write to us and let us know
what you think of this article.
Many thanks to the readers who commented on my earlier article.
Benoît Marchal is a software
engineer and consultant based in Namur, Belgium, who has been working
extensively in Java and XML. He runs his own software company,
Pineapplesoft. He also likes
teaching and writing; his book
XML by Example has been published by Que.
(12.99)
Any sample code included above is provided for your use on an "AS IS" basis, under the Netscape License Agreement - Terms of Use