SoupProject::MiX

電話帳 - DOMによる実装

よくプログラミング入門書などで扱われるような電話帳です。
他のサンプルに比べ少し大物ですが、ソースを追いかけてみましょう。

DOMをどう料理するか

DOMはXML文書全体やXML文書の構成要素を一つのオブジェクトとして操作する機能を 提供します。
SAXと違い単なる文字列処理ではなくオブジェクトを操作して行く感覚で プログラミングができます。
今回は電話帳を作るのですが、実装には大きく分けて二通りの方法があると思います。

ここでは、MiXのDOMオブジェクト関連の機能を説明することを 主眼としていますので、後者の方法で実装します。

#include <MiX/MiX.h>

#include <iostream>
#include <fstream>
#include <string>

typedef MiX::Element<char> Person;
typedef std::map<std::string,Person*> Index;

ここではPersonとIndexを定義しています。
PersonはElementオブジェクトと等しく、IndexはstringとElementへのポインタのmapです。
名前の通りPersonは電話帳に登録された人を、Indexは電話帳の索引を表す型とします。
次に電話帳そのものを表すクラスPhoneBookを定義します。

class PhoneBook {
  MiX::Document<char>& doc_;
  Index index_;

index_はこのPhoneBookの索引。doc_はこの電話帳自体を表すDOMのDocumentオブジェクトを表しています。
この二つのメンバがPhoneBookのデータです。これらを操作するメソッドを見てみましょう。

  MiX::Document<char>& loadDocument(std::string fname){
    std::ifstream fin(fname.c_str());
    if(!fin){
      MiX::Document<char>& ret = MiX::Document<char>::create("PhoneBook");
      MiX::Attribute<char>::create("version","1.0",ret);
      MiX::Attribute<char>::create("encoding","UTF-8",ret);
      return ret;
    }else{
      MiX::DOM_Parser<char> parser;
      parser.setIgnoreSpace(true);
      return parser.parse(fin);
    }
  }

loadDocumentメソッドはファイル名を与えるとファイルを読み込んでDOMパーサに かけて、DOMのDocumentオブジェクトを返します。もしファイルが存在しない場合、 空のドキュメントを作ってそれを返します。
DOMParser<...>::setIgnoreSpace(bool)はXML文書中に現われるスペース やタブ、改行を無視するかどうかを設定するメソッドです。単なる文書整形のため にタブや改行が使われている場合、これを設定すると、パージング時にそれらの 空白を無視してくれるため処理がとても簡単になります。ただし、空白が意味を持つ 場合は使ってはいけません。
これは先述のdoc_を初期化するためのメソッドです。

  Index loadIndex(MiX::Document<char>& doc){
    Index ret;
    MiX::NodeList<char>::iterator it = doc.getRoot().getChildren().begin();
    MiX::NodeList<char>::iterator last = doc.getRoot().getChildren().end();    
    for( ;it!=last;++it){
      if((*it)->getType()==MiX::Node_Element){
	Person* p = dynamic_cast<Person*>(*it);
	ret.insert(std::make_pair((*p)("Name").getText(),p));
      }
    }
    return ret;
  }

loadIndexメソッドはDocumentオブジェクトを与えると、そこから索引を生成して くれます。
これを使ってindex_を初期化します。
doc.getRoot()でドキュメントのルートノードを得て、そのgetChildrenで子ノード のリストを得ています。 そして、イテレータを用いて全ての子Elementに対して、 名前とそのElement自身へのポインタをindex_に登録しています。
これで索引のできあがりです。
ちなみに、ここはgetTypeで型を判別し、Element<...>にdown-castして いますが、メンバテンプレートが使える環境なら型指定イテレータという特殊な イテレータを使うことでスマートに記述できます。

MiX::NodeList<char>::Iterator it = doc.getRoot().getChildren().Begin<Element<char> >();
MiX::NodeList<char>::Iterator last = doc.getRoot().getChildren().End<Element<char> >();
for( ;it!=last;++it)
  ret.insert(std::make_pair((*it)("Name").getText(),&(*it)));

と、このように*itはElement<char>&しか指さないことが保証され、 down-castも不要になります。
また*itはポインタではなくて、参照を指すようになりますので(**it)なんていう 不自然な参照外しから解放されます。

public:
  PhoneBook(std::string fname)
    : doc_(loadDocument(fname)),index_(loadIndex(doc_)){
  }

コンストラクタです。ファイル名を元にloadDocumentおよびloadIndexを呼んで 自分自身を生成します。

  void store(std::string fname){
    std::ofstream fout(fname.c_str());
    fout << doc_.toString(true) << std::endl;
    fout.close();
  }

storeメソッドはファイル名に自分自身を格納するためのメソッドです。
このシンプルさを見てください。
DOMオブジェクトをそのままいじくってるので、格納はそのDOMオブジェクトを ファイルに流し込んでやるだけです。
もし、一度適当な型に変換して操作した場合は、ここで専用のXML文字列化処理を 行う必要がありますが、DOMオブジェクトを直接いじった場合文字列化はtoString メソッドでOKです。
toStringの引数はインデントを行うか否かです。

  void createPerson(std::string name,std::string phone){
    MiX::Element<char>& e=MiX::Element<char>::create("Person",doc_.getRoot());
    MiX::Element<char>& n=MiX::Element<char>::create("Name",e);
    MiX::Element<char>& p=MiX::Element<char>::create("Phone",e);
    n.setText(name);
    p.setText(phone);
    index_.insert(std::make_pair(name,&e));
  }

createPersonは電話帳に新たな人を追加するためのメソッドです。
doc_に対してDOMオブジェクトを構築して、索引index_に追加したデータを登録しています。

  Index& getIndex(){
    return index_;
  }
};

getIndexはindex_に対するアクセサです。
以上でPhoneBookクラスは終りです。
あとはこまごまとした部分を作っていきます。

void outputPerson(std::ostream& os,Person& p){
  os << p("Name").getText() << " : " << p("Phone").getText() << std::endl;
}

outputPersonはPersonを出力ストリームに出力する関数です。

std::string input(const char* prompt){
  std::string ret;
  std::cout << prompt << ": " << std::flush;
  std::cin >> ret;
  return ret;
}

inputは指定された文字列を表示し、ユーザからの入力を受け取る関数です。
BASICのINPUT文と一緒です。

char prompt(){
  std::string s = input("add/delete/find/list/clear/quit [a,d,f,l,c,q]");
  return s.at(0);
}

promptはコマンド入力を待つプロンプトです。

void error(const char* errmsg){
  std::cout << "Error: " << errmsg << std::endl;
}

エラー文字列を標準出力に出力します。
エラー文字列だということが判別できるように"Error: " も出力します。
最後にmain関数です。

int main(){
  PhoneBook pb("phonebook.xml");

  char cmd = '\0';
  while(cmd!='q'){
    cmd = prompt();
    switch(cmd){

「a」が入力されたら、新しい人を登録します。

    case 'a' : case 'A' : {
      std::string name = input("Name");
      Index::iterator it = pb.getIndex().find(name);
      if(it!=pb.getIndex().end()) error("already exists.");
      else {
	std::string phone = input("Phone");
	pb.createPerson(name,phone);
      }
      break;
    }

「d」が入力されたら、名前から人間を検索しPhoneBookから削除します。

    case 'd' : case 'D' : {
      std::string name = input("Name");
      Index::iterator it  =pb.getIndex().find(name);   
      if(it==pb.getIndex().end()){
	error("not found!");
      }else{
	it->second->destroy();
	pb.getIndex().erase(it);
      }
      break;
    }

「f」が入力されたら、名前からPersonを検索しPersonを出力します。

    case 'f' : case 'F' : {
      std::string name = input("Name");
      Index::iterator it=pb.getIndex().find(name);
      if(it!=pb.getIndex().end()) 
	outputPerson(std::cout,*(it->second));
      else error("not found.");
      break;
    }

「l」が入力されたら、全Personに出力します。

    case 'l' : case 'L' : {
      std::cout << pb.getIndex().size() <<"entries:" << std::endl;
      Index::iterator it = pb.getIndex().begin();
      Index::iterator last = pb.getIndex().end();
      for( ;it!=last;++it) outputPerson(std::cout,*(it->second));
      break;
    }

「c」が入力されたら、全Personを削除します。

    case 'c' : case 'C' : {
      Index index = pb.getIndex();
      Index::iterator it = index.begin();
      Index::iterator last = index.end();
      for( ;it!=last;++it){ 
	it->second->destroy();
	pb.getIndex().erase(it);
      }
      break;
    }
    // 終了
    case 'q' : case 'Q' : 
    default :
      break;
    }
  }

終了時にstoreを呼んでファイルに保存します。

  pb.store("phonebook.xml");
}