PerlでAtomPubサーバを作ろう!

第1回もっとも簡単なAtomPubサーバを作ってみる

はじめに

はじめまして。井上武(たける)といいます。

2007年後半はAtomPub(Atom Publication Protocol)に関するニュースが目白押しでした。7月には、日本で初めてのAtomPub接続実験(Interop)が行われました。10月にはIETFでRFCとして承認され、仕様が確定しました。11月になるとGoogleからOpenSocialが発表されました。OpenSocialは、AtomPubをベースとしたSNS APIです。12月にはAtomPubの理論的背景を解説した『RESTful Web サービス』の日本語版が発売されました。

このように、AtomPubを取り巻く環境は、理論から実装、基本から拡張までとても賑やかです。AtomPubを使うと、Webサービスをシンプルでわかりやすく仕上げることができます。これが、AtomPubが注目を集めている理由です。

この連載では、CPANモジュールのCatalyst::Controller::Atompubを使ってAtomPubサーバを実装する方法を紹介します。まず、テキストデータのみを扱う簡易サーバから始めて、写真付きブログサーバを作り、最後にGoogle Data APIs(GData)を実装します。GDataはOpenSocialに検索機能を提供する技術です。

サンプルコードはこちらからダウンロードできます。

Catalyst::Controller::Atompub 開発の経緯

Catalyst::Controller::Atompubは、昨年夏に開発が始まった比較的新しいモジュールです。

最初は昨年7月末に開催されたAtomPubの接続実験のために開発されました。この時点ではまだ独立したモジュールではなく、Webアプリケーションとして実装されていました。このとき、IRCで参加されていた宮川さんとの短いやり取りがきっかけで、Catalystモジュールとして開発することになりました。

Catalystに詳しい中川さんにアドバイスをいただきながら開発を進め、2007年9月10日に最初のバージョンがCPANにアップされました。その後、AtomPub仕様策定を指揮したTim Brayにテストしてもらったり、米国で行われた接続実験に参加するなどして(日本からのリモート参加でしたが⁠⁠、徐々に実装が磨かれていきました。国産AtomPubクライアントを開発されている丸本さんにもたくさんテストしていただきました。その結果、現在では多くのAtomPubクライアントと接続できることが確認されています。

現在、Catalyst::Controller::AtompubはCodeReposで開発が進められています。バグを見つけたり機能拡張してくださった方は、こちらに直接commitしていただいてもかまいません。

Catalystとは

Catalystは、MVC(Model-View-Controller)パターンを利用したPerlのWebアプリケーションフレームワークです。多くの雑誌やWebページに解説がありますので、詳細はそちらを参考にしてください。

AtomPubは、Webサービスのコントロールフロー(制御の流れ)を定義するプロトコルです。Catalyst::Controller::Atompubモジュールは、その名の通りCatalystのコントローラです。プログラマは、Catalyst::Controller::Atompubをサブクラス化してAtomPubサーバに必要な機能を実装します。

AtomPubとは

AtomPubは、将来のWebサービス標準技術として期待されているプロトコルです。RESTにもとづくシンプルなコンセプトでありながら、テキストに限らずあらゆるWebリソースを扱うことができるなど幅広い応用力を持ちます。

gihyo.jpでは朝倉さんと坂野さんが書かれた記事Web APIの次世代標準プロトコル「Atom Publishing Protocol」を読むことができます。プロトコルの詳細についてはこちらをご覧ください。

ここでは、AtomPubサーバを実装するために、最低限知っておかなければならないことを説明します。

AtomPubのリソース

AtomPubはWeb上のリソースを扱うプロトコルです。まずはリソースから説明します。AtomPubでは、エントリや画像ファイルなどを「メンバリソース」と呼び、それらの入れ物を「コレクションリソース」と呼びます。

AtomPubのリソース
AtomPub のリソース

メンバリソースがAtomエントリのときは「エントリリソース」と呼ばれ、それ以外のときは「メディアリソース」のように呼び分けられます。AtomPubサーバは複数のコレクション持つことができます。この連載で作成するブログサーバでは、テキストと写真を別々のコレクションに保持します。

今回はエントリリソースについて説明し、メディアリソースは次回に説明します。

リソースの操作

コレクションとメンバはそれぞれのURIを持ちます。そのURIに対してクライアントがHTTPリクエストを送ると、メンバの列挙(List)やCRUD(Create-Read-Update-Delete,作成・読み取り・更新・削除)が行われます。

コレクションのURIにHTTP GETリクエストが送られると、サーバはメンバが列挙されたAtomフィードを返します。コレクションのURIにメンバがPOSTされると、サーバはコレクションに追加します。さらに、サーバは新しいメンバのURIを決定し、HTTPレスポンスのLocationヘッダでクライアントに通知します。

コレクションからメンバ一覧を取得
コレクションからメンバ一覧を取得
コレクションにメンバを追加
コレクションにメンバを追加

メンバのURIにHTTP GETリクエストが送られると、サーバはメンバを返します。メンバがPUTされると、古いメンバを置き換えます。DELETEが送られると、そのメンバを削除します。このように、メンバのURIはメンバを編集(修正・削除)するために用いられることから、⁠Edit URI」と呼ばれることもあります。

メンバを取得
メンバを取得
メンバを更新分
メンバを更新
メンバを削除
メンバを削除

5つの操作を表にまとめます(表中のメソッド属性については後ほど説明します⁠⁠。RESTにもとづく美しいデザインになっているのがわかります。なお、RESTについてはりこらぼのricollab Web Tech Blog ≫ REST入門(1Webアプリケーションのアーキテクチャ⁠⁠』を参考にしてください。

AtomPubのリソースと操作
URI HTTPメソッド 操作 メソッド属性
コレクションGETメンバの列挙list
コレクションPOSTメンバの追加create
メンバ GET メンバの取得 read
メンバ PUT メンバの更新 update
メンバ DELETE メンバの削除 delete

このように、AtomPubを使うと、シンプルでわかりやすいAPIを提供することができます。このシンプルさが、Webサービスの標準技術として期待を集めている理由です。

Catalyst::Controller::Atompubの特徴

ここまで見てきたように、AtomPubのコンセプトはとてもシンプルです。2種類のリソースと5種類の操作があるだけです。

ところが、コンセプトは簡単であっても、実装レベルになると複雑な面が少なからず存在します。たとえば、メディアリソースのメタデータを表現する補助リソース(メディアリンクエントリ)や、コレクションのメタデータを表すXML文書(サービス文書)などが必要になります。また、XMLフォーマットのチェックやURIの生成、キャッシュ・バージョンチェックなどの処理も必要になります。

Catalyst::Controller::Atompubは、プログラマがこういった枝葉末節にとらわれることなく、5つの操作に集中できることを目指しています。

Catalystのインストール

AtomPubサーバを実装する前に、必要なモジュールをインストールしておきます。

% sudo cpan -i Catalyst::Runtime Catalyst::Plugin::ConfigLoader \
  Catalyst::Plugin::Static::Simple Catalyst::Model::DBIC::Schema \
  DBIx::Class::Schema::Loader DBD::SQLite Catalyst::View::TT \
  Catalyst::Controller::Atompub Catalyst::Action::RenderView

連載が進むにつれて他のモジュールを使うこともありますが、そのときは同じようにしてインストールしてください。

準備が終わったら、MyBlogというWebアプリケーション(のひな形)を作ります。

% catalyst.pl MyBlog
% cd MyBlog/

モデルとテーブルの作成

Catalyst::Controller::Atompubでは、好きな種類のモデルを使うことができます。ここでは、Catalyst::Model::DBICを利用します。DBICは、Perlでよく使われるO/Rマッパーです。

テーブル作成

まず、エントリを格納するテーブルを作成します。データベースにはSQLiteを使うことにします。データベース名をtest.dbとし、次のようなテーブルを作成します。

MyBlog % sqlite3 test.db
sqlite> CREATE TABLE entries (
   ...>   id     INTEGER PRIMARY KEY,
   ...>   edited INTEGER,
   ...>   uri    TEXT UNIQUE,
   ...>   xml    BLOB
   ...> );

idはエントリの通し番号です。editedはエントリの最終更新日時です。UNIX time(1970年からの秒数)に変換してから格納するため、整数型(INT)にしています。uriはエントリのURIです。xmlには、XMLを文字列として格納します。

モデルの作成

次に、モデルを実装します。ヘルパスクリプトを使ってひな形を作成します。

MyBlog % perl script/myblog_create.pl model DBIC DBIC::Schema MyBlog::Model::Schema dbi:SQLite:dbname=test.db

コマンドの引数は次の通りです。

モデル作成スクリプトの引数
引数説明
model作成対象(MVCのいずれか)
DBIC作成するクラス名(フルパスは MyBlog::Model::DBIC)
DBIC::Schemaスーパークラス名(フルパスはCatalyst::Model::DBIC::Schema)
MyBlog::Model::Schemaスキーマ情報を扱うクラス名
dbi:SQLite:dbname=test.dbデータベースへの接続情報

コマンドを実行すると、lib/MyBlog/Model/DBIC.pmが作成されます。

続いて、データベースのスキーマ情報(テーブルの構成)を扱うMyBlog::Model::Schemaモジュールを作成します。DBIx::Class::Schema::Loaderをサブクラス化すると、スキーマ情報をデータベースから自動的に取得できるので、そのようにしておきます。

lib/MyBlog/Model/Schema.pm
package MyBlog::Model::Schema;
use base qw(DBIx::Class::Schema::Loader);
1;

コレクションコントローラの作成

コレクションを実装するコントローラを作成し、リソース操作(List+CRUD)を実装します。

ヘルパスクリプトを使ってコントローラのひな形を作成します。

MyBlog % ./script/myblog_create.pl controller EntryCollection Atompub::Collection

コマンドの引数は次の通りです。

コントローラ作成スクリプトの引数
引数説明
controller作成対象(MVCのいずれか)
EntryCollection作成するクラス名(フルパスはMyBlog::Controller::Collection)
Atompub::Collection スーパークラス名(フルパスはCatalyst::Controller::Atompub::Collection)

コマンドを実行すると、lib/MyBlog/Controller/EntryCollection.pmが作成されます。ファイルにはリソース操作のスケルトンが記述されていますので、順に埋めていきます。

Catalyst::Controller::Atompubでは、リソースを操作するメソッドに"Atompub"という属性を付与し、属性の引数で操作の種類を表します。たとえば、リソースの列挙(List)を実装するメソッドには、次のように":Atompub(list)"という属性を付けます。なお、メソッド名はなんでもかまいません。

メソッド属性
sub get_feed :Atompub(list) {
    # メンバを列挙する...
}

それでは、5つの操作を実装します。数が多いですが、ひとつひとつは難しくありません。

メンバの列挙 (List)

メンバの列挙(lib/MyBlog/Controller/EntryCollection.pm)
sub get_feed :Atompub(list) {
    my($self, $c) = @_;

    # フィード (XML::Atom::Feed) のひな型
    my $feed = $self->collection_resource->body;

    # entriesテーブルからエントリを取得する (更新日時の新しい順)
    my $rs = $c->model('DBIC::Entries')
               ->search({}, { order_by => 'edited DESC' });

    # フィードのひな形にエントリを追加する
    while (my $entry_resource = $rs->next) {
        my $entry = XML::Atom::Entry->new(\$entry_resource->xml);
        $feed->add_entry($entry);
    }

    # 成功したらtrueを返す
    return 1;
}

このメソッドが呼ばれる前に、スーパークラス(Catalyst::Controller::Atompub::Collection)はフィードのひな形を作成します。ひな形にはidやlink,titleが設定します。

このメソッドでは、ひな形を受け取り、エントリを追加します。AtomPubの枝葉末節はスーパークラスが処理するので、プログラマはリソースの操作(この場合はエントリの追加)のみを実装します。

メンバの追加(Create)

メンバの追加 ⁠lib/MyBlog/Controller/EntryCollection.pm)
sub create_entry :Atompub(create) {
    my($self, $c) = @_;

    # スーパークラスが生成したURIと更新日時を取得する
    my $uri    = $self->entry_resource->uri;
    my $edited = $self->edited->epoch; # (UNIX time 形式に変換)

    # POSTされたエントリ本体を取得する (XML::Atom::Entry)
    my $entry = $self->entry_resource->body;

    # entriesテーブルにエントリとメタデータを格納する
    $c->model('DBIC::Entries')->update_or_create({
        edited => $edited,
        uri    => $uri,
        xml    => $entry->as_xml,
    });

    # 成功したらtrueを返す
    return 1;
}

このメソッドが呼ばれる前に、スーパークラスはエントリのURI(EditURI)を決定します。また、エントリにidやlink,app:edited(更新日時)などの要素を適切に追加します。このメソッドが呼ばれた後には、Locationヘッダを設定します。

このメソッドでは、エントリとURI、更新日時をデータベースに格納します。

ここで、上のコードで呼ばれているDBICのupdate_or_createメソッドについて補足します。createではなくこのメソッドが呼ばれているのを奇妙に感じる人もいると思います。確かに、createメソッドを呼ぶのが自然ですし、そのようにして問題ありません。update_or_createを呼んでいる理由は、第3回でエントリのURIを変更するときに説明します。

メンバの取得(Read)

メンバの取得 ⁠lib/MyBlog/Controller/EntryCollection.pm)
sub get_entry :Atompub(read) {
    my($self, $c) = @_;

    # リクエストされたURI
    my $uri = $c->req->uri;

    # entriesテーブルからエントリを検索する
    my $rs = $c->model('DBIC::Entries')->find({ uri => $uri });

    # エントリリソースをセットする
    my $entry = XML::Atom::Entry->new(\$rs->xml);
    $self->entry_resource->body($entry);

    # 成功したらtrueを返す
    return 1;
}

このメソッドでは、リクエストされたURIに対応するエントリを検索して、$self->entry_resource->bodyにセットします。

メンバの更新(Update)

メンバの更新(lib/MyBlog/Controller/EntryCollection.pm)
sub update_entry :Atompub(update) {
    my($self, $c) = @_;

    # リクエストされた URI
    my $uri = $c->req->uri;

    # スーパークラスが生成した更新日時を取得する
    my $edited = $self->edited->epoch; # (UNIX time 形式に変換)

    # PUTされたエントリ本体を取得する (XML::Atom::Entry)
    my $entry = $self->entry_resource->body;

    # entriesテーブルからエントリを検索し、更新する
    $c->model('DBIC::Entries')->search({ uri => $uri })->update({
        edited => $edited,
        xml    => $entry->as_xml,
    });

    # 成功したらtrueを返す
    return 1;
}

POSTの場合と同様に、スーパークラスがエントリに必要な要素を追加します。

このメソッドでは、リクエストされたURIに対応するレコードを検索し、エントリと更新日時を更新してください。

メンバの削除(Delete)

メンバの削除(lib/MyBlog/Controller/EntryCollection.pm)
sub delete_entry :Atompub(delete) {
    my($self, $c) = @_;

    # リクエストされたURI
    my $uri = $c->req->uri;

    # entriesテーブルからエントリを削除する
    $c->model('DBIC::Entries')->search({ uri => $uri })->delete;

    # 成功したらtrueを返す
    return 1;
}

このメソッドでは、リクエストされたURIに対応するレコードを検索し、削除します。

以上でコレクションの作成が終わりました。

コレクションコントローラに関する補足

ここでは、エントリを変更せずにそのままデータベースに格納しましたが、AtomPubの仕様ではサーバはエントリを自由に修正してよいとされています。必要に応じて修正してください。

リソースを操作するときに、HTTPレスポンスヘッダを設定してもかまいません。その場合、スーパークラスがそのヘッダを上書きすることはありません。

Catalystの起動

ここまでで、AtomPubサーバの基本部分は完成です。Catalystを起動します。

MyBlog % perl script/myblog_server.pl

ブラウザから http://localhost:3000/entrycollection にアクセスしてください(Catalystはデフォルトで3000番ポートを使います⁠⁠。次のような空のAtomフィードを取得できると思います。

http://localhost:3000/entrycollection
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>EntryCollection</title>
  <updated>2008-01-13T18:22:44+09:00</updated>
  <id>http://localhost:3000/entrycollection</id>
  <link rel="self" href="http://localhost:3000/entrycollection"/>
</feed>

今回は、Catalyst::Controller::Atompubの基本的な使い方を説明しました。次回は、画像を扱うコレクションを作成し、写真付きブログサーバとして完成させます。

おすすめ記事

記事・ニュース一覧