级别: 初级 Elliotte Rusty Harold, 副教授, Polytechnic University
2004 年 12 月 01 日 RELAX NG 基本上能够完成 W3C XML Schema 语言所能做的一切事情,包括验证文本内容的约束和使用 W3C XML Schema 简单类型规定的属性值。但是,有些约束只有完全通过图灵测试的语言才能表达,而 RELAX NG 不属于这种语言。所幸的是,您可以通过定制验证代码来动态扩展 RELAX NG,用 Java™ 编程语言编写代码来检查 RELAX NG 自身不能指定的约束。这样做需要实现三个接口:
Datatype、
DatatypeLibrary 和
DatatypeFactory。本文将通过检查一个数是否是素数来说明这些接口的实现。
过去三年中,RELAX NG XML 模式语言获得了极大成功,这在很大程度上要归功于它那令人难以置信的清晰、直接的语法,尤其是与 W3C XML Schema 语言相比。很多团队,包括 OpenOffice、DocBook 和 Text Encoding Initiative 等等,都已经采用了 RELAX NG 模式语言。甚至在 W3C 内部,RELAX NG 已经开始代替 W3C 模式,SVG 和 XHTML 工作小组都使用 RELAX NG 编写模式,然后再将它们转换成 DTD 和 W3C XML Schemas。虽然没有要求 RELAX NG 一定要支持 XML 模式数据类型,但是一些主要的实现(如 Jing 和 Sun 的 Multischema Validator)都支持这些数据类型。
对同样的事情,RELAX NG 比 W3C XML Schema 语言做得好得多,但是,人们在赞叹这些优点之余,却忽略了 RELAX NG 实际上还能做得更多。具体地说,与 W3C XML Schema 语言不同,RELAX NG 不仅限于预定的基本数据类型集合和一个有限的扩展方面的集合。RELAX NG 允许开发人员定义自己的类型库,该类型库可以断言程序能够验证的任何约束。比如,W3C 模式不能验证这些约束:
- 数字是素数。
- 字符串的左括号都有相应的右括号。
-
SKU 属性的值和产品数据库中的一个记录匹配。
- 通过查询字典文件确定元素的内容没有拼写错误。
用纯粹的 RELAX NG 代码也不能验证这些约束。但是不同于 W3C XML Schema 语言,您可以用 Java、C#、Python 或者其他语言编写用户定义的类型库,从而扩展 RELAX NG。因为这些语言是图灵完整的(请参阅
参考资料),基本上能够检查可用于字符串的任何条件。甚至还可以超出字符串本身的边界,比较与外部条件的一致性,如库存目录或当前公司股票价格。本质上,可以用任意复杂的验证条件扩展 RELAX NG 内置的验证规则。
本文将讨论如何通过 Java 接口实现这类扩展,很多 RELAX NG 处理程序,包括 Jing 和 Sun 的 Multischema Validator,都支持 Java 接口。我使用的具体例子是检查一个数是否为素数。测试素数的代码与将自定义库连接到 RELAX NG 的支持代码没有多大关系,很容易看出从哪里插入更复杂的算法或验证条件。扩展需要用到以下三个类:
-
实现
org.relaxng.datatype.Datatype 接口的类表示素数数据类型:
该类负责检查一个给定的字符串是否为素数。
-
实现
org.relaxng.datatype.DatatypeLibrary 接口的类:
给定类型的本地名之后该类负责加载适当的数据类型类。
-
实现
org.relaxng.datatype.DatatypeLibraryFactory 接口的工厂类:
给定类型库的名称空间 URI 后,该类负责加载适当的数据类型库。
现在将分别介绍每一个类。
素数 DatatypeLibraryFactory
这个工厂类很简单,只有一个方法
createDatatypeLibrary,用于接收类型库的名称空间 URI,并返回库的一个实例。如果名称空间与类型库不匹配,那么该方法将返回 null,RELAX NG 将到其他地方寻找正确的库,如清单 1 中的代码所示。
清单 1. DatatypeLibraryFactory 的实现
package com.elharo.xml.relaxng;
import org.relaxng.datatype.*;
public class PrimeDatatypeLibraryFactory
implements DatatypeLibraryFactory {
public final static String namespace
= "http://ns.cafeconleche.org/relaxng/primes";
public DatatypeLibrary createDatatypeLibrary(String namespace) {
if (PrimeDatatypeLibraryFactory.namespace.equals(namespace)) {
return new PrimeDatatypeLibrary();
}
return null;
}
}
|
 |
调试技巧
如果发现这个库无法工作,那么请在该方法的一开始调用
System.out.println,以确定是否找到并加载了这个类型库。
|
|
RELAX NG 使用 Java 服务 API 发现工厂。在类路径的某些 JAR 文件或目录中的
META-INF/services/ 目录中添加一个纯文本文件(plain text file)
org.relaxng.datatype.DatatypeLibraryFactory。该文件中的每一行都包含捆绑到 JAR 中的工厂类的完整包限定名,该例中只有
com.elharo.xml.relaxng.PrimeDatatypeLibraryFactory。
素数的 DatatypeLibrary
DatatypeLibrary 实现类稍微复杂一点(参见
清单 2)。每个名称空间都有一个
DatatypeLibrary,但是它需要两个方法。
createDatatype 方法接受本地名作为参数,如“prime”,然后返回适当的数据类型对象。这个简单的例子只有一种类型,但多数库都会在同一名称空间中提供多个简单类型。
createDatatypeBuilder 方法返回一个
org.relaxng.datatype.DatatypeBuilder 对象。更复杂的类型库可以使用该方法建立依赖于上下文和不同参数的数据类型,如作用域中的基准 URI 和名称空间前缀。但是这个例子不需要任何上下文,因此
createDatatypeBuilder 仅仅返回用素数数据类型配置的一个
org.relaxng.datatype.helpers.ParameterlessDatatypeBuilder 实例。
清单 2. DatatypeLibrary 的实现
package com.elharo.xml.relaxng;
import org.relaxng.datatype.*;
import org.relaxng.datatype.helpers.*;
public class PrimeDatatypeLibrary implements DatatypeLibrary {
public Datatype createDatatype(String typeLocalName)
throws DatatypeException {
if ("prime".equals(typeLocalName)) {
return new PrimeDatatype();
}
throw new DatatypeException("Unsupported type: " + typeLocalName);
}
public DatatypeBuilder createDatatypeBuilder(String baseTypeLocalName)
throws DatatypeException {
return new ParameterlessDatatypeBuilder(
createDatatype("prime")
);
}
}
|
素数数据类型
这个公共 API 的最后一部分是数据类型本身,它是
org.relaxng.datatype.Datatype 接口的实例。这个数据类型是我们所编写的最复杂的一个类,它要完成多项工作:
- 确定给出的字符串是否是该数据类型的有效实例。
- 创建表示该数据类型实例的对象。
- 根据数据类型的语义比较两个对象是否等同。
- 计算这些对象散列值。
- 提供类型的流验证器。
- 确定类型是否为 ID 类型。
- 确定该类型是否依赖上下文。
下面依次讨论每种功能。
检查有效性
有两个方法验证字符串,如清单 3 所示。如果字符串是该数据类型的有效实例,那么
isValid 将返回 true,否则将返回 false。如果字符串是非法的,则
checkValid 将抛出
DatatypeException 异常。
清单 3. 检查给定字符串有效性的方法
public boolean isValid(String literal, ValidationContext context) {
return isPrime(literal);
}
public void checkValid(String literal, ValidationContext context)
throws DatatypeException {
if (!isValid(literal, context)) {
throw new DatatypeException(literal + " is not a prime number");
}
}
|
这两个方法依赖于一个私有的
isPrime 方法,后者实现了一种简单(效率不高)的测试是否为素数的算法,如清单 4 所示。用 2 (最小的素数)与输入值平方根之间的每个整数去除输入值,然后获得一个余数。如果余数不为零,则该数不是素数。当然还有效率更高的素数测试方法,不过这种算法最容易理解。
清单 4. 测试是否为素数的算法
private boolean isPrime(String literal) {
try {
int candidate = Integer.parseInt(literal);
if (candidate < 2) return false;
double max = Math.sqrt(candidate);
for (int i = 2; i <= max; i++) {
if (candidate % i == 0) return false;
}
return true;
}
catch (NumberFormatException ex) {
return false;
}
}
|
如果输入的字符串不是一个数字,则该方法也将返回 false。
流式验证
某些数据类型可能包含比这里的数字更多的原始文本。比如,假设一个 Base-4 编码的 MPEG 要对内嵌的校验和进行测试。在这种极端的情况下,数据的大小可能超出了 Java 字符串的最大容量。这种情况下,验证器可以请求使用
Datatype 的
DatatypeStreamingValidator,后者知道如何每次验证输入中的一小部分,而不必一次将它们全都装入内存。该例中,字符串没有那么大,因此
Datatype 仅仅返回
org.relaxng.datatype.helpers.StreamingValidatorImpl 类的一个实例。
public DatatypeStreamingValidator createStreamingValidator(
ValidationContext context) {
return new StreamingValidatorImpl(this, context);
}
|
这个类只存储通过字符串传递的所有数据,然后验证整个字符串。更高效的实现需要处理更多的数据,而不仅仅是直接实现
DatatypeStreamingValidator 接口。
对象的表示
Datatype 类必须提供类型的某种对象表示,以便验证器可用来进行相等性比较和计算散列码。该例中,
java.lang.Integer 类恰好可以满足这个要求。在其他情况下,可能需要使用
java.lang.String 或者专门编写的定制类。无论选择哪种方法,
PrimeDatatype 中的
createValue 方法都将文字字符串转化成这种类型的对象,如清单 5 所示。
清单 5. 将文字字符串转化成对象的代码
public Object createValue(String literal, ValidationContext context) {
if (isPrime(literal)) {
return Integer.valueOf(literal);
}
return null;
}
|
某些情况下,可以采用的一种优化措施是使用 flyweight 设计模式,为类型中每个不同的值创建单独的对象,而不是为每个不同的类型创建单独的对象。
使用这种类型对象的惟一方法是将其传递给
Datatype 中的
sameValue 和
valueHashCode 方法。使这两种方法一致的方法与使
equals 和
hashCode 一致的方法相同。对于这个例子,只需要让这两个方法依赖于
Integer 的
equals 和
hashCode 方法,如清单 6 所示。
清单 6.
sameValue 和
valueHashCode 方法
public boolean sameValue(Object value1, Object value2) {
if (value1 == null) return value2 == null;
else return value1.equals(value2);
}
public int valueHashCode(Object value) {
return value.hashCode();
}
|
假设验证器不会像这些方法那样,传递由同一对象的
createValue 方法创建的对象。如果出现这种情形,通常这些方法的行为是未定义的,虽然这里的具体实现可能会尝试做某些合理的事情。
ID
getIdType 方法确定该类型是否存在某种形式的 ID 约束。有 4 种可能的情形,分别用
Datatype 类中的命名常量标识:
-
Datatype.ID_TYPE_ID
-
Datatype.ID_TYPE_IDREF
-
Datatype.ID_TYPE_IDREFS
-
Datatype.ID_TYPE_NULL
素数数据类型不是某一种类的 ID 或者 ID 引用,因此其
getIdType 方法返回
Datatype.ID_TYPE_NULL:
public int getIdType() {
return ID_TYPE_NULL;
}
|
上下文
最后一个方法是
isContextDependent。素数的验证不依赖于上下文,因此该方法只需要返回 false:
public boolean isContextDependent() {
return false;
}
|
打包、安装和使用类型库
编写完类型库之后,还要将它们打包,以便验证器使用。不要忘记
META-INF/services/org.relaxng.datatype.DatatypeLibraryFactory 文件,它包含了
DatatypeLibraryFactory 类的名称。将这个 .jar 文件添加到验证器的类路径中,然后像往常那样运行验证器。比方说,清单 7 中所示的 XML 文档保存在 integers.xml 中。
清单 7. integers.xml 文件
<?xml version="1.0"?>
<numbers>
<number>2</number>
<number>3</number>
<number>4</number>
<number>5</number>
<number>6</number>
</numbers>
|
现在假设模式如清单 8 所示,它引用了 primes.rng 文件中的 primes 数据类型库。
清单 8. 要求整数必须是素数的 RELAX NG 模式
<?xml version="1.0"?>
<element name="numbers" xmlns="http://relaxng.org/ns/structure/1.0">
<oneOrMore>
<element name="number">
<data type="prime"
datatypeLibrary="http://ns.cafeconleche.org/relaxng/primes"/>
</element>
</oneOrMore>
</element>
|
现在像清单 9 所示范的那样使用
java 解释器运行该模式。
清单 9. 用定制类型库验证
$ java -cp primetype.jar:msv.jar com.sun.msv.driver.textui.Driver
primes.rng integers.xml
start parsing a grammar.
validating integers.xml
Error at line:5, column:21 of file:///Users/elharo/integers.xml
4 is not a prime number
Error at line:7, column:21 of file:///Users/elharo/integers.xml
6 is not a prime number
the document is NOT valid.
|
成功了!验证器正确地将 4 和 6 标记为合数(非素数)。
结束语  |
注意可运行的 JAR
必须将验证器的 JAR 以及定制类型库的 JAR 直接添加到类路径中,如果使用“java -jar”将验证器看作可运行的 JAR,验证器就找不到定制的类型库,从而显示类似下面这样的消息:
"http://ns.cafeconleche.org/relaxng/primes"
is not a recognized data type vocabulary
6:75@file:///Users/elharo/primes.rng
failed to load a grammar.
|
|
这样就完成素数的验证。您可以编写包含多个简单类型的库,也可以定义有更加复杂的验证规则的类型,但是任何类型库都需要用几个类来定义类型和建立加载这种类型的工厂。本文示范了如何用命令行用户接口(UI)来进行验证,但是也可使用图形化的用户接口(GUI)来进行验证,或者使用 Java API for RELAX Verifiers (JARV) 或 Java API for XML Processing (JAXP) 1.3 验证包将验证功能集成到自己的程序中。因为类型库是使用服务 API 动态加载的,所以完全不需要修改 Java 代码。只需要将类型库 JAR 放在类路径中,然后在模式中引用这些类型就可以了。您不必再局限于 W3C 的简单数据类型。您可以彻底检查任何字符串是否符合任何可确定的规则集。您可以改变类型库来适应自己的业务需求,而不必调整业务规则以适应模式语言。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| Pluggable prime number datatype for RELAX NG | x-custyp_primedatatype.zip | 3 KB | HTTP |
|---|
参考资料
关于作者
对本文的评价
|