编写更加安全的 XSLT 样式表

在 XML 转换中添加自动错误捕捉功能

XSLT 样式表中很容易出现不易发现的错误。无论是静态错误检查,还是动态错误检查都对此毫无帮助:只有通过功能测试才能发现这些错误。XSLT 2.0 引入了多种新选项和全新的可能性,您可以利用其中的部分选项使您的样式表更加安全、测试更加轻松。了解这些 XSLT 2.0 特性(如类型系统),为您的样式表添加通过其他方式不可能实现的错误检查功能。

Erik Siegel, XML 专家, Xatapult

Erik Siegel 的照片Erik Siegel 是荷兰的一名自由 XML 专家。他在职业生涯中担任过许多职位—研究员、程序员、系统分析师、顾问。在过去的 5 年中,XML 逐渐走入他的生活。他的主要客户是出版业客户,他的 XML 工作包括顾问、培训、架构开发和 XSLT 编程。您可以访问 www.xatapult.com,找到有关 Erik 和他的公司的更多信息。



2011 年 12 月 05 日

常用缩写词

  • DTD:文档类型定义
  • IDE:集成开发环境
  • XSLT:可扩展样式表语言转换

如果您曾经编写过 XSLT 样式表,那么或许就会知道使之在现实环境中更加安全并不简单。举例来说,微不足道的键入错误就可能导致严重的麻烦。XPath 元素和属性名称中的简单错误也无法被 XSLT 处理器的错误检查机制检测到。如果您写下了 /Filename ,而正确的写法应该是 /FileName, 那么您只能通过您自己的测试和调试工作发现问题:无法被 XSLT 引擎捕捉到。

这里给出一个示例。假设有一个 XML 文档,如 清单 1 所示

清单 1. 示例 XML 文档
<Things>
    <Thing thingid="12345FFD3">...</Thing>
    <Thing thingid="86779EAD0">...</Thing>
    ...
</Things>

在 XSLT 样式表中,有一个命名模板,它对某件事情执行某些操作(参见 清单 2)。

清单 2. 处理某件事情的命名模板
<xsl:template name="ProcessThing">
    <xsl:param name="ThingId"/>
    <xsl:for-each select="/Things/Thing[@id eq $ThingId]">
        <!-- Do something with the thing -->
    </xsl:for-each>
</xsl:template>

您能否发现错误?此模板的创作者忘记(或不知道)一件事情的属性的标识符应该拼写为 thingid,而不是 id。如果您运行此标记,不会出现任务错误消息:for-each 循环根本不会执行。或许您会注意到,或许您不会注意到。毕竟错误深嵌在复杂转换的某一处。如果您没有注意到此错误,代码进入生产,拼写错误将使某些计算的重要部分无法执行。

另外还有一个相关的问题。尽管我们一直关注相反的方面,但并非所有输入文档都会根据一个架构或 DTD 进行验证。如果文档的作者写下的原本就是 <Filename> 而非预期的 <FileName>,情况又该如何?

一种更轻松的想法就是自动捕捉这些错误和其他错误,至少是最明显的那些错误,并使 XSLT 处理器在发生糟糕的情况时通知您。这篇文章探讨了如何使您的 XSLT 样式表更加安全,并使 XSLT 处理器捕捉通常会被直接忽略的错误。它将从软件工程的角度观察样式表,并展示如何使之更加健壮和安全。这篇文章假设您对 XSLT 有基本认识。

XSLT v2 类型系统

工作环境

本文大多数示例多依靠 XSLT 版本 2 的特性,对于 XSLT 版本 1 无效。我使用 Saxon Home Edition 9.x 测试了所有这些示例,但一切都符合 XSLT v2 标准,在其他处理器上应该无法正常工作。

为了理解本文中描述的大多数方法,您必须理解 XSLT v2 类型系统。(此处仅为粗略介绍:有关更多信息,请参见 参考资料 部分。)XSLT v2 引入了许多新特性,包括数据类型。高级选项包括引用架构和使用其中定义的类型。然而,所述方法仅使用基本类型系统。

XSLT v2 的新特性之一就是为您的变量和参数提供显式数据类型的能力。为此使用了 XML 架构的类型系统,并附加了一些额外的语法和寓意。您可以使用 as 属性来提供数据类型—举例来说:

<xsl:variable name="TestVariable" as="xs:integer" select="123"/>

其他基本数据类型包括 xs:stringxs:booleanxs:doublexs:datexs:dateTime。切记在某个位置定义 xs 名称空间前缀(最好在样式表的根元素中):

xmlns:xs="http://www.w3.org/2001/XMLSchema"

在 XSLT v2 中,您还可以在变量中存储树片段(文档、节点、属性等)。增加一些语法,即可为此确定您的变量的数据类型。 清单 3 提供了一些示例。

清单 3. 包含树片段的变量示例
<xsl:variable name="TheCompleteDocument" as="document-node()" select="/"/>
<xsl:variable name="AnyElement" as="element()" select="*[1]"/>
<xsl:variable name="FirstThingElement" as="element(Thing)" select="/Things/Thing[1]"/>
<xsl:variable name="FirstAttribute" as="attribute()" select="@*[1]"/>
<xsl:variable name="IdAttribute" as="attribute()" select="@id"/>

这种做法非常好,但还能加以改进:XSLT v2 中的所有变量都是序列—即一组值的有序集合—其中可以包含 0 个、1 个或更多的值。因此,“普通” 变量(如上例所示)仅仅是特殊情况—即仅包含一个值的序列。为了发挥序列的力量,可为一个或多个值添加一个加号(+)、为零个或多个值添加一个星号(*),或者为类型指定末尾处的零个或多个值添加一个问号(?)。没错,这就像是旧式 DTD 中一样。 清单 4 提供了几个示例。

清单 4. 序列示例
<xsl:variable name="MultipleStrings" as="xs:string+" select="('This', 'That')"/>
<xsl:variable name="EmptySetOfIntegers" as="xs:integer?" select="()"/>
<xsl:variable name="SetOfAllThings" as="element(Thing)*" select="//Thing"/>

这种方法还有更加强大的力量:您的变量中可以包含所有常见 XPath 结构。 清单 5 提供了一个示例。

清单 5. 使用 XSLT v2 的 XPath 结构
<xsl:for-each select="$SetOfAllThings">
    <!-- Do something with the things -->
</xsl:for-each>
<xsl:variable name="LastString" as="xs:string" select="$MultipleStrings[last()]"/>
<xsl:variable name="ThingCount" as="xs:integer" select="count($SetOfAllThings)"/>
<xsl:variable name="AllThingIdAttributes" as="attribute(thingid)*" 
              select="$SetOfAllThings/@thingid"/>

这对使您的样式表更加健壮有着怎样的帮助?类型系统最出色的方面在于,在运行时,XSLT 处理器实际上会根据变量的类型检查变量值,并在不匹配的时候(包括其多样性)发出警告。如果您将输入文档内的重要值置入具有恰当类型的变量中,只要发生问题,您就能看到错误。


捕捉类型不匹配、预期之外的元素和属性

在监督 XPath 元素和属性名称的拼写时(包括样式表中和输入文档中),您可以做些什么?您或许会反复检查、反复审读、执行严格的调试,然后双手合十祈祷一切正常。但您真正希望的是使 bug 显示在所出现的错误消息中。

使用变量

如果将 清单 1 中的示例修改为 清单 6 所示的形式,则错误将立即弹出。

清单 6. 清单 1 中的 XML 示例及变量
<xsl:template name="ProcessThing">
    <xsl:param name="ThingId"/>
    <xsl:variable name="ThingToProcess" as="element(Thing)"
        select="/Things/Thing[@id eq $ThingId]"/>
    <xsl:for-each select="$ThingToProcess">
        <!-- Do something with the thing -->
    </xsl:for-each>
</xsl:template>

在运行时,XSLT 处理器会注意到 /Things/Thing[@id eq $ThingId] 未能返回预期的 <Thing> 元素,而是返回了一个空序列。然而,ThingsToProcess 变量的类型定义是 element(Thing),准确地表明了一个 Thing 元素。此元素不适用,因此将弹出一条错误消息,并停止转换过程。

这里还有一个优点。如果有两件事具有相同的 ID,您也会看到错误。 $ThingToProcess 变量仅能包含单独一件事

为了使这个模板更加安全,我按照如下方法确定了参数的数据类型:

<xsl:param name="ThingId" as="xs:string" required="yes"/>

这种做法会捕捉忘记使用参数的情况(由于 required="yes" 元素),并传递空值或多个值。

您可以通过多种方式利用这项技术。举例来说,我通常会在根元素的属性中包含重要的全局值。为了保证这些值在我的代码中全局可用,我在使用前将其存储在顶级变量之中:

<xsl:variable name="GlobalId" as="xs:string" select="/*/@id"/>
<xsl:variable name="GlobalName" as="xs:string" select="/*/@name"/>

为变量提供一个数据类型,可以是 xs:string 这样的一般类型,捕捉预计之外的属性缺失—这帮助我节约了大量时间。因此,应该首先将输入文档中的重要值置入变量。为这些变量提供一个数据类型,确保使用正确的类型和正确的多样性。

使用全部捕捉

如果您的样式表中包含的大多数是匹配模板,则应考虑添加一个全部捕捉模板,即便在看起来似乎不需要此模板的情况下也是如此。

设想以下场景:您为输入中的所有元素编写了匹配模板,使用 <xsl:apply-templates> 传播控制。如果名称的类型有误,或者有人更改了您的输入文档,会发生怎样的情况?此时元素的默认模板将开始生效,并执行静默 <xsl:apply-templates>。也许这如您所愿,也许不是。最好能够获得错误,以便调查正在发生的情况。清单 7 中所示的一个简单的全部捕捉模板即可实现此目标。

清单 7. 简单的全部捕捉模板
<xsl:template match="*">
    <xsl:message terminate="yes">
        Unexpected element: <xsl:value-of select="name()"/>
    </xsl:message>
</xsl:template>

更安全的命名模板

问题的另外一个来源就是命名模板及其参数。通过 as 属性提供数据类型参数已经捕捉了许多错误。但还有更多错误。

避免参数错误

在 XSLT v2 中,您不能将参数传递给未在其参数列表中定义的命名模板。这种限制实际上是与 XSLT v1 的少数不兼容性之一,但这很好,因为能够消除参数名称中的键入错误。

您可能还不了解,使命名模板更加安全的一个技巧就是内置的 XSLT v2 特性:您可以按需标记参数:

<xsl:template name="DoSomething">
    <xsl:param name="Subject" as="xs:string" required="yes"/>
    <!-- ... -->
</xsl:template>

现在,您必须 在调用 DoSomething 时提供一个 Subject 参数。这种技巧对于很容易遗忘参数的长参数列表尤为有用。

因此,为 DoSomething 提供之前定义的命名模板,以下两个调用都是非法的,将被捕捉:

<xsl:call-template name="DoSomething">
    <xsl:with-param name="subject" select="'Safer stylesheets'"/>
</xsl:call-template>
<xsl:call-template name="DoSomething"/>

检查上下文

命名模板对于将代码划分为较小的区块、避免代码内容重复非常有用。举例来说,如果您的代码中有多个位置处理一个 Thing 元素(按照相同的方式),那么您可能希望编写一个命名模板,如 清单 8 所示。

清单 8. 将 Thing 作为上下文元素处理
<xsl:template name="HandleThing">
    <!-- Current element must be a <Thing>! -->
    <!-- ... -->
</xsl:template>

同样,我们希望在当前元素不是 Thing 的情况下显示错误。您可以按照 清单 9 所示完成此步骤。

清单 9. 处理 Thing 并检查上下文
<xsl:template name="HandleThing">
    <xsl:param name="ThingToHandle" as="element(Thing)" select="."/>
    <xsl:for-each select="$ThingToHandle">
        <!-- Now the current element is a <Thing> or we get an error! -->
        <!-- ... -->
    </xsl:for-each>
</xsl:template>

您可能会将 ThingToHandle 声明为变量,而不是参数。然而,使用参数还有一项额外的优势:您现在可以在当前元素不是 Thing 的情况下使用 HandleThing。只需在 ThingToHandle 参数中为其传递应该处理的元素即可(如 清单 10 所示)。

清单 10. 处理不是当前上下文的 Thing
<xsl:template match="/">
    <!-- Only handle the first thing: -->
    <xsl:call-template name="HandleThing"> 
        <xsl:with-param name="ThingToHandle" select="/*/Thing[1]"/>
    </xsl:call-template>
</xsl:template>

提示和技巧

这里有两条创建更加安全的 XSLT 样式表的最终提示:断言和 normalize-space() 函数。

执行断言

大多数语言都具有一项称为断言 的特性,这是一种会在满足某项条件时停止处理的语句,例如重要变量中出现超出预料的值。XSLT 没有断言,但有两种方法可以停止 XSLT 处理器: <xsl:message terminate="yes"> 和 XPath error() 函数。将这两种方法之一与 <xsl:if> 相结合,即可创建出色的断言。例如:

<xsl:if test="empty(/*/Thing)">
    <xsl:message terminate="yes">No things found in input document</xsl:message>
</xsl:if>

也可使用 error() 函数:

<xsl:if test="empty(/*/Thing)">
    <xsl:value-of select="error((), 'No things found in input document')"/>
</xsl:if>

在决定要使用哪种方法之前,应测试它们在您的 IDE 和运行时环境中的标签。在我的系统设置中,error() 函数提供了最有用的结果:错误消息。<xsl:message> 告诉我特定的一行中发生了错误,但并未显示错误消息本身。

normalize-space() 与美观打印

使您的样式表更加安全的最后一项提示就是大量使用 normalize-space() 函数。原因在于,XML 编辑器的美观打印特性可能会引入不必要的换行符以及(更值得注意的)空格。举例来说,假设 XML 输入文档中包含这样一个深度嵌套的元素:

<DeeplyNestedElement>This is an example of a pretty print error</DeeplyNestedElement>

现在,粗心的作者可能会单击美观打印按钮,您的元素就会变为下面这样的形式:

<DeeplyNestedElement>This is an example of a pretty
     print error</DeeplyNestedElement>

现在,pretty 和 print 这两个词之间出现了一个换行符和大量的空格。如果您要生成的是 HTML,那么不会产生任何问题,但如果您的代码依赖于元素的准确内容,则这种方法就不适用。使用 normalize-space() 可将此设置正确:它将删除开头和结尾处的空格,并将所有其他空格序列转为单独一个空格。

您必须小心谨慎:normalize-space() 也可能会删除必要的 空格和换行符。然而,依靠文本中准确空格的输入在我的世界中非常少见。


结束语

世界并不完美(尤其是在编程方面),所以有些事情您可能预料不 到。这篇文章展示了多种捕捉或防范 XSLT 处理中通过其他方式可能无法发现的错误的方法。具体来说,类型系统为您提供了可能未预料到,但非常有用的错误捕捉可能性。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=778784
ArticleTitle=编写更加安全的 XSLT 样式表
publish-date=12052011