Extension Elements, Extension Functions, and Fallback
Processing
Extending the Saxon Processor
More Examples
Summary
To this point, we've spent a lot of time learning how to use the built-in features of XSLT and XPath to get things done. We've also talked about the somewhat unusual processing model that makes life challenging for programmers from the world of procedural languages (a.k.a. Earth). But what do you do if you still can't do everything with XSLT and XPath?
In this section, we'll discuss the XSLT extension mechanism that allows you to add new functions and elements to the language. Unfortunately, Version 1.0 of the XSLT standard doesn't define all of the details about how these things should work, so there are some inconsistencies between processors. The good news is that if you write an extension function or element that works with your favorite processor, another vendor can't do something sinister to prevent your functions or elements from working. On the other hand, if you decide to change XSLT processors, you'll probably have to change your code.
Most examples in this chapter are written for the Xalan processor. We'll discuss how to write stylesheets that can work with multiple processors, and we'll briefly look at the differences between the various APIs supported by those processors. In addition, Xalan comes with extensions written in Java, but you can use other languages, as well. We'll look at extensions written in Jython (formerly JPython), JavaScript, and Jacl.
Section 14 of the XSLT standard defines two kinds of extensions: extension elements and extension functions. Section 15 of the specification defines fallback processing, a way for stylesheets to respond gracefully when extension elements and functions aren't available. We'll talk about these items briefly, then we'll move on to some examples that illustrate the full range of extensions.
An extension element is an element that should be processed by a piece of code external to the XSLT processor. In the case of the Java version of Xalan, our stylesheet defines the Java class that should be loaded and invoked to process the extension element. Although the implementation details vary from one XSLT processor to the next, we'll discuss how an extension element can access the XPath representation of our source document, how it can generate output, and how it can move through the XPath tree to manipulate the source document.
The whole point of extensions is to allow you to add new capabilities to the XSLT processor. One of the most common needs is the ability to generate multiple output documents. As we saw earlier, the document() function allows you to have multiple input documents -- but XSLT doesn't give us any way to create multiple output documents. Xalan, Saxon, and XT all ship with extensions that allow you to create such documents. Here's an XML document that we'll use for several of our examples in this chapter:
<?xml version="1.0"?> <book> <title>XSLT</title> <chapter> <title>Getting Started</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>The Hello World Example</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>XPath</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>Stylesheet Basics</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>Branching and Control Elements</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>Functions</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>Creating Links and Cross-References</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>Sorting and Grouping Elements</title> <para>If this chapter had any text, it would appear here.</para> </chapter> <chapter> <title>Combining XML Documents</title> <para>If this chapter had any text, it would appear here.</para> </chapter> </book>
For our first example, we want to create a stylesheet that converts the document to HTML, writing the contents of each <chapter> element to a separate HTML file. Here's what that stylesheet looks like:
<?xml version="1.0"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" extension-element-prefixes="redirect"> <xsl:output method="html"/> <xsl:template match="/"> <xsl:choose> <xsl:when test="element-available('redirect:write')"> <xsl:for-each select="/book/chapter"> <redirect:write select="concat('chapter', position(), '.html')"> <html> <head> <title><xsl:value-of select="title"/></title> </head> <body> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> <xsl:if test="not(position()=1)"> <p><a href="chapter{position()-1}.html">Previous</a></p> </xsl:if> <xsl:if test="not(position()=last())"> <p><a href="chapter{position()+1}.html">Next</a></p> </xsl:if> </body> </html> </redirect:write> </xsl:for-each> </xsl:when> <xsl:otherwise> <html> <head> <title><xsl:value-of select="/book/title"/></title> </head> <xsl:for-each select="/book/chapter"> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> </xsl:for-each> </html> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="para"> <p><xsl:apply-templates select="*|text()"/></p> </xsl:template> </xsl:stylesheet>
Let's go through the relevant parts of this example. To begin with, our <xsl:stylesheet> element defines the redirect namespace prefix and tells the XSLT engine that the prefix will be used to refer to an extension element.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" extension-element-prefixes="redirect">
The syntax of everything we've done so far is according to the standard, although there's a fair amount of latitude in what the XSLT engines do with the information we've defined. For example, when defining the redirect namespace, Xalan uses the value here as a Java class name. In other words, Xalan attempts to load the class org.apache.xalan.xslt.extensions.Redirect when it encounters an extension element or function defined with this namespace. The way other XSLT processors use the namespace URI can vary.
To this point, we've simply defined our extension class so Xalan can find our code, load it, and invoke it. Our next step is to actually use it:
<xsl:when test="element-available('redirect:write')"> <xsl:for-each select="/book/chapter"> <redirect:write select="concat('chapter', position(), '.html')"> <html> <head> <title><xsl:value-of select="title"/></title> </head> <body> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> <xsl:if test="not(position()=1)"> <p><a href="chapter{position()-1}.html">Previous</a></p> </xsl:if> <xsl:if test="not(position()=last())"> <p><a href="chapter{position()+1}.html">Next</a></p> </xsl:if> </body> </html> </redirect:write> </xsl:for-each> </xsl:when>
This code does several things:
It checks to see if our extension element is available. If it is, we'll use it; if not, the <xsl:otherwise> element will be evaluated instead.
For each <chapter> in our XML document, it calls an extension element from our Redirect class. In the example here, we're calling the <redirect:write> element, which opens a file and directs all the output generated by Xalan into that file. Notice that the filename here is generated automatically; the filename for the first <chapter> is chapter1.html, the filename for the second is chapter2.html, etc. This convenient naming convention creates a unique filename for each chapter and makes it easy to figure out which filename contains the output from each chapter.
It creates the HTML tags to define the document and its <title>. After creating the <head> section, it creates an <h1> for the chapter title, followed by a <p> generated from each <para> element in the XML source.
It generates hyperlinks between the different documents. If a given document is any chapter other than the first (not(position()=1)), it creates a link to the previous chapter. The filename for the previous chapter is generated with the expression chapter{position()-1}.html. If the document is any chapter other than the last (not(position()=last())), it creates a link to the next chapter. The filename for the next chapter is generated with the function call concat('chapter', position()+1, '.html').
In this example, we used both the curly brace notation and the <xsl:attribute> element. Both work the same way; for the rest of this chapter, we'll use the curly brace notation to save space. (For more information, see the discussion of Section 3.3, "Attribute Value Templates" in Chapter 3, "XPath: A Syntax for Describing Needles and Haystacks".)
After any required hyperlinks have been generated, it writes the closing HTML tags and ends the <redirect:write> element. Ending the <redirect:write> element closes the file.
An individual output file looks like Figure 8-1.
This particular chapter contains both a Previous and a Next link. The first chapter won't have a Previous link, and the last chapter won't have a Next; other than that, all individual chapters are formatted the same way.
That code covers how we generate multiple output files when the extension element is available. When it isn't available, we simply generate a single HTML file that contains the text of all the chapters:
<xsl:otherwise> <html> <head> <title><xsl:value-of select="/book/title"/></title> </head> <xsl:for-each select="/book/chapter"> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> </xsl:for-each> </html> </xsl:otherwise>
In the <xsl:otherwise> element, we create a single HTML element, then process each <chapter> in turn. The output is a single large file; not ideal for a really large document, but an acceptable alternative when our extension element isn't available.
In this relatively simple example, we've broken a single XML document into multiple HTML files, we've generated useful filenames for all of them, and we've automatically built hyperlinks between the different HTML files. If we add, delete, or move a chapter, we can simply rerun our stylesheet and all the files and links between them will be updated. For now, we've simply discussed how to use an extension; we'll talk about how to write your own extension later in this chapter.
So far, we've used an extension function to convert a single XML document into multiple HTML files. Unfortunately, our stylesheet only works with the Xalan XSLT processor. How can we write a stylesheet that will work with multiple XSLT processors? The answer is to define more extension elements, one for each processor. Here's a stylesheet that works with Xalan, Saxon, and XT:
<?xml version="1.0"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" xmlns:saxon="http://icl.com/saxon" xmlns:xt="http://www.jclark.com/xt" extension-element-prefixes="redirect saxon xt"> <xsl:output method="html"/> <xsl:template match="/"> <xsl:choose> <xsl:when test="contains(system-property('xsl:vendor'), 'James Clark')"> <xsl:for-each select="/book/chapter"> <xt:document method="xml" href="chapter{position()}.html"> <html> <head> <title><xsl:value-of select="title"/></title> </head> <body> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> <xsl:if test="not(position()=1)"> <p><a href="chapter{position()-1}.html">Previous</a></p> </xsl:if> <xsl:if test="not(position()=last())"> <p><a href="chapter{position()+1}.html">Next</a></p> </xsl:if> </body> </html> </xt:document> </xsl:for-each> </xsl:when> <xsl:when test="contains(system-property('xsl:vendor'), 'Apache')"> <xsl:for-each select="/book/chapter"> <redirect:write select="concat('chapter', position(), '.html')"> <html> <head> <title><xsl:value-of select="title"/></title> </head> <body> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> <xsl:if test="not(position()=1)"> <p><a href="chapter{position()-1}.html">Previous</a></p> </xsl:if> <xsl:if test="not(position()=last())"> <p><a href="chapter{position()+1}.html">Next</a></p> </xsl:if> </body> </html> </redirect:write> </xsl:for-each> </xsl:when> <xsl:when test="contains(system-property('xsl:vendor'), 'SAXON')"> <xsl:for-each select="/book/chapter"> <saxon:output href="chapter{position()}.html"> <html> <head> <title><xsl:value-of select="title"/></title> </head> <body> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> <xsl:if test="not(position()=1)"> <p><a href="chapter{position()-1}.html">Previous</a></p> </xsl:if> <xsl:if test="not(position()=last())"> <p><a href="chapter{position()+1}.html">Next</a></p> </xsl:if> </body> </html> </saxon:output> </xsl:for-each> </xsl:when> <xsl:otherwise> <html> <head> <title><xsl:value-of select="/book/title"/></title> </head> <xsl:for-each select="/book/chapter"> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> </xsl:for-each> </html> </xsl:otherwise> </xsl:choose> </xsl:template> <xsl:template match="para"> <p><xsl:apply-templates select="*|text()"/></p> </xsl:template> </xsl:stylesheet>
All we've done here is add more <xsl:when> elements, each of which tries to figure out which XSLT processor we're using. The difference here is that the XT processor doesn't implement the element-available() function, so we can't use it in any stylesheet that will be processed by XT. To get around this problem, we use the system-property() function to get the vendor property. If the vendor contains the string "James Clark," we know that we're using XT. We test the other processors similarly. If we find that we're using an XSLT processor we recognize, we use its extension functions to split the output into multiple HTML files; otherwise, we write all the output to a single file. Obviously, maintenance of this stylesheet is more involved, but it does give us the freedom to switch XSLT processors. (The other downside is that we depend on the value of the vendor system property; if the next release of Saxon identifies the vendor as Saxon instead of SAXON, our stylesheet won't work properly.)
As you might guess, an extension function is defined in a piece of code external to the XSLT processor. You can pass values to the function, and the function can return a result. That result can be any of the datatypes supported by XPath. In addition, various XSLT processors are free to allow extension functions to return other datatypes, although those other datatypes must be handled by some other function that does return one of XPath's datatypes.
As we outlined the functions and operators available in XPath and XSLT, you probably noticed that the mathematical functions at your disposal are rather limited. In this example, we'll write an extension that provides a variety of trignometric functions.
Our scenario here is that we want to generate a Scalable Vector Graphics (SVG) pie chart from an XML document. Our XML document contains the sales figures for various regions of a company; we need to calculate the dimensions of the various slices of the pie graph for our SVG document. Here's the XML source we'll be working with:
<?xml version="1.0" ?> <sales> <caption> <heading>3Q 2001 Sales Figures</heading> <subheading>($ millions)</subheading> </caption> <region> <name>Southeast</name> <product name="Heron">38.3</product> <product name="Kingfisher">12.7</product> <product name="Pelican">6.1</product> <product name="Sandpiper">29.9</product> <product name="Crane">57.2</product> </region> <region> <name>Northeast</name> <product name="Heron">49.7</product> <product name="Kingfisher">2.8</product> <product name="Pelican">4.8</product> <product name="Sandpiper">31.5</product> <product name="Crane">60.0</product> </region> <region> <name>Southwest</name> <product name="Heron">31.1</product> <product name="Kingfisher">9.8</product> <product name="Pelican">8.7</product> <product name="Sandpiper">34.3</product> <product name="Crane">50.4</product> </region> <region> <name>Midwest</name> <product name="Heron">44.5</product> <product name="Kingfisher">9.3</product> <product name="Pelican">5.7</product> <product name="Sandpiper">28.8</product> <product name="Crane">54.6</product> </region> <region> <name>Northwest</name> <product name="Heron">36.6</product> <product name="Kingfisher">5.4</product> <product name="Pelican">9.1</product> <product name="Sandpiper">39.1</product> <product name="Crane">58.2</product> </region> </sales>
Our goal is to create an SVG file that looks like that in Figure 8-2.
To make things really interesting, we'll generate an HTML page that embeds the SVG file. We'll use the Redirect extension we used earlier to generate an HTML file and an SVG file in a single transformation. If we view the HTML page in a web browser, we can use Adobe's SVG plug-in to make the graphic interactive. If we move the mouse over a given slice of the pie, the legend will change to show the sales details for that region of the company. Thus, we'll also have to create all the different legends and generate the JavaScript code to make the various SVG elements visible or hidden in response to mouse events. Figure 8-3 shows how the graphic looks if we move the mouse over the pie slice for the Southwest region.
XPath's limited math functions won't allow us to calculate the dimensions of the various arcs that make up the pie chart, so we'll use extension functions to solve this problem. Fortunately for us, Java provides all the basic trigonometric functions we need in the java.lang.Math class. Even better, Xalan makes it easy for us to load this class and execute its methods (such as sin(), cos(), and toRadians()).
We'll go over the relevant details as they appear in the stylesheet. First, we have to declare our namespace prefixes, just as we did when we used an extension element:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" extension-element-prefixes="java redirect" xmlns:java="http://xml.apache.org/xslt/java">
We associated the java namespace prefix with the string "http://xml.apache.org/xslt/java"; Xalan uses this string to support extension functions and elements written in Java. Before we use the extension functions to generate the SVG file, we'll take care of the HTML. First, we generate the <head> section of the HTML document, including the JavaScript code we need to make the pie chart interactive:
<xsl:template match="sales"> <html> <head> <title> <xsl:value-of select="caption/heading"/> </title> <script language="JavaScript1.2"> <xsl:comment> <xsl:call-template name="js"/> <xsl:text>// </xsl:text></xsl:comment> </script> </head> <body> <center> <embed name="pie" width="650" height="500" src="saleschart.svg"/> </center> </body> </html>
The HTML file we create generates an HTML <title> element from the XML document, calls the named template that generates the JavaScript code, then embeds the SVG file that we'll generate in a minute. The template we use to generate the JavaScript code is worth a closer look. Here's the code:
<xsl:template name="js"> <xsl:text> function suppress_errors () { return true; } function does_element_exist (svg_name, element_name) { // First, redirect the error handler so that if the SVG plug-in has // not yet loaded or is not present, it doesn't cause the browser to // issue a JavaScript error. var old_error = window.onerror; window.onerror = suppress_errors; // Now attempt to get the SVG object. var svgobj = document.embeds[svg_name]. getSVGDocument().getElementById(element_name); // Reset the error handler to the browser's default handler. window.onerror = old_error; // Return appropriate value. if (svgobj == null) return false; else return true; } function mouse_over (target_id) { var svgdoc = document.pie.getSVGDocument(); var svgobj; var svgstyle; var detail_name = 'details' + target_id; svgobj = svgdoc.getElementById(detail_name); if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'visible'); } </xsl:text> <xsl:for-each select="/sales/region"> <xsl:text>svgobj = svgdoc.getElementById('legend</xsl:text> <xsl:value-of select="position()"/><xsl:text>');</xsl:text> <xsl:text> if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'hidden'); } </xsl:text> </xsl:for-each> <xsl:text> // Propagate the event to other handlers. return true; } function mouse_out () { var svgdoc = document.pie.getSVGDocument(); var svgobj; var svgstyle; </xsl:text> <xsl:for-each select="/sales/region"> <xsl:text>svgobj = svgdoc.getElementById('legend</xsl:text> <xsl:value-of select="position()"/><xsl:text>');</xsl:text> <xsl:text> if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'visible'); } </xsl:text> <xsl:text>svgobj = svgdoc.getElementById('details</xsl:text> <xsl:value-of select="position()"/><xsl:text>');</xsl:text> <xsl:text> if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'hidden'); } </xsl:text> </xsl:for-each> <xsl:text> // Propagate the event to other handlers. return true; } </xsl:text> </xsl:template>
We begin with the functions (suppress_errors() and does_element_exist()) that we'll need for error checking and handling. The mouse_over() function is more complicated. When the user moves the mouse over a particular section of the pie, we need to make some SVG elements visible and others invisible. We'll use a naming convention here; for each <region> in our original document, we'll generate a legend entry and a set of details. Originally, the legend is visible and all details are hidden. When our mouse_over() function is called, it makes all the legend elements hidden and makes the appropriate details element visible. Here's how the generated code looks:
function mouse_over (target_id) { var svgdoc = document.pie.getSVGDocument(); var svgobj; var svgstyle; var detail_name = 'details' + target_id; svgobj = svgdoc.getElementById(detail_name); if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'visible'); } svgobj = svgdoc.getElementById('legend1'); if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'hidden'); } ... // Propagate the event to other handlers. return true; }
The section that begins svgdoc.getElementById('legend1') repeats for each <region> in the XML source document. The repeated code ensures that all legend elements are hidden. This code handles the mouse over event; our final task is to handle the mouse out event. The generated mouse_out() function looks like:
function mouse_out () { var svgdoc = document.pie.getSVGDocument(); var svgobj; var svgstyle; svgobj = svgdoc.getElementById('legend1'); if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'visible'); } svgobj = svgdoc.getElementById('details1'); if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'hidden'); } ... // Propagate the event to other handlers. return true; }
The mouse_out() function makes sure that all legend elements are visible and that all details elements are hidden. Although these two functions are relatively simple, they work together to make our pie chart interactive and dynamic.
TIP: One final note about our generated JavaScript code; notice how we invoked the named template inside an <xsl:comment> element:<script language="JavaScript1.2"> <xsl:comment> <xsl:call-template name="js"/> <xsl:text>// </xsl:text></xsl:comment> </script>Scripting code is typically written inside a comment in an HTML file, allowing browsers that don't support scripting to safely ignore the code. The end of the generated JavaScript code looks like this:
// Propagate the event to other handlers. return true; } // --></script>We used an <xsl:text> element to draw the double slash at the end of the script. The double slash is a JavaScript comment, which tells the JavaScript processor to ignore the --> at the end of the script. Without this slash, some JavaScript processors attempt to process the end of the comment and issue an error message. Keep this in mind when you're generating JavaScript code with your stylesheets; if you don't, you'll have trouble tracking down the occasional errors that occur in some browsers.
Now that we've built the HTML file, here's how we draw each wedge of the pie:
Calculate the total sales for the entire company and store it in a variable. Calculating total sales is relatively expensive because the XSLT processor has to navigate the entire tree. We'll need to use this value many times, so we calculate it once and store it away. Here's the calculation for total sales:
<xsl:variable name="totalSales" select="sum(//product)"/>
For each slice of the pie, we calculate certain values and pass them as parameters to the region template. First, we determine the color of the slice and the total sales for this particular region of the company. We use the position() function and the mod operator to calculate the color, and we use the sum() function to calculate the sales for this region of the company.
If this is the first slice of the pie, we'll explode it. That means that the first slice will be offset from the rest of the pie. We will set the variable $explode as follows:
<xsl:variable name="explode" select="position()=1"/>
The last value we calculate is the total sales of all previous regions. When we draw each slice of the pie, we rotate the coordinate axis a certain number of degrees. The amount of the rotation depends on how much of the total sales have been drawn so far. In other words, if we've drawn exactly 50 percent of the total sales so far, we'll rotate the axis 180 degrees (50 percent of 360). Rotating the axis simplifies the trigonometry we have to do. To calculate the total sales we've drawn so far, we use the preceding-sibling axis:
<xsl:with-param name="runningTotal" select="sum(preceding-sibling::region/product)"/>
Inside the template itself, our first step is to calculate the angle in radians of the current slice of the pie. This is the first time we use one of our extension functions:
<xsl:variable name="currentAngle" select="java:java.lang.Math.toRadians(($regionSales div $totalSales) * 360.0)"/>
We store this value in the variable currentAngle; we'll use this value later with the sin() and cos() functions.
Now we're finally ready to draw the pie slice. We'll do this with an SVG <path> element. Here's what one looks like; we'll discuss what the attributes mean in a minute:
<path onclick="return false;" onmouseout="mouse_out();" style="fill:orange; stroke:black; stroke-width:2; fillrule:evenodd; stroke-linejoin:bevel;" transform="translate(100,160) rotate(-72.24046757584189)" onmouseover="return mouse_over(2);" d="M 80 0 A 80 80 0 0 0 21.318586104178774 -77.10718440274366 L 0 0 Z "> </path>
The following stylesheet fragment generated this intimidating element:
<path style="fill:{$color}; stroke:black; stroke-width:2; fillrule:evenodd; stroke-linejoin:bevel;" onmouseout="mouse_out();" onclick="return false;"> <xsl:attribute name="transform"> <xsl:choose> <xsl:when test="$explode"> <xsl:text>translate(</xsl:text> <xsl:value-of select="(java:java.lang.Math.cos($currentAngle div 2) * 20) + 100"/> <xsl:text>,</xsl:text> <xsl:value-of select="(java:java.lang.Math.sin($currentAngle div 2) * -20) + 160"/> <xsl:text>) </xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>translate(100,160) </xsl:text> </xsl:otherwise> </xsl:choose> <xsl:text> rotate(</xsl:text> <xsl:value-of select="-1 * (($runningTotal div $totalSales) * 360.0)"/> <xsl:text>)</xsl:text> </xsl:attribute> <xsl:attribute name="onmouseover"> <xsl:text>return mouse_over(</xsl:text> <xsl:value-of select="$position"/><xsl:text>);</xsl:text> </xsl:attribute> <xsl:attribute name="d"> <xsl:text>M 80 0 A 80 80 0 </xsl:text> <xsl:choose> <xsl:when test="$currentAngle > 3.14"> <xsl:text>1 </xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>0 </xsl:text> </xsl:otherwise> </xsl:choose> <xsl:text>0 </xsl:text> <xsl:value-of select="java:java.lang.Math.cos($currentAngle) * 80"/> <xsl:text> </xsl:text> <xsl:value-of select="java:java.lang.Math.sin($currentAngle) * -80"/> <xsl:text> L 0 0 Z </xsl:text> </xsl:attribute> </path>
The style attribute defines various properties of the path, including the color in which the path should be drawn, with which the path should be filled, etc. With the exception of the color, everything in the style attribute is the same for all slices of the pie. The transform attribute does two things: it moves the center of the coordinate space to a particular point, then it rotates the axes some number of degrees. The center of the coordinate space is moved to a slightly different location if the $explode variable is true. The extent to which the axes are rotated depends on the percentage of total sales represented by the previous regions of the company. Moving the center of the coordinate space and rotating the axes simplifies the math we have to do later.
That brings us to the gloriously cryptic d attribute. This attribute contains a number of drawing commands; in our previous example, we move the current point to (80,0) (M stands for move), then we draw an elliptical arc (A stands for arc) with various properties. Finally, we draw a line (L stands for line) from the current point (the end of our arc) to the origin, then we use the Z command, which closes the path by drawing a line from wherever we are to wherever we started.
If you really must know what the properties of the A command are, they are the two radii of the ellipse, the degrees by which the x-axis should be rotated, two parameters called the large-arc-flag and the sweep-flag that determine how the arc is drawn, and the x- and y-coordinates of the end of the arc. In our example here, the two radii of the ellipse are the same (we want the pie to be round, not elliptical). Next is the x-axis rotation, which is 0. After that is the large-arc-flag, which is 1 if this particular slice of the pie is greater than 180 degrees, 0 otherwise. The sweep-flag is 0, and the last two parameters, the x- and y-coordinates of the end point, are calculated. See the SVG specification for more details on the path and shape elements.
Our next task is to draw all of the legends. We'll draw one legend to identify each slice of the pie; after that, we'll create a separate legend for each slice of the pie. Initially, all of the separate legends will be invisible (<g style="visibility:hidden">, in SVG parlance), and the basic legend for the pie will be visible. As we mouse over the various slices of the pie, different legends will become visible or invisible. First, we'll draw the basic legend, using the mode attribute of the <apply-templates> element:
<xsl:apply-templates select="." mode="legend"> <xsl:with-param name="color" select="$color"/> <xsl:with-param name="regionSales" select="$regionSales"/> <xsl:with-param name="y-legend-offset" select="90 + (position() * 20)"/> <xsl:with-param name="position" select="position()"/> </xsl:apply-templates>
When we apply our template, we pass in several parameters, including the color of the box in the legend entry and the y-coordinate offset where the legend entry should be drawn. We call this template once for each <region> element, ensuring that our legend identifies each slice of the pie, regardless of how many slices there are. For each slice, we draw a box filled with the appropriate color, with the name of the region next to it.
Our final task is to draw the details for this region of the company. We'll draw the name of the region in the same color we used for the pie slice, then list all sales figures for the region. Here's what the template looks like:
<xsl:template match="region" mode="details"> <xsl:param name="color" select="black"/> <xsl:param name="position" select="'0'"/> <xsl:param name="y-legend-offset"/> <g style="visibility:hidden"> <xsl:attribute name="id"> <xsl:text>details</xsl:text><xsl:value-of select="$position"/> </xsl:attribute> <text style="font-size:14; font-weight:bold; text-anchor:start; fill: {$color}" x="220"> <xsl:attribute name="y"> <xsl:value-of select="$y-legend-offset"/> </xsl:attribute> <xsl:value-of select="name"/><xsl:text> Sales:</xsl:text> </text> <xsl:for-each select="product"> <text style="font-size:12; text-anchor:start" x="220"> <xsl:attribute name="y"> <xsl:value-of select="$y-legend-offset + (position() * 20)"/> </xsl:attribute> <xsl:value-of select="@name"/> <xsl:text>: </xsl:text><xsl:value-of select="."/> </text> </xsl:for-each> </g> </xsl:template>
Notice that we draw this item to be invisible (style="visibility:hidden"); we'll use our JavaScript effects to make the various legends and details visible or hidden. In our stylesheet, we draw the title of the current region using the same color we used for the slice of the pie, followed by the sales figures for each product sold in this region.
Here's the complete stylesheet:
<?xml version="1.0" encoding="ISO-8859-1"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" extension-element-prefixes="java redirect" xmlns:java="http://xml.apache.org/xslt/java"> <xsl:output method="html"/> <xsl:strip-space elements="*"/> <xsl:template name="js"> <xsl:text> function suppress_errors () { return true; } function does_element_exist (svg_name, element_name) { // First, redirect the error handler so that if the SVG plug-in has // not yet loaded or is not present, it doesn't cause the browser to // issue a JavaScript error. var old_error = window.onerror; window.onerror = suppress_errors; // Now attempt to get the SVG object. var svgobj = document.embeds[svg_name]. getSVGDocument().getElementById(element_name); // Reset the error handler to the browser's default handler. window.onerror = old_error; // Return appropriate value. if (svgobj == null) return false; else return true; } function mouse_over (target_id) { var svgdoc = document.pie.getSVGDocument(); var svgobj; var svgstyle; var detail_name = 'details' + target_id; svgobj = svgdoc.getElementById(detail_name); if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'visible'); } </xsl:text> <xsl:for-each select="/sales/region"> <xsl:text>svgobj = svgdoc.getElementById('legend</xsl:text> <xsl:value-of select="position()"/><xsl:text>');</xsl:text> <xsl:text> if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'hidden'); } </xsl:text> </xsl:for-each> <xsl:text> // Propagate the event to other handlers. return true; } function mouse_out () { var svgdoc = document.pie.getSVGDocument(); var svgobj; var svgstyle; </xsl:text> <xsl:for-each select="/sales/region"> <xsl:text>svgobj = svgdoc.getElementById('legend</xsl:text> <xsl:value-of select="position()"/><xsl:text>');</xsl:text> <xsl:text> if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'visible'); } </xsl:text> <xsl:text>svgobj = svgdoc.getElementById('details</xsl:text> <xsl:value-of select="position()"/><xsl:text>');</xsl:text> <xsl:text> if (svgobj != null) { svgstyle = svgobj.getStyle(); svgstyle.setProperty ('visibility', 'hidden'); } </xsl:text> </xsl:for-each> <xsl:text> // Propagate the event to other handlers. return true; } </xsl:text> </xsl:template> <xsl:template match="/"> <xsl:apply-templates select="sales"/> </xsl:template> <xsl:template match="sales"> <html> <head> <title> <xsl:value-of select="caption/heading"/> </title> <script language="JavaScript1.2"> <xsl:comment> <xsl:call-template name="js"/> <xsl:text>// </xsl:text></xsl:comment> </script> </head> <body> <center> <embed name="pie" width="650" height="500" src="saleschart.svg"/> </center> </body> </html> <redirect:write select="concat('sales', 'chart', '.svg')"> <svg width="450" height="300"> <text style="font-size:24; text-anchor:middle; font-weight:bold" x="130" y="20"> <xsl:value-of select="caption/heading"/> </text> <text style="font-size:14; text-anchor:middle" y="40" x="130"> <xsl:value-of select="caption/subheading"/> </text> <xsl:variable name="totalSales" select="sum(//product)"/> <xsl:for-each select="region"> <xsl:variable name="regionSales" select="sum(product)"/> <xsl:variable name="color"> <xsl:choose> <xsl:when test="(position() mod 6) = 1"> <xsl:text>red</xsl:text> </xsl:when> <xsl:when test="(position() mod 6) = 2"> <xsl:text>orange</xsl:text> </xsl:when> <xsl:when test="(position() mod 6) = 3"> <xsl:text>purple</xsl:text> </xsl:when> <xsl:when test="(position() mod 6) = 4"> <xsl:text>blue</xsl:text> </xsl:when> <xsl:when test="(position() mod 6) = 5"> <xsl:text>green</xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>orange</xsl:text> </xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:variable name="explode" select="position()=1"/> <xsl:apply-templates select="."> <xsl:with-param name="color" select="$color"/> <xsl:with-param name="regionSales" select="$regionSales"/> <xsl:with-param name="totalSales" select="$totalSales"/> <xsl:with-param name="runningTotal" select="sum(preceding-sibling::region/product)"/> <xsl:with-param name="explode" select="$explode"/> <xsl:with-param name="position" select="position()"/> </xsl:apply-templates> <xsl:apply-templates select="." mode="legend"> <xsl:with-param name="color" select="$color"/> <xsl:with-param name="regionSales" select="$regionSales"/> <xsl:with-param name="y-legend-offset" select="90 + (position() * 20)"/> <xsl:with-param name="position" select="position()"/> </xsl:apply-templates> <xsl:apply-templates select="." mode="details"> <xsl:with-param name="color" select="$color"/> <xsl:with-param name="position" select="position()"/> <xsl:with-param name="y-legend-offset" select="110"/> </xsl:apply-templates> </xsl:for-each> </svg> </redirect:write> </xsl:template> <xsl:template match="region"> <xsl:param name="color" select="'red'"/> <xsl:param name="runningTotal" select="'0'"/> <xsl:param name="totalSales" select="'0'"/> <xsl:param name="regionSales" select="'0'"/> <xsl:param name="explode"/> <xsl:param name="position" select="'1'"/> <xsl:variable name="currentAngle" select="java:java.lang.Math.toRadians(($regionSales div $totalSales) * 360.0)"/> <path style="fill:{$color}; stroke:black; stroke-width:2; fillrule:evenodd; stroke-linejoin:bevel;" onmouseout="mouse_out();" onclick="return false;"> <xsl:attribute name="transform"> <xsl:choose> <xsl:when test="$explode"> <xsl:text>translate(</xsl:text> <xsl:value-of select="(java:java.lang.Math.cos($currentAngle div 2) * 20) + 100"/> <xsl:text>,</xsl:text> <xsl:value-of select="(java:java.lang.Math.sin($currentAngle div 2) * -20) + 160"/> <xsl:text>) </xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>translate(100,160) </xsl:text> </xsl:otherwise> </xsl:choose> <xsl:text> rotate(</xsl:text> <xsl:value-of select="-1 * (($runningTotal div $totalSales) * 360.0)"/> <xsl:text>)</xsl:text> </xsl:attribute> <xsl:attribute name="onmouseover"> <xsl:text>return mouse_over(</xsl:text> <xsl:value-of select="$position"/><xsl:text>);</xsl:text> </xsl:attribute> <xsl:attribute name="d"> <xsl:text>M 80 0 A 80 80 0 </xsl:text> <xsl:choose> <xsl:when test="$currentAngle > 3.14"> <xsl:text>1 </xsl:text> </xsl:when> <xsl:otherwise> <xsl:text>0 </xsl:text> </xsl:otherwise> </xsl:choose> <xsl:text>0 </xsl:text> <xsl:value-of select="java:java.lang.Math.cos($currentAngle) * 80"/> <xsl:text> </xsl:text> <xsl:value-of select="java:java.lang.Math.sin($currentAngle) * -80"/> <xsl:text> L 0 0 Z </xsl:text> </xsl:attribute> </path> </xsl:template> <xsl:template match="region" mode="legend"> <xsl:param name="color" select="'red'"/> <xsl:param name="regionSales" select="'0'"/> <xsl:param name="y-legend-offset" select="'0'"/> <xsl:param name="position" select="'1'"/> <g> <xsl:attribute name="id"> <xsl:text>legend</xsl:text><xsl:value-of select="$position"/> </xsl:attribute> <text> <xsl:attribute name="style"> <xsl:text>font-size:12; text-anchor:start</xsl:text> </xsl:attribute> <xsl:attribute name="x"> <xsl:text>240</xsl:text> </xsl:attribute> <xsl:attribute name="y"> <xsl:value-of select="$y-legend-offset"/> </xsl:attribute> <xsl:value-of select="name"/> <xsl:text> (</xsl:text> <xsl:value-of select="$regionSales"/> <xsl:text>) </xsl:text> </text> <path> <xsl:attribute name="style"> <xsl:text>stroke:black; stroke-width:2; fill:</xsl:text> <xsl:value-of select="$color"/> </xsl:attribute> <xsl:attribute name="d"> <xsl:text>M 220 </xsl:text> <xsl:value-of select="$y-legend-offset - 10"/> <xsl:text> L 220 </xsl:text> <xsl:value-of select="$y-legend-offset"/> <xsl:text> L 230 </xsl:text> <xsl:value-of select="$y-legend-offset"/> <xsl:text> L 230 </xsl:text> <xsl:value-of select="$y-legend-offset - 10"/> <xsl:text> Z</xsl:text> </xsl:attribute> </path> </g> </xsl:template> <xsl:template match="region" mode="details"> <xsl:param name="color" select="black"/> <xsl:param name="position" select="'0'"/> <xsl:param name="y-legend-offset"/> <g style="visibility:hidden"> <xsl:attribute name="id"> <xsl:text>details</xsl:text><xsl:value-of select="$position"/> </xsl:attribute> <text style="font-size:14; font-weight:bold; text-anchor:start; fill: {$color}" x="220"> <xsl:attribute name="y"> <xsl:value-of select="$y-legend-offset"/> </xsl:attribute> <xsl:value-of select="name"/><xsl:text> Sales:</xsl:text> </text> <xsl:for-each select="product"> <text style="font-size:12; text-anchor:start" x="220"> <xsl:attribute name="y"> <xsl:value-of select="$y-legend-offset + (position() * 20)"/> </xsl:attribute> <xsl:value-of select="@name"/> <xsl:text>: </xsl:text><xsl:value-of select="."/> </text> </xsl:for-each> </g> </xsl:template> </xsl:stylesheet>
In this example, we've used XSLT extension functions to add new capabilities to the XSLT processor. We needed a couple of simple trigonometric functions, and Xalan's ability to use existing Java classes made adding new capabilities simple. You can use this technique to invoke methods of Java classes anywhere you need them. Best of all, we didn't have to write any Java code to make this happen.
One of the nice features of Xalan's extension mechanism is that it uses the Bean Scripting Framework (BSF), an open source library from IBM that allows you to execute code written in a variety of scripting languages. We'll take the HTML/SVG stylesheet we just discussed and implement it again, writing the extension functions in Jython.
NOTE: Other languages supported by the Bean Scripting Framework include NetRexx, PerlScript, Jacl, Tcl, VBScript, and pnuts. If you're using a Microsoft platform, BSF also supports Windows Script Technologies, so you may have even more choices if you're running some flavor of Windows.
As you would expect, we must do several things to identify our extension code to Xalan. We'll cover them, and then look at the source of the various extension functions. First we need to define the namespace prefixes we'll use:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:jython-extension="http://www.jython.org/" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" extension-element-prefixes="redirect" xmlns:lxslt="http://xml.apache.org/xslt" exclude-result-prefixes="lxslt">
We still need the Redirect class, so that prefix is still with us. The other two prefixes are jython-extension, associated with the URL of the Jython home page (though the value could be anything), and lxslt. Xalan uses this prefix to implement scripting languages. Our next step is to actually write the Jython code. With Xalan, this code goes inside an <lxslt:component> element:
<lxslt:component prefix="jython-extension" functions="cos sin toRadians"> <lxslt:script lang="jpython"> import math def cos(d): return math.cos(d) def sin(d): return math.sin(d) def toRadians(d): return d / 180 * math.pi </lxslt:script> </lxslt:component>
The prefix attribute associates this <lxslt:component> with the jython-extension prefix, and the functions attribute lists all of the functions supported by this script. The <lxslt:script lang="jpython"> tells Xalan to use the Jython interpreter (the current version of BSF requires us to use lang="jpython", the language's former name) whenever these functions are invoked. Now that we've set everything up, all we have to do is invoke the extension functions:
<xsl:variable name="currentAngle" select="jython-extension:toRadians(($regionSales div $totalSales) * 360.0)"/>
Other than the jython-extension extension before the function call, the rest of our stylesheet is exactly the same. Notice that the Python math library does not define a toRadians function, so we had to define that function ourselves. The other two functions are part of the library, so all we had to do was invoke them.
One final point: when we invoke these extension functions written in other languages, the Java CLASSPATH must be set up correctly. If the class libraries for Jython or Javascript or whatever scripting language you're using can't be found, the extension functions will fail. Our example here uses jython.jar, available at http://www.jython.org.
We promised we'd look at extensions in JavaScript, as well. Here's how the <lxslt:component> element looks when we write the extension functions in JavaScript:
<lxslt:component prefix="javascript-extension" functions="cos sin toRadians"> <lxslt:script lang="javascript"> function cos(d) { return Math.cos(d); } function sin(d) { return Math.sin(d); } function toRadians(d) { return d * Math.PI / 180; } </lxslt:script> </lxslt:component>
Here is the <lxslt:component> element with the extension functions written in Jacl:
<lxslt:component prefix="jacl-extension" functions="cosine sine toRadians"> <lxslt:script lang="jacl"> proc cosine {d} {expr cos($d)} proc sine {d} {expr sin($d)} proc toRadians {d} {expr $d * 3.1415926535897932384626433832795 / 180.0} </lxslt:script> </lxslt:component>
Again, most of our task is to use existing features of the language. In the JavaScript and Jacl code, the cos() and sin() functions are part of the language, and we wrote our own versions of the toRadians() function. Jacl doesn't define a constant for pi, so we hardcoded the first 32 digits into the Jacl version of toRadians().
If the code that implements a given extension element can't be found, we need some relatively graceful way for the stylesheet to handle the situation. XSLT defines the <xsl:fallback> element to handle this case. In an earlier stylesheet, we used the element-available() function to determine whether a given function is available. In this case, we'll use the <xsl:fallback> to transform our document if the Redirect extension can't be found:
<?xml version="1.0"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:redirect="org.apache.xalan.xslt.extensions.Redirect" extension-element-prefixes="redirect"> <xsl:output method="html"/> <xsl:template match="/"> <xsl:for-each select="/book/chapter"> <redirect:write select="concat('chapter', position(), '.html')"> <html> <head> <title><xsl:value-of select="title"/></title> </head> <body> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> <xsl:if test="not(position()=1)"> <p><a href="chapter{position()-1}.html">Previous</a></p> </xsl:if> <xsl:if test="not(position()=last())"> <p> <a> <xsl:attribute name="href"> <xsl:value-of select="concat('chapter', position()+1, '.html')"/> </xsl:attribute> Next </a> </p> </xsl:if> </body> </html> <xsl:fallback> <xsl:if test="position()=1"> <html> <head> <title><xsl:value-of select="/book/title"/></title> </head> <body> <xsl:for-each select="/book/chapter"> <h1><xsl:value-of select="title"/></h1> <xsl:apply-templates select="para"/> </xsl:for-each> </body> </html> </xsl:if> </xsl:fallback> </redirect:write> </xsl:for-each> </xsl:template> <xsl:template match="para"> <p><xsl:apply-templates select="*|text()"/></p> </xsl:template> </xsl:stylesheet>
In our example, we only invoke the fallback processing once. This approach assumes that if something's wrong with the extension, it will fail the first time and be completely inaccessible. Using <xsl:fallback>, we know that the contents of the <xsl:fallback> element will be invoked if anything goes wrong when the stylesheet processor attempts to use an extension element. If you'd like more complete control over fallback processing, you can use the element-available() and function-available() functions as we did in our earlier example.
Copyright © 2002 O'Reilly & Associates. All rights reserved.