构建可移植 XSLT 工具

一个用于创建轻量级 XML 创作工具的实践指南

为诸如帮助系统、维护文档和 Wiki 等创作项目而创建的 XML 文档日益复杂化,并且更多地依赖于内部和外部文档链接。本篇实践指南将介绍如何创建能帮助您自动化重复创建 XML 文档任务的轻量级工具。

Lewis Marshall, 技术主管, Inmedius Inc.

Lewis Marshall 专注于基于 XML 和 SGML 规范的发布和编辑系统。他的主要技术兴趣包括 XSL 系列、xschema 和私有编辑器技术,如 ACL 和 FOSI。Lewis 是 Inmedius Inc. 的一名技术主管。



2011 年 3 月 07 日

本文主要介绍处理创作的 XML 文档 的一些问题。所谓创作的 XML 文档,这里指的是来源于内容创建者的数据内容,一般是由一个 DTD 或模式所引导的。许多环境会出于各种原因(比如,一致性、效率和节约成本)而采用引导创作内容。将 DTD 置于适当的位置,就有可能实现 100% 的一致性。如果作者受 DTD 的控制(和限制),那么就一定能够保证结果是可预见的,对吗?

常用缩写词

  • DOM:文档对象模型
  • DTD:文档类型定义
  • HTML:超文本标记语言
  • URI:统一资源标识符
  • W3C:世界万维网联盟
  • XML:可扩展标记语言
  • XSLT:可扩展样式表语言转换

然而,对大多数 DTD 的密切观察就会发现相当程度的灵活性 — 特别是当这个 DTD 源于普通资源或者来自一个标准或规范时。这并不是 DTD 范例的一个缺点,但是如果 XML 数据集周边的基础架构 — 昂贵的发布渠道、文档管理系统和翻译过程— 以某种方式与文档发生关联,那么问题就很难办。

通常的实践做法是应用样式控制来限制文档差异。实现样式控制的方法是手供对数据集进行交叉检查,但这代价比较大,还有一个更有效的方法是自动检查。

什么适合进行自动化?

内容创建者拥有一个 XML 数据集,以及一系列需要完成的任务。这些任务能够自动化吗?

特殊规则

航空航天部和国防部已经提出了两个重要倡议,提议范化这一过程:

  • 业务规则交换。这个属于 S1000D 规范的 DTD 描述了适用于其他 S1000D 文档的一组业务规则;一般,这些业务规则会分析 S1000D 数据中的技术数据,从而保证大型项目的一致性。
  • 简化英语。这是一组基于审批过的单词词典的编写规则,以简化技术文档的歧义。

解决这个问题,通常也就是简单地回答一个问题:“这个任务是否是可预计、可重复和可定义的?” 更简单的检查是不需要解析文本内容而只需要关注文档结构的方法:

  • 新的内容是否有一个交叉引用目标?是否其他人很有可能会链接到这个主题?
  • 这个清单是否有多个项目?如果没有,它就不是一个清单。
  • 是否所有重要的安全信息都在任务清单之前出现?最好预先警告用户可能出现触电。

您可以确定一个已知的文档模型,或者作为一般文档。毕竟,这是关系到可扩展性的问题:如果工具只有一个用途,而且可以通过自动化节省时间,那么采用一个简单的独立的脚本来处理业务逻辑可能就是一种合理的方法。如果是一个多用途且用户可以定制的工具,那么就需要更强大的可配置方法。这里我采用后一种方法。


一个示例工具

XSLT 是一种万能 XML 处理语言。然而,它并非唯一的选择,因为许多 XML 是使用 DOM 技术处理的,而本文所介绍的一些方法也可以采用 DOM 技术实现。但是当您希望执行一些修改操作时,XSLT 是最理想的工具。

这里的 XSLT 例子包含了一个 HTML 来演示如何简单地将工具作为独立的应用程序部署。这个组合使用的 XSLT 版本为 1.0(见 参考资料),并且嵌入的脚本是使用 Microsoft® JScript® 脚本语言编写的。

处理一个文档并根据业务逻辑返回一组错误消息

第一步是捕获业务逻辑。根据本次练习的目的,您需要对本文的 XML 源进行检查。检查规则是基于针对这些文档作者的样式指南。

这些检查是通过检查文档结构来保证编写的内容是完整的,而不是通过分析文本内容实现的。XPath 是进行这些检查的理想候选工具。

通过 XML 词汇编码格式化和一般化化这些检查

这个方法意味着要执行整个设计过程,包括定义如何最佳地捕获文档错误,如何对错误进行分类,以及如何处理这些错误。为什么不直接将这个错误检查直接嵌入到 XSLT 中呢?这种技术的优点是在错误检查编码到一个 XML 词汇之后,这个工具就成为处理一个或多个配置的通用代码。用户可以选择不同的规则集处理不同的文档数据:

XML 名称空间

使用名称空间不是必需的,但这是一种设计选择。如果不需要,那么可以直接删除查询中的 err: 名称空间前缀。请转到 参考资料 讨论名称空间使用的链接。

  • 定义测试文档的方法。根据本实践的目的,这里的设计所选择的是 XPath。
  • 定义 “通过 — 失败” 准则。使用基于 XPath 表达式的文档检查来查询一个或多个节点是否存在。
  • 定义失败的严重性。每一个检查都可以按以下方式分类:
    • 强制性。错误检查过程第一个实例执行时就失败。
    • 建议性。没有过程错误,但是这个过程将实例标记为错误。
    • 条件性。是强制性的一种变形,这个检查更复杂一些,它基于节点 XPath 表达式的返回值额外执行一次环境检查。
  • 创建和导入一个映射文件。这个文件应该进行以下文档检查:
    • 为文档定义一个名称空间 — 例如:

      <err:document xmlns:err="http://error.com/mynamespace">
    • 创建每个错误定义。
    • 概括说明文档执行的每一个错误检查:

      <err:element type="structure" name="dw-document" 
      context="/dw-document" enforce="yes">
    • 说明在元素上执行的错误检查:

      <err:element type="element" name="ol" context="./li" pass="&gt;=2"/>

关于完整的示例测试集,见 参考资料

在您定义了错误检查语法之后,您就可以定义一个或多个应用到不同数据集的规则集。

创建处理规则集文件的 XSLT

XSLT 可能有两个输出流:日志消息和优化文档源(如果执行修改操作)。

XSLT 扩展

XSLT 扩展的实现是与供应商相关的:任何支持这些扩展的处理器都使用不同的名称空间来定义 script 元素。参考资料 中有关于 XSLT 扩展的信息链接。

这个设计是使用 XSLT 输出流来创建一个新的优化文档和一个 XSLT 扩展来将日志消息写到另一个输出流。这个单独的例子将日志消息添加到一个 HTML 日志窗格上。

这些错误检查可以被分成两种不同的类别:顶级结构性检查和元素级检查。XSLT 首先执行顶级检查;然后如果可能的话(即,之前所有检查均通过),它会使用常用 XSLT 模板来处理文档内容。

为了创建这个 XSLT,我们需要执行以下步骤:

  1. 在这个 XSLT 中定义一个 script 元素,以定义嵌入的脚本。首先,创建一个日志环境,然后创建一个函数来存储这些消息,如 清单 1 所示。
    清单 1. 在 XSLT 中定义嵌入脚本
    <msxsl:script language="JScript" implements-prefix="xslext">
    <![CDATA[
    
      var messages = new Array();
      var msgct = 0;
    
      function addMsg( msg ){
        messages[msgct++] = msg;
        return "";
      }
    
    ]]>
    </msxsl:script>
  2. 添加一个模板来处理这些消息。清单 2 显示这些处理代码。
    清单 2. 添加一个模板
    <xsl:template name="handlemsg">
    
      <xsl:param name="msg"/>
      <xsl:param name="terminate">no</xsl:param>
      <xsl:param name="lvl">1</xsl:param>
    
      <xsl:variable name="logmsg">
        <!-- Indent the log messages to help with readability -->
        <xsl:choose>
          <xsl:when test="$lvl=2">  &#x2022; </xsl:when>
          <xsl:when test="$lvl=3">    &#x2022; </xsl:when>
          <xsl:when test="$lvl=4">      &#x2022; </xsl:when>
          <xsl:when test="$lvl=4">        &#x2022; </xsl:when>
        </xsl:choose>
        <xsl:value-of select="$msg"/>
      </xsl:variable>
    
      <xsl:variable name="log" select="xslext:addMsg( string( $logmsg ) )"/>
    
      <xsl:if test="$terminate='yes'">
        <xsl:variable name="errormsg"
                      select="xslext:addMsg( 'ERROR: Error checking caused 
                        the process to stop' )"/> 
      <!-- If the error msg force termination, the process must first output 
           all existing log messages -->
        <xsl:variable name="output" select="xslext:outputMsgs( $logfileout )"/>
        <xsl:message terminate="yes"></xsl:message>
      </xsl:if>
    
     </xsl:template>

    这个模板是从整个 XSLT 上调用的,用来处理发送到消息扩展函数的消息。

  3. 使用一个全局文档变量来定义进行判断的 XPath 表达式,并创建一个接收表达式的函数。清单 3 所示就是这段代码。
    清单 3. 创建一个全局变量
    <msxsl:script language="JScript" implements-prefix="xslext">
    <![CDATA[
    
      var xpathdoc = null;
    
      function setUpXPath( ns, trialexpr ){
        var xml = ns.nextNode().xml;
        try{
          xpathdoc = new ActiveXObject( "Msxml2.DOMDocument.3.0" );
          xpathdoc.loadXML( xml );
          return trialexpr + ": " + xpathdoc.selectNodes( trialexpr ).length;
        } catch(e) {
          return "ERROR: " + e.description;
        }
      }
    
    ]]>
    </msxsl:script>

    清单 3 显示了一个创建作为进一步执行 XPath 判断的环境节点的 DOM 文档。

  4. 在 XSLT 的主体中调用这个初始化函数,如 清单 4 所示。
    清单 4. 添加一个初始化函数
    <xsl:call-template name="handlemsg">
      <xsl:with-param name="msg">Setup '
        <xsl:value-of select="xslext:setUpXPath( $root, 
                                   concat( '//', name($root) ) )"/>
      '</xsl:with-param>
    </xsl:call-template>

    说明这个扩展函数是如何使用名称空间前缀调用的(见本例的 xslext)。这个前缀是这个自定义函数与一些 XSLT 的标准函数的区别所在,如 number()string()contains()

  5. 处理顶级文档测试:
    1. 定义一个规则集文件参数:

      <xsl:param name="rulesetfile"></xsl:param>

      给这个参数赋值一个文件 URI。这个例子会在运行时接收用户选择。

    2. 创建一个处理每一个测试的模板:

      xsl:template name="process-check"

      这个模板是按以下方式执行的。首先,您要创建一个扩展函数,使用 xpathdoc 作为环境节点,然后估计规则文件中的测试表达式集:


      function evalXPath( exp ){
        try{
          return xpathdoc.selectNodes( exp ).length;
        } catch(e) {
          return "Exception: " + e.description;
        }
      }

      如果成功,这段代码会返回一个整数:这个数应该大于等于 1。0 表示测试执行成功,但是没有发现匹配的结果;如果函数抛出一个异常或者 XPath 表达式编写错误时会出现一个错误描述信息。

    3. 调用这个函数,然后将返回值存储到一个变量中:

      <xsl:variable name="check"
                    select="xslext:evalXPath( string( $context ) )"/>

      其中 $context 是为 err:element 设置的表达式字符串(例如,/dw-document//meta-dcsubject)。

      如果 $check 的值大于或等于 1,那么这个测试就设置为 Enforce,这样测试就通过了。

      如果 $check 的值为 0,并且测试未被设置为 Enforce,那么测试是通过了,但是用户会看到一条警告消息。

      否则,测试就失败了,并且这个过程会停止。您可以通过一个将 terminate 设置为 Yes 的 xsl:message 强制停止这个测试(见 清单 2)。日志消息会调用这个模板,而 terminate 参数会被设置为 Yes。

    4. 为所有需要处理的强制性测试定义一个节点集:

      document($rulesetfile)//err:element[@type='structure'][@enforce='yes']
    5. 处理其他所有非强制性的顶级测试:

      document($rulesetfile)//err:element[@type='structure'][not(@enforce='yes')]
  6. 处理元素级测试。

    这些测试是在各个模板中处理的。为了保持这个过程的通用性,这个 XSLT 具有一个简单的处理元素的模板:


    xsl:template match="node()"

    在这个通用的模板中,您要设置一个变量来判断这个规则集是否包含一个可适用测试:


    <xsl:variable name="match"
                  select="document($rulesetfile)//err:element[@type='element']
                                                             [@name=$name]"/>

    其中 $name 是定义为当前元素的名称。

    如果 $match 为 True,那么这个测试的环境是使用另一个扩展函数运行的。这个函数类似于顶级 XPath 判断,它从 XSLT 接收当前节点,并执行表达式判断,如 清单 6 所示。

    清单 6. 判断一个表达式的函数
    function evalXPathAgainstNode( node, exp ){
      try{
        return node.nextNode().selectNodes( exp ).length;
      } catch(e) {
        return "Exception: " + e.description;
      }
    }

    如果这个函数返回一个整数值(既不是 0 也不是一条错误消息),那么这个整数会传递给另一个函数,来根据在 pass 属性中所定义的 “通过 — 失败” 准则测试数字:


    <err:element type="element" name="ol" context="./li" 
            pass="&gt;=2" />
  7. 判断 ol 元素有 2 个或 2 个以上的 li 孩子节点,如 清单 7 所示。
    清单 7. 判断 li 元素的个数
    function evalExpr( str, pass ){
      return eval( str + pass );
    }
    ...
    <xsl:variable name="eval" 
                  select="xslext:evalExpr( $check, $pass )"/>
  8. 这个 XSLT 会返回类似于 清单 8 的日志结果。
    清单 8. XSLT 日志结果
    Start
    Setup '//dw-document: 1'...
     · Check (Top-level document?) '1'
     · Conditional check '(Document ID missing?) '1' (1==1) == true'
     · Conditional check '(Article missing?) '1' (1==1) == true'
     · Conditional check '(Meta field (document type) missing?) '1' (1==1) == true'
     · Conditional check '(Meta field (subject) missing?) '1' (1==1) == true'
     · Conditional check '(Article title missing?) '1' (1==1) == true'
     · Conditional check '(Document author missing?) '1' (1==1) == true'
     · Conditional check '(Published date missing?) '1' (1==1) == true'
     · Check (Missing abstract?) '1'
     · Conditional check '(Dates out of sync?) '0' (00) == 0'
     · Conditional check '(Broken internal links?) '0' (0==0) == true'
     · Context checking 'heading' (./a[@name]) '(1==1) == true'...
     · Error context checking 'heading' (./a[@name]) '(0==1) == false'...
     · Context checking 'heading' (./a[@name]) '(1==1) == true'...
     · Context checking 'ol' (./li) '(3>=2) == true'...
    / End

下一步是什么?

在创建了一个执行检查、确定错误和修改文档的过程之后,下一步显然就是修改元素。这个例子包含了添加到文档的基本代码。

如果规则集显示 err:element 有一个孩子元素 err:onfail,那么代码可以是以下任意一种:

  • <err:insertbefore></err:insertbefore>
  • <err:insertatstart></err:insertatstart>
  • <err:insertatend></err:insertatend>
  • <err:insertafter></err:insertafter>

insert 元素包含了修正文档的 XML 标签 — 例如:

<err:insertatstart>
        <a name="function:generate-id()" /></err:insertatstart>

这个 XSLT 需要对它进行处理。

然后,您可以创建一个模板来遍历一个节点集:

<xsl:template name="copy-nodeset">

err:insertbeforeerr:insertatstarterr:insertatenderr:insertafter 元素的内容传递到 XSLT 中这个模板的相关位置 — 例如:

<-- Add 'err:insertbefore' here -->
<xsl:element name="{name()}">

  <xsl:copy-of select="@*"/>

  <-- Add 'err:insertatstart' here -->

  <xsl:apply-templates/>

  <-- Add 'err:insertatend' here -->

</xsl:element>

<-- Add 'err:insertafter' here -->

这个模板会对 function:generate-id() 方法进行特殊处理。

为了保证完整性,我们需要添加文档插入内容的日志:

点击查看代码清单

  ...
  · Error context checking 'heading' (./a[@name]) '(0==1) == false'...
  ·Adding content at start of 'heading'· Error context checking 'heading' (./a[@name]) '(0==1) == false'...
  ·Adding content at start of 'heading'
  ...

结束语

本文介绍了如何使用 XSLT 来分析文档结构以确定是否满足一定业务规则的要求。这个过程可以以两种重要方式执行一个重要的函数:第一,协助内容创建者实现创作目标 — 例如,用户可以脱机工作并多次执行测试来验证他们完成了特定的任务;第二,作为正式工作流文档的一部分 — 例如,这个工具可嵌入到一个文档知识库工作流中,而 “通过 — 失败” 准则可以控制一个可管理文档在编辑、审阅和接受状态之间的转换。

将业务逻辑与 XSLT 分离,从而使工具更具灵活性。这段代码会变得更加通用,因为一个代码库就可以应用多个规则集。XSLT 比 DOM 更加强大,而且支持使用转换过程来修改文档。


下载

描述名字大小
示例 XSLT 代码xslt_source.zip9KB

参考资料

学习

获得产品和技术

  • Business Rules Exchange(Svante Ericsson,TPSMG,2004 年 9 月):考虑使用一个 XML 词汇对项目相关的文档创建规则进行编码。它是基于验证文档的 DTD 规定创作规则的。
  • IBM 产品评估试用版软件:下载或 IBM SOA Sandbox for People,并开始使用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

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=630892
ArticleTitle=构建可移植 XSLT 工具
publish-date=03072011