这篇技巧讨论了使用 xsl:message 元素帮助理解和调试可扩展样式表语言转换(Extensible Stylesheet Language Transformation,XSLT)样式表的不同方法。
回显打印是调试困难问题的最古老的方法之一。不管怎么说,这仍然是最简单、最快捷的方法。如果不能确定为何函数没有按预期工作,用 printf() 或类似的工具在控制台上打印几个变量就可以了解幕后发生了什么。
当然,必须有能够输出调试结果的控制台。XSLT 不一定具有 printf() 这样的东西。不过 XSLT 有一个与 printf() 对应的元素:xsl:message。xsl:message 元素不改变 XSLT 样式表生成的结果树,仅仅输出一些消息让程序员看到。一般输出到控制台,但也可能是对话框或者日志文件。无论输出到哪里,都是一种非常好的调试辅助手段。
xsl:message 元素是可选的。没有要求处理程序一定支持它。不过大部分处理程序都支持它,而且通常把消息输出到控制台。
是否激活了某个模板?
如果输出不同于预期结果,首先要检查是否确实激活了模板。有很多原因可能造成模板被跳过,比如:
match属性中的元素名打错了
- 错误的名称空间,尤其是当试图把默认名称空间中的元素与样式表中没有前缀的名称匹配时
- 模式不匹配
这仅仅是部分可能的原因 。还可能有其他原因造成应该被激活的模板没有激活。为了检查是否执行了模板,可以在模板的开头加上 xsl:message 元素说明处理程序确实执行到了这里。比方说,假设要将 XHTML 文档转化成没有标记的普通文本。可以将类似 清单 1 所示的消息添加到模板中:
清单 1. 用于调试的匹配 HTML 的模板规则
<xsl:template match="/">
<xsl:message>Matched root node</xsl:message>
<xsl:apply-templates select="*"/>
</xsl:template>
<xsl:template match="html" xmlns:html="http://www.w3.org/1999/xhtml">
<xsl:message>Matched html element</xsl:message>
<book><xsl:apply-templates select="html"/></book>
</xsl:template>
|
运行该样式表的时候,如果看到 Matched root node 消息,就知道到了那里。如果没有看到 Matched html element 消息,就知道并没有执行到这里。这样就提供了确定问题所在的线索。很可能是 xsl:apply-templates 的 select 属性或者 html template 的 match 属性错了(这个例子是后一种情况,第二个模板应该与 html:html 匹配)。无论哪种情况,都会告诉您到哪里查找最可能出问题的地方。
包含 xsl:if 或 xsl:choose 语句的模板有多个分支。可以在每个分支中加上 xsl:message 元素来确定执行了哪一个(如果有的话)。比如,清单 2 显示了插入到 DocBook XSL 样式表中以确定为何转换无效的代码:
清单 2. 增加了调试指令的 DocBook XSL 模板
<xsl:choose>
<xsl:when test="caption">
<xsl:message>CAPTION!</xsl:message>
<fo:table-and-caption id="{$id}"
xsl:use-attribute-sets="table.properties">
<xsl:apply-templates select="caption" mode="htmlTable"/>
<fo:table xsl:use-attribute-sets="table.table.properties">
<xsl:choose>
<xsl:when test="$fop.extensions != 0 or
$passivetex.extensions != 0">
<xsl:message>EXTENSIONS!</xsl:message>
<xsl:attribute name="table-layout">fixed</xsl:attribute>
</xsl:when>
</xsl:choose>
<xsl:attribute name="width">
<xsl:choose>
<xsl:when test="@width">
<xsl:message>WIDTH ATTRIBUTE!</xsl:message>
<xsl:value-of select="@width"/>
</xsl:when>
<xsl:otherwise>
<xsl:message>NO WIDTH ATTRIBUTE!</xsl:message>100%
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:call-template name="make-html-table-columns">
<xsl:with-param name="count" select="$numcols"/>
</xsl:call-template>
<xsl:apply-templates select="thead" mode="htmlTable"/>
<xsl:apply-templates select="tfoot" mode="htmlTable"/>
<xsl:choose>
<xsl:when test="tbody">
<xsl:message>TBODY!</xsl:message>
<xsl:apply-templates select="tbody" mode="htmlTable"/>
</xsl:when>
<xsl:otherwise>
<xsl:message>NO TBODY!</xsl:message>
<fo:table-body>
<xsl:apply-templates select="tr" mode="htmlTable"/>
</fo:table-body>
</xsl:otherwise>
</xsl:choose>
</fo:table>
</fo:table-and-caption>
<xsl:copy-of select="$footnotes"/>
</xsl:when>
<xsl:otherwise>
<xsl:message>NO CAPTION!</xsl:message>
<fo:block id="{$id}"
xsl:use-attribute-sets="informaltable.properties">
<fo:table table-layout="auto"
xsl:use-attribute-sets="table.table.properties">
<xsl:attribute name="width">
<xsl:choose>
<xsl:when test="@width">
<xsl:message>WIDTH ATTRIBUTE!</xsl:message>
<xsl:value-of select="@width"/>
</xsl:when>
<xsl:otherwise>
<xsl:message>NO WIDTH ATTRIBUTE!</xsl:message>
100%
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:call-template name="make-html-table-columns">
<xsl:with-param name="count" select="$numcols"/>
</xsl:call-template>
<!--<xsl:apply-templates mode="htmlTable"/>-->
<xsl:choose>
<xsl:when test="tbody">
<xsl:message>TBODY!</xsl:message>
<xsl:apply-templates select="tbody" mode="htmlTable"/>
</xsl:when>
<xsl:otherwise>
<xsl:message>NO TBODY!</xsl:message>
<fo:table-body>
<xsl:apply-templates select="tr" mode="htmlTable"/>
</fo:table-body>
</xsl:otherwise>
</xsl:choose>
</fo:table>
</fo:block>
<xsl:copy-of select="$footnotes"/>
</xsl:otherwise>
</xsl:choose>
|
XSLT 模板可能比较复杂。要注意您的消息。我曾经多次犯过这样的错误,在错误的地方放上了 NO TBODY! 这样的消息声明。结果看到的消息完全是误导。调试困难的问题之前一定要安排好缩进和空白。通常,清理缩进本身就能发现问题所在。


|
回页首 |
|
检查节点和节点集
有时候最好计算得到一个结果而不是原样输出文字。这样可以减少偶然误导自己的可能性。所幸的是,xsl:message 元素的内容实际上是完整的 XSLT 模板。这就意味着可以包含的不仅仅是简单的字符串消息。比如 清单 3 可以输出激活模板的上下文节点的完整的祖先树:
清单 3. 列举上下文节点祖先的模板
<xsl:template match="foo">
<xsl:message>
<xsl:for-each select="ancestor-or-self::*">
<xsl:value-of select="name(.)"/> /
</xsl:for-each>
</xsl:message>
...
</xsl:template>
|
也可打印当前元素的所有属性,如 清单 4 所示:
清单 4. 列举上下文节点属性的模板
<xsl:template match="foo">
<xsl:message>
<xsl:for-each select="attribute::*">
<xsl:value-of select="name(.)"/>="<xsl:value-of select="."/>"
</xsl:for-each>
</xsl:message>
...
</xsl:template>
|
基本上能够生成调试样式表需要的任何信息。但是,如果模板没有因为您所认为的原因而激活,复杂的模板可能失败。有时候最好从一个简单的、仅仅确定上下文节点的表达式开始:
<xsl:message>
<xsl:value-of select="name(.)"/>: <xsl:value-of select="."/>
</xsl:message>
|
这个消息看起来似乎没有什么意义。无论如何总该知道匹配的是什么节点吧?但事实上并非这么显而易见。这类调试的目标之一是发现代码思维模型偏离实际的地方。如果存在 bug,某些代码很可能没有按照您的预期工作。因此,有必要检查一下您 “知道的” 每一点是否为真。如果最终发现某一处并不像您所想像的那样,这就是问题所在。用 Artemus Ward 的话来说,“并非我们清楚的事情带来麻烦。往往是我们自以为知道的东西造成困扰。”


|
回页首 |
|
测试条件
有时候问题仅在特定的条件下才出现。可以应用 xsl:if 以便只有当其他某些条件满足时才生成输出。这一点对于必须考虑限制输出结果的大型文档尤其重要。比方说,如果要调试一个只有遇到包含行内图像的段落时才会出现问题的样式表。不需要对整本书中的每个段落都打印消息,只有那些包含行内图像的段落才须如此。比如,清单 5 中的模板只有当 para 元素包含 image 时才会输出消息:
清单 5. 段落中包含图像时输出消息的模板
<xsl:template match="para">
<xsl:if test="image">
<xsl:message>
Para: <xsl:value-of select="."/>"
</xsl:message>
</xsl:if>
...
</xsl:template>
|
可以逐个测试元素的值,将消息限制到只有出现 bug 的一种情况。比如 清单 6 中,只有当段落包含特定的单词时才会输出消息:
清单 6. 段落中包含字符串 “BCEL” 时输出消息的模板
<xsl:template match="para">
<xsl:if test="contains(., 'BCEL')">
<xsl:message>
Para: <xsl:value-of select="."/>"
</xsl:message>
</xsl:if>
...
</xsl:template>
|


|
回页首 |
|
是否使用了预期的处理程序?
 |
|
为了彻底覆盖 Java 1.4 以及后续版本中的内置处理程序,必须将 Xalan JAR 文件放到 jre/lib/endorsed 中。不能使用 -classpath 以及 -Xbootclasspath/p: 命令行参数。
|
|
最难诊断的是 XSLT 处理程序造成的问题。如果使用的不是您预料的处理程序,这类问题尤其难以发现。使用 Java™ 语言的时候这种情况很常见,因为类路径问题意味着使用的常常不是您所希望的版本或品牌的处理程序。比如,Sun 的 JDK 1.4.0 捆绑了问题较多的 Xalan 2.2d10。即使在类路径中添加了更稳定更健壮的 Xalan 2.7,使用的可能仍然是较老的捆绑版本。
确定所用处理程序最简单的办法是用 xsl:message 元素打印出它的名字。xsl:vendor 函数系统属性包含所用处理程序的名称。
<xsl:message>
Processor: <xsl:value-of select="system-property('xsl:vendor')"/>
</xsl:message>
|
通常返回 Apache Software Foundation (Xalan XSLTC) 或 SAXON from Michael Kay of ICL 之类的字符串。如果发现使用的是 Xalan,而您想的本来是 Saxon,这就可以解释一些问题。不过,您通常真正关心的是库的版本。在 Xalan 中可以使用一个简单的扩展函数来得到库的版本,该函数调用 static org.apache.xalan.Version.getVersion() 方法,如 清单 7 所示:
清单 7. 输出 Xalan 版本号的样式表
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:pre="xalan://org.apache.xalan.Version"
>
<xsl:template match="/">
<xsl:message>
Processor: <xsl:value-of select="system-property('xsl:vendor')"/>
<xsl:text> <xsl:text>
<xsl:value-of select="pre:getVersion()"/>
</xsl:message>
</xsl:template>
</xsl:stylesheet>
|
下面是使用 JDK 1.5.0_06 转换时得到的结果:
$ java org.apache.xalan.xslt.Process -IN test.xml -XSL test.xsl
Processor: Apache Software Foundation (Xalan XSLTC) Xalan Java 2.4.1
|
向 jre/lib/endorsed 添加最新版本的 Xalan 后的结果如下:
$ java org.apache.xalan.xslt.Process -IN test.xml -XSL test.xsl
file:///Users/elharo/Documents/articles/tips/test.xsl; Line #8; Column #18;
Processor: Apache Software Foundation Xalan Java 2.7.0
|
 |
XSLT 2
XSLT 2 定义了标准的 xsl:product-name 和 xsl:product-version 系统属性,它们返回产品的名称和版本号。我所知道的 XSLT 1 处理程序都还不能识别这两个函数,如果使用 XSLT 2,这是更有效的检查所用处理程序的方法。
|
|
值得一提的是,2.4.1 和 2.7.0 版本之间的差别有时候很明显,从后一个版本开始 Xalan 输出 URL 和每个消息的行列号。
当然只有在使用 Xalan 的时候才能用这种方法分辨。如果您认为自己在使用 Xalan 而实际上用的是其他处理程序,很可能出现的问题会警告您这一点。但是其他大部分处理程序都由类似的功能。最新版本的 Saxon 实际上在 xsl:vendor 字符串中包含了版本号,这一点非常方便。


|
回页首 |
|
停止转换
在很少的情况下,可能会发现转换一团糟难以继续下去。也许因为输入与预料的相差甚远,希望放弃或停止处理。这种情况下,使用 terminate="yes" 属性的 xsl:message 元素可以停止转换。比如 清单 8 中,如果输入中包含 XHTML 名称空间之外的任何元素则该模板放弃处理:
清单 8. 如果文档包含非 XHTML 元素则停止处理的模板
<xsl:template match="/">
<xsl:for-each select="descendant::*">
<xsl:if test="namespace-uri() != 'http://www.w3.org/1999/xhtml'">
<xsl:message terminate="yes">
Non-XHTML element encountered: <xsl:value-of select="name()"/>
</xsl:message>
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="*"/>
</xsl:template>
|
理想情况下,结果树应该在生成任何输出之前建立,以便在遇到这样的消息处理程序时可以什么也不输出。不同的处理程序对此的处理相差很大。有一些处理程序,包括 xsltproc,在终止之前仍然生成一些输出。如果在样式表中包含该消息,要保证任何依赖于输出的处理能够发现输出提前停止了。


|
回页首 |
|
结束语
函数性语言(如 XSLT)与过程性语言(如 C)相比,调试的时候更加微妙。有时候最简单的办法就是重新拿起传统的回显打印这个工具,在使用的时候将任何可能有关的内容输出到控制台。xsl:message 元素允许样式表在控制台或程序员能够看到的其他地方输出消息。消息可以是简单的信号、复杂的节点信息组合或者条件数据。xsl:message 元素可以让您比较处理程序在文档中应该看到和实际看到的内容。期望和现实之间的差别就是问题所在。 |