Finding out about conditional and looping structures
Testing for conditions with xsl:if
Deciding among a set of conditions with xsl:choose
Looping through a set of nodes with xsl:for-each
A re you a prophead? (Prophead is short for propeller head or geek.) If you aren’t sure, I’m going to give you a simple test that can tell you exactly whether you qualify. Here goes:
if-else
for
while
switch-case
When you read those lines, what was your reaction? Did you have a quizzical look on your face? Did you groan? A nod of the head? Or perhaps you screamed out in excitement and felt your pulse racing?
Well, if you cried out in exultation and felt your heart skipping a beat, my guess is that you’ve got programming in your blood and are as “excited as a prophead” about getting into a discussion on adding programming logic to your XSLT stylesheets.
Kidding aside, no matter your reaction, I think you’ll find this chapter to be extremely useful to expand the scope of what you can do with XSLT.
Conditional and looping commands, such as if and for, are essential to nearly every programming language on the planet. Conditional structures allow you to execute parts of a program when certain conditions are met. Looping statements enable you to cycle through a series of values or objects and perform actions with them. The practical effect is that both kinds give you greater control and flexibility to do what you are trying to do in your code.
Ever wonder how different XSLT is from traditional programming languages? Consider this: When you learn C++, Java, or Visual Basic, conditional and loop statements are some of the first things you learn about. In XSLT, however, conditionals and loops aren’t as important, primarily because of the way XSLT makes use of template rules. A template rule certainly has something akin to conditional and looping structure built into it: For all the nodes that match its pattern, do something or else skip the template altogether.
Having said that, although you can do a heck of a lot in XSLT without ever touching these control structures, there are certain tasks you just cannot perform without them.
The most basic of all control structures is the if statement. It performs one or more actions if certain conditions are met. XSLT uses xsl:if instruction for this purpose:
<xsl:if test=”expression”>
do something
</xsl:if>
In effect, this instruction says: if the test expression is true, then process the lines inside the start and end xsl:if tags.
For example, the following xsl:if instruction sends the literal text Extra large size is required to the result tree if the current node has a size attribute that equals XL :
<xsl:if test=”@size=’XL’”>
Extra large size is required.
</xsl:if>
Let me give you a fuller example to demonstrate xsl:if, starting with the students.xml file shown in Listing 7-1 as my source document.
Listing 7-1: students.xml
<?xml version=”1.0”?>
<!-- students.xml -->
<school name=”Elliot Academy”>
<student id=”601” name=”Jordan”>
<class name=”Language Arts” days=”5”>Sentence diagramming</class>
<class name=”Reading” favorite=”true” days=”5”>Lord Of The Rings</class>
<class name=”Writing” days=”3”>Colonial Times</class>
<class name=”Geography” days=”2”>African Sahel</class>
<class name=”Math” days=”5” section=”6.42”>Decimals</class>
<class name=”Science” days=”3” level=”advanced”>Volcanos</class>
<class name=”History” days=”3”>American Presidents</class>
<class name=”Art” days=”1”>Drawing</class>
</student>
<student id=”401” name=”Jared”>
<class name=”Language Arts” days=”5” section=”4.56”>Punctuation</class>
<class name=”Reading” days=”5”>Voyage Of The Dawntreader</class>
<class name=”Writing” days=”3”>Haiku Poetry</class>
<class name=”Geography” favorite=”true” days=”2”>African Sahel</class>
<class name=”Math” days=”5” section=”4.45”>Fractions</class>
<class name=”Science” days=”3” level=”basic”>Insects</class>
<class name=”History” days=”3”>American Presidents</class>
<class name=”Art” days=”1”>Paper Mache</class>
</student>
<student id=”301” name=”Justus”>
<class name=”Language Arts” days=”5” section=”3.80”>Capitalization</class>
<class name=”Reading” days=”5”>Sherlock Holmes Solves Them All</class>
<class name=”Writing” days=”3”>Penmanship</class>
<class name=”Geography” days=”2”>African Sahel</class>
<class name=”Math” favorite=”true” days=”5” section=”3.30”>Division</class>
<class name=”Science” days=”3” level=”basic”>Vertebrates</class>
<class name=”History” days=”3”>American Presidents</class>
<class name=”Art” days=”1”>Clay Sculptures</class>
</student>
</school>
Suppose I want to create a text-based report for each of the three students. If the desired output were the same for each of them, then I could use a normal template rule along with xsl:apply-templates or xsl:value-of. However, in this case, I’d actually like to provide literal text that is customized for each student and make variations in my output based on each student. Given these requirements, xsl:if becomes a great tool at my disposal, because I can use it to test for a specific student, and if that expression has a true value, then I can tell the processor to write the customized output to the result tree. For example, for a student named Jordan, the following xsl:if instruction is used:
<xsl:template match=”student”>
<xsl:if test=”@name=’Jordan’”>
*******************************************
Jordan is a 6th grader with an id
of <xsl:value-of select=”@id”/>
Emphasizing:
Reading: <xsl:value-of select=”class[@name=’Reading’]”/>
Writing: <xsl:value-of select=”class[@name=’Writing’]”/>
*******************************************
</xsl:if>
</xsl:template>
As the template rule is processed and run on each student element, the XSLT processor faces a fork in the road for each node when it gets to the xsl:if statement. Either the conditions of test are met or they are not. If so, then the text inside is added to the result document. If not, then it is ignored. Here’s what the complete stylesheet, which contains a test for each student, looks like:
<!-- students.xsl -->
<xsl:stylesheet version=”1.0” xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”>
<xsl:output method=”text”/>
<xsl:template match=”student”>
<xsl:if test=”@name=’Jordan’”>
*******************************************
Jordan is a 6th grader with an id
of <xsl:value-of select=”@id”/>
Emphasizing:
Reading: <xsl:value-of select=”class[@name=’Reading’]”/>
Writing: <xsl:value-of select=”class[@name=’Writing’]”/>
*******************************************
</xsl:if>
<xsl:if test=”@name=’Jared’”>
*******************************************
Jared is a 4th grader with an id
of <xsl:value-of select=”@id”/>
Emphasizing:
Language: <xsl:value-of select=”class[@name=’Language Arts’]”/>
Writing: <xsl:value-of select=”class[@name=’Writing’]”/>
*******************************************
</xsl:if>
<xsl:if test=”@name=’Justus’”>
*******************************************
Justus is a 3rd grader with an id
of <xsl:value-of select=”@id”/>
Emphasizing:
Reading: <xsl:value-of select=”class[@name=’Reading’]”/>
Geography: <xsl:value-of select=”class[@name=’Geography’]”/>
*******************************************
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The text document generated looks like this:
*******************************************
Jordan is a 6th grader with an id
of 601
Emphasizing:
Reading: Lord Of The Rings
Writing: Colonial Times
*******************************************
*******************************************
Jared is a 4th grader with an id
of 401
Emphasizing:
Language: Punctuation
Writing: Haiku Poetry
*******************************************
*******************************************
Justus is a 3rd grader with an id
of 301
Emphasizing:
Reading: Sherlock Holmes Solves Them All
Geography: African Sahel
*******************************************
Arguably the most important part of any xsl:if instruction is its test attribute. Its expression must be true in order for the xsl:if content to be processed. In addition to looking for a specific element or attribute value as I did in the preceding example, you can do a variety of tests inside the expression.
To test if an attribute exists, you use:
<xsl:if test=”@favorite”>
Alternatively, you can use the XPath’s built-in function not() to test for the opposite value:
<xsl:if test=”not(@favorite)”>
XPath also allows you to use or and and operators to enable you to test more than one condition. If you want just one of two or more expressions to evaluate to true, then use or. But if you want all expressions to evaluate to true, then use and. For example, the following test expression is looking for an element with a name attribute that has a value of Language Arts, Reading, or Writing :
<xsl:if test=”(@name=’Language Arts’) or (@name=’Reading’) or (@name=’Writing’)”>
To show the use of the and operator, the test expression that follows is looking for an element that has a days attribute of 5 and has a parent with a name attribute that equals Justus :
<xsl:if test=”(@days=’5’) and (../@name=’Justus’)”>
To evaluate numeric expressions, you can use the traditional forms of comparison that you learned back in 3rd grade math: <, <=, >, >=, and =. However, in XML, you can’t use the < character in your XSLT stylesheet, because XML reserves this character for marking the start of an element tag. If you need to use the less than sign, use <. For example, rather than using 6 < 9, you use what’s in the following expression:
<xsl:if test=”6 < 9”>
Additionally, in place of using <= to mean “less than or equal to,” you use 6 <= 9.
The complete list of comparison operators is shown in Table 7-1.
Operator | Means |
---|---|
> | Greater than |
>= | Greater than or equal to |
< | Less than |
<= | Less than or equal to |
= | Equals |
!= | Not equal |
I have been using the equals sign in previous examples, but I can use its opposite — the != operator — to test for results that aren’t equal. So, if I want to search on elements with days attributes that aren’t equal to 5, then I use:
<xsl:if test=”@days != 5”>
The following stylesheet uses each of these xsl:if instructions:
<?xml version=”1.0”?>
<xsl:stylesheet version=”1.0” xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”>
<xsl:output method=”text”/>
<xsl:template match=”class”>
<!-- if favorite attribute exists -->
<xsl:if test=”@favorite”>
<xsl:value-of select=”@name”/> is the favorite class of <xsl:value-of select=”../@name”/><xsl:text>
</xsl:text>
</xsl:if>
<!-- if favorite attribute does not exist -->
<xsl:if test=”not(@favorite)”>
<xsl:value-of select=”@name”/> is not the favorite class of <xsl:value-of select=”../@name”/><xsl:text>
</xsl:text>
</xsl:if>
<!-- if name is language arts, reading, or writing -->
<xsl:if test=”(@name=’Language Arts’) or (@name=’Reading’) or (@name=’Writing’)”>
<xsl:text>English class topic: </xsl:text><xsl:value-of select=”.”/><xsl:text>
</xsl:text>
</xsl:if>
<!-- if favorite attribute exists -->
<xsl:if test=”(@days=’5’) and (../@name=’Justus’)”>
<xsl:text>One of Justus’ 5-day/week classes: </xsl:text><xsl:value-of select=”@name”/><xsl:text>
</xsl:text>
</xsl:if>
<!-- if days less than 3 -->
<xsl:if test=”@days < 3”>
<xsl:text>Minor subject: </xsl:text><xsl:value-of select=”@name”/><xsl:text>
</xsl:text>
</xsl:if>
<!-- if days greater than or equal to 3 -->
<xsl:if test=”@days >= 3”>
<xsl:text>Major subject: </xsl:text><xsl:value-of select=”@name”/><xsl:text>
</xsl:text>
</xsl:if>
<!-- if days does not equal 5 -->
<xsl:if test=”@days != 5”>
<xsl:text>Not a full-time subject: </xsl:text><xsl:value-of select=”@name”/><xsl:text>
</xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When applied to the XML source shown in Listing 7-1, the following output is then generated:
Language Arts is not the favorite class of Jordan
English class topic: Sentence diagramming
Major subject: Language Arts
Reading is the favorite class of Jordan
English class topic: Lord Of The Rings
Major subject: Reading
Writing is not the favorite class of Jordan
English class topic: Colonial Times
Major subject: Writing
Not a full-time subject: Writing
Geography is not the favorite class of Jordan
Minor subject: Geography
Not a full-time subject: Geography
Math is not the favorite class of Jordan
Major subject: Math
Science is not the favorite class of Jordan
Major subject: Science
Not a full-time subject: Science
History is not the favorite class of Jordan
Major subject: History
Not a full-time subject: History
Art is not the favorite class of Jordan
Minor subject: Art
Not a full-time subject: Art
Language Arts is not the favorite class of Jared
English class topic: Punctuation
Major subject: Language Arts
Reading is not the favorite class of Jared
English class topic: Voyage Of The Dawntreader
Major subject: Reading
Writing is not the favorite class of Jared
English class topic: Haiku Poetry
Major subject: Writing
Not a full-time subject: Writing
Geography is the favorite class of Jared
Minor subject: Geography
Not a full-time subject: Geography
Math is not the favorite class of Jared
Major subject: Math
Science is not the favorite class of Jared
Major subject: Science
Not a full-time subject: Science
History is not the favorite class of Jared
Major subject: History
Not a full-time subject: History
Art is not the favorite class of Jared
Minor subject: Art
Not a full-time subject: Art
Language Arts is not the favorite class of Justus
English class topic: Capitalization
One of Justus’ 5-day/week classes: Language Arts
Major subject: Language Arts
Reading is not the favorite class of Justus
English class topic: Sherlock Holmes Solves Them All
One of Justus’ 5-day/week classes: Reading
Major subject: Reading
Writing is not the favorite class of Justus
English class topic: Penmanship
Major subject: Writing
Not a full-time subject: Writing
Geography is not the favorite class of Justus
Minor subject: Geography
Not a full-time subject: Geography
Math is the favorite class of Justus
One of Justus’ 5-day/week classes: Math
Major subject: Math
Science is not the favorite class of Justus
Major subject: Science
Not a full-time subject: Science
History is not the favorite class of Justus
Major subject: History
Not a full-time subject: History
Art is not the favorite class of Justus
Minor subject: Art
Not a full-time subject: Art
The second type of control statement in XSLT is the xsl:choose instruction. While xsl:if is the equivalent of a true/false test, xsl:choose is akin to a multiple choice test. Using it, you can choose among two or more options or optionally default to an alternative if no other condition is met.
xsl:choose has a xsl:when subelement that is used to test for each condition and an optional xsl:otherwise to specify a default response. The xsl:choose syntax looks like:
<xsl:choose>
<xsl:when test=”expression”>
do something
</xsl:when>
<xsl:when test=”expression2”>
do something
</xsl:when>
<xsl:when test=”expression3”>
do something
</xsl:when>
<xsl:otherwise>
do something
</xsl:when>
</xsl:choose>
When the XSLT processor encounters an xsl:choose element, the processor evaluates each xsl:when instruction in sequential order looking for a true result of the test expression. When the first true result is found, then that xsl:when’s content is processed, and the processor bypasses the remaining xsl:when and xsl:otherwise elements of the xsl:choose instruction. If no xsl:when evaluates to true, then the xsl:otherwise element is used.
For example, suppose the following code snippet is run against an element that has a name attribute with a value of Larry (such as <emp name=”Larry”>):
<xsl:choose>
<xsl:when test=”@name=’Moe’”>
My name is Moe
</xsl:when>
<xsl:when test=”@name=’Curly’”>
My name is Curly
</xsl:when>
<xsl:when test=”@name=’Larry’”>
My name is Larry
</xsl:when>
<xsl:otherwise>
I am not a stooge!
</xsl:when>
</xsl:choose>
The XSLT processor evaluates the first xsl:when statement, but continues after it returns a false value. The same thing happens with the second xsl:when. But when the processor gets to the third xsl:when instruction, it returns a true value, so the literal string My name is Larry is output to the result tree. The xsl:otherwise is ignored in this case, because a true test expression was already found.
To demonstrate xsl:choose, I transform the XML document in Listing 7-1 into a new XML fallclasses structure. The changes are as follows:
For class name=”Reading” elements, new schedule=”9am” and priority=”A” attributes are added.
For class name=”Writing” elements, new schedule=”10am” and priority=”B” attributes are added.
For class name=”Math” elements, new schedule=”11am” and priority=”C” attributes are added.
For every other class element, new schedule=”postlunch” and priority=”D” attributes are added.
The stylesheet to perform this transformation is as follows:
<?xml version=”1.0”?>
<xsl:stylesheet version=”1.0” xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”>
<xsl:output method=”xml”/>
<!-- Replace school element with fallclasses -->
<xsl:template match=”/”>
<xsl:text>
</xsl:text><fallclasses teacher=”ksw”>
<xsl:apply-templates/>
</fallclasses>
</xsl:template>
<!-- Transform class elements -->
<xsl:template match=”class”>
<xsl:choose>
<xsl:when test=”@name=’Reading’”>
<class student=”{../@name}” subject=”{@name}” assignment=”{.}” schedule=”9am” priority=”A”/>
</xsl:when>
<xsl:when test=”@name=’Writing’”>
<class student=”{../@name}” subject=”{@name}” assignment=”{.}” schedule=”10am” priority=”B”/>
</xsl:when>
<xsl:when test=”@name=’Math’”>
<class student=”{../@name}” subject=”{@name}” assignment=”{.}” schedule=”11am” priority=”C”/>
</xsl:when>
<xsl:otherwise>
<class student=”{../@name}” subject=”{@name}” assignment=”{.}” schedule=”postlunch” priority=”D”/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
In the first template rule, the school document element is replaced by adding a fallclasses element. In the second rule, the xsl:choose instruction is run against each class element. For all Reading class elements, the first xsl:when statement is run, while Writing class elements act on the second and Math elements on the third. For the remaining elements, the xsl:otherwise is triggered.
After the transformation, I have a shiny new XML document:
<?xml version=”1.0” encoding=”utf-8”?>
<fallclasses teacher=”ksw”>
<class student=”Jordan” subject=”Language Arts” assignment=”Sentence diagramming” schedule=”postlunch” priority=”D”/>
<class student=”Jordan” subject=”Reading” assignment=”Lord Of The Rings” schedule=”9am” priority=”A”/>
<class student=”Jordan” subject=”Writing” assignment=”Colonial Times” schedule=”10am” priority=”B”/>
<class student=”Jordan” subject=”Geography” assignment=”African Sahal” schedule=”postlunch” priority=”D”/>
<class student=”Jordan” subject=”Math” assignment=”Decimals” schedule=”11am” priority=”C”/>
<class student=”Jordan” subject=”Science” assignment=”Volcanos” schedule=”postlunch” priority=”D”/>
<class student=”Jordan” subject=”History” assignment=”American Presidents” schedule=”postlunch” priority=”D”/>
<class student=”Jordan” subject=”Art” assignment=”Drawing” schedule=”postlunch” priority=”D”/>
<class student=”Jared” subject=”Language Arts” assignment=”Punctuation” schedule=”postlunch” priority=”D”/>
<class student=”Jared” subject=”Reading” assignment=”Voyage Of The Dawntreader” schedule=”9am” priority=”A”/>
<class student=”Jared” subject=”Writing” assignment=”Haiku Poetry” schedule=”10am” priority=”B”/>
<class student=”Jared” subject=”Geography” assignment=”African Sahal” schedule=”postlunch” priority=”D”/>
<class student=”Jared” subject=”Math” assignment=”Fractions” schedule=”11am” priority=”C”/>
<class student=”Jared” subject=”Science” assignment=”Insects” schedule=”postlunch” priority=”D”/>
<class student=”Jared” subject=”History” assignment=”American Presidents” schedule=”postlunch” priority=”D”/>
<class student=”Jared” subject=”Art” assignment=”Paper Mache” schedule=”postlunch” priority=”D”/>
<class student=”Justus” subject=”Language Arts” assignment=”Capitalization” schedule=”postlunch” priority=”D”/>
<class student=”Justus” subject=”Reading” assignment=”Sherlock Holmes Solves Them All” schedule=”9am” priority=”A”/>
<class student=”Justus” subject=”Writing” assignment=”Penmanship” schedule=”10am” priority=”B”/>
<class student=”Justus” subject=”Geography” assignment=”African Sahel” schedule=”postlunch” priority=”D”/>
<class student=”Justus” subject=”Math” assignment=”Division” schedule=”11am” priority=”C”/>
<class student=”Justus” subject=”Science” assignment=”Vertebrates” schedule=”postlunch” priority=”D”/>
<class student=”Justus” subject=”History” assignment=”American Presidents” schedule=”postlunch” priority=”D”/>
<class student=”Justus” subject=”Art” assignment=”Clay Sculptures” schedule=”postlunch” priority=”D”/>
</fallclasses>
The xsl:for-each element allows you to perform a set of instructions on each node returned from its select attribute. Its syntax is:
<xsl:for-each select=”expression”>
do something
</xsl:for-each>
Essentially, the xsl:for-each element means that for each of the nodes returned by the select expression, perform the instructions in between the start and end tags.
At this point, you may be asking yourself, “Doesn’t a template rule do the same thing as xsl:for-each?” After all, a template rule processes the template instructions for each node returned from its match pattern. It is certainly true that template rules perform similar operations and are usually the best way to perform routines in XSLT. In fact, before automatically using xsl:for-each, see if you can do the same task with an ordinary template rule.
A common mistake many newcomers to XSLT make if they’ve programmed in other languages is to overuse xsl:for-each rather than simply using template rules. After all, xsl:for-each looks much more familiar to a programmer than does xsl:template. However, XSLT asks you to think differently.
I’ve found the xsl:for-each instruction works best when you need to loop through a set of nodes at the same time you are applying a template to another node.
To illustrate xsl:for-each in action, suppose I want to create a list of classes for each student in the Listing 7-1 document. I use the following stylesheet to accomplish this task:
<xsl:stylesheet version=”1.0” xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”>
<xsl:output method=”text”/>
<!-- Create list of classes for each student -->
<xsl:template match=”student”>
<xsl:value-of select=”@name”/>’s Classes:
<xsl:for-each select=”class”>
<xsl:value-of select=”@name”/>
<xsl:if test=”position()!=last()”>
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
To compile a list for each student, a template rule is created to return the student element nodes. After using an xsl:value-of instruction to write the student’s name, I use xsl:for-each to loop through each class element and print the class name to the output document.
In the result document, I’d like to separate each class name in the list with a comma, but if I just added <xsl:text>, <xsl:text> after the xsl:value-of instruction, I’d get a trailing comma at the end of my list. As a result, I add xsl:text inside an xsl:if instruction that tests using the built-in XPath functions position() and last() to determine whether the node is the last one in the given node set. (See Chapter 11 for a complete discussion on built-in functions.)
The result document is as follows:
Jordan’s Classes:
Language Arts, Reading, Writing, Geography, Math, Science, History, Art
Jared’s Classes:
Language Arts, Reading, Writing, Geography, Math, Science, History, Art
Justus’s Classes:
Language Arts, Reading, Writing, Geography, Math, Science, History, Art