级别: 初级 Doug Tidwell (dtidwell@us.ibm.com), 计算机传道士, IBM
2003 年 6 月 01 日 本章根据 O'Reilly 书籍 XSLT 改编,它显示了如何创建 XSLT 扩展函数和扩展元素,演示了如何使用它们来生成交互式圆饼图、查询数据库并根据样式表构建 JPEG 图形。加上有关使用停止处理的详细信息,它告诉样式表:当扩展元素和函数不可用时如何发挥作用。大量的代码样本演示了在 Xalan for Java、Saxon、Jython(JPython)、JavaScript 和 Jacl 中使用 XSLT 扩展的方法。
经出版商 O'Reilly and Associates 许可使用。
本摘录来自书籍的后面部分,假设您已经知道如何使用 XSLT 和 XPath 的内置特性来完成一些事情(如果需要某些背景知识, 请参阅
参考资料)。但是,如果使用 XSLT 和 XPath 仍不能完成需要做的每件事情,那么您怎么办呢?
那就是 XSLT 扩展机制的所在;它允许将新函数和元素添加到语言。 遗憾的是,XSLT 标准的版本 1.0 没有定义关于这些事情应该如何工作的所有详细信息, 所以在 XSLT 处理器之间存在一些不一致性。 好消息是,如果您编写了与您所喜爱的处理器一起使用的扩展函数或元素,那么另一个供应商就不能干某些有害的事情来防止您的函数或元素工作。 另一方面,如果决定更改 XSLT 处理器,那么您可能不得不更改代码。
本摘录中的大多数示例都是为 Xalan 处理器编写的(请参阅
参考资料)。我们将讨论如何编写可以与多个处理器一起使用的样式表, 而且还将简要地查看那些处理器所支持的各种 API 之间的差异。 另外,Xalan 附带了 Java 语言编写的扩展,但也可以使用其它语言。 我们将查看以 Jython(以前是 JPython)、JavaScript 和 Jacl 编写的扩展。
 |
书籍 XSLT
本文是根据 O'Reilly 的 developerWorks 内部 XML 专家 Doug Tidwell 的书籍
XSLT(于 2001 年 9 月出版)的第 8 章改编的。有关更多信息,请参阅该书的 O'Reilly 页面(请参阅
参考资料)。
|
|
扩展元素、扩展函数和停止处理
XSLT 标准的第 14 节定义了两种类型的扩展:扩展元素和扩展函数。 规范的第 15 节定义了停止处理,是样式表在扩展元素和函数不可用时作出适度响应的方法。我们将简要地讨论这些项,然后转到一些阐明扩展的完整范围的示例。
扩展元素
扩展元素是应该由在 XSLT 处理器外部的一段代码处理的元素。 以 Xalan 的 Java 版本为例,样式表定义应该装入并调用以处理扩展元素的 Java 类。 尽管实现细节随着从一个 XSLT 处理器到下一个处理器的变化而改变, 我们将讨论扩展元素如何访问源文档的 XPath 表示、它如何生成输出以及它如何通过 XPath 树移动来操纵源文档。
示例:生成多个输出文件
扩展的全部要点是允许您将新能力添加到 XSLT 处理器。最公共的需要之一是生成多个输出文档的能力。就象我们早先看到的一样,
document() 函数允许有多个输入文档 ― 但 XSLT 没有向我们提供任何创建多个输出文档的方法。Xalan、Saxon 和 XT 都附带允许您创建这种文档的扩展。这里是一个 XML 文档,将用于本摘录中的几个示例:
<?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> |
对于第一个示例,希望创建一个将文档转换成 HTML 的样式表, 并将每个
<chapter> 元素的内容写入一个单独的 HTML 文件。
这里是样式表的内容。
让我们一起完成该示例的相关部分。首先,
<xsl:stylesheet> 元素定义了
redirect 名称空间前缀并告诉 XSLT 引擎该前缀将用于引用扩展元素。
<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"> |
虽然 XSLT 引擎在处理我们所定义的信息时有相当大的自由度,但迄今为止我们所做的每件事的语法都按照该标准。例如,当定义
redirect 名称空间时,Xalan 将这里的值用作 Java 类名。 换句话说,当 Xalan 遇到用该名称空间定义的扩展元素或函数时,它试图装入类
org.apache.xalan.xslt.extensions.Redirect 。其它 XSLT 处理器使用名称空间 URI 的方法可能不同。
至此,我们简单地定义了扩展类,以便 Xalan 可以找到代码,装入它并调用它。 下一步是要实际使用它:
<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> |
该代码做几件事情:
个别输出文件看起来如图 1。
图 1. 个别输出文件
这个特殊章节同时包含
Previous 和
Next 链接。第一章不会有
Previous 链接,最后一章不会有
Next ;此外,所有个别章节都以相同方法编排格式。
该代码涉及当扩展元素可用时如何生成多个输出文件。 当它不可用时,只生成一个包含所有章节文本的 HTML 文件:
<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:otherwise> 元素中,创建单个 HTML 元素, 然后依次处理每个
<chapter> 。输出是单个大文件;对于真正大的文档并不理想, 但当扩展元素不可用时,它是一个可接受的替代方法。
在这个相对简单的示例中,已经将单个 XML 文档分成多个 HTML 文件, 已经为它们的全部生成了有用的文件名,并且已自动构建了不同 XML 文件之间的超链接。 如果添加、删除或移动一个章节,只要重新运行样式表, 就会更新所有文件以及它们之间的所有链接。目前为止, 我们只讨论了如何使用扩展;稍后,将在本摘录中讨论如何编写您自己的扩展。
示例:使用来自多个处理器的扩展函数
目前为止,我们已使用了扩展函数,来将单个 XML 文档转换成多个 HTML 文件。 不幸的是,我们的样式表只能与 Xalan XSLT 处理器一起使用。 如何编写将与多个 XSLT 处理器一起使用的样式表呢?答案是,要定义更多的扩展元素,每个处理器一个扩展元素。这里是
与 Xalan、Saxon 和 XT 一起使用的样式表。
这里我们已做的所有事情是添加更多的
<xsl:when> 元素, 每一个尝试断定我们正在使用的那个 XSLT 处理器。这里的差别是,XT 处理器不执行
element-available() 函数,所以我们不能在 XT 将处理的任何样式表中使用它。要避开这个问题,我们使用
system-property() 函数来获得
vendor 特性。如果供应商包含字符串“James Clark”, 那么我们知道我们正在使用 XT。我们用相似的方法测试其它处理器。如果发现正在使用的 XSLT 处理器是我们认可的,则使用它的扩展函数,将输出拆分成多个 HTML 文件; 否则,将所有输出写入单个文件中。 很明显,该样式表的维护更为复杂,但它提供了切换 XSLT 处理器的自由。 (其它不利方面是我们依赖
vendor 系统特性的值; 如果 Saxon 的下一个发行版将供应商标识为
Saxon ,而不是
SAXON ,那么我们的样式表将不能正确工作。)
扩展函数
如您猜测的那样,在 XSLT 处理器外部的一段代码中定义扩展函数。 可以将值传递给该函数,而该函数可以返回结果。 那个结果可以是受 XPath 支持的任何数据类型。另外,各种 XSLT 处理器可以自由地允许扩展函数返回其它数据类型,尽管这些其它数据类型必须由确实返回 XPath 的数据类型之一的某一其它函数处理。
示例:三角函数库
在我们概述 XPath 和 XSLT 中的可用函数和运算符时,您或许会注意到,您可使用的数字函数是相当有限的。 在这个示例中,我们将编写一个提供多种三角函数的扩展。
我们这里的方案是想要从 XML 文档生成“可伸缩向量图形(SVG)”饼图。XML 文档包含公司各区域的销售数字; 我们需要为 SVG 文档计算饼图各部分的大小。 这里是将使用的 XML 源代码:
<?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> |
目的是创建类似图 2 的 SVG 文件。
图 2. 目标 SVG 文件格式
为了使事情变得真正有趣,我们将生成嵌入 SVG 文件的 HTML 页面。将使用早先使用的
Redirect 扩展,以在单一转换中生成一个 HTML 文件和一个 SVG 文件。 如果在 Web 浏览器中查看 HTML 页面,可以使用 Adobe 的 SVG 插件,使图形成为交互式的。 如果将鼠标移到给定的扇形区上,图注将改成显示公司那个区域的销售详细信息。 因此,我们还必须创建所有不同图注并生成 JavaScript 代码, 以使各种 SVG 元素变为可视的或隐藏的,以响应鼠标事件。 图 3 显示了 Southwest 区域的扇形区上移动鼠标时图看起来的样子。
图 3. 响应鼠标事件的 SVG 图表变化
XPath 的有限数学函数不允许我们计算构成饼图的各种弧的大小, 所以将使用扩展函数来解决这个问题。幸运的是,Java 在
java.lang.Math 类中提供了我们所需的所有基本三角函数。 更幸运的是,Xalan 使装入该类并执行其方法(如
sin() 、
cos() 和
toRadians() )变得更加容易。
当相应的详细信息出现在样式表中时,我们将对它们仔细检查。 首先,必须声明名称空间前缀,就象在使用扩展元素时所做的那样:
<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"> |
将
java 名称空间前缀与字符串“http://xml.apache.org/xslt/java”相关联;Xalan 使用该字符串来支持以 Java 编写的扩展函数和元素。 在使用扩展函数生成 SVG 文件之前,将考虑 HTML。 首先,生成 HTML 文档的
<head> 部分
, 包括使饼图变成交互式所需的 JavaScript 代码。
创建的 HTML 文件从 XML 文档生成一个 HTML
<title> 元素, 调用生成 JavaScript 代码的已命名的模板,然后嵌入将很快生成的 SVG 文件。 用来生成 JavaScript 代码的模板值得仔细看一看。这里是代码:
<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> |
我们从错误检查和处理所需的函数(
suppress_errors() 和
does_element_exist() )开始。
mouse_over() 函数更为复杂。 当用户在饼图的特定区域上移动鼠标时,需要使某些 SVG 元素变成可视的而使其它的不可见。这里将使用命名约定;对于原始文档中的每个
<region> , 将生成一个图注项和一组详细信息。 最初,图注是可视的,而且所有详细信息都是隐藏的。 当调用
mouse_over() 函数时,它使所有图注元素变成隐藏的, 并且使适当的详细信息元素变成可视的。这里是生成代码的样子:
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;
} |
对于 XML 源文档中的每个
<region> 重复以
svgdoc.getElementById('legend1') 开始的部分。重复的代码确保所有图注元素都是隐藏的。该代码处理鼠标越过事件;最终任务是处理鼠标离开事件。 生成的
mouse_out() 函数如下:
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;
} |
mouse_out() 函数确保所有图注元素都是可视的,而所有详细信息元素都是隐藏的。虽然这两个函数都相对简单,它们一起工作使饼图变成交互式和动态的。
|
提示:生成的 JavaScript
有关已生成的 JavaScript 代码的注解;注意调用
<xsl:comment> 元素内部已命名模板的方法:
<script language="JavaScript1.2">
<xsl:comment>
<xsl:call-template name="js"/>
<xsl:text>// </xsl:text></xsl:comment>
</script>
|
通常,脚本代码都是写在 HTML 文件中的一个注释内, 允许不支持脚本的浏览器安全地忽略该代码。 生成的 JavaScript 代码的结束部分类似这样:
// Propagate the event to other handlers.
return true;
}
// --></script>
|
使用
<xsl:text> 元素在脚本的结束部分绘制双斜杠。 双斜杠是 JavaScript 注释,它告诉 JavaScript 处理器忽略脚本结束部分的
--> 。如果没有这个斜杠,一些 JavaScript 处理器会试图处理注释的结束部分并发出错误消息。当使用样式表生成 JavaScript 代码时,请记住这一点;如果没有这个斜杠,那么在跟踪某些浏览器中 偶然发生的错误时会遇到麻烦。
|
现在,我们已经构建了 HTML 文件,这里是绘制饼图的每一块的方法:
-
计算整个公司的销售总额并将它存储在一个变量中。 计算销售总额的代价高昂,因为 XSLT 处理器必须浏览整个树。 需要多次使用这个值,所以对它计算一次并将它存储起来。这里是销售总额的计算:
<xsl:variable name="totalSales" select="sum(//product)"/>
|
-
对于每个扇形区,计算某些值并将它们作为参数传递给
region 模板。首先,确定扇形区的颜色和公司的特定区域的销售总额。 使用
position() 函数和
mod 运算符计算颜色, 使用
sum() 函数计算公司这个区域的销售。
-
如果这是第一个扇形区,我们将分离它。这意味着,第一个扇形区将从剩余的饼图偏移。 如下设置变量
$explode :
<xsl:variable name="explode" select="position()=1"/>
|
-
最后计算的一个值是所有以前区域的销售总额。 当绘制每个扇形区时,将坐标轴旋转一定度数。 旋转的度数取决于到目前为止已经绘制的销售总额。 换句话说,如果到目前为止刚好绘制了销售总额的 50%,则将轴旋转 180 度(360 的 50%)。 旋转轴简化了我们必须执行的三角学算法。要计算到目前为止已经绘制的销售总额, 使用
preceding-sibling 轴:
<xsl:with-param name="runningTotal"
select="sum(preceding-sibling::region/product)"/>
|
-
在模板本身内部,第一步是要计算当前扇形区弧的角度。 这是第一次使用扩展函数之一:
<xsl:variable name="currentAngle"
select="java:java.lang.Math.toRadians(($regionSales div
$totalSales) * 360.0)"/>
|
将这个值存储在变量
currentAngle 中; 稍后将这个值用于
sin() 和
cos() 函数。
-
现在,我们终于准备绘制扇形区。将使用 SVG
<path> 元素完成它。
这里是其中一个看起来的样子;我们将马上讨论属性的含义。
下列样式表片段生成了这个填充元素。
style 属性定义路径的各种特性, 包括应该用什么颜色绘制路径,以及应该填充哪个路径等等。 除了颜色以外,对于所有扇形区,
style 属性中的每一项都是相同的。
transform 属性完成两件事:它将坐标空间的中心移到特定点, 然后将轴旋转一定的度数。 如果
$explode 变量是真,坐标空间的中心被移到略微不同的位置。 轴的旋转程度取决于由公司的以前区域表示的销售总额的百分比。移动坐标空间的中心并旋转轴简化了我们稍后必须完成的数学计算。
那把我们带到非常神秘的
d 属性。这个属性包含许多绘制命令; 在上一个示例中,将当前点移到
(80,0) (
M 代表移动), 然后绘制带各种特性的椭圆弧(
A 代表弧)。最后, 从当前点(弧的末端)到起始点绘制一条线(
L 代表线), 然后使用
Z 命令,该命令通过从我们所要到的地方到起始点绘制一条线来封闭路径。
如果真的必须知道
A 命令的特性是什么,它们是椭圆的两条半径、x-轴应该旋转的度数、称为 large-arc-flag 和 sweep-flag 的两个参数(它们确定绘制弧的方法)以及弧末端的 x 和 y 坐标。在这里的示例中,椭圆的两个半径是相同的(希望饼图是圆的,而不是椭圆的)。其次是 x-轴旋转,它是
0 。接下来是 large-arc-flag, 如果这个特定的扇形区大于 180 度,则它是
1 ,否则为
0 。sweep-flag 是
0 ,计算最后两个参数(端点的 x 和 y 坐标)。 有关
path 和
shape 元素的更多详细信息,请参阅 SVG 规范。
-
下一个任务是绘制所有图注。我们将绘制一个图注来标识每个扇形区; 接着,将为每个扇形区创建一个单独的图注。 最初,所有单独图注将是不可见的(在 SVG 用法中,
<g style="visibility:hidden"> ), 而饼图的基本图注将是可视的。当鼠标在各种扇形区上移动时,不同的图注将变成可视的或不可视的。 首先,使用
<apply-templates> 元素的
mode 属性绘制基本图注:
<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>
|
当应用模板时,我们传入几个参数,包括图注项中框的颜色以及应该在其上绘制图注项的 y 轴偏移量。不管有多少个扇形区,为每个
<region> 元素调用这个模板一次, 确保图注标识每个扇形区。对于每个扇形区,绘制一个框并且用适当的颜色填充,它的旁边带有区域名。
-
最后的任务是绘制公司的这个区域的详细信息。 将用扇形区所使用的颜色来绘制区域名,然后列出该区域的所有销售数字。
这里是模板看起来的样子。
注意,将该项绘制成不可视的(
style="visibility:hidden" ); 将使用 JavaScript 效果来使各种图注和详细信息变成可视的或隐藏的。 在样式表中,使用扇形区所使用的颜色来绘制当前区域的标题,后跟每个产品在该区域销售的销售数字。
这里是完整的样式表。
在这个示例中,我们使用了 XSLT 扩展函数,来将新能力添加到 XSLT 处理器。 需要几个简单的三角函数,以及 Xalan 使用现有 Java 类以使添加新能力变得简单的能力。 可以在需要 Java 类方法的任何地方使用这一技术来调用它们。 最好的是,我们不必编写任何 Java 代码就可以调用它们。
 |
注意:关于 BSF
Bean Scripting Framework 支持的其它语言包括 NetRexx、PerlScript、Jacl、Tcl、VBScript 和 pnuts。 如果正在使用 Microsoft 平台,BSF 还支持“Windows 脚本技术”, 如果正在运行 Windows 的某些版本,您甚至可能有更多选项。
|
|
示例:用其它语言编写扩展
Xalan 的扩展机制的优良特性之一是,它使用 Bean Scripting Framework(BSF), 来自 IBM 的一种开放源码库,它允许您执行用多种脚本编制语言编写的代码。 我们将采用刚才讨论的 HTML/SVG 样式表并再次实现它,用 Jython 编写扩展函数。
正如您可能期望的那样,我们必须完成几件事情,以向 Xalan 标识扩展代码。我们将涵盖它们,然后查看各种扩展函数的源代码。 首先,需要定义将使用的名称空间前缀:
<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"> |
仍需要
Redirect 类,以便我们仍可以使用这个前缀。其它两个前缀是与 Jython 主页的 URL 相关的
jython-extension (虽然值可以是任何类型)和
lxslt 。Xalan 使用这个前缀来实现脚本编制语言。下一步是真正编写 Jython 代码。使用 Xalan,
这个代码进入
<lxslt:component> 元素的内部。
prefix 属性将这个
<lxslt:component> 与
jython-extension 前缀关联, 并且
functions 属性列出了该脚本支持的所有函数。
<lxslt:script lang="jpython"> 告诉 Xalan,每当调用这些函数时,就使用 Jython 解释器(BSF 的当前版本要求使用
lang="jpython" ,即这个语言的以前名称)。 现在,我们已经设置了每件事情,我们要做的是调用扩展函数:
<xsl:variable name="currentAngle"
select="jython-extension:toRadians(($regionSales div
$totalSales) * 360.0)"/> |
除了函数调用之前的
jython-extension 扩展外,样式表的其余部分都完全相同。 注意,Python
math 库没有定义
toRadians 函数, 所以我们必须自己定义该函数。其它两个函数是库的一部分,所以我们要做的就是调用它们。
最后一点:当调用这些用其它语言编写的扩展函数时,Java
CLASSPATH 的设置必须正确。如果找不到 Jython 或 Javascript 的类库或正在使用的脚本语言,则扩展函数将失败。 这里的示例使用可从
http://www.jython.org 获得的
jython.jar 。
我们承诺过还会查看用 JavaScript 编写的扩展。当用 JavaScript 编写扩展函数时,
这里是
<lxslt:component> 元素看起来的样子
。
这里是用 Jacl 编写的
带扩展函数的
<lxslt:component> 元素
。
此外,大多数任务是使用语言的现有特性。在 JavaScript 和 Jacl 代码中,
cos() 和
sin() 函数是语言的一部分, 并且我们编写自己的
toRadians() 函数版本。Jacl 没有为
pi 定义常数,所以我们将前 32 位数字硬编码到
toRadians() 的 Jacl 版本中。
停止处理
如果找不到实现给定扩展元素的代码,则样式表需要某种相对适合的方法来处理这种情况。XSLT 定义
<xsl:fallback> 元素来处理这种情况。在前面的样式表中,我们使用了
element-available() 函数来确定给定的函数是否可用。 在这种情况下,如果找不到
Redirect 扩展,则将使用
<xsl:fallback> 来转换文档
。
在示例中,只调用停止处理一次。这种方法假设: 如果扩展有错误,它将首次失败并且完全不能使用它。 使用
<xsl:fallback> ,我们知道,如果在样式表处理器试图使用扩展元素时任何事情出错,那么将调用
<xsl:fallback> 元素的内容。 如果在停止处理时想要有更完整的控制,则可以按照我们在前面的示例中执行的那样使用
element-available() 和
function-available() 函数。
扩展 Saxon 处理器
Michael Kay 的出色的 Saxon 处理器还提供了一种扩展机制。Saxon 的扩展性机制的优良特性之一是,可以实现您自己的排序功能。当在以前几章中讨论
<xsl:sort> 元素时, 我们提到它有
lang 属性,用于定义要排序的某些内容的语言。Xalan 当前不支持这种属性(虽然在阅读本文之时, 它可能支持),Saxon 让您创建您自己的扩展函数以处理排序。扩展函数必须扩展
com.icl.saxon.sort.TextComparer 类。这里是将用来阐明该函数的样本 XML 文档:
<?xml version="1.0"?>
<wordlist>
<word>campo</word>
<word>luna</word>
<word>ciudad</word>
<word>llaves</word>
<word>chihuahua</word>
<word>arroz</word>
<word>limonada</word>
</wordlist> |
这个文档包含西班牙单词,其排序不同于用英文排序。 (在西班牙语中,“ch”和“ll”是分别排在“c”和“l”之后的单独字母。) 我们将编写使用三个
<xsl:template> 的样式表来阐明扩展函数是如何工作的。
这里是样式表。
当对文档运行样式表时,它用三种不同的
mode 调用这三个模板。 第一个模板只按照
<word> 元素在原始文档中出现的那样列出它们, 第二个模板使用缺省排序序列来排序
<word> 元素, 第三个模板使用西班牙排序的传统规则来排序
<word> 元素。 相当与众不同的是,实现排序功能的代码是简单的。 这里是完整的清单:
package com.icl.saxon.sort;
import java.text.ParseException;
import java.text.RuleBasedCollator;
import java.util.Locale;
public class Compare_es extends TextComparer
{
private static String smallnTilde = new String("\u00F1");
private static String capitalNTilde = new String("\u00D1");
private static String traditionalSpanishRules =
("< a,A < b,B < c,C " +
"< ch, cH, Ch, CH " +
"< d,D < e,E < f,F " +
"< g,G < h,H < i,I < j,J < k,K < l,L " +
"< ll, lL, Ll, LL " +
"< m,M < n,N " +
"< " + smallnTilde + "," + capitalNTilde + " " +
"< o,O < p,P < q,Q < r,R " +
"< s,S < t,T < u,U < v,V < w,W < x,X " +
"< y,Y < z,Z");
private static RuleBasedCollator rbc = null;
static
{
try
{
rbc = new RuleBasedCollator(traditionalSpanishRules);
}
catch (ParseException pe)
{
System.err.println("Error creating RuleBasedCollator: " + rbc);
}
}
public int compare(Object a, Object b)
{
if (rbc != null)
return rbc.compare((String)a, (String)b);
else
return 0;
}
} |
(有关
traditionalSpanishRules 字符串的说明,请参阅
java.text.RuleBasedCollator 类的文档。)
当 Saxon 遇到
lang 属性为
es 的
<xsl:sort> 元素,它试图装入名为
com.icl.saxon.sort.Compare_es 的 Java 类。如果可以装入那个类, 那么当 Saxon 排序
<word> 元素时,它调用该类的
compare 方法。当对前面的示例文档运行样式表时,结果如下:
Word list - unsorted:
campo
luna
ciudad
llaves
chihuahua
arroz
limonada
Word list - sorted with default rules:
arroz
campo
chihuahua
ciudad
limonada
llaves
luna
Word list - sorted with Spanish rules:
arroz
campo
ciudad
chihuahua
limonada
luna
llaves
|
在输出中,西班牙语排序例程将
chihuahua 放在
ciudad 后面,并且将
llaves 放在
luna 后面。 使用少于 20 行的代码,我们已经能够将新的排序功能添加到样式表中。大多数工作都是由 Saxon 处理器和
java.text.RuleBasedCollator 类的方法为我们完成的。
Saxon 文档包含有关用您自己的代码扩展 Saxon 的更多信息。 当查看本摘录中的示例时,需要编写的大多数 Java 扩展将是一段简单代码, 它仅仅使 Java 库方法和类可用于 XSLT 处理器。
更多示例
可以使用 XSLT 扩展机制,以将 XSLT 处理扩展到文本或标记生成之外,以及从非 XML 源码读取信息。
从 XML 内容生成 JPEG 文件
在将 XML 内容转换成 Web 站点的 HTML 文件时,有时想要对一段文本的外观有完整控制。 在这个示例中, 将使用扩展函数使 XML 元素的文本转换成 JPEG 图形。代码将装入 JPEG 背景图,在它上面绘制 XML 文档的文本,然后将图形写入新的 JPEG 文件中。将重新使用第一个示例中的 XML 文件来演示扩展函数。
样式表将每个
<title> 元素传递到扩展函数。调用扩展时,还将传入背景 JPEG 的名称、输出文件的名称(我们称为
title1.jpg、
title2.jpg等等)以及有关字体名、字体大小和其它参数的各种信息。
这里是样式表看起来的样子。
背景 JPEG 看起来如图 4。
图 4. 背景 JPEG 图像
图 5 显示了由 XML 样本文档、样式表和扩展函数组合创建的几个图形。
图 5. XML <title> 元素的已生成 JPEG 文件
这些文件分别是
title1.jpg和
title8.jpg。扩展函数已经获取适当的
<title> 元素的文本,在背景图的上面绘制它, 然后将新图像作为 JPEG 图形写出。
让我们看一下对扩展函数的调用:
<xsl:value-of
select="jpeg:createJPEG(title, 'bg.jpg', <br>
concat('title', position(), '.jpg'),
'Swiss 721 Bold Condensed', 'BOLD', 22, 52, 35)"/> |
首先,看一下调用本身。我们这里编写的是作为函数名的
jpeg:createJPEG 。在样式表中定义名称空间前缀
jpeg 。将该前缀与字符串
xalan://JPEGWriter 关联;该字符串告诉 Xalan:应该把用该前缀调用的任何函数作为已命名类
JPEGWriter 的方法。如果使用的 XSLT 处理器不是 Xalan,那么定义和调用扩展函数的方法或许会不同。
接着,让我们看一下函数的参数。传递八个不同参数:
- 应该写在 JPEG 图像中的文本。该文本作为
NodeList 传入, 它是可在 Xalan API 中使用的数据类型之一。在上一个示例中,选择了包含在当前节点中的所有
<title> 元素。
- 应当使用的背景图像的文件名。该文件名作为
String 传入。
- 创建的 JPEG 图像的文件名。将创建该图像,然后写入该文件名中。 注意,在示例中,通过并置字符串“title”、当前节点的位置以及字符串“.jpg”来生成文件名。 这个过程确保所有的标题图都有唯一的文件名。它还使确定哪个 JPEG 与给定的
<title> 元素匹配很方便。
-
想要使用的字体的名称。该名称是
String 。
- 想要使用的字体样式。我们已经编写了接受三个不同值(
BOLD 、
ITALIC 和
BOLDITALIC )的函数。这些值反映了 Java
Font 类使用的那三个值。
- 字体的点大小。注意,该字体大小作为 Java
Double 传递到扩展函数;XPath 和 XSLT 没有定义
Integer 类型。函数所做的第一件事情是, 将
Double 值转换成
int 以简化算术指令。
- 文本应该开始的 x 偏移。使用 Java
Canvas 对象, 其坐标系统从左上角开始。x 偏移值确定应该从背景 JPEG 上的哪个位置开始绘制文本。 与字体大小一样,该值是转换成
int 的
Double 。
- 文本应该开始的 y 偏移。
的确可以修改这个函数以支持其它选项,如文本的颜色、文本上阴影效果的深度、阴影的位置等等。还可以创建带不同方法说明的不同函数版本,从而允许一些对
createJPEG 函数的调用默认某些参数。这种方法的好处是, 可以通过更改 XSLT 样式表来访问扩展函数中的各种行为。
这里是
扩展函数的代码本身。
注意,使用
while 循环来检查字体大小。如果用当前字体大小绘制文本字符串将在图形内无法适合,我们将尝试减少字体大小,直到它适合为止。假设这个
<chapter> 元素:
<chapter>
<title>A chapter in which the title is so very long, most people
don't bother reading it</title>
<para>If this chapter had any text, it would appear here.</para>
</chapter> |
扩展生成如图 6 中所示的 JPEG。
图 6. 生成的带有太多文本的图像
访问带扩展元素的数据库
在这个示例中,将要构建一个扩展元素,而不是扩展函数。编写扩展函数时,我们所必须担心的就是函数调用上传递给我们的数据。 我们没有考虑文档树、上下文或其它任何事情。但是,有了扩展元素,我们必须更加知道文档作为一个整体。 代码使用扩展元素的属性来连接数据库,运行查询,然后将结果集作为节点集(明确地,Xalan
XNodeSet )返回。那个节点集将被插入输出树,为我们提供动态构建文档的能力。XML 文档定义了数据库访问和查询的参数,然后扩展元素在后台如魔法般地执行任务。 这里是 XML 文档看起来的样子:
<?xml version="1.0"?>
<report>
<title>HR employee listing</title>
<section>
<title>Employees by department</title>
<dbaccess driver="COM.ibm.db2.jdbc.app.DB2Driver"
database="jdbc:db2:sample" tablename="employee" where="*"
fieldnames='workdept as "Department", lastname as "Last Name",
firstnme as "First Name"'
order-by="workdept" group-by="workdept, lastname, firstnme"/>
</section>
</report> |
注意:XML 文档的属性中包含有关数据库连接的所有供应商特定的信息。 那意味着,我们可以将扩展元素与任何符合 JDBC 的数据库一起使用。 下列
文档也正好起作用。
第一个清单使用 DB2,第二个清单使用 Sybase,最后一个清单使用 MySQL。 样式表使用数据库访问扩展元素,以用填充了数据库查询结果的 HTML 表替换
<dbaccess> 元素。 在样本文档中,XML 输入精确地反映了将用来与数据库交互的 SQL 语句。 扩展元素使用
<dbaccess> 元素的元素和属性,从数据库取出数据, 然后相应地对它进行格式化。
调用扩展元素的样式表类似于这样:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:db="DatabaseExtension"
extension-element-prefixes="db">
<xsl:output method="html"/>
<xsl:template match="/">
<html>
<head>
<title>
<xsl:value-of select="report/title"/>
</title>
</head>
<body>
<h1>
<xsl:value-of select="report/title"/>
</h1>
<xsl:for-each select="report/section">
<h2>
<xsl:value-of select="title"/>
</h2>
<xsl:for-each select="dbaccess">
<db:accessDatabase/>
</xsl:for-each>
</xsl:for-each>
</body>
</html>
</xsl:template>
</xsl:stylesheet> |
样式表很简单。名称空间声明
xmlns:db="xalan://DatabaseExtension" 将 Java 类
DatabaseExtension 与名称空间前缀
db 关联。 无论样式表何时处理名称空间前缀为
db 的 XML 元素,都会调用我们的代码来完成处理。 注意,在样式表中,使用了扩展元素
<db:accessDatabase> ; 这告诉 Xalan 调用
DatabaseExtension 类的
accessDatabase() 方法。
在这个示例中,我们希望扩展元素查看
<dbaccess> 元素的各种属性,从它在那里找到的信息构建 SQL 查询, 连接到请求的数据库,然后放入表示数据库查询结果的结果树元素。 要使示例保持简单,将使扩展元素在一个 HTML
<table> 元素中返回那些结果;如果希望的话,可以编写扩展元素来生成其它类型的输出。扩展元素返回
XNodeSet ;返回的
XNodeSet 中的节点被添加到结果树中。
为了使扩展元素起作用,它必须完成下列几件事情:
- 查找需要处理的
<dbaccess> 元素。
- 使用
<dbaccess> 元素的
driver 属性来确定要使用的 JDBC 驱动程序。一旦有了这个值,就需要装入驱动程序。通过指定数据库驱动程序, 允许我们在同一个 XML 文档中使用不同的数据库。 在前面的样本 XML 文件中,三个查询指定由 DB2、Sybase 和 MySQL 管理的数据库; 因为 JDBC 本身是供应商无关的,可以将扩展元素与任何符合 JDBC 的数据库一起使用。
- 检查
<dbaccess> 元素的
tablename 、
where 、
fieldnames 、
group-by 和
order-by 属性,以构建 SQL 查询语句。
- 连接到由
<dbaccess> 元素的
tablename 属性指定的数据库。
- 执行查询语句。
- 根据 JDBC
ResultSet 对象中的项构建表。要构建表,必须获得一个DOM
Document 对象;将该对象用作 factory 方法, 以在扩展元素返回的节点集中创建所有节点。将创建
<table> 元素,然后对于结果集中的每行,将创建一个
<tr> 元素(使用适当的
<td> 元素作为其子元素)并将它附加到该表。对于 Xalan,使用
DOMHelper 类来获得将用于创建所有节点的
Document 对象。
- 返回结果集。创建
XNodeSet ,将
<table> 元素(及其所有子元素)与之相连,然后返回它。该结果被自动添加到输出文档。
现在,我们已经讲了要做的事情,让我们
看一下代码。
Xalan 中的扩展元素与两个变量(
XSLProcessorContext 对象和
ElemExtensionCall 对象)一起被调用。在这里的代码中, 将使用
XSLProcessorContext 对象来获得上下文节点。一旦有了上下文节点(
<dbaccess> 元素),就可以获得源代码树中各种
<dbaccess> 元素属性的值。
在扩展元素中做的第一件事是声明要返回到 Xalan 的
XNodeSet 。接着, 创建
DOMHelper 对象并使用
getDOMFactory 方法来创建
DOM Document 对象, 它将用作创建新节点的 factory:
XNodeSet dbResult = null;
DOMHelper dh = new DOMHelper();
Document doc = dh.getDOMFactory(); |
下一个任务是实例化 JDBC 驱动程序。要使代码更灵活,在
<dbaccess> 元素的
driver 属性中指定驱动程序。在前面的 XML 示例中,使用了 MySQL、Sybase 和 DB2 的驱动程序。假设到这一点的任何事情已经成功, 我们将构建查询字符串。要简化事情,示例假设将构建
SQL SELECT 语句;自由扩展该代码以完成更复杂的事情。查询是根据
<dbaccess> 元素的各种属性构建的。
一旦构建了查询,就连接到适当的数据库。该数据库是用
<dbaccess> 元素的
database 属性指定的。(在前面的 XML 示例中, 注意 DB2、Sybase 和 MySQL 以不同的方法指定数据库。在属性中指定它会使扩展元素更灵活。) 连接到该数据库,执行查询语句并获得
ResultSet 对象作为返回。
一旦有了
ResultSet ,作业就相对简单。需要创建一个 HTML 表,并且表中的每一行都包含来自
ResultSet 的一行。在前面的代码中, 调用
Document 对象以创建每个新代码。这里有一些示例:
while (rs.next())
{
tr = doc.createElement("tr");
for (int i = 1; i <= columnCount; i++)
{
td = doc.createElement("td");
td.appendChild(doc.createTextNode(rs.getString(i)));
tr.appendChild(td);
}
table.appendChild(tr);
}
dbResult = new XNodeSet(table); |
在这个示例中,用 DOM
createElement 方法创建
<tr> 元素。注意,当希望将文本添加到节点时,使用
createTextNode 方法来创建文本节点并将它作为子节点添加。在刚才显示的循环中,获取
ResultSet 的每一行并为它创建一个
<tr> 元素。为
ResultSet 中的每一列创建一个
<td> 元素,然后将它附加到
<tr> 元素。当行完成时,将
<tr> 元素附加到
<table> 中。
一旦处理完整个
ResultSet ,通过将
<table> 元素传递到
XNodeSet 构造器来创建一个新的
XNodeSet 。这一技术可用于创建任何数目的节点,包括元素、属性、文本和注释。
例如,这里是创建 HTML
<table> 元素并将
border="1" 属性添加到其中的方法:
Element table = doc.createElement("table");
table.setAttribute("border", "1"); |
最后一步只是清除所有 JDBC 资源并将
XNodeSet 返回到 Xalan:
rs.close();
stmt.close();
con.close();
...
return dbResult; |
XNodeSet 中的节点被直接发送到输出文档,它们在输出文档中作为普通的 HTML 文档出现,如图 7 所示。
图 7. 带有来自扩展元素的输出的 HTML 文件
通过这种扩展,能够动态地生成节点,然后将它们添加到输出文档。 每次处理这个样式表时,扩展将各种数据库中的最新数据添加到输出中。 可以通过添加高速缓存、连接池以及性能和可伸缩性的其它特性来改进这一扩展元素; 这个示例的要点是向您显示扩展元素是如何工作的。 无论其限制是什么,扩展元素的最佳特性是可以将它与任何符合 JDBC 的数据库一起使用。 不管数据库供应商是否支持它,都可以使用这个代码从任何数据库生成 HTML(或 XML)。
 |
使扩展标准化
在编写本文之时,正在进行使扩展函数和扩展元素在 XSLT 处理器之间标准化的工作。 EXSLT 项目就是这样一种努力。有关扩展的 EXSLT 库的更多信息,请访问它们的网站(请参阅
参考资料)。
|
|
总结
在这个摘录中,我们已经运行了全部的扩展函数和扩展元素,演示了如何将复杂的处理能力添加到样式表中。所有示例都是完备的;可以将这些函数和元素组合起来,以完成真正复杂的事情。 例如,可以使用数据库扩展来抽取数据库中的实时销售数据,然后将它转换成交互式图形。
当然,如果需要有关 XSLT 和 XPath 的更多详细信息,我希望您订购这本书并参考它的其余八个章节。
参考资料
关于作者  | 
|  | Doug Tidwell 是 developerWorks 的计算机传道士,他将基于开放标准计算的好消息撒布到供应商征服的世界。他乐于听取您的意见、问题和关注,尤其是有关他的书籍; 可以通过
dtidwell@us.ibm.com与他取得联系。 是的,那些是新眼镜,谢谢注意到这一点。
|
对本文的评价
|