CMX API ver.0.21チュートリアル
第7回「独自フォーマットを定義する」

北原 鉄朗(JST CrestMuse/関西学院大学理工学研究科)
2007年10月19日

はじめに

これまでは,すべてすでに定義されたXMLフォーマットを扱ってきましたが,CMX APIでは,独自のXMLフォーマットも,ファイルラッパクラスを定義するだけで同じように扱えるようになっています.ここでは,独自フォーマットの作り方と使い方について学んでいきます.

例題:my-address-book-xml

単純な例として,音楽からいったん離れて,第3回で架空のXMLフォーマットとして取り上げたmy-address-book-xmlを取り上げましょう.my-address-book-xmlは,次のような形をしていました.

  <my-address-book-xml>
    <person>
      <name>Tetsuro Kitahara</name>
      <address>XX, YY, Osaka, Japan</address>
    </person>
  </my-address-book-xml>
トップレベルタグがmy-address-book-xmlで,その中にperson要素が並びます.上の例では1つしかないですが,実際には複数並べることができるものと考えます.各々のperson要素にはname要素とaddress要素が1つずつこの順で並ぶものとします.

DTDファイルを書く

まず,このXML形式の定義を表すDTDを書きましょう.ここではDTDの説明は省略しますが,このXML形式に対応するDTDは次のようになります.

<!ELEMENT my-address-book-xml (person*) >
<!ELEMENT person (name, address) >
<!ELEMENT name (#PCDATA) >
<!ELEMENT address (#PCDATA) >
これをmy-address-book-xml.dtdというファイルに保存したとすると,上であげたXMLドキュメントをちゃんと書くとすると次のようになります.
  <?xml version="1.0" ?>
  <!DOCTYPE my-address-book-xml SYSTEM "my-address-book.dtd" >
  <my-address-book-xml>
    <person>
      <name>Tetsuro Kitahara</name>
      <address>XX, YY, Osaka, Japan</address>
    </person>
  </my-address-book-xml>

ファイルラッパクラスを書く

次に,ファイルラッパクラスを書きましょう.これはCMXFileWrapperクラスを継承して作ります.

import jp.crestmuse.cmx.filewrappers.*;

public class MyAddressBookWrapper extends CMXFileWrapper {

}
上のコードでは単に継承しただけで,新しいコードを一切書いていません.XMLドキュメントの処理で共通に使用されそうな処理はほとんどCMXFileWrapper内で実装済みなので,ここで記述するのはmy-address-book-xmlに特化した部分だけです.とりあえず,この状態のままおいておきましょう.

コマンドから自作のファイルラッパクラスを利用する

すでに何度か述べてきたとおり,コマンドはCMXCommandクラスを継承して定義します.CMXCommandクラスはXMLドキュメントを読み込む処理はすでに書いてあって,自作のクラスでは,読み込んだ後の処理だけを書けばよいようになっています.ファイルから読み込んだXMLドキュメントは,ファイルラッパクラスと呼んでいる,CMXFileWrapperクラスのサブクラスのインスタンスとして渡されます.どのファイルラッパクラスをインスタンス化すべきかは,読み込んだXMLドキュメントのトップレベルタグで判断しています.たとえば,トップレベルタグがscore-partwiseであれば,これはMusicXMLですのでMusicXMLWrapperオブジェクトが生成されます.

自作のファイルラッパクラスを利用しようとすると,ここで問題が発生します.自作のXMLフォーマットのトップレベルタグ(ここではmy-address-book-xml)と自作のファイルラッパクラス(ここではMyAddressBookWrapper)とが結び付いていることは,CMXFileWrapperは知る由もないということです.実は,知る由はあるのです.CMXFileWrapperクラスにはaddClassTableというメソッドが用意されています.このメソッドを使うと,自作のXMLフォーマットのトップレベルタグと自作のファイルラッパクラスの組み合わせを登録することができます.次のようにします.

import jp.crestmuse.cmx.commands.*;
import jp.crestmuse.cmx.filewrappers.*;

public class MyAddressBookTest extends CMXCommand {
  protected void run() {
    // 処理内容を書く
  }

  public static void main(String[] args) {
    MyAddressBookTest t = new MyAddressBookTest();
    try {
      // ↓addClassTableを使って自作のXMLフォーマットとファイルラッパクラスを登録する
      CMXFileWrapper.addClassTable("my-address-book-xml", "MyAddressBookWrapper");
      t.start(args);
    } catch (Exception e) {
      t.showErrorMessage(e);
      System.exit(1);
    }
  }
}

上であげたXMLドキュメントのサンプルをファイルに保存して,このプログラムを実行してみてください.ファイル名だけ表示されて終わったら成功です.何か例外が投げられて止まったらDTDファイル,XMLファイル,プログラムのどれかが間違っているはずですので,丹念に見直してください.

自作のファイルラッパクラスをどこかのパッケージに置いてある場合は,パッケージ名もふくめた名前を指定してください.たとえば,MyAddressBookWrapperクラスがfoo.barというパッケージにあるなら,

CMXFileWrapper.addClassTable("my-address-book-xml", "foo.bar.MyAddressBookWrapper");
となるはずです.

CMXFileWrapperクラスのaddClassTableメソッドが働いていることを確かめるため,ためしにそこの文をコメントアウトしてコンパイル・実行をしてみてください.コンパイルは通ると思いますが,実行すると

jp.crestmuse.cmx.filewrappers.InvalidFileTypeException
という例外が投げられて終わるはずです.これは,未知のファイル形式であることを表しています.addClassTableをすることで,自作のファイル形式を扱えるようになったということが分かると思います.

自作のファイルラッパクラスに自作のノードインターフェイスクラスを定義する

my-address-book-xmlは,person要素が並んでいる形になってますので,person要素を扱うノードインターフェイスとしてPersonクラスを作ってみましょう.これは,MusicXMLWrapperクラスにならってMyAddressBookWrapperクラスの内部クラスとして定義してみましょう.

import jp.crestmuse.cmx.filewrappers.*;
import org.w3c.dom.*;

public class MyAddressBookWrapper extends CMXFileWrapper {
  class Person extends NodeInterface {
    protected Person(Node node) {
      super(node);
    }
    protected String getSupportedNodeName() {
      return "person";
    }
  }
}
ここで,getSupportedNodeNameというメソッドは,そのクラスが扱う要素名を返すようにオーバーライドします.これは,このインスタンスを生成するときに不適切な要素が渡されていないかチェックするのに使用されます.

person要素の中にはname要素とaddress要素があるので,それぞれの中身(テキスト)を返すメソッドを作りましょう.指定した子要素のテキストを返すメソッドとしてgetChildTextというのがNodeInterfaceクラスにすでに用意されていますから,次のようにすればOKということになります.

import jp.crestmuse.cmx.filewrappers.*;
import org.w3c.dom.*;

public class MyAddressBookWrapper extends CMXFileWrapper {
  class Person extends NodeInterface {
    protected Person(Node node) {
      super(node);
    }
    protected String getSupportedNodeName() {
      return "person";
    }
    String name() {
      return getChildText("name");
    }
    String address() {
      return getChildText("address");
    }
  }
}
NodeInterfaceクラスを継承して作るクラス(この場合はPersonクラス)で使いそうなメソッドはある程度NodeInterfaceクラスに実装してあります(今回のgetChildTextのように).情報の取り出しでどんなメソッドを使えば迷うときはNodeInterfaceクラスのAPIリファレンスドキュメントを調べてみることをおすすめします.

次に,person要素をすべて探索して各々に対応するPersonオブジェクトを返してくれるメソッドを書いてみましょう.名前をgetPersonListとしましょう.CMXFileWrapperクラスにselectNodeListというメソッドがあります.これはXPath式を指定してそれを満たす要素を返すというものです.XPathの詳細はwebや書籍で勉強していただくとして,/my-address-book-xml/personとすれば,my-address-book-xmlというトップレベルタグのすぐ下のperson要素の集合を返してくれます.このメソッドはNodeListオブジェクトが返ってきます.結局,getPersonListメソッドは次のようになります.

  Person[] getPersonList() {
    NodeList nl = selectNodeList("/my-address-book-xml/person");
    int length = nl.getLength();
    Person[] personlist = new Person[length];
    for (int i = 0; i < length; i++) {
      personlist[i] = new Person(nl.item(i));
    }
    return personlist;
  }
このメソッドをMyAddressBookWrapperクラスのメソッド(MyAddressBookWrapper.Personクラスのではない!)として定義しましょう.CMXFileWrapperクラスについても,これを継承して作るクラスで使いそうなメソッドはある程度実装してありますからCMXFileWrapperクラスのAPIリファレンスドキュメントをチェックしてみましょう.

ちなみに,Personクラスやその中のいくつかのメソッドはいわゆるpackage privateになってますので,他のパッケージからも利用する可能性があるのでしたら,publicにする必要があります.

自作のノードインターフェイスを利用する

以上で一通りファイルラッパができあがったので,これを自作のコマンドから利用してみましょう.上で定義したMyAddressBookTestのrunメソッドを次のように書き換えてみましょう.

  protected void run() {
    MyAddressBookWrapper book = (MyAddressBookWrapper)indata();
    MyAddressBookWrapper.Person[] personlist = book.getPersonList();
    System.out.println("name\taddress");
    for (int i = 0; i < personlist.length; i++) {
      System.out.println(personlist[i].name() + "\t" + personlist[i].address());
    }
  }
実行すると,タブ区切りの形式で画面に出力してくれるはずです.

CMXFileWrapperクラスの設計のはなし

ここでは,CMX APIの使いかたからちょっと離れて,CMXFileWrapperクラスがどういう風に設計されているかについて触れておきます.ここの内容は分からなくても使う分には支障はないので,興味のある方だけお読みください.

XMLフォーマットごとにファイルラッパクラス(CMXFileWrapperクラスのサブクラス)が用意され,CMXCommandクラスを通じてインスタンス化されるというのはすでにご存知と思います.CMXCommandクラスのソースコードを見ると,runAllメソッドの中に

    indata = readInputData(filename);
というのがあるので,ここでファイルの読み込みをしていることがわかります.readInputDataメソッドを見ると,
    return CMXFileWrapper.readfile(filename, this);
と書いてあるので,実際には,CMXFileWrapperクラスのreadfileメソッドがファイルの読み込み処理をしていることが分かります.CMXFileWrapperクラスのreadfileメソッドには次のように書いてあります.
  public static CMXFileWrapper readfile(String filename, 
                                        CMXInitializer init) throws (省略) {
    initXMLProcessors();
    Document doc = builder.parse(new File(filename));
    String toptagname = doc.getDocumentElement().getTagName();
    CMXFileWrapper f = createInstance(toptagname);
    (中略)
    return f;
  }
詳細は省略しますが,1行めはXMLパーサなどを初期化しています.2行めで指定されたファイルをパージングしてDocumentオブジェクトを取得しています.3行めでトップレベルタグの名前を取得しています.4行めでトップレベルタグ名を引数に取ってcreateInstanceメソッドを呼び出しています.ということは,このcreateInstanceメソッドがトップレベルタグ名に応じたクラスラッパオブジェクトを生成しているということになります.

このcreateInstanceメソッドはどのように組めばよいでしょうか.たとえば,トップレベルタグ名がscore-partwiseならMusicXMLWrapperオブジェクト,deviationならDeviationInstanceWrapperオブジェクト,MIDIFileならMIDIXMLWrapperオブジェクトを返すようにするには,どうしたらいいでしょうか.

すぐに思いつくのはif文で分岐する方法だと思います.たとえば,こんな感じです.

  private CMXFileWrapper createInstance(String toptagname) {
    if (toptagname.equals("score-partwise"))
      return new MusicXMLWrapper();
    else if (toptagname.equals("deviation"))
      return new DeviationInstanceWrapper();
    else if (toptagname.equals("MIDIFile"))
      return new MIDIXMLWrapper();
    else ...
  }
この方法は分かりやすくていいのですが,重大な問題点があります.後から拡張できないということです.今回の例のように,MyAddressBookWrapperクラスを作ってmy-address-book-xmlというトップレベルタグに結びつけたくても,このメソッドを書き換えるしか方法はありません.また,最大でファイルラッパクラスの個数分だけtoptagname.equals(...)を繰り返しますので,計算量の点でも好ましくありません.

そこで,CMXFileWrapperではトップレベルタグ名とファイルラッパクラスをHashMapで管理しています.CMXFileWrapperクラスのcreateInstanceメソッドでは

  CMXFileWrapper f = 
    (CMXFileWrapper)CLASS_TABLE.get(toptagname).newInstance();
と書いてあります.CLASS_TABLEに対してたとえばget("score-partwise")を行うとMusicXMLWrapperクラスを表すClassオブジェクトが返されますので,それに対してnewInstanceメソッドを呼び出すことでMusicXMLWrapperオブジェクトを生成しています.score-partwiseというトップレベルタグ名をキーにMusicXMLWrapperクラスを表すClassオブジェクトをCLASS_TABLEに登録するという処理を行っているのがaddClassTableメソッドで,CMXFileWrapperクラスのstaticブロックを見ると定義済みのファイルラッパクラスを登録しているのが分かります.このような枠組みになっているおかげで,最初にaddClassTableメソッドを呼び出すことで,簡単に独自のファイルラッパクラスを利用できるようになるわけです.詳しくは,CMXFileWrapperクラスのソースコードを眺めてみてください.

おわりに

今回は,いままでと趣向を変えて,独自にXMLフォーマットを用意して,これをCMX APIの枠組みで利用するということを考えました.XMLであれば,比較的簡単に独自のフォーマットをCMX APIに取り込むことができるというのが分かっていただけたかと思います.次回は,独自のオプションの組み込み方について学んでいきます.