内容


使用模式信息处理实例数据

采用纯 XSLT 函数库,让应用程序可以使用与 XPath 2 模式相关的节点测试

XPath 2 引入了一些新的序列类型构造,允许应用程序在 XML 模式中使用类型定义和元素声明,从而在 XML 实例文档中匹配元素。在应用程序中使用这些构造可以减少代码的复杂性,并且提高了应用程序的通用性。

不幸的是,目前这些构造只能用于模式感知的 XPath 主机语言处理器,比如模式感知的 XSLT 或 XQuery 处理器,并且这些模式感知处理器经常紧密耦合验证过程和实例数据处理过程。这意味着应用程序必须执行验证过程才能使用模式信息,而不管是否有必要这样做(实际上在很多情况下,实例数据到达实例数据处理器之前,已经针对某个特定模式进行了验证 —— 因此没有必要再执行验证过程)。在这些场景中,处理实例数据需要用到的是模式信息,而不是验证。

在本文中,我将介绍用纯 XSLT 实现的函数库,它使应用程序可以从新的 XPath 2 构造获得益处;同时,它将把验证过程从实例数据处理过程中分离。您可以将函数库和任何基础 XSLT 处理器一起使用,利用模式信息处理实例数据,而不需经过不必要的验证过程;这个方法也可用于 XQuery 应用程序。

与 XPath 2 模式相关的节点测试

在 XPath 2 中,可以在一个 XPath 表达式中使用两种与模式有关的节点测试,以根据模式类型定义和元素声明选择元素节点。这两种构造的基本语法是:

  • element(ElementNameOrWildCard, TypeName)
  • schema-element(ElementName)

其中,ElementName 表示实例文档中的元素的限定名;ElementNameOrWildCard 是一个元素名或 “*”;而 TypeName 表示 XML 模式文档中的类型定义的限定名。

一般而言,带有类型名的构造 element() 匹配类型定义直接或间接来自特定类型的元素。构造 schema-element() 匹配在构造指定的头元素的替代组中声明的元素。

例如,

  • element(Person, PersonType) 匹配名为 Person、类型为 PersonType 的元素。
  • Element(*, PersonType) 匹配一个元素节点,该节点的类型必须是 PersonType,或者直接或间接来自 PersonType,而不考虑其名称。
  • schema-element(Person) 匹配一个元素节点,该节点必须在以 Person 开头的代替组中声明。

详细的节点测试语法和语义参见 参考资料 中的 XPath 2 规范。

使用节点测试的优点

与简单地基于实例文档的程序相比,充分利用与模式相关的节点测试的程序可以获得简单性通用性扩展性 等优点:

  • 简单性:程序可以使用这些节点测试,选择基于共同组 head 元素或类型定义的一组元素,而不是逐一列举所有具体的元素名。
  • 通用性:程序设计可以基于模式定义,从而使程序可以处理遵循某一模式的一类文件。
  • 扩展性:可以轻松修改在节点测试范围内新添加的元素,只需添加新的处理代码,而不必更改现有的代码。

这三个特性提高了生产率。我将在本文后面进一步展示这些优点。

使用节点测试的缺点

虽然与模式相关的节点测试具有以上这些优点,但是它们不能用于大部分 XSLT 或 XQuery 处理器,因为大部分处理器只提供基本功能,不支持 XML 模式。另外,虽然高级模式感知处理器确实支持这些节点测试,但它们的数量很少,并且在实际操作中常常耦合了验证过程和数据处理过程。

换句话说,使用这些处理器需要重复地执行验证过程,这样才能在程序中使用模式信息,即使已经知道文档对于这个模式是有效的。此外,您只能静态地加载应用程序使用的模式文档;程序不允许动态地加载和使用模式。

解决这些问题需要采取其他方式,将模式信息公开给应用程序,并允许它们不需要经过验证就能利用模式信息。这正是我在本文介绍的函数库的驱动力。

实现函数库

函数库实际上提供一些函数,它们通过给定的实例元素、模式类型定义和模式元素声明执行节点测试。函数库包含以下三种类型的函数:

  • 加载模式文档
  • 执行模式测试
  • 在模式文档中定位类型定义或元素声明

看看这些函数是如何实现的。

加载模式

函数库提供一个函数 load-schemas-by-URI()load-schemas-by-URI() 函数使用 XPath doc() 函数读取和解析位于特定 URI 的模式文档。它返回文档节点的序列,包含由 URI 指定的主模式文档和所有由主模式导入或包含的模式文档。

如清单 1 所示,实际上模式加载过程只涉及两个步骤。

清单 1. 通过 URI 加载模式
<xsl:function name="xsdf:load-schemas-by-URI" as="document-node()*">
   <xsl:param name="schemaURI" as="xsd:string"/>

   <xsl:variable name="mainSchema" select="doc($schemaURI)" 
                 as="document-node()?"/>
   <xsl:variable name="allschemas" select="$mainSchema, 
                                     xsdf:load-referenced-schemas($mainSchema, 
                                     document-uri($mainSchema))"/>
   <xsl:sequence select="xsdf:remove-duplicate-schemas($allschemas)"/>
  </xsl:function>

一个步骤是加载所有引用的模式。这通过像清单 2 一样递归地调用函数 load-referenced-schemas() 来执行。

清单 2. 加载引用模式
<xsl:function name="xsdf:load-referenced-schemas" 
              as="document-node()*">
    <xsl:param name="docNode" as="document-node()"/>
    <xsl:param name="docLocs" as="xsd:string"/>

    <xsl:for-each select="$docNode/xsd:schema/(xsd:include|xsd:import)">
       <xsl:variable name="schemaURI" 
                     select="resolve-uri(@schemaLocation, base-uri(.))"/>
       <xsl:if test="not(contains($docLocs, $schemaURI))">
          <xsl:variable name="schemaNode" select="doc($schemaURI)"/>
                  <xsl:variable name="schemaLocations" 
                                select="concat($docLocs, '| ', $schemaURI)"/>
                  <xsl:sequence select="$schemaNode"/>
          <xsl:sequence select="xsdf:load-referenced-schemas($schemaNode, 
                                     $schemaLocations)"/>
       </xsl:if>
    </xsl:for-each>
</xsl:function>

load-referenced-schemas() 函数从主模式的文档节点开始,然后递归地调用它自身,加载所有由 <xsd:import><xsd:include> 元素指定的引用模式。对每个导入或包含的模式执行相同的步骤,以加载由这些导入或包含的模式指定的引用模式。一直重复这个过程,直到遇到既不导入又不包含任何其他模式的模式。

额外的参数 docLocs 根据特定的导入和包含路径记录已加载模式的 URI,避免在循环引用时发生无限递归引用。

为了演示模式中可能的循环引用,我将模式的导入和包含描绘成一个图表,其中的节点表示模式文档、链接表示两个模式之间的导入或包含关系。图 1 显示的示例是一个主模式,它导入或包含了模式 11、模式 12 和模式 13。

图 1. 表示模式引用的图表
表示模式引用的图表
表示模式引用的图表

模式 11 导入或包含模式 23,模式 23 又导入或包含模式 12,依此类推。路径 main schema-schema12-schema24-schema23-schema12 展示了典型的循环引用。

load-referenced-schemas() 函数将已加载模式的 URL 存储到一个变量,并且将该值传递给对这个函数的后续调用。这样,URL 已出现在参数中的循环引用模式将不再加载,而是作为递归函数的结束点。

虽然这个函数确保重复出现在单一路径的模式只加载一次,但它不能阻止多次加载出现在多个路径的模式。例如,模式 12 由于出现在以下两个路径中而被加载两次:

主 schema-schema11-schema23-schema12-schema24
主 schema-schema12-schema24-schema23

要删除重复的模式节点,调用函数 remove-duplicate-schemas()

代替和类型派生测试

该库的两个函数模仿了 XPath 2 节点测试的两个功能 —element(Name, Type)schema-element(Name),已在 与 XPath 2 模式相关的节点测试 中描述。这两个函数被定义为:

  • 第一个:xsdf:is-substitutable-for(element() $elementNode, xsd:QName $groupHeadElementQName, document-node()+ schemaDocs) -> xsd:Boolean

    获取实例文档中的一个元素节点、一个元素 QName 和一个模式文档节点的序列。如果实例文档中的元素被声明为位于以元素 QName 指定的元素为首的代替组中,则返回 true。

  • 第二个:xsdf:is-derived-from(element() $elementNode, xsd:QName $typeName, document-node()+ schemaDocs) -> xsd:boolean

    获取一个实例元素节点、一个类型 QName 和一个模式文档节点的序列。如果实例元素声明为具有直接或间接来自类型 QName 指定的类型,则返回 true。

两个函数都要求提供的模式文档声明或定义要测试的实例元素,以及代替组 head 元素或引用类型。

这些函数首先为提供的模式文档中的实例元素查找元素声明。然后遍历元素声明的代替链或类型层次结构,以决定元素声明是否满足代替或类型衍生要求。

清单 3. 示例模式
<element name="shipComment" type="string"
         substitutionGroup="ipo:comment"/>
<element name="customerComment" type="string"
         substitutionGroup="ipo:comment"/>

例如,在 清单 3 的模式片段中,两个元素 <app:shipComment><app:customerComment> 都声明为可以代替 清单 3 的模式片段中的 <ipo:comment> 元素。当它们出现在清单 4 的实例文档中时,路径表达式 item/element()[is-substitutable-for(., xsd:QName(ipo:comment), $schemaDocs) 会选择两个元素 <app:shipComment><app:customerComment>,前提是 $schemaDocs 表示包含这个示例涉及的所有元素声明和类型定义的模式文档。

清单 4. 示例实例
<item>
      …
   <app:shipComment>
       Some comments on shipping
   </app:shipComment>
   <app:customerComment>
       Some comments from customers
   </app:customerComment>
      …
</item>

定位元素声明和类型定义

为了方便基于模式定义的元素类型测试,该库实现了许多为模式文档中给定的实例元素节点定位元素声明的函数。目前,这些函数的实现基于这样一个假设:所有局部或全局声明的元素都属于某个特定的名称空间。

尽管您能够通过元素名称和名称空间轻松定位一个全局声明的元素,但不能通过名称和名称空间惟一地定位局部声明的元素,因为出现在不同上下文中的局部元素声明可能具有相同的名称和名称空间。这里使用的算法首先沿着来自实例元素的父轴向后遍历,直到遇见一个全局元素。接下来,从这个全局元素声明开始,这个算法沿着前一个步骤形成的路径,在全局声明的元素的内容中继续查找下一个元素的元素声明。然后重复这个过程,直到找到路径中最后一个元素的声明 —— 它便是所查找的实例元素。

例如,假设清单 5 中的元素 X、Y 和 Z:

清单 5. 示例实例
<X>
   <Y>
      <Z>some text</Z>
   </Y>
</X>

在清单 6 中声明。

清单 6. 模式 A
<element name="X" type="TypeX"/>
<complexType name="TypeX">
   <sequence>
      <element name="Y" type="TypeY"/>
   </sequence>
</complexType>

<complexType name="TypeY">
   <sequence>
     <element name="Z" type="string"/>
  </sequence>
</complexType>

要找到模式 A 中元素 <Z> 的声明,需要将路径构造成 X/Y/Z,因为 <X> 是最近的祖先元素,它是全局声明的。下一步是查找来自类型定义 TypeX 的 <Y> 元素的声明。最后元素 <Z> 的声明位于它的父元素 <Y> 的类型定义 TypeY 中。

这个算法的限制是局部元素不能声明为与全局元素同名。

这个库的其他一些函数的目的是从类型定义中成功搜索局部元素声明。这些函数包括从模型组(比如 <sequence>、<choice> 和 <all>)、组定义 <group> 以及某个类型定义的所有基类型定义的内容模型查找局部元素声明的函数。

如果您对这个库的详细实现感兴趣,可以从 下载 找到这些函数的代码。

函数库的使用

当您使用这个库来处理带有模式定义的实例数据时,应用程序首先调用函数 load-schema-by-URI() 加载模式,然后使用测试函数处理已加载的模式文档。

在这里介绍的典型示例是一个应用程序,它处理以地理标记语言(Geography Markup Language,GML)编码的地理数据,GML 是表示地理数据的标准化 XML 词汇表。这个应用程序的功能仅仅检查用 GML 实例文档编码的地理特性是否包含至少一个几何元素。目标是展示如何使用这个库开发具有模式信息的通用应用程序。

与 GML 有关的一些知识能够帮助您理解这个示例应用程序使用模式信息处理实例数据的威力和动机。在 GML 中,除了其他东西以外,标准的模式集还包含一个抽象元素 gml:_Feature,表示用于描述地理特征位置的地理特性和各种几何元素。不同领域的应用程序可以开发各自的词汇表,方法是从核心模式直接引用预定义的元素或类型定义,或者使用 XML 模式类型派生扩展核心词汇表。基本规则是:

  • 应用程序定义的地理特性必须适用于预定义的元素 gml:_Feature
  • 应用程序定义的几何类型必须源自预定义的几何类型 gml:AbstractGeometryType

这些规则表明一个应用程序不能预知具体的特性元素名或几何元素名,它们有可能出现在 GML 实例文档。因此,基于具体的实例元素名的应用程序不能处理任何 GML 实例数据。对于这种情况,基于模式信息的应用程序开始派上用场。

清单 7 的脚本基本上选择了任意的特性元素,这是通过选择所有可用于替换在标准核心模式中声明的抽象特性元素 gml:_Feature 实现的。

清单 7. 示例应用程序
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
   xmlns:xsd="http://www.w3.org/2001/XMLSchema"
   xmlns:xsdf="http://www.schemaquery.org"
   xmlns:gml="http://www.opengis.net/gml">



  <xsl:import href="reflection.xsl"/>
  <xsl:output indent="yes"></xsl:output>

 <xsl:variable name="schemas" 
        select="xsdf:load-schemas-by-URI('gmlAppSchema.xsd')"/>

 <xsl:template match="/">		  			
    <xsl:apply-templates select="//element()[xsdf:is-substitutable-for
       (., xsd:QName('gml:_Feature'), $schemas)]"/>		  			
 </xsl:template>

 <xsl:template 
    match="element()[xsdf:is-substitutable-for(., xsd:QName('gml:_Feature'), $schemas)]">
 
   <xsl:if test="some $element in .//element() satisfies xsdf:is-derived-from
      ($element, xsd:QName('gml:AbstractGeometryType'), $schemas)">
   <xsl:message>
        feature that has geometries: <xsl:value-of select="local-name(.)"/>
   </xsl:message>
   
   </xsl:if>

  </xsl:template>
</xsl:stylesheet>

为了测试匹配的特性是否在其后代中包含几何元素,这个处理匹配特性元素的模板规则使用了路径表达式,路径表达式从标准核心模式选择类型定义源自预定义类型 gml:AbstractGeometryType 的元素。通过在已知的标准核心模式中使用元素声明或类型定义,应用程序可以在实例文档中(该文档遵循模式,不用预先知道实例文档中的具体元素名)选择和处理任意的元素。具体特性元素可以命名为 <Road><Building><River>。不管使用哪个名称,它们都匹配基于它们的代替组 head 元素的路径表达式。因此,这个应用程序在以下方面又超越了仅仅基于实例文档的应用程序:

  • 简单性:通过使用单个 head 元素或类型定义来选择一组元素,使代码更加紧凑高效。
  • 通用性:由于不需要列出所有可能的元素名,使代码更加通用,可以包含可能出现在实例文档中的不同特性元素的变体。
  • 扩展性:添加新的特性元素并不需要更改代码。即使需要特殊处理某些特性元素,也可以通过其他模板解决,而不需要修改现有代码。

清单 7 中的示例应用程序展示了这个库的典型用法。我想强调一些要点,使您明白哪些是特定于这个应用程序的,哪些是适用于使用该库的任何应用程序的:

  • 声明名称空间:除了针对 XSLT 的名称空间声明之外,示例脚本还包含 3 个其他名称空间声明:
    1. 其中之一是带有前缀 xsdf 的名称空间,用于在库中定义的函数。任何使用该库的应用程序都需要这个名称空间声明,因为用户定义的函数必须存在某个名称空间中。
    2. 带有前缀 xsd 的名称空间用于使用 XML 模式构造函数 xsd:Qname()
    3. GML 名称空间(带有前缀 gml)用于引用在地理标记语言中定义的元素 gml:_Feature 和类型 gml:_AbstractGeometryType。这个名称空间声明是特定于示例脚本的;其他应用程序根据所处理的元素可能有不同的名称空间声明。
  • 导入函数库:为了使用这个库提供的函数,需要一个应用程序,通过 XSLT 指令 <xsl:import> 将包含在文件 reflection.xsl 中的函数库导入到的应用程序中。
  • 加载模式:为了从模式中使用元素声明或类型定义,应用程序通过库中提供的带有指向主模式文档位置的 URI 的函数 load-schema-by-URI(),将模式文档和所有导入的或包含的模式读入到一个变量中。
  • 使用节点测试函数:尽管示例脚本中的节点测试函数为一个特定的 XML 词汇表选择元素,但它展示了使用在 XPath 表达式中确定谓词的节点测试函数的通用模式,这就像在模式中根据代替组 head 元素或类型定义选择元素一样。

结束语

我在这里介绍的函数库使应用程序可以将模式信息用于任何 XSLT 2 基本处理器。有了模式信息后,您可以大大简化程序,更重要的是,可以提高应用程序的生产率。

另外,这个函数库是用纯 XSLT 实现的,这将为代码提供很强的可移植性。这个方法也适用于 XQuery,因为这个库完全允许用 XQuery 来实现。


下载资源


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=349523
ArticleTitle=使用模式信息处理实例数据
publish-date=11032008