本文主要围绕如何使用多个文档来组成目标的模式而展开讨论。XML Schema 中为实现这一目标提供了多种机制:包括通过扩展和约束从基类型中派生新的类型;提供重定义机制从而依靠基类型的更新而使派生类型进行更新;利用置换组和抽象定义以及相应的机制来控制元素和类型在实例中的表现。总之,XML Schema 提供了丰富的机制以支持大型的模式的撰写。
在XML Schema Ⅰ,Ⅱ中描述的购买订单模式文档是包含在一个单独的文档中的,并且这个模式文档中的大多数构造,比如元素声明和类型定义,其构造相对都是很随意的。实际上,模式文档作者常常会想通过多个文档组合模式文档的构造,并且可以基于现有的类型定义来建立新的类型定义。在本文中,我们将来看看这样的构造和创建的机制。
由多个文档组成的模式文档
随着模式文档变得越来越大,为了便于维护、访问控制并兼顾可读性,常常会考虑把他们的内容分在几个模式文档中 。基于这些原因,我们把模式文档中关于 address 的构造从先前的po.xsd 中取出,并放在一个新的文件address.xsd 中。修改后的购买订单模式文档称为ipo.xsd :
<schema targetNamespace="http://www.example.com/IPO" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ipo="http://www.example.com/IPO"> <annotation> <documentation xml:lang="en"> International Purchase order schema for Example.com Copyright 2000 Example.com. All rights reserved. </documentation> </annotation> <!-- include address constructs --> <include schemaLocation="http://www.example.com/schemas/address.xsd" /> <element name="purchaseOrder" type="ipo:PurchaseOrderType" /> <element name="comment" type="string" /> <complexType name="PurchaseOrderType"> <sequence> <element name="shipTo" type="ipo:Address" /> <element name="billTo" type="ipo:Address" /> <element ref="ipo:comment" minOccurs="0" /> <element name="items" type="ipo:Items" /> </sequence> <attribute name="orderDate" type="date" /> </complexType> <complexType name="Items"> <sequence> <element name="item" minOccurs="0" maxOccurs="unbounded"> <complexType> <sequence> <element name="productName" type="string" /> <element name="quantity"> <simpleType> <restriction base="positiveInteger"> <maxExclusive value="100" /> </restriction> </simpleType> </element> <element name="USPrice" type="decimal" /> <element ref="ipo:comment" minOccurs="0" /> <element name="shipDate" type="date" minOccurs="0" /> </sequence> <attribute name="partNum" type="ipo:SKU" use="required" /> </complexType> </element> </sequence> </complexType> <simpleType name="SKU"> <restriction base="string"> <pattern value="\d{3}-[A-Z]{2}" /> </restriction> </simpleType> </schema>
而包含address 结构的文件address.xsd 为:
<schema targetNamespace="http://www.example.com/IPO" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ipo="http://www.example.com/IPO"> <annotation> <documentation xml:lang="en"> Addresses for International Purchase order schema Copyright 2000 Example.com. All rights reserved. </documentation> </annotation> <complexType name="Address"> <sequence> <element name="name" type="string" /> <element name="street" type="string" /> <element name="city" type="string" /> </sequence> </complexType> <complexType name="USAddress"> <complexContent> <extension base="ipo:Address"> <sequence> <element name="state" type="ipo:USState" /> <element name="zip" type="positiveInteger" /> </sequence> </extension> </complexContent> </complexType> <complexType name="UKAddress"> <complexContent> <extension base="ipo:Address"> <sequence> <element name="postcode" type="ipo:UKPostcode" /> </sequence> <attribute name="exportCode" type="positiveInteger" fixed="1" /> </extension> </complexContent> </complexType> <!-- other Address derivations for more countries --> <simpleType name="USState"> <restriction base="string"> <enumeration value="AK" /> <enumeration value="AL" /> <enumeration value="AR" /> <!-- and so on ... --> </restriction> </simpleType> <!-- simple type definition for UKPostcode --> </schema>
多样化购买订单的结构和address 的结构现在被包含在两个模式文档ipo.xsd 和 address.xsd 里,为了把这两个结构作为国际化的购买订单模式文档的一部分,换句话来说,就是要在国际化的购买订单的命名空间中包含它们。 ipo.xsd 包含include 元素:
<include schemaLocation="http://www.example.com/schemas/address.xsd"/>
include 元素的作用是引入在address.xsd 中的定义和声明,并且把他们作为国际化的购买订单模式文档的目标命名空间的一部分。使用 include 的一个要点是,被包含成员的目标命名空间必须和包含方的目标命名空间一样,在这里这个目标命名空间就是"http: //www.example.com/IPO" 。使用include 机制来包含定义和声明能够有效地把这些成员添加到已存在的目标命名空间中来。在PO 国际化的购买订单的后面,我们描述了一个类似的机制,该机制允许你在引入组件的时候,对它们进行一些修改。
在例子中,只有一个包含文档和一个被包含文档。在实际中可以使用多个include 元素来包含多个文档。文档可以包括那些自身还包含其他文档的文档。然而,只有在模式文档中所有被包含的部分都由一个目标命名空间声明时,嵌套文档才是合法的。
要声明与那些定义在多个模式文档中的模式相一致,实例文档只需要引用" 最顶层" 的文档并使用这些模式文档共同的命名空间就可以了。 把包含在不同文档中的所有定义聚集在一起是处理器的责任。在上面的例子中,其实例文档ipo.xml( 参见下面的代码) 只引用了共同的目标命名空间"http: //www.example.com/IPO" 和(隐含的)一个模式文档"http: //www.example.com/schemas/ipo.xsd" 。处理器有责任要去获取模式文档address.xsd. 。在后面我们会描述模式文档如何能够由多个命名空间来共同验证实例文档的内容。
<?xml version="1.0"?> <ipo:purchaseOrder xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ipo="http://www.example.com/IPO" orderDate="1999-12-01"> <shipTo exportCode="1" xsi:type="ipo:UKAddress"> <name>Helen Zoe</name> <street>47 Eden Street</street> <city>Cambridge</city> <postcode>CB1 1JR</postcode> </shipTo> <billTo xsi:type="ipo:USAddress"> <name>Robert Smith</name> <street>8 Oak Avenue</street> <city>Old Town</city> <state>PA</state> <zip>95819</zip> </billTo> <items> <item partNum="833-AA"> <productName>Lapis necklace</productName> <quantity>1</quantity> <USPrice>99.95</USPrice> <ipo:comment>Want this for the holidays!</ipo:comment> <shipDate>1999-12-05</shipDate> </item> </items> </ipo:purchaseOrder>
通过扩展来派生类型
为了建立自己的address 结构,通常通过建立一个称为address 的复合类型来开始( 参阅前面的address.xsd) 。address 类型包含一些地址的基本元素:姓名、街道和城市( 这样的定义不一定对所有的城市都适用,但能够满足这个例子的目的) 。从这一个基础的复合类型出发,可以得到两个新的复合类型,他们包含所有在源类型中原有的定义,同时还添加了用来特指在美国和英国使用的地址的附加元素。在这里通过扩展现有的类型来得到新的类型( 复合类型) 的技术和在前面的文章中扩展简单类型使用的技术是一样的。两者的不同点仅仅在于这里的基类型是复合类型而前面的章节中基类型是简单类型而已。
在这里,定义了两个新的类型,USAddress 和UKAddress ,使用 complexType 类型。另外,指明新类型的内容模型是复杂的,因此使用complexContent 元素来包含下层子元素。并且我们指明我们通过extension 元素的base 属性来扩展基类型address 。
当一个复合类型是通过扩展而被派生的时候,他的有效内容模型是基类型的内容模型加上在这个类型派生的过程中指定的内容模型。甚至,这两个内容模型将可被看作是一个有序组的两个子模型。在UKAddress 这个例子中,UKAddress 的内容模型是address 的内容模型加上postcode 元素的声明和一个exportCode 属性。具体的来说,通过派生定义的UKAddress 与下面的这个在一个文档中进行完整定义的UKAddress 模式是等价的:
<complexType name="UKAddress"> <sequence> <!-- content model of Address --> <element name="name" type="string" /> <element name="street" type="string" /> <element name="city" type="string" /> <!-- appended element declaration --> <element name="postcode" type="ipo:UKPostcode" /> </sequence> <!-- appended attribute declaration --> <attribute name="exportCode" type="positiveInteger" fixed="1" /> </complexType>
在实例文档中使用派生类型
在这个例子里,购买订单是应对客户订单的响应而生成的,这一响应可能需要包含不同国家的不同形式的送货地址或者支付地址。下面的国际化的购买订单实例 ipo.xml 显示了这种情况下的示例:货物将被运到英国而帐单则是寄到美国。显然,较好的方案是国际化的购买订单的模式文档并不要去清楚地说明每个国际化的支付和送货地址可能的组合。甚至能够仅仅通过建立新的Address 类型的派生,来添加国际化地址的复合类型。XML Schema 允许将billTo 和ShipTo 元素定义为Address 类型( 参阅ipo.xsd) ,而在使用Address 类型实例的地方使用国际化的Address 类型的实例。换句话说,如果在文档中一个地方需要Address 类型,然后在这里出现的内容与UKAddress 类型相符的话,这个实例文档仍将是正确的( 在这里假设UKAddress 内容本身是正确的) 。为了使XML Schema 的这个特性工作,并为了能识别这个类型到底是从何派生而来,在实例文档中的派生类型必须能被显式地识别。类型是通过xsi:type 属性来识别,这个属性是XML Schema 实例命名空间的一部分。在例子ipo.xml 中,派生类型UKAddress 和USAddress 的使用是通过设置的xsi:type 属性来识别的。
<?xml version="1.0"?> <ipo:purchaseOrder xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ipo="http://www.example.com/IPO" orderDate="1999-12-01"> <shipTo exportCode="1" xsi:type="ipo:UKAddress"> <name>Helen Zoe</name> <street>47 Eden Street</street> <city>Cambridge</city> <postcode>CB1 1JR</postcode> </shipTo> <billTo xsi:type="ipo:USAddress"> <name>Robert Smith</name> <street>8 Oak Avenue</street> <city>Old Town</city> <state>PA</state> <zip>95819</zip> </billTo> <items> <item partNum="833-AA"> <productName>Lapis necklace</productName> <quantity>1</quantity> <USPrice>99.95</USPrice> <ipo:comment>Want this for the holidays!</ipo:comment> <shipDate>1999-12-05</shipDate> </item> </items> </ipo:purchaseOrder>
通过约束来派生复合类型
除了通过扩展内容模型来派生新的复合类型外,也可以通过约束现有类型的内容模型来得到新的类型。复合类型的约束在概念上和对简单类型的约束是一样的,不同点仅仅在于对复合类型的约束需要包括一个类型声明而不仅仅在简单类型约束中是一个简单类型值的可接受范围。一个通过约束得到的复合类型和基类型非常相象,不同点仅仅在于他的声明比在基类型中的声明有更多的限制。实际上,新类型所表现的值是基类型所表现值的一个子集( 和简单类型约束的情况一样) 。换句话说,一个为兼容基类型值而开发的应用如果接受到约束后类型的值就不会有任何的兼容性问题。
举例来说,假设在国际化的购买订单中我们想要更新item 列表的定义,以使得在一个订单中至少包含一个item ,而在ipo.xsd 中的模式允许一个items 元素没有任何子item 元素出现。为了建立这个新的ConfirmedItem 类型。使用常用的方法来建立这个新的类型,通过指明它是从基类型items 限制而得到的,并且对item 元素出现的最小数量提供了一个新的值( 更进一步的限制) 。需要注意的是,由约束而派生出来的类型必须重复定义所有要包含在派生类型中的原基类型的定义组成部分。
<complexType name="ConfirmedItems"> <complexContent> <restriction base="ipo:Items"> <sequence> <!-- item element is different than in Items --> <element name="item" minOccurs="1" maxOccurs="unbounded"> <!-- remainder of definition is same as Items --> <complexType> <sequence> <element name="productName" type="string" /> <element name="quantity"> <simpleType> <restriction base="positiveInteger"> <maxExclusive value="100" /> </restriction> </simpleType> </element> <element name="USPrice" type="decimal" /> <element ref="ipo:comment" minOccurs="0" /> <element name="shipDate" type="date" minOccurs="0" /> </sequence> <attribute name="partNum" type="ipo:SKU" use="required" /> </complexType> </element> </sequence> </restriction> </complexContent> </complexType>
通过更新,ConfirmItems 被定义为至少要包含一个子元素,而不允许包含零个元素,当然也不是至少要包含多个子元素。这一定义的实质就是,将子元素出现数量的允许范围从最少零个改变为最少一个。值得注意的是,所有ConfirmItems 类型元素也被视为item 类型的元素,被处理器接受。
为了进一步显示约束,使用下面表格中的几个例子来显示了类型定义中的元素和属性声明是如何被约束的( 这个表格展示了元素的约束语法) 。
基类型 |
约束 |
注解 |
|
Default=”1” |
在先前没有给出任何值定义的地方设置一个默认值 |
|
Fixed=”100” |
在先前没有给出任何值定义的地方设置一个固定值 |
|
Type=”String” |
在先前没有给出类型定义的地方定义一个类型 |
(minOccurs,maxOccurs) |
(minOccurs,MaxOccurs) |
|
(0,1) |
(0,0) |
将一个原本可选的组件排除在派生类型之外,这一声明的实现也许可以通过限制类型定义从而忽略成分的声明。 |
(0,unbounded) |
(0,0)(0,37) |
|
(1,9) |
(1,8)(2,9)(4,7)(3,3) |
|
(1,unbounded) |
(1,12)(3,unbounded)(6,6) |
|
(1,1) |
- |
不能进一步限制minOccurs 或者maxOccurs |
重新定义类型和组
在由多个文档组成的模式文档中描述了如何通过具有相同目标命名空间的外部模式文件来提供额外的定义和声明。include 机制使你能够原样地使用外部建立的模式组件,而无需作任何修改。在本节之前,描述了如何通过扩展和约束来得到新的类型定义,而在这里描述的redefine 机制将允许你重新定义从外部模式文件获得的简单和复合类型、元素组和属性组。像include 机制一样,redefine 同样需要满足以下条件:引入的外部组件与重定义的模式文档应当具有相同的目标命名空间,当然不包含目标命名空间的外部模式组件也可以被重新定义。 然后,被重定义的组件部分就成为重定义模式的目标命名空间的一部分。
为了演示redefine 机制,在国际化的购买订单ipo.xsd 中我们使用它来代替include 机制,并且我们使用这一机制来修正包含在address.xsd 中的复合类型Address 的定义:
<schema targetNamespace="http://www.example.com/IPO" xmlns="http://www.w3.org/2001/XMLSchema" xmlns:ipo="http://www.example.com/IPO"> <!-- bring in address constructs --> <redefine schemaLocation="http://www.example.com/schemas/address.xsd"> <!-- redefinition of Address --> <complexType name="Address"> <complexContent> <extension base="ipo:Address"> <sequence> <element name="country" type="string" /> </sequence> </extension> </complexContent> </complexType> </redefine> <!-- etc. --> </schema>
redefine 元素表现的形式非常像 include 元素。它从address.xsd 文件中引入其包含的所有的声明和定义。复合类型Address 的定义使用常见的扩展语法,来添加 country 元素到Address 类型的定义中去。然而,这里比较特别的是,应该注意到它的基类型也是Address ,这是redefine 所特有的 。在除redefine 元素之外的地方,任何类似的试图为定义的类型取与其基类型有同样名字,从而定义复合类型的情况( 即使在同样的命名空间中) 将会导致出错。但是在redefine 的场合下则不会产生错误,在这里Address 的扩展定义仅仅是Address 的重新定义。
现在Address 类型被重新定义了,这一扩展定义被应用到所有使用Address 的模式成分。举例来说,address.xsd 包含从Address 类型派生出来的国际化的地址类型的定义,这个派生出来的类型将重新应用重定义后的Address 类型作为它的基类型,我们可以通过下面的实例来查看这一方式的应用:
.... <shipTo exportCode="1" xsi:type="ipo:UKAddress"> <name>Helen Zoe</name> <street>47 Eden Street</street> <city>Cambridge</city> <!-- country was added to Address which is base type of UKAddress --> <country>United Kingdom</country> <!-- postcode was added as part of UKAddress --> <postcode>CB1 1JR</postcode> </shipTo> ....
这个例子是周全地构造的,以使得这样重新定义的Address 类型不会和从原始的Address 定义中派生的类型相冲突。但是需要注意的是,使用redefine 还是很容易会造成冲突 。举例来说,如果派生的国际化的地址类型是通过添加一个country 元素来扩展Addrss 类型,然后我们又重新定义了Address 类型,同样是添加了一个同样名字的元素到Address 的内容模型中去,那此时冲突就会发生。一般来说,在一个内容模型中的两个同名元素( 并且在一个目标命名空间内) ,如果具有不同类型,那么就会产生非法错误,因此此时试图按照这种方式重定义Address 类型就将会导致错误。一般的来说,redefine 没有提供避免类似错误的保护,它应该被慎重的使用。
置换组
XML Schema 提供了一个机制,称为置换组(Substitution Groups) ,允许原先定义好的元素被其他元素所替换。 更明确的,这个置换组包含了一系列的元素,这个置换组中的每一个元素都被定义为可以替换一个指定的元素,这个指定的元素称为头元素(Head Element) ,需要注意的是头元素必须作为全局元素声明。具体地来看,我们声明了两个元素customerComment 和shipComment ,并且将它们分配到一个置换组,该组的头元素为comment 。因此customerComment 和shipComment 能够在任何能够使用 comment 的地方使用。在置换组中的元素必须具有与头元素相同的类型,或者,它们的类型是头元素类型的派生类型。为了声明这两个新元素,并且使它们能替换comment 元素,我们使用如下的语法:
< element name = "shipComment" type = "string" substitutionGroup = "ipo:comment" />
< element name = "customerComment" type = "string" substitutionGroup = "ipo:comment" />
当这部分声明被添加到了国际化的购买订单模式文档中后, shipComment 和 customerComment 就能够在实例文档中替换 comment 了,下面是一个实例文档的例子:
.... <items> <item partNum="833-AA"> <productName>Lapis necklace</productName> <quantity>1</quantity> <USPrice>99.95</USPrice> <ipo:shipComment>Use gold wrap if possible</ipo:shipComment> <ipo:customerComment> Want this for the holidays! </ipo:customerComment> <shipDate>1999-12-05</shipDate> </item> </items> ....
注意 ,当一个实例文档包含元素置换时,替换元素的类型是从它们的头元素那里派生的,此时,并不需要使用我们在先前述的xsi:type 结构来识别这些被派生的类型。
当定义了置换组后,并非意味着不能使用头元素,而只能只用这个置换组中的元素。它只是提供了一个允许元素可替换使用的机制。
抽象元素和类型
XML Schema 提供了一个机制来强迫替换一个特定的元素或者类型。当一个元素或者类型被声明为 "abstract" 时,那么它就不能在实例文档中使用。当一个元素被声明为 "abstract" 的时候,元素的置换组的成员必须出现在实例文档中。当一个元素相应的类型被定义声明为 "abstract" 时,所有关联该元素的实例必须使用 "xsi:type" 来指明一个类型,这个类型必须是非抽象的,同时是在定义中声明的抽象类型的派生类型。
我们再来查看一下 Error! No text of specified style in document. 中描述的置换组的例子,如果,特别的,要求在实例中不允许使用 comment 元素也许可以使这个模式定义更加清晰,这样实例就必须使用 customerComment 和 shipComment 元素来代替原来的 comment 元素。为了声明 comment 元素为抽象元素,我们需要修改相应的在国际化的购买订单模式文档 ipo.xsd 中的原始声明,将其改为如下形式:
< element name = "comment" type = "string" abstract = "true" />
随着 comment 元素被声明为抽象元素,国际化的购买订单的实例现在只能包含 customerComment 元素和 shipComment 元素才是有效的。把一个元素声明为抽象元素,需要使用置换组。 声明一个类型为抽象类型则只要在实例文档中使用从该抽象类型派生的类型就可以了( 当然,还要通过 xsi:type 属性来识别) 。考虑下面的模式定义:
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://cars.example.com/schema" xmlns:target="http://cars.example.com/schema"> <complexType name="Vehicle" abstract="true" /> <complexType name="Car"> <complexContent> <extension base="target:Vehicle" /> </complexContent> </complexType> <complexType name="Plane"> <complexContent> <extension base="target:Vehicle" /> </complexContent> </complexType> <element name="transport" type="target:Vehicle" /> </schema>
transport 元素并不是抽象元素,因此它能够在实例文档中出现。然而因为它的类型定义是抽象类型的,如果在实例中没有使用 "xsi:type" 属性来引用派生类型,那么它将不能出现在实例文档中 。这意味着下面的这个例子是无法通过模式校验的: < transport xmlns = "http://cars.example.com/schema" />
上述例子无法通过模式校验的原因是因为 transport 元素的类型是抽象的,而下面的这个实例片断则是能够通过模式校验的: < transport xmlns = "http://cars.example.com/schema"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi:type = "Car" />
因为它使用了一个非抽象类型 Car 来替换 Vehicle ,并且非抽象类型 Car 是类型 Vehicle 的派生类型。