XMLのスキーマ定義の仕方(2)

http://d.hatena.ne.jp/daisuke-m/20091104/1257355231 の続き。

XML Schemaの記述環境(下記)が揃ったのが前回まで。

  • sample1.xml → イメージXML。想定されるXMLの具体例。
  • profiles.xsd → 記述対象のXML Schemaファイル
  • SchemaTest.java → 検証用テストケース


この環境で、XML Schemaを下記の仕様に従って作っていく。

  • 基本の名前空間http://xet.jp/xml/ns/sample/profiles とする
  • profilesの子要素にはprofile要素が1つ以上存在する
    • profileには必ずid属性が必要で、値は1以上の整数でなければならない
    • profileの子要素は、name(必須), email(省略可), skype(省略可), webs(省略可)が順番に出現
    • skypeの内容は、[A-Za-z0-9]+ の正規表現にマッチしなければならない*1
    • webs要素の直下には、web要素が0個以上存在する
      • web要素にはtype属性が必須。値は "blog", "twitter", "wassr", "other" の何れか
      • webの子要素には title(省略可), url(必須) が順番に出現
  • 最後のprofileの後には、他の名前空間の要素であればどんな要素でも自由に書くことができる

イメージXMLからスキーマを指定する

XMLファイルの中で、「このXMLは、このスキーマに従って作られています」という意思表示をすることができる。

<profiles
    xmlns="http://xet.jp/xml/ns/sample/profiles"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xet.jp/xml/ns/sample/profiles ../../main/resources/profiles.xsd">

xsiという名前空間prefixを定義し、xsi:schemaLocation に「定義対象の名前空間」と「xsdファイルのパス(URL)」を空白区切りで記述する。今はローカル内での参照なので、ファイルへのパスを記述しているが、xsdをリリースするときはxsdファイルのURLを記述する。

例えばMaven2のPOMファイルのxsdは http://maven.apache.org/maven-v4_0_0.xsd にあるため、POMの xsi:schemaLocation 定義は下記のようになっている。

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

1つのXMLに複数のxsdを適用したい場合は、名前空間+スペース+URL のペアをさらにスペース区切りで記述すればいい。

この指定によって、XMLエディタがxsdを読み込んで、バリデーションや補完の機能を果たすようになる。Eclipseにプロジェクトをインポートした人は、補完を試してみると良いと思う。


XML Schemaファイルの概略

xs:schema要素の直下には、xs:element要素が並んでいる。これはお察しの通り、XMLの要素(element)を宣言するものだ。xs:element要素のname属性には、宣言したい要素名を記述する。この「宣言したい要素名」の名前空間は、targetNamespaceで指定した名前空間上に宣言される。

現在のところxs:elementしか並んでいないが、xs:attributeで属性の宣言をしたり、 xs:simpleType, xs:complexTypeなどの型宣言をすることもできる。

ノードと型

XMLには、ノード(node)という概念がある。まぁ「要素(element)」または「属性(attribute)」だと思っておけばいい*2

次に型。こいつは、ノードに対する制約をあらわす。Javaにおいて、変数に対して制約(ex. 整数しか格納できない等)を課す役割を果たすのも型。XML Schemaにおいて、型は2種類に分けられる。simpleTypeとcomplexTypeだ。

complexType
子要素や属性をもつ要素の型
simpleType
complexTypeではない、つまり内容に文字列しか含まない型 or 属性の型
<foo>bar</foo> <!-- これは内容に文字列しか含まないので simpleType -->
<baz/> <!-- これもsimpleType -->
<qux> <!-- quxは子要素を持つので complex -->
  <quux /> <!-- quuxは simple -->
</qux>
<foo bar=""/> <!-- fooは属性を持つので complex、barは属性なので simple -->

という感じだ。

で、話を戻すと、xs:schemaの子要素にはノード宣言と型定義を列挙するのだ。

profiles要素

さて、ここから各要素の宣言を見ていこう。

  <xs:element name="profiles">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="profile" maxOccurs="unbounded" />
        <xs:element ref="foo" />
        <xs:element ref="bar" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>

こんな宣言だ。中にcomplexTypeの定義がある。profilesには子要素があるので、complexTypeとなる。このprofile宣言は、「要素の宣言」と「型の定義」を同時に行ってしまっている。分けて考えるとこうなる。

  <xs:element name="profiles" type="profilesType" />

  <xs:complexType name="profilesType">
    <xs:sequence>
      <xs:element ref="profile" maxOccurs="unbounded" />
      <xs:element ref="foo" />
      <xs:element ref="bar" />
    </xs:sequence>
  </xs:complexType>
  • profile要素の宣言をして、その型としてprofilesTypeを参照するセクション
  • profilesType型を定義するセクション


次に、complexType定義の中には、xs:sequenceという要素がある。これは「子要素が出現する順番」の制約だ。他には xs:choice や xs:all がある。

sequence
子要素がこの順番で出現する
choice
示した要素のうち、いずれかが出現する
all
順番に制限はなく、示した要素のうち任意のものが、0〜1回出現する

参考 http://www.atmarkit.co.jp/fxml/tecs/036xsd/36.html

profiles要素には「profilesの子要素にはprofile要素が1つ以上存在する」「最後のprofileの後には、他の名前空間の要素であればどんな要素でも自由に書くことができる」という制約をつけたいので、profile→anyの順のsequenceと考えることができる。

profile要素の出現回数を制限するには、maxOccurs, minOccurs属性をつかう。最高/最低出現回数ですね。これらには0以上の整数またはunbounded(無限)を指定することができます。どちらもデフォルトは1です。というわけで、このままで良いですね。

次に、今はfoo,barで制約がかかっていますが「何でもOK」をどのように表現するか。

<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other" processContents="lax" />

ってのがあります。どんな要素が何回でも出現できるよ、という。

namespace属性では、「どういう名前空間の属性を許可するのかが設定できる」という制限を記述できる。"##local" を指定すると「名前空間を持たない任意の要素」、"##other" を指定すると「現在定義している名前空間"以外"に属する任意の要素」という意味になる。また、"http://xet.jp/xml/ns/sample/xxx http://xet.jp/xml/ns/sample/yyy" など、具体的な名前空間を指定することもできる。

例えば、「この部分にはXHTML1.0に従った内容を書くことができる!」としたければ "http://www.w3.org/2002/08/xhtml/xhtml1-strict.xsd" 等としておけばいい。

processContentsという属性については、興味があれば各自調査でw*3

さて、話が逸れたが、foo, barの要素指定の代りに、xsd:anyをつっこめばよいですね。

  <xs:element name="profiles">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="profile" maxOccurs="unbounded" />
        <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other" processContents="lax" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>

さらに、最後に追いやった foo,barの要素宣言は、もう使う事がなくなったので、消してしまおう。

ここで、sample1.xmlのfoo,barに、別の名前空間を割り当てておく。

<profiles
    xmlns="http://xet.jp/xml/ns/sample/profiles"
    xmlns:fooNS="http://xet.jp/xml/ns/sample/foo"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xet.jp/xml/ns/sample/profiles ../../main/resources/profiles.xsd">
  <profile id="1">
    <!-- 略 -->
  </profile>
  <fooNS:foo>
    <fooNS:bar/>
  </fooNS:foo>
  <fooNS:bar/>
</profiles>

これでテストケースを回してみよう。通るハズ。

このように、XML Schemaを定義する時、将来の拡張に備えて、しかるべき所に「何かが追加されるかもね」ってポイントを定義しておくと、柔軟性が高まる。後にfooNSに対するXML Schemaを別ファイルで定義して使う、とかね。

ちょっと修正した件

えっと、前のエントリを少し修正しています。

profilesの制約について、「最後のprofileの後には、他の名前空間の要素であればどんな要素でも自由に書くことができる」と、太字の部分を追加しました。そして、foo,barを別の名前空間にしました。

なぜかというと。

profileが何度でも現れる(a) → その後に何かが何度でも現れる(b)

という制約だと、profile要素が複数あった時、「どこまでがaの制約の範囲で、どこからがbの制約の範囲なのかわからん」ということになる。

XML Schemaでは、そういう曖昧な状況を、UPA (Unique Particle Attribution) という制約で排除しているらしい。まぁ、これを考慮してなかったので、修正した次第。良くできてるなぁ…。

参考:Unique Particle Attribution - 傭兵日記


さて、第三回に続く。

*1:実際のSkypeIDはピリオドやアンスコ等も使えますが、とりあえず。

*2:実際には、テキストや実体参照なども、個別のノードだったりする。

*3:この場合、laxじゃないと通りません。fooNSのスキーマを定義しないので。