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

データの保存形式や、設定ファイルの記述形式としてXMLを選択することがある。まぁ、その形式にXMLが適しているのかどうか、という話はおいといて、とにかくXMLを採用したとする。

そのXMLを読み書きするのはアプリケーションからのみ、というのであれば大した問題はないかもしれないが、「人間が編集する可能性がある」とした場合、XMLスキーマ定義をしておくのが親切であろう。

XMLスキーマとは、例えばこんな定義。

  • fooの子要素にはbar, baz, qux の順に要素が現れる
  • bazは省略できる
  • quxは1回以上、複数あらわれることもできる
  • qux要素にはquux属性が必ず必要、その他の属性は存在できない
  • quuxの値は、必ず1以上の整数でなければならない
  • などなど
<foo>
  <bar/>
  <qux quux="1"/>
  <qux quux="4"/>
</foo>


こういったXMLの構造を制約するのがXMLスキーマ定義だ。この定義を提供することにより、Javaのソースファイルをコンパイルした時に間違っていればエラーが出るのと同じ感覚で、間違ったXMLファイルを書くとエラーを表示する、というようなことができる。また、XMLエディタを使えば入力補完までできる。ここで現れて良い要素はコレかコレ。この要素ならばこの属性が必須、等の情報が分かるからだ。

このような補助をユーザに提供するのが、スキーマ言語の一つの役割だ。スキーマが定義されていなければ、なかなかエラーに気づけるものではない。

さて、XMLスキーマを定義する方法には、主に以下のような技術がある。

  • DTD (Document Type Definition)
  • XML Schema
  • RELAX (REgular Language description for XML


DTDは昔ながらの方法。しかし、DTDではデータ型の定義ができないことや、DTD自体がXML文書ではない等の欠点が指摘されている。そこで登場したのがXML Schemaだが、仕様が複雑でカオス。で、シンプルなスキーマ言語としてRELAXが登場した、というのが簡単な歴史らしい。

ひとつひとつ紹介したいのだが、実は自分はXML Schemaしか知らない。DTDとRELAXについては他サイトに任せ、ここではXML Schemaについて、簡単に作り方を紹介してみる。

ただし、以下を理解する前提知識として「XML名前空間」が必須となる。コイツについては、そのうち書くかもしれない。ひとまず各自学習のこと。

ちなみにXML Schema自体がXML文書であるので "XML Schemaを定義するXML Schema"も存在するw 気になる人は見てみるとよいw 複雑怪奇だけどねw

お題

<?xml version="1.0" encoding="UTF-8"?>
<profiles
    xmlns="http://xet.jp/xml/ns/sample/profiles">
  <profile id="1">
    <name>都元 ダイスケ</name>
    <email>dai.0304_at_gmail.com</email>
    <skype>cuervo1800</skype>
    <webs>
      <web type="blog">
        <title>都元ダイスケ IT-PRESS</title>
        <url>http://d.hatena.ne.jp/daisuke-m/</url>
      </web>
      <web type="twitter">
        <url>http://twitter.com/daisuke_m/</url>
      </web>
      <web type="wassr">
        <url>http://wassr.jp/user/daisukem/</url>
      </web>
    </webs>
  </profile>
  <profile id="2">
    <name>じゅんいち☆かとう</name>
    <webs>
      <web type="blog">
        <title>じゅんいち☆かとうの技術日誌</title>
        <url>http://d.hatena.ne.jp/j5ik2o/</url>
      </web>
      <web type="twitter">
        <url>http://twitter.com/j5ik2o/</url>
      </web>
    </webs>
  </profile>
  <foo>
    <bar/>
  </foo>
  <bar/>
</profiles>
  • 基本の名前空間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の後には、他の名前空間の要素であればどんな要素でも自由に書くことができる


次で手抜きするために、foo, barが 名前空間 "http://xet.jp/xml/ns/sample/profiles" に属しているので、最後の項目が矛盾しているが、後で何とかするw(本当は、foo,barは "他の名前空間" の要素じゃなきゃいけない)

このくらいにしておこうかな…。

まずは手抜き

イメージするXMLを書いたら、まずは手を抜こうじゃないか。便利なツールがあるので、そいつにイメージXMLを食わせる。

http://www.hitsw.com/xml_utilites/

このサイトの「XML Document to XML Schema」ってところで、Generate XML Schemaする。そうすると「このXMLだったら、まぁこんな感じじゃね?」というXML Schemaを吐いてくれる。こいつを修正していこう。

<?xml version="1.0" encoding="UTF-8" ?>

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="bar" type="xs:string" />

  <xs:element name="email">
    <xs:complexType mixed="true" />
  </xs:element>

  <xs:element name="foo">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="bar" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>

  <xs:element name="name">
    <xs:complexType mixed="true" />
  </xs:element>

  <xs:element name="profile">
    <xs:complexType>
      <xs:choice>
        <xs:element ref="email" />
        <xs:element ref="name" />
        <xs:element ref="skype" />
        <xs:element ref="webs" />
      </xs:choice>
      <xs:attribute name="id" type="xs:NMTOKEN" use="required" />
    </xs:complexType>
  </xs:element>

  <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>

  <xs:element name="skype">
    <xs:complexType mixed="true" />
  </xs:element>

  <xs:element name="title">
    <xs:complexType mixed="true" />
  </xs:element>

  <xs:element name="url">
    <xs:complexType mixed="true" />
  </xs:element>

  <xs:element name="web">
    <xs:complexType>
      <xs:choice>
        <xs:element ref="title" />
        <xs:element ref="url" />
      </xs:choice>
      <xs:attribute name="type" type="xs:NMTOKEN" use="required" />
    </xs:complexType>
  </xs:element>

  <xs:element name="webs">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="web" maxOccurs="unbounded" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>

</xs:schema>

とりあえず、出て来た定義を profiles.xsd ファイルに保存しよう。で、イメージとなるXMLをsample1.xmlとして保存しよう。

まぁ、大変残念な感じなのだが、生成した XML Schema は sample1.xml を妥当だと判定しません。色々オカシイですw 多分、このツールのバグです。でもまぁ、枠組み作りとして使えなくもないので紹介した次第。

修正点は下記の通り。

  • xs:choiceとなっている所をxs:sequenceに修正(2カ所)
  • profile要素の定義()の中でemail, name, skype, webs が並んでいるが、name, email, skype, webs に並び替える
  • の属性に minOccurs="0" を追加
  • の属性に minOccurs="0" を追加
  • (web要素定義の) の属性に minOccurs="0" を追加

この修正により、このXML Schemaが、sample1.xmlを妥当だと判断するようになる。

基本構造

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
		targetNamespace="http://xet.jp/xml/ns/sample/profiles"
		xmlns="http://xet.jp/xml/ns/sample/profiles"
		xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- 略 -->
</xs:schema>

ルート要素はとなる。その属性にとりあえず、targetNamespaceとxmlnsを指定してしまう。「定義したいスキーマ名前空間」を指定する。

http://www.w3.org/2001/XMLSchema名前空間のprefixはxsdとする流派とxsとする流派などがある。個人的にxsd派なのだが*2、ツールにあわせてここではxsをそのまま使う。

整理する

イメージXMLの中で使ったfoo,barは「どんな要素でも自由に書くことができる」という意味で適当に書いた要素だ。最終的に消えるはずなので、最後の部分に移動させてしまおう。
また、xs:element の並び順を、分かりやすい様に profiles, profile, name, email, skype, webs, web, title, url にしてしまおう。

検証環境の整備

自分の書いたXMLXML Schema間の整合性をチェックすべく、環境を整備しよう。つまり、sample1.xmlが、profiles.xsdの定義に従っているかどうか(妥当かどうか)を随時チェックできる環境を整える。Eclipseのプロジェクト作ってしまえー。

以下のテストケースを回すことによって、sample1.xmlとprofiles.xsdの整合性チェックができる。

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;

import org.junit.Test;
import org.w3c.dom.Document;

/**
 * サンプルXMLファイルが、XML Schema定義に従っているかどうかをテストするクラス。
 * 
 * @author daisuke
 */
public class SchemaTest {
  
  /**
   * sample1.xmlが、profiles.xsdに従っているかどうかテストする。
   * 
   * @throws Exception 例外が発生した場合
   */
  @Test
  public void test_sample1() throws Exception {
    // XML SchemaのDOMを作る
    DocumentBuilderFactory factory0 = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder0 = factory0.newDocumentBuilder();
    Document xsd = builder0.parse(SchemaTest.class.getResourceAsStream("/profiles.xsd"));
    
    // XML Schemaオブジェクトを作る
    SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    Schema schema = schemaFactory.newSchema(new Source[] {
      new DOMSource(xsd)
      // 複数のXML Schemaを同時に満たすかどうかチェックするには、ここに列挙すればよい。
    });
    
    // 検証対象XMLのDOMを作る
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    factory.setSchema(schema);
    DocumentBuilder builder = factory.newDocumentBuilder();
    Document document = builder.parse(SchemaTest.class.getResourceAsStream("/sample1.xml"));
    
    // 妥当性検証
    Validator validator = schema.newValidator();
    validator.validate(new DOMSource(document)); // 妥当でなければ例外が飛ぶ
  }
}

ここまでのまとめ

っていうか、プロジェクトファイルを提供するので、これをダウンロードしてインポートしてしまえー。

xsd-tutorial-1.0.0.zip 直

長くなってきたので続きは明日。

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

*2:根拠はない。なんとなくだ。