RELAX NG XMLスキーマ言語は、過去3年間で大きな成功を収めました。この理由の主なものは、(特にW3C XML Schema言語と比べて)RELAX NGの持つ、信じがたいほどクリーンで単純な構文によるものです。OpenOffice や DocBook、そして Text Encoding Initiative などを含む多くのグループが、RELAX NGスキーマ言語を採用しています。RELAX NGはW3C内で、W3Cのスキーマすらも置き換え始めています。W3CではSVGとXHTMLのワーキング・グループが、共にそのスキーマをRELAX NGで書いており、その後でDTDやW3C XML Schemaに変換しています。RELAX NGでは、XMLスキーマ・データ型のサポートを強制していませんが、現実にはJingやSunのMultischema Validatorなどの主要な実装では、これをサポートしています。
同じことをするのであれば、W3C XML Schema言語よりもRELAX NGの方がずっと良い・・・、という興奮の声にもかかわらず、より多くのことがRELAX NGで可能だということが見過ごされてしまっています。特にRELAX NGは、W3C XML Schema言語とは異なり、拡張に対して事前に規定され制限された一つの基本データ型の集合に限定されているわけではありません。RELAX NGでは、開発者がプログラムで検証できるような任意の制約を自由に設定できる、タイプ・ライブラリを定義できるのです。例えばW3Cのスキーマでは、次のような制約の妥当性を検証することはできません。
- ある数字が素数であるか否か。
- 文字列の左括弧がすべて、右括弧と対応しているか否か。
-
SKU属性が、製品データベース中のレコードと対応しているか否か。 - ある要素の内容が、(辞書ファイルと検証して)正しい綴りか否か。
こうした制約は、純粋なRELAX NGコードでも検証することはできません。しかし、W3C XML Schema言語とは異なり、JavaやC#、Pythonなど他の言語で書かれたユーザ定義のタイプ・ライブラリを使うことで、RELAX NGを拡張することができるのです。こうした言語は、チューリングが完全(参考文献参照)なので、文字列に対して課しうる制約条件は、どんなものでも検証できます。さらに文字列自体の枠を越えて、例えば倉庫の在庫や会社の株価など、外部条件との整合性までも比較することができます。つまりRELAX NGに組み込みの妥当性検証規則を、任意に合成された妥当性検証条件によって拡張することができるのです。
この記事では、そうした拡張のためのJavaインターフェースについて紹介します。こうしたインターフェースは、(JingやSunのMultischema Validatorを含めた)多くのRELAX NGプロセッサがサポートしているものです。ここでは具体例として、数字が素数であるか否かを検証します。素数かどうかを確認するコードは、RELAX NGにカスタム・ライブラリを組み込むためのサポート・コードとは、かなり独立しています。ですから、もっと複雑なアルゴリズムや妥当性検証条件をどこに差し込めばよいかが簡単に分かるでしょう。この拡張には、次の3つのクラスが必要です。
-
素数データ型を表す
org.relaxng.datatype.Datatypeインターフェースを実装するクラス: このクラスは、与えられた文字列が実際に素数であることを確認する責任を持ちます。 -
org.relaxng.datatype.DatatypeLibraryインターフェースを実装するクラス: このクラスは、与えられた型のローカル名から、正しいデータ型のクラスをロードする責任を持ちます。 -
org.relaxng.datatype.DatatypeLibraryFactoryインターフェースを実装するファクトリ・クラス: このクラスは、与えられたライブラリの名前空間URIから、正しいデータ型ライブラリをロードする責任を持ちます。
では、それぞれのクラスを説明しましょう。
このファクトリ・クラスは簡単です。これには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;
}
}
|
RELAX NGは、JavaサービスAPIを使ってファクトリを検索します。org.relaxng.datatype.DatatypeLibraryFactory という単純テキスト・ファイルをクラス・パスにあるディレクトリか、META-INF/services/ ディレクトリのいずれかのJARファイルに追加します。このファイルは、(各行に一つずつ)JARでまとめられたファクトリ・クラスの完全なパッケージ限定名を含んでいます。この場合は、単にcom.elharo.xml.relaxng.PrimeDatatypeLibraryFactory です。
DatatypeLibrary実装クラスは、ほんの少しだけ複雑です(リスト2参照)。名前空間毎に一つのDatatypeLibraryがありますが、これは2つのメソッドを必要とします。createDatatypeメソッドは、例えば「prime」などのローカル名を取り、そしてそれに適切なデータ型オブジェクトを返します。この単純な例では一つの型しかありませんが、大部分のライブラリでは、同じ名前空間に複数の単純型を用意しています。createDatatypeBuilderメソッドは、org.relaxng.datatype.DatatypeBuilderオブジェクトを返します。より複雑なタイプ・ライブラリでは、文脈(context)や、スコープ内の基底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インターフェースのインスタンスです。このデータ型は、書かなければならない最も複雑なクラスです。このクラスは、下記のような幾つかの責任を負っています。
- 与えられた文字列が、そのデータ型の有効なインスタンスかどうかを判定する。
- そのデータ型のインスタンスを表すオブジェクトを作る。
- データ型の意味に照らして、2つのオブジェクトが同一かどうかを比較する。
- こうしたオブジェクトに対するハッシュ・コードを算出する。
- その型に対するストリーミング・バリデータを提供する。
- その型がID型かどうかを判定する。
- その型が文脈依存かどうかを判定する。
これらの責任を、それぞれ順番に説明しましょう。
リスト3のコードが示すように、2つのメソッドが文字列の妥当性を検証します。もし文字列がそのデータ型の妥当なインスタンスであれば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");
}
} |
この2つのメソッドは、privateのisPrimeメソッドに依存しています。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;
}
}
|
このメソッドは、入力文字列が数字ではない場合にも 偽 を返します。
一部のデータ型は、ここで想定している数字より多くの、生のテキストを含んでいることもあります。例えば、埋め込まれたチェックサムに対して検証される、Base64でエンコードされた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 design pattern)を使うことがあるかも知れません。
このタイプのオブジェクトを使うには、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メソッドが作ったものではない、こうしたメソッドに対して、どんなオブジェクトも渡してはならないことになっています。もし渡してしまうと、こうしたメソッドは、この特定の実装では何らかの妥当な振る舞いをしようとしますが、一般的な場合この振る舞いは未定義なのです。
getIdTypeメソッドは、そのタイプが何らかのID制約を表明しているものかどうかを判定します。ここでは4つの可能性があり、それぞれは、Datatypeクラスにある、名前付き定数で識別されます。
-
Datatype.ID_TYPE_ID -
Datatype.ID_TYPE_IDREF -
Datatype.ID_TYPE_IDREFS -
Datatype.ID_TYPE_NULL
primeデータ型は、どのIDでもIDの参照でもないので、そのgetIdTypeメソッドは、Datatype.ID_TYPE_NULL を返します。
public int getIdType() {
return ID_TYPE_NULL;
}
|
最後のメソッドは、isContextDependent です。素数が有効かどうかは、文脈に依存しないので、このメソッドは単に 偽 を返します。
public boolean isContextDependent() {
return false;
}
|
タイプ・ライブラリをパッケージし、インストールし、そして使用する
タイプ・ライブラリが書けたら、バリデータが使えるようにパッケージします。DatatypeLibraryFactoryクラスの名前を含む、META-INF/services/org.relaxng.datatype.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に示すような、primeタイプ・ライブラリを参照するスキーマが、primes.rngというファイルの中に見つかったとしましょう。
リスト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 が合成数(つまり非素数)である、と正しく判定しています。
これがすべてです。単純な一つの型にとどまらず、複数の型を含むライブラリを書くことができ、より複雑な妥当性検証の規則を持った型を定義することができるのです。しかも、どんなタイプ・ライブラリでも必要なのは、型を定義し、それをロードするためのファクトリを設定する少数のクラスだけです。ここでは、コマンドラインのユーザ・インターフェース(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 | 30KB | HTTP |
- この記事で使用しているサンプル・コードを ダウンロードしてください。
-
RELAX NG home page には、RELAX NGのソフトウェアやライブラリ、チュートリアル、仕様書などがあります。
- Kohsuke KawaguchiのMultischema validator をダウンロードしてください。
-
RELAX NG Pluggable Datatype Libraries specification を読んでください。
- James ClarkのJing には、文字列内の括弧が対応していることを検証する、データ型の実装サンプルがあります。
-
JARV とJAXP 1.3 の妥当性検証インターフェースを使って、RELAX NGの妥当性検証を皆さんのコードに組み込んでください。
- Wikipediaで、Turing completeness に関して調べてください。
- developerWorksのDeveloper Bookstore には、Elliotte Rusty Harold著によるEffective XML やThe XML Bible を含めて、XMLに関連した書籍が豊富に取り揃えられていますので、ぜひご覧ください。
- developerWorksのXMLゾーン には、他にもXML関連の資料が豊富に用意されています。
- XMLおよび関連技術においてIBM認証開発者になる方法については、こちら を参照してください。

Elliotte Rusty Haroldはニューオーリンズ出身であり、時たま、おいしいgumbo(オクラ入りのスープ)を食べに帰っています。ただし現在はニューヨークのブルックリン近郊のProspect Heightsに、妻のBethと猫のCharm(charmed quarkからとりました)とMarjorie(義理の母の名前からとりました)と一緒に住んでいます。彼はPolytechnic Universityのコンピューター・サイエンスの非常勤教授として、Java技術とオブジェクト指向プログラミングを教えています。彼のCafe au Lait Webサイトは、インターネット上で最も人気のある独立系Javaサイトの一つです。また、そこから派生したCafe con Lecheは、最も人気のあるXMLサイトの一つです。彼の最近の著作には『Java I/O, 2nd edition』があります。現在はXML処理用のXOM APIやJaxen XPathエンジン、Jesterテスト・カバレッジ・ツールなどに取り組んでいます。