XMLフロンティア探訪
第12回 どんなデータ型も利用可能なRELAX NG

日本発のXMLスキーマ言語として登場したRELAX。昨年秋には、その後継となるRELAX NGがOASISから発表された。今回はRELAX NGが持つ柔軟なデータ型定義について紹介していく。(編集局)

川俣 晶
株式会社ピーデー
2002/5/11

今回の主な内容
SGMLから引き継いだ遺産
XML Schemaのデータ型ライブラリも利用可
整数型の内容を持つ要素の定義
不正な値についてもチェック可能
繰り返し出てくる記述をまとめる
スキーマを複数のファイルから構成する
複数の定義を合成する
スキーマをモジュール化する

SGMLから引き継いだ遺産

 XMLの祖先に当たるSGMLは、主に文書を扱うことを意図した言語だった。SGMLで主に想定されたデータとは、文書の本文や見出しや脚注などであって、整数や実数を扱うことはあまり想定されていなかった。そのために、SGMLからXMLが引き継いだスキーマ言語DTDで指定可能なデータ型は、文字列であったり、トークンであったりと、数値処理には縁のないものばかりであった。

 しかし今日では、ありとあらゆるデータをXMLで記述するということが行われており、その中で整数や実数を扱うことも珍しくない。スキーマ言語にも、文字列やトークンだけでなく、整数や実数も指定可能であることが求められている。

 しかし、データとして数値を扱うといっても、数値の表現には微妙に異なる複数の方法があって、単純に決められるものではない。一例を挙げれば、.NET Frameworkでは複数のプログラム言語の間で相互運用性が実現されているが、それを達成するためにデータ型を統一するCLS(Common Language Specification)という仕様を必要としている。逆に、これを導入した結果、個々のプログラム言語で(以前のバージョンとは)データ型の扱いが変わってしまったという事例もある。つまり、新しいデータ型を利用可能にするためにデータ型のセットを作っても、また非互換の環境が1つ増えるだけで、好ましいことではない。

 前回の、「実は新構文になっているRELAX NG」では、RELAXからRELAX NGにバージョンアップしたことで構文が変わった点について紹介した。今回はこのデータ型について、RELAX NGでどう扱うことができるのか、紹介していこう。

XML Schemaのデータ型ライブラリも利用可

RELAX NGの仕様を解説したOASISのページ

 RELAX NGでは、RELAX NG独自のデータ型セットを作るのではなく、外部のデータ型ライブラリを利用する方法を選択している。そのため、データ型ライブラリさえあれば、どのようなデータ型の定義も利用可能になる。極端なことをいえば、XML文書中で整数を「千二十四」のような漢数字で表現するデータ型ライブラリがあると仮定すれば、そのような表現を使うXML文書型を定義することもできる。

 ではデータ型をRELAX NG自身で扱わないとしたら、RELAX NGは何を扱うものなのだろうか? RELAX NGは(XML文書の)文書ツリーの構造を扱う言語である。それに対して、データ型ライブラリは文字の並びの妥当性を調べるものだといえる。つまり、目的も性格も異なるものなので、それぞれの機能が別個に実現されることは適切な判断といえる。余談だが、XML Schemaでも、両者は仕様書のPart1とPart2に分けて記述されている。

 さて、理屈のうえではこのように何でも使えるが、実際にはどうだろうか。現在使われ得るデータ型ライブラリとしては、RELAX NG自身にビルドインされたものと、XML Schema Part2対応のデータ型ライブラリの2種類がある。

 RELAX NG自身にビルドインされたデータ型ライブラリは、文字列とトークンのみをサポートしているが、これではDTDと同等レベルの内容のスキーマを記述する場合には出番があるかもしれないが、RELAX NGで期待される用途には表現力不足だろう。そこで、一般的にはXML Schema Part2対応のデータ型ライブラリを使用することになる。これを使えば整数や実数など、多くのデータ型が利用可能になる。

整数型の内容を持つ要素の定義

 能書きはこのあたりにして、実際に使用した例を見てみよう。整数型の内容を持つことが許された要素aが存在するスキーマをRELAX NGで記述してみた。

編集注:以下のXML文書には説明のために先頭に行番号を付けています。また、背景が水色のXML文書が、RELAX NGによるスキーマで、背景が緑色のXML文書は、スキーマを検証するためのXML文書の例や検証結果です。

1: <?xml version="1.0"?>
2: <element name="a" xmlns="http://relaxng.org/ns/structure/1.0"
3: datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:   <data type="integer"/>
5: </element>

 このスキーマのポイントは2つである。1つは、3行目で指定されたdatatypeLibrary属性である。この属性は、データ型ライブラリを識別するURIを記述する。ここに記述されたhttp://www.w3.org/2001/XMLSchema-datatypesは、XML Schemaのデータ型を識別するURIである。これを記述することにより、この属性が記述された要素と、その子要素で使用されるデータ型は、このURIで指定されたデータ型になる。そのため、4行目に記述されたintegerというデータ型名は、このデータ型ライブラリによって解釈される。

 問題は4行目である。前回は要素の内容としては空の<empty/><text/>を記述していたが、それらの代わりにdata要素を用いれば、そこでデータ型を明示的に指定することが可能になる。具体的なデータ型の名前は、type属性で指定する。繰り返しになるが、type属性で指定する名前は、データ型ライブラリに依存するものであり、データ型ライブラリが変われば利用できる名前や種類が変化することになる。ここで記述されたintegerというデータ型名は、3行目の属性で指定されたXML Schemaのデータ型ライブラリに含まれるものである。

 さて、実際にこのスキーマを以下の2つのXML文書で検証してみよう(実際の動作は、以下のXML文書を上記のスキーマで検証する、ということになるが)。最初の文書は、要素aの内容が整数として記述されている場合である。

1: <?xml version="1.0"?>
2: <a>123</a>

 このXML文書を検証してもエラーは起きない。では、要素aの内容が整数ではなく単なる文字列の場合はどうなるだろうか。以下の文書は、整数ではない文字列を記述した例である。

1: <?xml version="1.0"?>
2: <a>hello!</a>

 これをRELAX NG検証ソフトのjingで検証すると、以下のようなエラーが発生する。jingは、RELAX NGの開発者の1人ジェームス・クラーク(James Clark)氏自身が開発した検証ソフトだ。

1: Error at URL "file:/Q:/sample/t001a.xml", linenumber 2,
2: column number 10: badcharactercontentforelement

 bad character content for elementとは、要素の内容に含まれる文字が適切ではないことを意味している。つまり、整数であるべきところに、整数ではない文字を書いたことが問題である、という指摘だ。

不正な値についてもチェック可能

 具体的にスキーマによるデータ型の指定が有益である簡単な事例を見てみよう。人の名前と年齢を記述するスキーマを記述してみる。このとき、人の名前はどんな文字を使って記述しても構わないが、1文字も記述されない名なしでは困る。かといって文字数が多すぎても処理の都合上、具合が悪い。年齢も、マイナスの数値は受け付けられないし、極端に大きな年齢も受け付けられるべきではない。年齢十万歳などというデータがあれば、これはどう見ても正常に受け付けるべきデータではないだろう。人間だけでなく悪魔まで扱うならともかく、通常はシステムかデータの異常を疑った方がよい。

 このような条件を含めて、スキーマ言語を用いて文書型を記述して検証できれば、不特定多数からデータを受け付ける場合などに、よい防壁となるだろう。実際にこのように使用可能なスキーマを記述してみた。

1:  <?xml version="1.0"?>
2:  <element name="person" xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:    <element name="name">
5:      <data type="string">
6:        <param name="minLength">1</param>
7:        <param name="maxLength">127</param>
8:      </data>
9:    </element>
10:   <element name="age">
11:     <data type="integer">
12:       <param name="minInclusive">0</param>
13:       <param name="maxExclusive">200</param>
14:     </data>
15:   </element>
16: </element>

 このスキーマのポイントは、data要素の子要素に記述されたparam要素である。データ型ライブラリに情報を付け加える機能を持っている。ここで使用しているものは、facetと呼ばれるもので、XML Schemaのデータ型の仕様に含まれているものだ。データ型の種類ごとに、利用できるfacetの種類が決まっている。例えば、文字列型(string)なら、minLengthや、maxLengthが利用できる。これは許される文字列の最小の長さと最大の長さを設定するもので、この設定範囲より長くても短くても、エラーにすることができる。整数には、minInclusiveやmaxExclusiveが指定できる。minで始まる名前は最小値を指定し、maxで始まる名前は最大値を指定する。InclusiveとExclusiveはその値を含むか含まないかを意味する。

 このサンプルソースでは、minInclusiveが0なので、最小値は0で、0自身を含み、最大値は200で、200自身は含まないことになる。実際に、以下のXML文書をこのスキーマで検証すると、エラーは起こらない。

1: <?xml version="1.0"?>
2: <person>
3:   <name>YamadaTaro</name>
4:   <age>17</age>
5: </person>

 しかし、以下のXML文書はname要素の内容の文字列がない(長さ0)である。

1: <?xml version="1.0"?>
2: <person>
3:   <name></name>
4:   <age>17</age>
5: </person>

 これを検証すると以下のようなエラーになる。

1: Error at URL "file:/Q:/sample/t002a.xml", linenumber 3,
2: column number 8: bad character content for element

 また、以下のXML文書は、age要素の値が最大値を超えている。

1: <?xml version="1.0"?>
2: <person>
3:   <name>Yamada Taro</name>
4:   <age>1777</age>
5: </person>

 これを検証すると以下のようなエラーになる。

1: Error at URL "file:/Q:/sample/t002b.xml", linenumber4,
2: column number 11: badcharactercontentforelement

 このようなデータ型を意識したリッチな検証が、スキーマを記述するだけで即座に実現できることの意義は大きいだろう。特に、どこまで信頼できるか分からないデータが飛び交うネットワーク世界では、受け取ったデータをこういうスキーマで検証して、受け付け可能かチェックするだけでも、トラブルを減らすことができるだろう。

 また、XML Schemaのデータ型を使用していれば、XML Schemaベースのシステムとの通信もうまく整合することになり、トラブルが起きにくいといえる。

繰り返し出てくる記述をまとめる

 次のようなスキーマについて考えてみよう。長方形を、2つの座標(XとY)で表現しており、座標は左上と右下の2つを指定するが、XとYのどちらも、その座標を示すために2つの浮動小数点の数値を記述するだけで、表記は同じだ。なお、doubleは浮動小数点表記の数値型を意味する。

1:  <?xml version="1.0"?>
2:  <element name="rectangle"> xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:    <element name="point">
5:      <element name="x">
6:        <data type="double"/>
7:      </element>
8:      <element name="y">
9:        <data type="double"/>
10:     </element>
11:   </element>
12:   <element name="point">
13:     <element name="x">
14:       <data type="double"/>
15:     </element>
16:     <element name="y">
17:         <data type="double"/>
18:     </element>
19:   </element>
20: </element>

 いうまでもなく、上記のスキーマではまったく同じ記述が2回以上繰り返されていてムダである。しかし、上から下に向かって書いていくスタイルを取る限り、同じパターンがあっても、それを必要な回数だけ繰り返し書くしかない。

 しかし、RELAX NGにはほかの方法もある。繰り返し使用されるパターンをdefine要素を用いてあらかじめ定義しておき、それを参照することができる。これを使って、同じパターンを繰り返し記述しないようにしたのが以下のスキーマである。

1:  <?xml version="1.0"?>
2:  <grammar xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:    <start>
5:      <ref name="document"/>
6:    </start>
7:  
8:    <define name="document">
9:      <element name="rectangle">
10:       <ref name="doublePoint"/>
11:       <ref name="doublePoint"/>
12:     </element>
13:   </define>
14:  
15:   <define name="doublePoint">
16:     <element name="point">
17:       <element name="x">
18:         <data type="double"/>
19:       </element>
20:       <element name="y">
21:         <data type="double"/>
22:       </element>
23:     </element>
24:   </define>
25: </grammar>

 これまで登場していなかった要素がいくつか出てきたので、説明しよう。最初に出てくるgrammer要素は、1つのスキーマ単位をまとめる要素である。なぜ、このような要素が必要とされるのかというと、今回のスキーマは複数のdefine要素から構成されるからである。トップレベルの要素は1つしか存在できないので、当然複数のdefine要素をまとめるために、何らかの要素がなければならないのである。

 次は、8行目や15行目に見えるdefine要素である。define要素の内容は、部分的なスキーマ定義である。これを10〜11行目のref要素を用いて参照すると、その内容がref要素に置き換えられる。参照を行うためには名前が必要なので、define要素にはname属性があり、名前を付けられるようになっている。ref要素からもname属性で、define要素で定義された名前を記述することができる。その際、これらのname属性に記述された名前が、一切、実際のXML文書に出現しないことに注意が必要である。要素や属性の名前と異なり、define要素で指定する名前は、あくまでスキーマ内でのみ使われるものであり、この名前を変更しても、XML文書の適合性は変わらないのである。

 最後に残った4行目のstart要素は、どの部分を最初に解釈するかを指定する。この例では、5行目でref要素でdocumentを指定しているので、8〜13行目が最初に解釈される。その結果、9行目のrectangle要素がトップレベルの要素になるのである。

 なお、このスキーマは以下のようなXML文書に適合する。

1:  <?xml version="1.0"?>
2:  <rectangle>
3:    <point>
4:      <x>1.0</x>
5:      <y>2.0</y>
6:    </point>
7:    <point>
8:      <x>3.0</x>
9:      <y>4.0</y>
10:   </point>
11: </rectangle>

スキーマを複数のファイルから構成する

 現実のスキーマは、時として大きなサイズに膨れ上がることがある。これを1つのファイルで扱うことはメンテナンス性を悪くする。そこで、スキーマを複数のファイルに分けて構築することが、よく行われる。DTDでも、スキーマを複数のファイルに分けて構成することは可能であったが、RELAX NGでも、もちろん可能である。

 以下は、今回の2番目のサンプルスキーマの中で、age要素に関する宣言を別ファイルに置いてみたサンプルソースである。まずは、age要素に関する宣言を取り除いたメインのスキーマから。

1:  <?xml version="1.0"?>
2:  <element name="person" xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:    <element name="name">
5:      <data type="string">
6:        <param name="minLength">1</param>
7:        <param name="maxLength">127</param>
8:      </data>
9:    </element>
10:   <externalRef href="age.rng"/>
11: </element>

 以下は、呼び出されるage要素に関する宣言を含んだスキーマである。

1: <?xml version="1.0"?>
2: <element name="age" xmlns="http://relaxng.org/ns/structure/1.0"
3: datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:   <data type="integer">
5:     <param name="minInclusive">0</param>
6:     <param name="maxExclusive">200</param>
7:   </data>
8: </element>

 1番目のスキーマの10行目になるexternalRef要素で、外部のファイルを指定することができる。そして、外部のファイルの内容が、その部分に取り込まれて置き換えられる。そのため、スキーマのファイルを分ける場合は、外部に置きたい部分だけを抜き出して、それをexternalRef要素で参照すればよい。

 ただし、注意点もある。抜き出された内容は、それ自身がXML文書になるので、XML文書として成立する内容である必要がある。例えば、開始タグだけを別ファイルにすることはできない。それから、名前空間の宣言はほかのXML文書には及ばないので、個々のファイルごとにxmlns属性(xmlns="http://relaxng.org/ns/structure/1.0")を付加する必要がある。datatypeLibrary属性も同様である。

複数の定義を合成する

 RELAX NGでは、同じ名前を持つdefine要素を複数記述することができる。そして、ref要素でこの名前を参照すると、複数の定義の合成として妥当となるパターンを得ることができる。これは、モジュール化を行う場合には極めて重要な機能なのだが、本当にパターンを合成できることを以下のスキーマで確認してみよう。

 ここでは、person要素の子として、name要素とage要素とcompany要素が持てるスキーマを記述してみた。ただし、name要素とage要素の定義を1つのdefine要素で記述し、company要素は別のdefine要素で記述している。

1:  <?xml version="1.0"?>
2:  <grammar xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:  
5:    <start>
6:      <ref name="person.element"/>
7:    </start>
8:  
9:    <define name="person.element">
10:     <element name="person">
11:       <interleave>
12:         <ref name="person.info"/>
13:       </interleave>
14:     </element>
15:   </define>
16:  
17:   <define name="person.info" combine="interleave">
18:     <element name="name">
19:       <data type="string">
20:         <param name="minLength">1</param>
21:         <param name="maxLength">127</param>
22:       </data>
23:     </element>
24:     <element name="age">
25:       <data type="integer">
26:         <param name="minInclusive">0</param>
27:         <param name="maxExclusive">200</param>
28:       </data>
29:     </element>
30:   </define>
31:  
32:   <define name="person.info" combine="interleave">
33:     <element name="company">
34:       <data type="string">
35:         <param name="minLength">0</param>
36:         <param name="maxLength">127</param>
37:       </data>
38:     </element>
39:   </define>
40:  
41: </grammar>

 このスキーマは、以下のようなXML文書を妥当とする。

1: <?xml version="1.0"?>
2: <person>
3:   <name>YamadaTaro</name>
4:   <age>17</age>
5:   <company>XYZCorporation</company>
6: </person>

 このスキーマでは、interleaveという合成方法を使って、複数の定義からパターンを合成している。11行目のinterleave要素は前回紹介したものだ。そして、その子要素に記述された内容を、順番に関係なく、1回ずつ出現させなければならないとするものである。そして、17行目と32行目のdefine要素に付加されたcombine="interleave"という属性に注目していただきたい。これは、定義が合成されるとき、interleaveの方法で合成されるべきであることを示す。その結果、このスキーマは順番と関係なく、name要素、age要素、company要素をperson要素の子要素として記述しなければならない、という構文を表現する。

 合成の方法としては、ほかにchoiceも使用することができる。こちらの方は、候補の中のいずれかの要素があれば妥当という機能を意味する。

スキーマをモジュール化する

 define要素の合成という機能を活用すると、拡張可能なスキーマを容易に記述することができる。例えば、a、b、cという要素を使用できるスキーマをdefine要素を駆使して記述しておけば、後からdという要素を追加することが、元のスキーマの定義を書き換えることなくできるわけだ。それぞれを別ファイルとして記述しておけば、要素や属性の追加削除も容易である。

 では実際に、上のサンプルスキーマを複数ファイルに分割してみよう。分割の方針は、person要素とname要素とage要素を宣言する主役となるファイルと、company要素を宣言するオプショナルなファイル、そして、これらのすべての要素をまとめて1つのスキーマを形成するファイルの3つに分けることにしよう。

 まず、person要素とname要素とage要素を宣言する主役となるファイルは以下のようになる。

1:  <?xml version="1.0"?>
2:  <grammar xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:  
5:    <define name="person.element">
6:      <element name="person">
7:        <interleave>
8:          <ref name="person.info"/>
9:        </interleave>
10:     </element>
11:   </define>
12:  
13:   <define name="person.info" combine="interleave">
14:     <element name="name">
15:       <data type="string">
16:         <param name="minLength">1</param>
17:         <param name="maxLength">127</param>
18:       </data>
19:     </element>
20:     <element name="age">
21:       <data type="integer">
22:         <param name="minInclusive">0</param>
23:         <param name="maxExclusive">200</param>
24:       </data>
25:     </element>
26:   </define>
27: </grammar>

 このファイルは、person要素を宣言するperson.elementのdefine定義と、person要素の子要素であることを期待されるname要素とage要素を宣言するperson.infoのdefine定義を含む。ここにstart要素を含めていないのは、使い方によってはperson.elementのdefine定義がトップレベルにならない場合もあり得るためだ。

 次に、company要素を宣言するオプショナルなファイルを記述してみた。以下がそれである。

1:  <?xml version="1.0"?>
2:  <grammar xmlns="http://relaxng.org/ns/structure/1.0"
3:  datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:    <define name="person.info" combine="interleave">
5:      <element name="company">
6:        <data type="string">
7:          <param name="minLength">0</param>
8:          <param name="maxLength">127</param>
9:        </data>
10:     </element>
11:   </define>
12: </grammar>

 このファイルには難しいところは何もない。ただ1つのdefine定義を含むだけである。

 次に、すべての要素をまとめて1つのスキーマを形成するファイルを記述してみた。以下がそれである。

1: <?xml version="1.0"?>
2: <grammar xmlns="http://relaxng.org/ns/structure/1.0"
3: datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
4:  
5: <include href="s005sub1.rng"/>
6: <include href="s005sub2.rng"/>
7:  
8: <start>
9: <ref name="person.element"/>
10: </start>
11: </grammar>

 このファイルの5〜6行目で使用されるinclude要素はhref属性で指定されたファイルをそのまま取り込むことを指定する。もし、company要素は使いたくないと思ったら、単純に6行目を消してしまえばよい。もし、ほかの要素も追加したければ、define要素を記述したファイルを作成して、include要素を増やしてそれにより参照すればよい。

 また、ほかの要素の中からperson要素を子要素として記述したい場合は、このファイルのstart要素内で参照するdefine定義名を変更すれば、ほかの要素をトップレベルの要素にすることも容易である。そして、これらの変更に際して、person要素、name要素、age要素を宣言したファイルは一切書き換える必要はなく、どの使い方にも対応することが大きな特徴だ。

 また、このテクニックは、要素だけでなく属性にも使用できる。つまり、属性を別のモジュールから付け加えることにも使える。実際にスキーマを記述する場合は、後から何かが追加される可能性のある要素や属性をdefine定義しておき、後からの拡張に備えることは必須のテクニックといえるだろう。

 このようにして作成されたものが、本連載の第10回「XHTMLモジュールを利用した言語開発 (完結編)」で紹介したRELAX NGによるXHTMLのスキーマである。実際にモジュール化されたスキーマを記述する場合は、これをお手本にするとよいだろう。

「連載 XMLフロンティア探訪」


XML & SOA フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

HTML5+UX 記事ランキング

本日月間