XML类型定义之DTD

这几天在重写Luit(一个java web框架)的配置解析部分,和servlet一样,luit支持且仅支持XML配置文件。为什么选择XML,而不是JSON,甚至是自己定义一种格式?理由如下:

  1. 我懒
  2. 大家都用xml
  3. xml的伸缩性满足要求

在正常情况下,我会按照如下方式解析配置:

  • 格式检查 -> 元素合法性检查 -> 解析DOM -> 必要值的提取

由于现在采用的是XML这种通用文件格式,格式检查由XML类库(如dom4j)完成,元素合法性被延后到值提取的阶段完成。试想如下场景:父元素必须包含某一子元素,但开发者却未添加。我目前通过抛出异常,然后记录到日志中解决。

但是显而易见,这种方式并不理想。将错误提示尽可能提前是框架开发者的责任,比如让开发者在编写配置文件的时候就发现问题?我敢说,这是个迫切的需求。

说来也惭愧,之前对于XML的认知就仅仅是标签语言,知其然,但不知其所以然。但在日常使用中,我发现IDE是能够识别web.xml的定义的,那么这里一定有某种东西定义了整个文件。

它就是DTD,全称Document Type Definition,可以看做是xml文件的模板,通俗来讲,就是制定规则的。虽说wiki中有描述“DTD限制较多,使用较不便”,但用在目前的这种需求下,它够用了。

打开web.xml,我们能看到这样一行:

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

这就是DTD文件,只不过它是外连的罢了,我们也可以将其内置在xml中,但我想应该没有多少人愿意这么做。

接下来我们来介绍下如何定义dtd,应该花不了各位多少时间。

0x00 XML组成

  • 元素: XML的主要构建模块,如HTML中的html,head等
  • 属性: 提供有关元素的额外信息,如script元素中的src
  • 实体: 定义普通文本的变量,如 
  • PCDATA: 会被解析的字符数据
  • CDATA: 不会被解析的字符数据

0x01 DTD声明语法

元素声明

<!ELEMENT 元素名称 类别>
<!ELEMENT 元素名称 (子元素1, 子元素2, ...)>

针对第一种语法,有两种既定的类型:

  • EMPTY
  • ANY

使用EMPTY,我们可以定义如下类型的元素

<br />

使用ANY,你可以在元素中嵌入任何内容,没有任何约束。

针对第二种语法,我们用一个例子来说明:

<book>
    <name>xxx</name>
    <price>20</price>
</book>

我们可以按照如下方式制定类型声明:

首先是book元素,它包括name和price两个子元素

<!ELEMENT book (name, price)>

然后递归到子元素,两个子元素均只包含PCDATA

<!ELEMENT name (#PCDATA)>
<!ELEMENT price (#PCDATA)>

补充一点内容,在(子元素, ...)中我们可以在子元素或者括号后使用类似正则表达式的符号:

? 表示0或1个
* 表示大于等于0个
+ 表示大于等于1个
| 表示或的关系

默认情况下,每个定义在括号内的子元素出现且只出现一次。我们可以用这几个符号修饰子元素或者括号,后者等同于修饰括号中的所有子元素。

用一个例子来说明下:

<!ELEMENT book (name | price)>

上述元素声明表示,book中非name既price,且子元素出现0或1次。

属性声明

<!ATTLIST 元素名称 属性名称 属性值类型 属性默认值>

相对而言,属性声明比较复杂,主要是属性值类型有点多,我们来一一说明。

属性值:

CDATA 不被解析地字符数据
(值1|值2|...) 值为列表中的其中一个
ID 值为唯一的id
IDREF 值为另一个元素的id
IDREFS 值为其它id的列表(空格分隔)
NMTOKEN 值为合法的标记名称
IDREFS 值为合法的标记名称的列表(空格分隔)
IDREF 值为另一个元素的id
ENTITY 值为一个实体
ENTITIES 值为实体列表(空格分隔)
NOTATION 值为符号的名称
xml: 值为一个预定义的xml值

默认值可以使用如下参数:

#REQUIRED 属性值是必须的
#IMPLIED 属性值不是必须的
#FIXED value 属性值固定为value

假如我们有如下xml文本:

<books>
    <book name="book1" price="20" author="CodeSun" payment="cash" />
    <book name="book2" author="CodeSun" payment="check" />
</books>

现有如下属性声明:

<!ATTLIST book
    name CDATA #REQUIRED
    price CDATA "free"
    author CDATA #FIXED "CodeSun"
    payment (cash|check) #REQUIERD
>

上述规则表明,必须指定name属性,price默认为free(如果未指定),author固定为CodeSun,payment只能非cache既check。这里给出的只是最基本的例子,属性列表可以写得很复杂,只不过最近脑力不足以想出更加复杂的例子了。

实体声明

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

这个比较简单,相当于变量,还是用一个例子来说明,首先我们有如下实体声明:

<!ENTITY author "CodeSun">

那么我们可以按照如下方式使用这个实体:

<blog>
    <name>dtd</name>
    <author>&author;</author>
</blog>

注意,每次使用实体的时候,都必须在其前方加上‘&’,后方加上‘;’,以表示这是个实体,需要解析。

0x02 DTD的使用方式

大家一定认为这段应该放到最前面,我放在这里只不过为了督促大家看完本文。。。

添加DTD规则的方式为使用DOCTYPE,语法如下:

<!-- 下述语法为内嵌声明 -->
<!DOCTYPE 根元素 [声明内容]>
<!-- 下述语法为外连声明 -->
<!DOCTYPE 根元素 SYSTEM "url">

等等,你们是不是要问为何web.xml中使用外连dtd的时候用的是PUBLIC?
是这样的,外部DTD可以分为私有DTD和共有DTD,其中私有DTD使用SYSTEM,而公有DTD使用的则是PUBLIC,并且使用如下语法:

<!DOCTYPE 根元素 PUBLIC DTD名称 "url">

其中DTD名称格式为"注册//组织//类型 标签//语言",这里的“注册”是指组织是否由ISO注册,是则填写'+',否则填写'-'。好了那就让我们来看最上面的那段DTD名称:

  • -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN

Sun Microsystems, Inc.是制定此DTD的组织,不是由ISO注册的,类型为DTD,标签为Web Application 2.3,语言为英文。

最后,问题就来了,SYSTEM就够我用的了,为何要整得这么高大上呢?至少目前不需要。

说两句: