Java Web开发详解
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第2章 DTD

本章要点

● 掌握内部DTD和外部DTD的编写

● 掌握元素类型声明

● 掌握实体声明

● 掌握属性表声明

● 了解记号声明

● 理解“格式良好的”和“有效的”XML文档

● 使用XMLSpy编写DTD

对于一个格式良好的XML文档,我们只能保证这个文档的格式符合XML规范,但是元素与元素的关系、元素与属性的关系,属性的取值是否正确,我们就无法得知了。对于一个格式良好的文档,如果仅仅是在有限的应用中使用,或者作为数据的存储传输,那么也能很好地满足我们的应用。但如果说要让其他的用户理解你的XML文档,或者和其他的应用进行数据交换,那么就有必要提供一种机制,来保证我们所写的XML文档和别人所写的XML文档其结构是相同的,元素与元素之间的关系是正确的,属性的取值也是符合要求的。

这种机制在XML标准中已经为我们提供了,也就是前面我们提到的DTD(Document Type Definition,文档类型定义)。在XML标准中,描述了如何创建DTD,以及如何将它与根据它的规则所编写的XML文档相关联,并且还定义了XML处理器应该如何对DTD进行处理。

有了DTD就可以检测XML文档的结构是否正确。DTD与XML的关系,就好像模型与产品的关系、城市规划图纸与城市的关系、建筑图纸与建筑的关系。房屋的建筑图纸(DTD)定义了房屋(XML文档)的结构,厨房应该在什么位置,阳台应该出现在哪,主卧应该是什么结构等。房屋在建设时(XML文档在编写时)必须严格按照图纸(DTD)来进行,房屋验收人员(XML验证处理器)将在房屋建成后(XML文档编写完成后)根据建筑图纸(DTD)来进行验收(验证)。

2.1 在XML文档中引入DTD

通过在XML文档中包含文档类型声明,来建立当前文档和DTD的关联。当进行有效性验证的XML处理器读到该指令时,它获取DTD,并根据其中定义的规则对文档进行检验。文档类型声明必须位于XML声明之后,且在根元素(文档元素)之前。不过,在XML声明和文档类型声明之间可以插入注释和处理指令。

我们可以直接在XML文档中定义DTD,也可以通过URI引用外部的DTD文件,或者同时采用这两种方式。

1.内部DTD

        <?xml version="1.0" encoding='GB2312' standalone="yes"?>
        <!DOCTYPE greeting [
            <!ELEMENT greeting (#PCDATA)>
        ]>

文档类型声明由<!开始,后面紧跟一个关键字DOCTYPE,然后是文档根元素的名称,接下来是标记声明块,标记声明块是放在左中括号([)和右中括号(])之间的,由一个或多个标记声明构成,最后由>结束。

在DTD中,所有的关键字都是大写的,就像在这里看到的ELEMENT、#PCDATA一样,在后面我们还会看到其他的关键字。不过,在DTD中定义的元素和属性的大小写是可以任意指定的,但是要注意,因为XML文档是大小写相关的,所以一旦给一个元素命名,那么在整个文档中要使用相同的大小写。例如:greeting和Greeting是两个不同的元素名。

在XML文档中定义DTD,比较直观,修改也较为方便,而且不用担心XML处理器找不到DTD,但是它也有一些缺点:

● 在文档中定义DTD会导致文档本身的长度增加,在传输数据时,即使不需要验证文档的有效性,这些声明也会随着文档一起传输;

● 如果多个XML文档要共用同一个DTD,我们就需要在每一个文档中加入DTD,这是相当繁琐的。

要解决这两个问题,我们可以将DTD放到一个单独的文件中去定义,在XML文档中,通过URI去引用外部的DTD文件。

2.外部DTD

在文档类型声明时,用关键字SYSTEM或PUBLIC来指出外部DTD文件的位置。使用SYSTEM关键字的声明语法如下:

        <!DOCTYPE 根元素的名字 SYSTEM "外部DTD文件的URI">

SYSTEM关键字表示文档使用的是私有的DTD文件,“外部DTD文件的URI”可以是相对URI或者绝对URI,相对URI是相对于文档类型声明所在文档的位置。“外部DTD文件的URI”这部分也被称为系统标识符(system identifier)。下面是使用一个外部DTD文件的例子:

        <!DOCTYPE greeting SYSTEM "hello.dtd">

我们将DTD的定义放在了hello.dtd文件中,注意要将hello.dtd放在和XML文档同一个目录下,这样XML处理器才能找到这个文件。在给DTD文件取名时,文件名可以随意取,但扩展名通常都是使用“.dtd”。

如果位于不同位置的多个XML文档要使用同一个DTD,我们可以使用绝对URI来指明DTD文件的地址。假定hello.dtd位于http://www.sunxin.org/xml/dtds/hello.dtd,可以在文档类型声明中使用此URI:

        <!DOCTYPE greeting SYSTEM "http://www.sunxin.org/xml/dtds/hello.dtd">

如果引用DTD的XML文档与DTD文件在同一个Web服务器上,我们也可以使用相对URL:

        <!DOCTYPE greeting SYSTEM "/xml/dtds/hello.dtd">
        <!DOCTYPE greeting SYSTEM "/dtds/hello.dtd">
        <!DOCTYPE greeting SYSTEM "../hello.dtd">

上面的3种形式都是允许的。

假如为了规范国内企业的人力资源管理,各企业的代表经过协商制定了人力资源管理方面的DTD,我们现在要引用这个DTD,就要用PUBLIC关键字而不是SYSTEM关键字。

使用PUBLIC关键字的声明语法如下:

        <!DOCTYPE 根元素的名字 PUBLIC "DTD的名称" "外部DTD文件的URI">

PUBLIC关键字用于声明公共的DTD,并且这个DTD还有一个名称,“DTD的名称”也称为公共标识符(public identifier)。这个DTD可以存放在某个公共的地方,XML处理程序会根据名称按照某种方式去检索DTD,如果XML处理器不能根据名称检索到DTD,就会使用“外部DTD文件的URI”(系统标识符)来查找该DTD。

DTD名称与XML名称略有不同,它们只能包含ASCII字母和数字字符、空格、回车符、换行符和一些标点符号:-'()+,./:=?;!*#@$_%。而且,公共DTD名称要遵守一些约定。如果一项DTD是ISO标准,它的名称要以字符串“ISO”开始。如果是一个非ISO的标准组织批准的DTD,它的名称以加号(+)开始。如果不是标准组织批准的DTD,它的名称以连字符(-)开始。这些开始字符或字符串后接双斜杠(//)和DTD所有者的名字,之后是另一个双斜杠和DTD描述的文档类型,接着又是一个双斜杠后接ISO 639语言标识符,如EN表示英语,ZH表示中文。在http://www.ics.uci.edu/ pub/ietf/http/related/iso639.txt处列有完整的ISO639标识符这段内容引用自《XML宝典》,作者:[美]Elliotte Rusty Harold,译者:马云 钟萍等,电子工业出版社出版。。例如我们定义的人力资源DTD可以采用下面的命名:

        -//xin sun//DTD HR 1.0//ZH

连字符(-)表示这个DTD不是由任何标准组织批准的,为xin sun所有,描述的是人力资源管理,用中文编写。完整的文档类型声明如下:

        <!DOCTYPE HR PUBLIC "-//xin sun//DTD HR 1.0//ZH" "http://www.sunxin.org /xml/dtds/hr. dtd">

有时候在一些HTML网页文档的开始处,可以看见类似于下面的文档类型声明:

        <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict. dtd">

现在我们应该能看懂这个声明了。它表明文档符合一项非标准(-)的HTML4.01的DTD,由W3C用英语编写,这个DTD位于http://www.w3.org/TR/html4/strict.dtd。

W3C公布的在Web文档中使用的有效的文档类型声明列表可以在http://www.w3.org/QA/2002/04/valid-dtd-list.html上找到。

提示

W3C并非一个由官方所批准的标准组织,它的成员由IBM、Microsoft这样的公司所组成。W3C只是发布推荐标准,但是由于该组织自身的权威性往往成为事实上的标准。

注意

文档类型声明与文档类型定义不是一个概念,DTD是文档类型定义(Document Type Definition)的缩写。

<!DOCTYPE greeting SYSTEM "hello.dtd">是文档类型声明,<!ELEMENT greeting (#PCDATA)>这种语法是文档类型定义。文档类型声明可以包含(内部DTD子集)或引用(外部DTD子集)一个文档类型定义。

在1.6.3节中,我们提到,如果我们的文档不依赖于外部文档,在XML声明中,可以通过standalone="yes"来声明这个文档是独立的文档。如果文档依赖于外部文档,可以通过standalone="no"来声明。当我们使用外部DTD文件时,就需要将属性standalone的值设置为"no"。在实际应用中,我们很少使用standalone属性,它的主要用途是作为XML处理器和其他应用程序的标志,表示是否需要获取外部内容。如果文档依赖于外部文档,即使我们不使用standalone属性,XML处理器也能够很好地进行处理,在这种情况下,XML处理器会假定standalone属性的值为“no”。

2.2 DTD的结构

DTD的结构一般由元素类型声明、属性表声明、实体声明、记号(notation)声明等构成。一个典型的文档类型定义文件会把将来所要创建的XML文档的元素结构、属性类型、实体引用等预先进行定义。

2.2.1 元素类型声明

一个DTD不仅要告诉XML处理器它所应用的XML文档的根元素是什么,而且还要告诉处理器文档的内容和结构,描述清楚文档结构中的每一个细节。

元素类型声明不但说明了每个文档中可能存在的元素,给出了元素的名称,而且给出了元素的具体类型。一个XML元素可以为空,也可以只包含字符数据,还可以有若干个子元素,而这些子元素同时又可以有它们的子元素。DTD正是通过元素之间的父子关系,描述了整个文档的结构关系。

元素类型声明采用如下的语法格式:

        <!ELEMENT 元素名称 元素内容说明>

元素内容说明可以指明以下5种可能的元素内容形式。

(1)#PCDATA

关键字#PCDATA说明元素包含字符数据,如例2-1所示。

例2-1 hr1.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE hr [
        <!ELEMENT hr (#PCDATA)>
        ]>
        <hr>人力资源标准</hr>

在内部DTD子集中的元素类型声明,指明这个XML文档有一个根元素hr,它的内容只能是字符数据,在本例中,hr元素的内容是“人力资源标准”。

(2)子元素

说明元素包含的是子元素。当一个元素只包含子元素,而没有字符数据时,则称此元素类型具有元素型内容(element content)。在该类型的元素声明时,通过内容模型来指定在其内容上的约束。内容模型是决定子元素类型和子元素出现顺序的一种简单语法。下面我们通过几个例子来看一下内容模型的语法。

在例2-2中,hr是根元素,它有一个子元素employee,而子元素employee只能包含字符数据。

例2-2 hr2.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE hr [
        <!ELEMENT hr (employee)>
        <!ELEMENT employee (#PCDATA)>
        ]>
        <hr>
            <employee>雇员信息</employee>
        </hr>

如果想进一步说明元素employee有三个子元素name、age、sex,并且希望这三个元素按照顺序出现在文档中。我们可以用圆括号将这三个元素括起来,然后用逗号(,)进行分隔,如例2-3所示。

例2-3 hr3.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE hr [
        <!ELEMENT hr (employee)><!ELEMENT employee (name,age,sex)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT age (#PCDATA)>
        <!ELEMENT sex (#PCDATA)>
        ]>
        <hr>
            <employee>
              <name>张三</name>
              <age>25</age>
              <sex>男</sex>
            </employee>
        </hr>

用逗号隔开的一系列子元素称为一个序列,表示这些元素在文档中要严格按照序列的顺序出现在文档中,在employee元素中首先出现的子元素是name,后面是age,最后是sex。要注意的是,在DTD中,元素声明的顺序是无关紧要的,你可以随意安排name、age和sex元素的声明顺序。

现在公司决定给员工提供两种方式发放工资:一种是以现金发放;另一种是往员工指定的银行卡上划账。每名员工只能选择其中的一种。这就要求我们在定义DTD的时候,指明这种情况,让文档编写者在编写文档时使用其中的一个元素。

我们可以用竖线(|)来分隔元素,如例2-4所示。

例2-4 hr4.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE hr [
        <!ELEMENT hr (employee)>
        <!ELEMENT employee (name,age,sex,salary)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT sex (#PCDATA)>
        <!ELEMENT age (#PCDATA)>
        <!ELEMENT salary (cash | credit_card)>
        …

这种元素内容模型称为选择,说明salary元素必须要有cash或credit_card中的其中一个子元素。

现在公司希望在XML文档中能体现员工的兴趣爱好,这就需要我们在DTD中声明employee元素时,为其增加子元素interest,并对interest元素进行声明。但我们现在碰到一个问题,一名员工的兴趣爱好可能有很多,也可能没有,那我们在DTD中要如何来体现这样的一种关系呢?我们可以用星号(*)来实现我们的目的,星号表示零或多个,如例2-5所示。

例2-5 hr5.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE hr [
        <!ELEMENT hr (employee)>
        <!ELEMENT employee ((name,age,sex),interest*)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT sex (#PCDATA)>
        <!ELEMENT age (#PCDATA)>
        <!ELEMENT interest (#PCDATA)>
        ]>
        <hr>
            <employee>
            <name>张三</name>
              <age>25</age>
              <sex>男</sex>
              <interest>篮球</interest>
              <interest>游泳</interest>
            </employee>
        </hr>

<!ELEMENT employee (name,age,sex,interest*)>这一句是表示:在employee元素中,有3个子元素name、age和sex,它们必须依次出现,然后可以有零个或多个interest子元素。所谓零个,意思是说在employee元素中,可以没有interest子元素。

如果公司希望每名员工至少要有一种兴趣爱好,那么我们可以用加号(+)来说明有一个或多个子元素,如下:

        <!ELEMENT employee (name,age,sex,interest+)>

表示在employee元素中,至少要有一个interest子元素。

我们用spouse元素来记录员工的配偶信息,有的员工是已婚,而有的员工是未婚,而一个人的配偶只有一个,也就是说,在employee元素中,应该包含零个或一个spouse子元素。这可以用问号(?)来说明这一点,如下:

        <!ELEMENT employee ((name,age,sex),interest+,spouse?)>

表示在employee元素中,可以有零个或一个spouse子元素。

利用括号、逗号、竖线、星号、加号和问号的组合,可以说明很复杂的内容模型。我们看一个例子:

        <!ELEMENT 简历 (名字,性别,年龄,(电话 | 手机),家庭住址?,兴趣爱好*,教育经历+,工作经验*)>

这说明简历中要有名字,接下来是性别和年龄,电话和手机任选一个,可以填一个家庭住址或者不填,然后是零个或多个兴趣爱好,至少要有一个教育经历,最后是零个或多个工作经验。

内容模型的规则虽然简单,但是可以产生灵活多样的结构,读者可以通过尝试一些复杂的模型,来更好地理解这些规则的应用。

(3)混合内容

表明元素既可以包含字符数据,也可以包含子元素。混合内容必须被定义零个或多个,如例2-6所示。

例2-6 employee.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE employee [
        <!ELEMENT employee (#PCDATA | name)*>
        <!ELEMENT name (#PCDATA)>
        ]>
        <employee>
            员工信息
            <name>张三</name>
        </employee>

<!ELEMENT employee (#PCDATA | name)*>这一句表示employee元素的内容可以包含零个或多个字符数据,包含零个或多个name子元素。

注意

在使用混合内容模型时,#PCDATA关键字必须是模型中的第一个选项,不能在模型中使用逗号、问号或加号。用竖线分隔的#PCDATA和元素的列表是合法的,其他用法都是不合法的。

(4)EMPTY

关键字EMPTY表明该元素既不包含字符数据,也不包含子元素,是一个空元素。如果在文档中元素本身已经表示了明确的含义,就可以在DTD中用关键字EMPTY来声明空元素。例如:

        <!ELEMENT br EMPTY>

表明br是一个没有内容的空元素。

(5)ANY

关键字ANY表明该元素可以包含任何的字符数据和子元素,只要它们不违反XML格式良好的约束就可以了。例如:

        <!ELEMENT employee ANY>

表明employee可以包含任何形式的内容。

在实际使用时,我们应该尽量避免使用ANY,一个定义明确的DTD,有助于我们理清文档的结构,更好地理解文档。

2.2.2 实体声明

有时候,你可能会在多个文档中调用同样的内容,比如公司名称、版权声明等,为了避免重复输入这些内容,我们可以声明一个实体来表示这些内容,在文档中只需要引用这个实体,经过XML处理器对文档进行分析处理后,引用实体的位置会被实体的内容所替换。

有两种类型的实体:一般实体(general entity)和参数实体(parameter entity)。一般实体是在文档内容中使用的实体,而参数实体则是在DTD中使用的已解析实体。不管是一般实体,还是参数实体都是用ENTITY关键字来声明。

(1)一般实体和参数实体

一般实体的声明语法如下:

        <!ENTITY 实体名 "实体内容">

引用实体的方式为:“&实体名;”。

下面我们看一个例子,如例2-7所示。

例2-7 website.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE website [
        <!ELEMENT website (name,copyright)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT copyright (#PCDATA)>
        <!ENTITY name "程序员之家">
        <!ENTITY copyright "&#169;2004, 程序员之家, All Rights Reserved">
        ]>
        <website>
            <name>&name;</name>
            <copyright>&copyright;</copyright>
        </website>

在这个例子中,我们声明了两个实体name和copyright,分别用来表示网站名称和版权声明信息。在声明实体copyright时,我们通过字符引用的方式(&#169;)来输入版权符号©。声明一个实体时,也可以使用另外的实体。这个例子在IE中的显示效果如图2-1所示。

图2-1 在文档中的实体引用被实体的内容所替换

参数实体只能在DTD中使用,它的声明语法如下:

        <!ENTITY % 实体名 "实体内容">

注意在声明时,ENTITY、%和实体名之间各有一个空格。引用实体的方式为:“%实体名;”。

下面我们看一个例子,如例2-8所示。

例2-8 website.dtd

        <?xml version="1.0" encoding="GB2312"?>
        <!ELEMENT website (name, copyright)>
        <!ELEMENT name (#PCDATA)>
        <!ELEMENT copyright (#PCDATA)>
        <!ENTITY % name "程序员之家">
        <!ENTITY copyright "&#169;2004, %name;, All Rights Reserved">
                                                  website.xml
        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE website SYSTEM "website.dtd">
        <website>
          <name>程序员之家</name>
          <copyright>&copyright;</copyright>
        </website>

在这个例子中,我们将DTD放到了一个单独的文件中,这是因为当参数实体引用出现在标记声明内部的时候,DTD应该在外部子集中(注意例2-8中website.dtd的最后一行)。在内部DTD子集中,参数实体引用不能在标记声明的内部出现,但可以在标记声明允许出现的地方出现。然而,对于外部DTD子集,则没有这个限制。

我们声明了一个参数实体name,然后通过实体引用(%name;)的方式将它应用到一般实体copyright的声明中。在website.xml中,用实体引用的方式(&copyright;)输入版权声明信息。这个例子在IE中的显示效果如图2-2所示。

图2-2 使用参数实体置换后的文档显示

在内部DTD中,参数实体引用只能在标记声明之外使用。我们看下面这个例子,如例2-9所示。

例2-9 website2.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE website [
        <!ELEMENT website (name, copyright)>
        <!ELEMENT name (#PCDATA)>
        <!ENTITY % cprt "<!ELEMENT copyright (#PCDATA)>">
        %cprt;
        <!ENTITY copyright "&#169;2004, 程序员之家, All Rights Reserved">
        ]>
        <website>
          <name>程序员之家</name>
          <copyright>&copyright;</copyright>
        </website>

在这个例子中,使用的是内部DTD子集,对参数实体的引用(%cprt;)出现在标记声明可以出现的位置,这是允许的。

在例2-9中,我们声明了一个参数实体cprt,它所表示的内容是一个元素声明,然后,我们在声明之外,通过实体引用的方式(%cprt;)间接地对元素copyright进行了声明。

在DTD中,所有的参数实体必须在引用之前进行声明。这意味着内部DTD子集不能引用在外部DTD中声明的参数实体,这是因为XML处理器将首先读取内部子集,也就是说,内部子集中的实体和属性表声明的优先级要比在外部子集中得高。

(2)内部实体和外部实体

内部实体在XML文档内部定义,实体的内容在声明中给出。内部实体都是已解析的实体,它们没有单独的物理存储对象。

上一小节介绍的一般实体和参数实体,可以称为内部一般实体和内部参数实体。

外部实体在单独的(外部)文件中定义,外部实体可以是已解析实体,也可以是未解析实体。外部一般实体的声明形式如下:

        <!ENTITY copyright SYSTEM "http://www.sunxin.org/copyright.xml">

关键字SYSTEM表明这是一个私有的外部一般实体,后面的URI称为该实体的系统标识符,用于给出外部文件的位置。

copyright.xml文件的内容为:

        <?xml encoding="GB2312"?>
        &#169;2004, 程序员之家, All Rights Reserved

<?xml version="1.0" encoding="GB2312"?>称为文本声明。文本声明类似于XML声明,不过文本声明没有standalone属性,并且version属性也是可选的。外部已解析实体可以使用不同于UTF-8的编码,使用文本声明来指明实体内容的编码方式。

每个外部已解析实体都应该以文本声明开始。文本声明只能在外部已解析实体的开头出现,不能出现在其他任何位置。在外部已解析实体中的文本声明不会作为替换文本的一部分而出现。

也可以使用PUBLIC关键字来声明公共的外部一般实体,其声明形式和使用了关键字PUBLIC的外部DTD声明类似。如下:

        <!ENTITY open-hatch PUBLIC "-//Textuality//TEXT Standard open-hatch
    boilerplate//EN"
                        "http://www.textuality.com/boilerplate/OpenHatch.xml">

"-//Textuality//TEXT Standard open-hatch boilerplate//EN"称为该实体的公共标识符,后面的URI部分为该实体的系统标识符。

下面的例子声明了一个外部一般未解析实体。

        <!ENTITY hatch-pic SYSTEM "../grafix/OpenHatch.gif" NDATA gif >

关键字NDATA表示该实体是一般未解析的实体,后面的gif是记号名称(notation name),说明实体的数据格式或指定一个外部的处理程序。

与一般实体类似,参数实体的替换文本也可以位于外部文件中。其声明形式和一般实体类似,在这里我们就不再举例了。

2.2.3 属性表声明

属性用于将名字-值对与元素进行关联。属性说明只能在开始标签和空元素标签中出现。属性表声明详细说明了与给定元素类型相关联的每一个属性的名字,数据类型和默认值(如果有的话)。属性表声明的语法如下:

        <!ATTLIST 元素名 属性名 属性类型 默认声明>

元素名是属性所属的元素的名字,属性名是属性的命名,属性类型则用来指定该属性是属于哪种类型,共有10种类型,默认声明用于说明在元素中该属性是否必须出现,如果不是必须出现,那么当该属性没有出现时,XML处理器应该如何处理。我们首先看一下默认声明。

(1)默认声明

默认声明可以有4种默认设置,#REQUIRED、#IMPLIED、#FIXED+默认值、只有默认值。

#REQUIRED

关键字REQUIRED说明必须为元素提供该属性。

如果我们要定义应用于论坛的DTD,论坛上的每一篇帖子都有作者元素,我们希望为每一个作者元素提供一个IP地址属性,这样,我们就知道发表帖子的用户来自哪里。在属性声明时,可以使用REQUIRED关键字,如例2-10所示。

例2-10 bbs1.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE bbs [
        <!ELEMENT bbs (article*)>
        <!ELEMENT article (title,author)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ATTLIST author ip CDATA #REQUIRED>
        ]>
        <bbs>
            <article>
                  <title>关于XML应用的问题</title>
                  <author ip="61.157.95.130">张三</author>
            </article>
        </bbs>

在这个例子中,声明元素author的属性ip时使用了关键字REQUIRED,说明在文档中,凡是出现元素author的地方,都要加上属性ip。

#IMPLIED

关键字IMPLIED说明元素可以包含该属性,也可以不包含该属性。

后来我们又发现帖子作者的ip属性无关紧要,于是在声明时,我们用关键字IMPLIED代替REQUIRED。如下所示:

        <!ATTLIST author ip CDATA #IMPLIED>

这样的话,在文档中使用元素author时,可以包含ip属性,也可以不包含。

#FIXED+默认值

关键字FIXED+默认值,说明一个固定的属性默认值,文档的编写者不能修改该属性的值。如果元素中不包含这个属性,XML处理器将以声明的默认值向应用程序报告该属性。

还是回到刚才论坛DTD的例子,我们想在用户所发表的每一篇帖子中,都加上版权归某某论坛所有,我们可以为元素article添加一个属性copyright,并且在声明时使用FIXED关键字,让该属性的值不能被修改,如例2-11所示。

例2-11 bbs2.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE bbs [
        <!ELEMENT bbs (article*)>
        <!ELEMENT article (title,author)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ATTLIST author ip CDATA #IMPLIED>
        <!ATTLIST article copyright CDATA #FIXED "版权归某某论坛所有">
        ]>
        <bbs>
            <article>
                <title>关于XML应用的问题</title>
                <author>张三</author>
            </article>
        </bbs>

在声明元素article的属性copyright时,使用了关键字FIXED,后面给出了一个默认值。在IE浏览器中的显示效果如图2-3所示。

图2-3 IE自动将默认属性值应用到元素中

我们注意到,在文档中的article元素,并没有包含属性copyright,但在IE中显示时,可以看到属性已经在元素article中了,这是因为内嵌在IE中的XML处理器自动以默认值给出了该属性。

只有默认值

与FIXED+默认值一样,如果元素不包含该属性,XML处理器将以声明的默认值向应用程序报告该属性。不同的是,这种声明方式属性的值是可以改变的。

对于论坛中的每一篇帖子,我们想设置一个类型属性,让它的默认值为文本格式,当然用户在发布帖子的时候,也可以选择以HTML格式发布,如例2-12所示。

例2-12 bbs3.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE bbs [
        <!ELEMENT bbs (article*)>
        <!ELEMENT article (title, author)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT author (#PCDATA)>
        <!ATTLIST author ip CDATA #IMPLIED>
        <!ATTLIST article copyright CDATA #FIXED "版权归某某论坛所有">
        <!ATTLIST article style CDATA "txt">
        ]>
        <bbs>
            <article>
                <title>关于XML应用的问题</title>
                <author>张三</author>
            </article>
            <article style="html">
                <title>如何在JSP中实现文件上传</title>
                <author>李四</author>
            </article>
        </bbs>

在声明元素article的属性style时,我们给出一个默认值“txt”。在XML文档中使用article元素,可以附上style属性,并给它赋值,也可以不带style属性。注意和#FIXED的区别,使用关键字FIXED+属性值,其属性的值是不能被更改的,在文档中使用属性时,它的值必须和声明时相同。上例在IE浏览器中的显示效果如图2-4所示。

图2-4 使用属性和没有使用属性的显示效果

注意看图2-4中article元素的属性设置,和例2-12的源文档进行对比。

在声明一个元素的属性时,可以将多个属性声明合并为一个声明,例1-18中的元素article有两个属性copyright和style,它们的声明可以合并成一个,如下所示:

        <!ATTLIST article copyright CDATA #FIXED "版权归某某论坛所有"
                          style CDATA "txt">

(2)属性类型

在属性表声明时,总共有10种属性类型可以选择,分别是:CDATA、Enumerated、ID、IDREF、IDREFS、ENTITY、ENTITIES、NMTOKEN、NMTOKENS和NOTATION。

CDATA

这是最常用的一种属性类型,表明属性值为字符数据,与元素内容说明中的#PCDATA相同。如果属性值需要出现小于号(<)和双引号("),可以通过预定义实体引用或字符引用的方式插入小于号和双引号。如果包含的和号(&)不是字符或实体引用的起始定界符,也必须使用预定义实体引用或者字符引用的方式插入。CDATA类型在上一小节我们已经使用过了,在这里就不再举例了。

Enumerated

在声明属性时,可以限制属性的取值只能从一个列表中选择,这类属性属于枚举类型。要注意,枚举类型的属性声明并没有使用关键字Enumerated,此处只是用于说明的目的。

枚举类型的属性有时候是很有用的,例如:person元素有一个sex属性,我们希望这个属性的取值只能是male或female,在声明属性时,将这两个值放到圆括号中,并用竖线(|)分隔,如下所示:

        !ATTLIST person sex (male | female) #REQUIRED>

列表中的可选属性值,不用加双引号(")或单引号('),但是在给属性赋值时,需要带上双引号或单引号。另外要注意的是,在给属性赋值时,不仅必须使用枚举类型声明中的可选值,而且还要注意属性值的大小写,Male、MALE、Female、FEMALE等都是无效的。

ID、IDREF、IDREFS

一个ID类型的属性值唯一标识XML文档中的一个元素。一个ID类型的属性值必须遵守XML名称定义的规则,以字母、下画线或冒号开头,名称中可以包含字母、数字、下画线及其他在XML标准中允许的字符,名称中不能带有空格。一个元素只能有一个ID类型的属性,ID类型的属性必须设置为#IMPLIED或者#REQUIRED,因为ID类型属性的每一个取值都是用来标识一个特定的元素,为ID类型的属性提供默认值,特别是固定的默认值是毫无意义的。

在一个公司里,通常都会用员工编号来唯一标识一名员工,我们看下面的例子,如例2-13所示。

例2-13 company.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE company [
        <!ELEMENT company (employee*)>
        <!ELEMENT employee (name)>
        <!ELEMENT name (#PCDATA)>
        <!ATTLIST employee sn ID #REQUIRED>
        ]>
        <company>
            <employee sn="E-200402100001">
                <name>张三</name>
            </employee>
            <employee sn="E-200410020006">
                <name>李四</name>
            </employee>
        </company>

张三和李四这两名员工的编号是唯一的,在文档中,employee元素的属性sn的类型是ID,所以它的值在文档中必须是唯一的,用于标识一个特定employee元素。

那么如何才能让ID类型的属性发挥作用呢?这就要用到IDREF类型的属性。IDREF类型的属性值为同一文档中另一个元素的ID类型的属性值,而这另一个元素的ID类型的属性值必须是已经存在的。利用ID和IDREF这两种类型的属性,我们可以在两个对象之间建立一种一对多的关系,如例2-14所示。

例2-14 company2.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE company [
        <!ELEMENT company (employee | manager)*>
        <!ELEMENT employee (name)>
        <!ELEMENT manager EMPTY>
        <!ELEMENT name (#PCDATA)>
        <!ATTLIST employee sn ID #REQUIRED>
        <!ATTLIST manager mgrid IDREF #REQUIRED>
        ]>
        <company>
            <employee sn="E-200402100001">
                <name>张三</name>
            </employee>
            <employee sn="E-200410020006">
                <name>李四</name>
            </employee>
            <manager mgrid="E-200402100001"/>
        </company>

在这个例子中,我们为company元素增加了一个子元素manager,声明它为空元素,带有一个IDREF类型的属性,其值必须是一个已经存在的ID类型的属性值。当我们需要查看经理的信息时,通过它的属性值“E-200402100001”可以对应到一个雇员,通过其子元素name,知道这个经理的名字叫“张三”。

实际上,上面的例子还可以改成如下形式,如例2-15所示。

例2-15 company3.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE company [
        <!ELEMENT company (employee*)>
        <!ELEMENT employee (name)>
        <!ELEMENT name (#PCDATA)>
        <!ATTLIST employee sn ID #REQUIRED>
        <!ATTLIST employee manager IDREF #IMPLIED>
        ]>
        <company>
            <employee sn="E-200402100001">
                <name>张三</name>
            </employee>
            <employee sn="E-200410020006" manager="E-200402100001">
                <name>李四</name>
            </employee>
            <employee sn="E-200411210002" manager="E-200402100001">
                <name>王五</name>
            </employee>
        </company>

注意和例2-14的区别,在本例中,我们去掉了manager子元素,为employee元素添加了一个属性manager,并用关键字IMPLIED声明employee元素可以包含该属性,也可以不包含该属性。从文档中,我们可以看出,员工张三没有经理,他应该是总经理,而员工李四和王五都有一个经理,通过manager属性的值,我们可以知道他们的经理是张三。

对比例2-14和例2-15,可以发现,例2-15通过增加一个属性(使用关键字IMPLIED声明),减少了不必要的元素,使组织结构更加清晰、合理,提高了可读性。

如果一个属性需要引用文档中多个ID类型的属性值,则可以把它声明为IDREFS类型。IDREFS类型的属性值是一系列以空格分隔的ID类型的属性值,而且必须与文档中已有的ID类型属性值相匹配。例如,去图书馆借书,一个人往往可以借阅多本图书,在一个表示图书馆图书信息和借阅信息的XML文档中,我们可以使用IDREFS类型的属性来表示一个人借阅了哪些图书。我们看下面的例子,如例2-16所示。

例2-16 library.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE library [
        <!ELEMENT library (books,records)>
        <!ELEMENT books (book+)>
        <!ELEMENT book (title)>
        <!ELEMENT title (#PCDATA)>
        <!ELEMENT records (item*)>
        <!ELEMENT item (date,person)>
        <!ELEMENT date (#PCDATA)>
        <!ELEMENT person EMPTY>
        <!ATTLIST book bookid ID #REQUIRED>
        <!ATTLIST person name CDATA #REQUIRED>
        <!ATTLIST person borrowed IDREFS #REQUIRED>
        ]>
        <library>
            <books>
              <book bookid="b-1-1">
                  <title>Struts 2深入详解</title>
              </book>
              <book bookid="b-1-2">
                  <title>Java Web开发详解</title>
              </book>
              <book bookid="b-1-3">
                  <title>Servlet/JSP深入详解</title>
              </book>
            </books>
            <records>
              <item>
                  <date>2004-03-14</date>
                  <person name="张三" borrowed="b-1-1 b-1-3"/>
              </item>
              <item>
                  <date>2004-05-08</date>
                  <person name="李四" borrowed="b-1-1 b-1-2 b-1-3"/>
              </item>
            </records>
        </library>

从文档中可以清楚地知道,张三借阅了《Struts 2深入详解》和《Servlet/JSP深入详解》这两本书,而李四则借阅了《Struts 2深入详解》、《Java Web开发详解》和《Servlet/JSP深入详解》这三本书。

ENTITY、ENTITIES

ENTITY类型的属性把外部的二进制数据链接到文档。ENTITY类型的属性值是在DTD中声明的未分析的一般实体的名称。例如,我们想在文档中包含一幅外部的图像,可以声明一个ENTITY类型的属性来引入图像:

        <!ATTLIST image src ENTITY #REQUIRED>

属性src的类型是ENTITY,它的值要求是一个未解析实体的名字,因此在DTD中,我们还需要声明一个外部的一般未解析实体:

        <!ENTITY logo SYSTEM "http://www.sunxin.org/logo.gif" NDATA gif>

关键字NDATA表示该实体是一般未解析实体,后面的gif是记号名称(notation name),说明实体的数据格式或指定一个外部的处理程序。记号gif的声明如下:

        <!NOTATION gif SYSTEM "iexplore.exe">

最后,在XML文档中,我们可以在src属性中引用图像:<image src="logo"/>。这句代码将http://www.sunxin.org/logo.gif文件与image元素关联在一起。对于XML处理器来说,它并不能处理未解析实体,它只是将外部未解析实体的引用信息传递给应用程序,由应用程序来决定如何获取和处理外部未解析实体。

ENTITIES类型和IDREFS类型的使用是类似的,它的值是多个以空格分隔的ENTITY类型的属性值。我们可以再添加一个实体声明:

        <!ENTITY banner SYSTEM "http://www.mybole.com.cn/banner.gif" NDATA gif>

然后将元素image的属性声明改为:

        <!ATTLIST image src ENTITIES #REQUIRED>

在XML文档中,通过src属性引用两幅图像:

        <image src="logo banner"/>

注意

对于未解析的实体,不能通过实体引用的方式去引用。未解析实体只能在声明为ENTITY或者ENTITIES类型的属性值中被引用。

NMTOKEN、NMTOKENS

NMTOKEN(name token),名称标记是任何命名字符的混合体。NMTOKEN类型的属性值是受限制的文本,只能包含名称字符,不能包含空白字符。XML名称不能以除字母、下画线和冒号之外的其他字符开头,而名称标记没有这个限制。所有的XML名称都是名称标记,但不是所有的名称标记都是XML名称。

为了限制文件名属性的取值不能有空格,我们可以采取如下属性声明方式:

        <!ATTLIST file name NMTOKEN #REQUIRED>

在文档中,可以按如下方式使用name属性:

        <file name="XML讲座.doc"/>

NMTOKENS类型与IDREFS和ENTITIES类似,它的值由多个名称标记构成,每个名称必须是有效的名称标记,它们之间以空格分隔。例如在DTD中声明:

        <!ATTLIST files name NMTOKENS #REQUIRED>

在文档中使用:

        <files name="XML讲座.doc JSP讲座.doc"/>

有时候,你可能会用NMTOKEN类型的属性来让用户输入特定的值,但是要注意的是:在使用NMTOKEN类型的属性时,其值是否有效,需要文档的作者自己去保证,XML处理器只能确保名称是合法的(也就是检查名称中有没有空格),而不会检查值的有效性。

NOTATION

NOTATION类型属性的值就是在记号声明中的名称。我们看一个例子,如例2-17所示。

例2-17 webpage.xml

        <?xml version="1.0" encoding="GB2312"?>
        <!DOCTYPE webpage [
        <!ELEMENT webpage (image)>
        <!ELEMENT image (#PCDATA)>
        <!NOTATION gif SYSTEM "image/gif">
        <!NOTATION jpg SYSTEM "iexplore.exe">
        <!ATTLIST image type NOTATION (gif | jpg) #REQUIRED>
        ]>
        <webpage>
            <image type="jpg">image info</image>
        </webpage>

在这个例子中,我们用两种方式进行了记号声明,然后在属性表声明中声明了一个类型为NOTATION的属性type,它的值只能是“gif”或者“jpg”。

2.2.4 记号声明

在现实中,有很多数据都是无法用XML来表示的,例如:声音、图像、影像等,对于这些数据,XML处理器通常都不支持。通过DTD中的记号声明(Notation Declaration),为非XML数据描述一种可能的格式,或者指定一个外部的处理程序。

记号声明有两种形式,一种是使用MIMEMIME(Multipurpose Internet Mail Extensions),多用途Internet邮件扩展,主要用于描述多媒体对象,读者如果想要了解更详细的信息,可以参看RFC1521。类型,形式是:

        <!NOTATION gif SYSTEM "image/gif">

另一种是使用URI路径,指出外部处理程序的位置:

        <!NOTATION gif SYSTEM "iexplore.exe">。

前面介绍过,使用实体类型的属性,可以将GIF图像文件与元素相关联。然而,XML处理器不能处理二进制格式的数据。通过使用记号声明,可以说明链接到XML文档的外部数据项的格式,或者指定相关的外部处理器。对XML文档进行处理的应用程序可以从XML处理器中得到外部未解析的实体和相应的记号的有关信息,然后决定对该实体数据的处理方式,例如,对可识别的图像格式直接显示,或者调用记号声明中指出的外部程序对实体数据进行处理。在实际应用中,对未解析实体的处理,总是由特定的应用程序来完成,XML处理器本身只是向应用程序报告未解析实体和记号的信息。

对于记号的声明,也可以使用PUBLIC关键字来代替SYSTEM关键字,并添加公共的名称和URI(用法类似于使用了关键字PUBLIC的外部DTD声明)。

2.3 在XMLSpy中创建DTD文档

要在XMLSpy中创建一个DTD文档,可以单击菜单【文件】→【新建…】,在弹出的“创建新文件”对话框中选择“dtd Document Type Definition”,单击“OK”按钮,如图2-5所示。

图2-5 在XMLSpy中新建DTD文档

要为现有的XML文档关联一个外部DTD,除了手写文档类型声明外,在XMLSpy中还可以通过菜单操作来为一个XML文档关联DTD。在XML文档编辑窗口中,单击菜单【DTD/模式】→【指定DTD...】,出现“选择文件”对话框,如图2-6所示。

图2-6 “选择文件”对话框

单击“浏览…”按钮,选择要关联的DTD文档。选定DTD文件后,单击“确定”按钮,XMLSpy会自动为你在XML文档中添加文档类型声明信息。

2.4 有效的XML

一个遵守XML语法规则,并遵守相应DTD文件约束的XML文档称为有效的XML文档。注意区分格式良好的XML和有效的XML,一个只要求遵循XML规范,一个不但要遵循XML规范,还要遵循相应的DTD约束。

将XML文档和它的DTD文件进行比较分析,看是否符合DTD规则的过程叫验证(validation),这个过程通常是通过一个支持有效性检查的XML处理器来完成的。在XMLSpy中,可用鼠标单击绿色的对号或按下功能键F8,对文档做有效性检查,如图2-7所示。

图2-7 在XMLSpy中检查文档是否是有效的

2.5 XML处理器/解析器

XML处理器主要用于分析处理XML文档,验证XML文档是否符合XML的规范。实际上类似的HTML处理器我们经常使用,只是没有注意罢了。当我们浏览Web页面的时候,在浏览器中的HTML处理器就开始工作了,因为HTML格式非常松散,所以在编写处理器的时候,需要大量的代码来处理和更正HTML文档中的语法错误,从而导致浏览器变得很大。而XML文档的格式简单,结构清晰,语法也较少,所以开发处理XML文档的应用程序相对要容易得多。

合乎规范的XML处理器可以分为两类:进行验证的和不进行验证的。

(1)不支持有效性检查的处理器

这一类处理器只负责检查XML文档和它内部的DTD子集是否满足“格式良好的”语法规定。这类处理器不会对XML文档所引用的外部DTD文档进行分析、进而检查XML文档的有效性,但对于出现在文档内部的DTD子集,仍旧会处理在内部DTD子集中的所有声明和参数实体,直到遇见第一个它们没有读取到的参数实体的引用。也就是说,这类处理器必须使用DTD声明中的信息来规范属性的值,包含内部实体的替换文本,以及提供默认的属性值。

(2)支持有效性检查的处理器

这一类处理器在检查文档是否符合“格式良好的”基本要求的基础上,进一步结合DTD检查文档是否符合DTD中对文档结构的规定,判定这个文档是否是“有效的”。处理器必须读取和处理整个DTD和在文档中引用的所有外部已解析实体。并报告出文档与DTD中的声明相冲突的地方,以及不满足DTD有效性约束的地方。

无论是哪一类处理器,都要报告在文档实体的内容中和它们读取到的任何其他已解析实体中违反XML“格式良好的”约束的情况。

支持对XML文档进行有效性检查的XML处理器有以下两个:

Apache的Xerces

Xerces是Apache的XML项目的一部分,它分别使用Java、C++和Perl编写了XML的处理器,支持有效性检查。Xerces是一个开放源代码的XML处理器,有关该处理器的详细介绍请参看网页:http://xerces.apache.org/xerces2-j/。

Oracle XML Parser

Oracle XML Parser是在Oracle XDK(XML Developers Kit)中提供的。XDK是Oracle公司提供的基于XML的工具包,可以用于Java、C++和PL/SQL。Oracle XDK是一个商业化软件,并非开放源代码产品。Oracle公司发布了只用于开发的许可证,允许你将此软件用于开发的目的。XDK包含在Oracle 10g数据库的安装包中,在Oracle的安装目录中可以找到它。

读者可从下面的网页查看XDK的信息:

        http://www.oracle.com/technology/tech/xml/xdkhome.html

XML处理器位于XML文档与使用XML文档的应用程序之间,它通过标准的API来向应用程序提供数据(参见第5章)。

2.6 小结

对于一个格式良好的XML文档,我们只能保证这个文档的格式符合XML规范,但是元素与元素的关系、元素与属性的关系,属性的取值是否正确,就需要通过DTD来进行验证了。

● 在XML文档中引入DTD,可以直接在XML文档中包含DTD,也可以通过URI引用外部的DTD文件,或者同时采用这两种方式。

● DTD的结构一般由元素类型声明、属性表声明、实体声明、记号声明等构成。一个典型的DTD文件会把将来所要创建的XML文档的元素结构、属性类型、实体引用等预先进行规定。