2009-08-18

モダンなXML出力方法について

はてなブックマーク   livedoor clip

XHTML2は残念なことになってしまいましたが、HTML5にしろフィードにしろ、XML構文が重要なことにはかわりのない昨今みなさんいかがお過しですか。ずいぶんとエントリの間隔が空いてしまって申し訳ありません。

さて、XMLの出力には

  • to_xml()のようなメソッドを呼ぶ
  • RubyのBuilderのようなDSLを使う
  • テンプレートエンジンを使う
  • 手書き

などの方法が考えられますが、今回の本題はこれらの方法によって生成される最終的なXML文書(インスタンス)はどう表現されるべきか、についてです。

エンコーディングはUTF-8で

これはもう説明不要ですね。UTF-8が処理できない環境というのは本当に少くなりました。モバイルや組込み等でどうしても対応できない場合以外はUTF-8を使うのが望ましいと思います。

実体参照の件

実体参照というのはXML内で文書の一部に名前を付けて再利用する仕組です。たとえば「&」という文字(文字はXML文書を構成する最小単位です)を出力するときは、XML文書に直接記述することができないので「&amp;」とエスケープします。XML仕様では&や<など5種類の実体参照がビルトインで定義されていますが(これを定義済み実体と呼びます)、DTDを使うことで自分で好きな実体参照を定義することもできます。たとえばハートマーク(♥)を出すために「&hearts;」と書くというのはよく知られているでしょう。この「&hearts;」という実体参照はHTMLのDTDで定義されています。

HTMLのDTDで定義されている実体参照を使うためには、DOCTYPE宣言(「<!DOCTYPE」で始まるものです)でHTML/XHTMLのDTDを読み込まなければなりません。HTMLで定義されている実体参照は個別のファイルで定義されているので()、特にHTMLでない文書型を使う場合でもこれを個別に読み込めばHTMLで定義されている実体参照を使うこともできます。

しかし、モダンなXML文書ではDOCTYPE宣言を付けないものもあります。たとえばAtom仕様にはDTDがありません。仕様にDTDがないとしても、DOCTYPE宣言を付けることは可能ですが、通常のライブラリはAtomフィードを出力するときにDOCTYPE宣言を付けないのが一般的です。

このような場合にハート記号を使いたくなったらどうすればいいのでしょうか。まずはその文字を直接入力できないか考えてみましょう。ハート記号を表示するのにわざわざ実体参照を使う必然性はないのです。エンコーディングがUTF-8であれば、その文字をそのまま出力すればよいでしょう。

もし何らかの理由でどうしても直接入力したくない場合は、実体参照を使うのではなく数値文字参照を使うのがよいでしょう。たとえばハート記号は&#9829;です。

xml:baseは使わずに絶対URIを利用する

xml:base属性を使うと相対URIを解決するための基底URIを設定できるのですが、
これをきちんと解釈してくれないクライアントやライブラリがいるのも事実です。
なので、ハイパーリンクとしてURIを埋め込むときには、たとえ同じサイト上
のリンクでも絶対URI(「http://」で始まるURI)を使うのがよいでしょう。

2009-02-09

はてなダイアリーAtomPubレビュー: その3 設計編

はてなブックマーク   livedoor clip

さて、このシリーズも今回で最後です。遅れに遅れて申し訳ありません…ちなみに昨年の12/22付けで仕様書が改訂され、私が指摘したところが直っていました。どうもありがとうございます>中の人

最後は設計編です。

今回、私がはてなダイアリーAtomPubの仕様を見ていて、設計面で疑問に思ったのは次の2点です。

  • X-HATENA-PUBLISH HTTP ヘッダ
  • エントリ文書の対称性

以下ではそれぞれについて具体的に検討してみます。

X-HATENA-PUBLISHヘッダ

いくつかのブログでも指摘されていますが、はてなダイアリーAtomPubではapp:draft 要素を利用しません。

はてなダイアリーAtomPub仕様書によると、以下のような理由があるそうです。

はてなダイアリーAtomPubでは、AtomPubで規定されているapp:draft要素を使用しません。はてなダイアリーでは日記エントリと下書きエントリを別々に管理しています。そのため、日記エントリと下書きエントリを一つのコレクションとして扱い、app:draft要素によって下書きかどうかを区別するような構成は不自然になります。むしろ、下書きエントリを日記エントリとして公開するという操作を行う仕組みであるほうが自然であると考え、以下のような方法をとっています。

なるほど、コレクションを分ける際の実装上の都合なわけですね。

これは、これで一つの設計判断だと考えます。コレクション間のエントリのコピーや移動は AtomPub では定義されていませんので、何らかの拡張仕様が必要になります。WebDAV の MOVE を使うなど、マニアックな方法も考えられますが、独自ヘッダは悪くない判断だと思います。

ただし2点、よくない点があります。

1点目は、X-HATENA-PUBLISH の値の仕様です。このヘッダが取りうる値は「1」なのですが、いったいこれは何を意味するのでしょうか。おそらく内部的に Perl の “1″ としてそのまま実装されているのでしょうが、これはたとえば “true” にするなど、もう少し可読性を考慮した方がよいでしょう。

また、”1″ 以外の値を値を入れたときの挙動も不明です。たとえば “111″, “01″, “hoge” などを入れたらどうなるのでしょうか。

2点目は、PUT メソッドの挙動です。

はてなダイアリーAtomPubの仕様では、下書きを公開するときは既存の下書きエントリに対して X-HATENA-PUBLISH を 1 にした PUT を送ります。このリクエストが成功すると、レスポンスとして 201 Created が返り、Location ヘッダに公開されたブログコレクションのエントリが入ります。これは少し違和感を感じるところです。

HTTP/1.1 における PUT の仕様は以下のとおりです。

PUT メソッドは、同封されたエンティティを供給される Request-URI の元に保存するように要求する。 Request-URI が既に存在するリソースを参照している場合は、同封されるエンティティはオリジンサーバにあるそれの修正版とみなされるべきである。 Request-URI が既存のリソースを指していない場合に、その URI がリクエストしているユーザエージェントによって新しいリソースとして定義する事ができる時は、オリジンサーバはその URI にリソースを作成できる。新しいリソースが作成された場合、オリジンサーバは 201 (Created) レスポンスをもってユーザエージェントに知らせなければならない。既存のリソースが更新された場合は、リクエストが成功し終了した事を示すために 200 (OK) か 204 (No Content) のいずれかのレスポンスコードを送るべきである。

つまり PUT のレスポンスは

  • その URI が存在しない場合はリソースを新規作成して 201 を返す
  • その URI が存在する場合は、リソースを変更して 200 か 204 を返す

となります。これは SHALL (べきである)なので、絶対に従わなければならないことはありませんが、いずれにせよ既存の URI に対しての PUT で 201 が返るというのは、仕様上のグレーゾーンでクライアントを混乱させる可能性があります。

私だったらどうするか

もし私がこの機能を設計するなら以下のように考えます。

下書き機能は AtomPub の app:draft 要素に非常に近い(というかほぼ同じ)機能であるため、いかにこれを活かすかを考えます。今回の問題はドラフト状態を公開すると URI が変更になってしまうところなのですが、これをいかに違和感なくクライアントに教えるか、が課題です。

PUT すると URI が変更される(リソースが移動するイメージ)というのがクライアントからの一番の違和感だと思うので、そこをどうにかするようにしたいですね。そのためには、ドラフトの URI を残しつつ、そのリソースに別の URI (エイリアス)を付与するのが良いように思います。

これには HTTP に標準で付いてくる Content-Location ヘッダを使うことで解決できそうです。この方式を採用したときのリクエスト・レスポンスは以下のようになります。

PUT /yohei/atom/draft/12345678 HTTP/1.1
Host: d.hatena.ne.jp

<entry xmlns="http://www.w3.org/2005/Atom"
    xmlns:app="http://www.w3.org/2007/app">
  <title>テスト</title>
  <app:control>
    <app:draft>yes</app:draft>
  </app:control>
  ...
HTTP/1.1 200 OK
Content-Location: http://d.hatena.ne.jp/yohei/20081224/abcdefg

/draft 以下の URI も依然として有効ですが(おそらく作者だけがアクセスできるリソースになるでしょう)、ダイアリーの方のリソースも別名で有効になります。

もちろん、サーバ側の実装の都合で、ドラフトエントリを公開するとドラフトコレクションからは必ず消さないといけないのかもしれません。上記のような理想形で設計したとおりに実装できるわけではないですが、一つのヒントとして提案してみます。

エントリ文書の対称性

2点目はエントリ文書の対称性についてです。
現在の仕様では、ブログコレクションで日記エントリを更新するとき、以下の
ようなリクエストを送ります。

PUT /yohei/atom/blog/20090203/xxx HTTP/1.1
Host: d.hatena.ne.jp
Content-Type: application/atom+xml; type=entry; charset=utf-8

<entry xmlns="http://www.w3.org/2005/Atom">
  <updated>2009-02-03T00:00:00Z</updated>
  <id>tag:d.hatena.ne.jp,2008:diary-yohei-20081226-p1
  <author><name>yohei</name></author>
  <title>test</title>
  <content type="text">this is a test</content>
</entry>

これに対して、以下のようなレスポンスが返ってきます。

HTTP/1.0 200 OK
Date: Wed, 04 Feb 2009 04:13:59 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 694
Content-Type: application/atom+xml;type=entry; charset=utf-8

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
  <updated>2008-12-26T00:00:00+09:00</updated>
  <published>2008-12-26T00:00:00+09:00</published>
  <app:edited xmlns:app="http://www.w3.org/2007/app">2008-12-26T00:00:00+09:00</app:edited>
  <id>tag:d.hatena.ne.jp,2008:diary-yohei-20081226-p1</id>
  <link rel="edit" href="http://d.hatena.ne.jp/yohei/atom/blog/20081226/p1"/>
  <link rel="alternate" type="text/html" href="http://d.hatena.ne.jp/yohei/20081226/p1"/>
  <author>
    <name>yohei</name>
  </author>
  <title>test</title>
  <content type="html">
                &lt;div class="section"&gt;
                        &lt;p&gt;this is a test&lt;/p&gt;
                &lt;/div&gt;
</content>
</entry>

このリソースを再び GET してみると以下のようになります。

GET http://d.hatena.ne.jp/yohei/atom/blog/20081226/p1 HTTP/1.1
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Wed, 04 Feb 2009 05:13:52 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 793
Content-Type: application/atom+xml;type=entry; charset=utf-8

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
  <updated>2008-12-26T00:00:00+09:00</updated>
  <published>2008-12-26T00:00:00+09:00</published>
  <app:edited xmlns:app="http://www.w3.org/2007/app">2008-12-26T00:00:00+09:00</app:edited>
  <id>tag:d.hatena.ne.jp,2008:diary-yohei-20081226-p1
  <link rel="edit" href="http://d.hatena.ne.jp/yohei/atom/blog/20081226/p1"/>
  <link rel="alternate" type="text/html" href="http://d.hatena.ne.jp/yohei/20081226/p1"/>
  <author>
    <name>yohei</name>
  </author>
  <title>test</title>
  <content type="html">
                &lt;div class="section"&gt;
                        &lt;p&gt;this is a test&lt;/p&gt;
                &lt;/div&gt;
</content>
  <hatena:syntax xmlns:hatena="http://www.hatena.ne.jp/info/xmlns#">this is a test</hatena:syntax>
</entry>

ブログコレクションのエントリを編集するときは、以下のことに気をつける必要があります。

  • 追加・更新を行うときは /entry/content/@type は “text” ではてな記法を入れる
  • サーバから取得できる /entry/content は @type=”html” ではてな記法がHTML にレンダリングされたものが入り、/entry/hatena:syntax にはてな記法が入る

これはクライアント側ではてな記法を入れる要素について注意しながら、はてな独自のロジックを実装しなければならない点で、あまり良い設計ではないと言えるでしょう。

この問題を解決する方法はいくつか考えられますが、完璧なソリューションは存在しないと思います。私が即興で考えたソリューションのそれぞれのメリット・デメリットを挙げておきます。

/entry/content に常にはてな記法を入れる

Pros
既存の AtomPub クライアントをほぼそのまま利用可能
Cons
既存のブログクライアントのユーザははてな記法を使うことを理解しなければならない
フィードをフィードリーダーで見たときにはてな記法が出てしまう

/entry/hatena:syntax を常に使うようにする

Pros
content を HTML にできる
Cons
AtomPub クライアントが hatena:syntax を理解しなければ更新ができない

まとめ

先日、会社の超先輩エンジニア(ソフトだけじゃなくてエレキよりの設計も含めて全般されてきた方です)の方と話をしたのですが、そのときに「設計はバランスだよ」という言葉をもらいました。

今回の件でもわかるとおり、設計というのはあちらを立てればこちらが立たず、ということが多く、全てを満足させることは不可能です。

私はよく「REST 的に理想的な設計にするにはどうしたらいいんでしょう」というような質問を受けるのですが、正直なところ理想ばかり追いかけても仕方がないだろう、と思います。REST はあくまでも設計のためのヒントなのであり、それ自体が目的になってしまったら本末転倒です。

高い理想をバックグラウンドに、現実的でバランスの取れた設計をするのが優れたエンジニアなのではないでしょうか。

2008-10-27

はてなダイアリーAtomPubレビュー: その2 ドキュメンテーション編

はてなブックマーク   livedoor clip

その1を公開してからだいぶ時間が経ってしまいましたが、その2を公開します。今回はドキュメンテーション編です。

レビュー対象ははてなダイアリーAtomPubの仕様書です。普段私が業務で仕様書をレビューするときに気をつけている観点をご紹介しながら、いくつか指摘をしてみます。

なお、レビューの指摘はどうしても細かくなってしまうので、粗探し的と感じられてしまうかもしれませんが、技術的な完成度を上げるためには必要な作業なのでご容赦ください。

基本的な文章チェック

まず、仕様書に記述されている文章の「てにをは」や文法レベルでのチェックをします。表記ゆれなども含まれます。

今回の仕様書は、たとえば以下のような記述がありました。

  • 「ヘッダ」と「ヘッダー」、「XML文書」と「XML文章」、「はてなID」と「はてなアカウントのid」などの表記ゆれ。document に対する訳語は通常は「文書」を用います
  • 「~することができます」という冗長表現。私もよくやってしまうのですが、これは「~できる」にとした方がシンプルで読みやすくなります
  • 「GETをリクエストする」と「リクエストXML文章をPOSTする」。これも表記ゆれの一種ですが、私だったら「GETする」「XML文書をPOSTする」にします
  • 単純なコピペミスっぽいですが、下書きコレクションの段落で、「XXXXXXXXXXは実際には」で切れてしまっている文章がありました

こういった基本的な文章のバグは、ワードの文書校正ツールなどを使うと簡単に検出できるものも多いので、機械的に処理するのがおすすめです。

必須項目のチェック

次に、仕様書として必要な項目が足りているかを確認します。

基本設計仕様書やテスト仕様書など、フォーマットがある程度決っているものであれば、それに沿っているかをチェックします。今回の場合は Web API の仕様書なので、これが標準、というフォーマットはないと思いますが、以下の項目が足りないかなと思いました。

目次
仕様書は全体が長いので、さっと目を通せるように目次を入れる方がよいでしょう
エラー仕様
Web API の場合、エラー仕様というのはなかなか書きづらいのですが、それ
でも不正な操作に対して、API がどのように反応するかを一言記述しておく方
がよいと思います
セキュリティに関する考察
RFC などでは必須になっています

変更点、拡張点のチェック

AtomPub のように、ベースとなる仕様書があり、それに基づいた形でAPIを設計する場合は、オリジナルとの変更点や拡張点を明記する方がよいでしょう。逆に言うとオリジナルの方で詳しく記述されている部分は、概要だけを書いて詳細はオリジナルを参照してもらう方がよいと思います。

今回のAPI仕様でいえば、変更点は X-HATENA-PUBLISH ヘッダ、拡張点は hatena:syntax 要素になります。

オリジナルの仕様を変更した場合は、なぜそのような設計になったのか、その理由を書いておく方が親切です。特にある程度標準に従っている場合、利用者はオリジナルの方法に慣れているため、変更点はつまづきポイントになるからです。

この理由とはたとえば、レガシーシステムの実装上の都合であるとか、開発効率の問題であるとか、はたまたオリジナル仕様が間違っている、などが考えられます。残念ながら、現状のはてなダイアリーAtomPubの仕様では、なぜ AtomPub 標準の app:draft 要素ではなく X-HATENA-PUBLISH が必要だったのかの記述はありませんでした。

拡張点については、はてなダイアリーAtomPubの仕様では hatena:syntax 要素がはてな記法を入れるために導入されたことが明記されているので問題ありませんね。

技術的なチェック

最後に、技術的な正確性を検証します。

今回、私が気づいたものとしては以下の点があります。

  • content/@type が text/html なのに、要素内容のタグがエスケープされていません(実装は正しくされています)
  • パラメータ付きURIは URI Templates の記法を利用した方がよいでしょう(/はてなID/atom/draft/XXXXXXXXXX → /{はてなID}/atom/draft/{entry-id})
  • HTTP/1.1 201 CREATED の Reason-Phrase は Created が一般的です(CREATEDでもHTTP仕様としては間違いではありません)

まとめ

技術文書にはプログラムだけでは伝えきれない設計の意図を伝えるという目的があります。プログラムもそうですが、文書もひとりで書くだけではなくレビューすることが大切です。人に読んでもらうことで初めてわかることも多々あるので、ソースコードレビューと同じくらいドキュメントのレビューにも力を入れるべきです。

また、文章の書き方自体の勉強も大切です。私も偉そうなことは言えないのですが、学生のころにかずさんの正確な文章の書き方を読んだり、檜山さん(プロのテクニカルライター)のライティングスキルを目の当たりにしたりして(参考)、技術者のライティングスキルの重要性を認識しました。

ということで、このエントリでライティングスキルもプログラミングスキルと同じくらい重要である、と認識してくれる人が増えることを願っています。

ちなみにこのレビューシリース、まだ続きます。

2008-09-25

はてなダイアリー AtomPub レビュー: その1 実装編

はてなブックマーク   livedoor clip

はてなさんがダイアリーAtomPubインターフェースをリリースしていました。

私は AtomPub が大好きなので、少しだけ試してみました。簡単にレビューを書こうと思ってエントリを起したのですが、意外と長くなりそうなので3部構成でお送りします。まずは実装編です。

認証

とりあえず普通のGETリクエストをサービス文書に送ってみます。http://d.hatena.ne.jp/{hatena-id}/atom がサービス文書の URI です。

GET http://d.hatena.ne.jp/yohei/atom HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 401 Unauthorized
Date: Fri, 19 Sep 2008 07:00:10 GMT
Server: Apache/2.2.3 (CentOS)
WWW-Authenticate: WSSE profile="UsernameToken"
Content-Length: 16
Content-Type: text/plain;charset=utf-8
Set-Cookie: b=xxx; path=/; expires=Thu, 14-Sep-28 05:05:06 GMT; domain=.hatena.ne.jp
Vary: Accept-Encoding

401 Unauthorized

401 が返ってきました。WWW-Authenticate ヘッダによれば、WSSE 認証が必要なようです。

レスポンスヘッダで一つ気になるのが、なぜか Set-Cookie ヘッダがあることです。Cookie 認証のなごりでしょうか…。セキュリティの観点からもこのヘッダはなくした方がよさそうです。ちなみに expires の年が2桁で 28 なのもちょっと困りますね。UIの方は普通の Cookie を返すので、何かのバグなんでしょうか。以下の例でも全て Set-Cookie ヘッダが返ってくるのですが、省略しています。

サービス文書

ということで、X-WSSE ヘッダを加えてサービス文書を GET してみます。以下、全てのリクエストで認証が必要ですが、長いので、次以降の例ではこのヘッダは省略します。

GET http://d.hatena.ne.jp/yohei/atom HTTP/1.1
Accept: */*
X-Wsse: UsernameToken Username="yohei", PasswordDigest="foo", Nonce="bar", Created="2008-09-19T22:53:55+09:00"
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 05:05:06 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 676
Content-Type: application/atomsvc+xml;charset=utf-8
Vary: Accept-Encoding

<?xml version="1.0" encoding="utf-8"?>
<service xmlns="http://www.w3.org/2007/app">
  <workspace>
    <atom:title xmlns:atom="http://www.w3.org/2005/Atom">Hatena::Diary - yohei</atom:title>
    <collection href="http://d.hatena.ne.jp/yohei/atom/draft">
      <atom:title xmlns:atom="http://www.w3.org/2005/Atom">yoheiさんの下書き</atom:title>
      <accept>application/atom+xml;type=entry</accept>
    </collection>
    <collection href="http://d.hatena.ne.jp/yohei/atom/blog">
      <atom:title xmlns:atom="http://www.w3.org/2005/Atom">yoheiさんの日記</atom:title>
      <accept>application/atom+xml;type=entry</accept>
    </collection>
  </workspace>
</service>

サービス文書が取れました。はてなダイアリーAtomPub では二つのコレクションリソースが用意されているのがわかります。draftコレクション(http://d.hatena.ne.jp/{hatena-id}/atom/draft)とblogコレクション(http://d.hatena.ne.jp/{hatena-id}/atom/blog)です。両方とも、Atom エントリ文書が POST できることがわかります。

ちなみに HEAD してみると…

HEAD http://d.hatena.ne.jp/yohei/atom HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 05:07:26 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 676
Content-Type: application/atomsvc+xml;charset=utf-8
Vary: Accept-Encoding

nil

HEAD リクエストへのレスポンスなのに、ボディに “nil” とあるのが残念です。

コレクションリソース

次にコレクションリソースです。まずは GET してみました。

GET http://d.hatena.ne.jp/yohei/atom/blog HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 05:34:08 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 20722
Content-Type: application/atom+xml;charset=type=feed
Vary: Accept-Encoding

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <updated>2006-11-24T00:00:00+09:00</updated>
  <id>tag:d.hatena.ne.jp,2006:diary-yohei</id>
  <title>傭兵日記</title>
  <author>
    <name>yohei</name>
  </author>
  <link rel="self" href="http://d.hatena.ne.jp/yohei/atom/blog"/>
  <link rel="next" href="http://d.hatena.ne.jp/yohei/atom/blog?page=2"/>
  <entry>
  ...

プレーンな Atom フィードが返ってきました。rel=”next” なリンクもありますね。これは便利です。

ということで、2ページ目を GET してみました。

GET http://d.hatena.ne.jp/yohei/atom/blog?page=2 HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 200 OK
Date: Fri, 19 Sep 2008 07:06:51 GMT
Server: Apache/2.2.3 (CentOS)
Content-Length: 24964
Content-Type: application/atom+xml;charset=type=feed
Vary: Accept-Encoding
X-Cache: MISS from unknown

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <updated>2006-11-24T00:00:00+09:00</updated>
  <id>tag:d.hatena.ne.jp,2006:diary-yohei</id>
  <title>傭兵日記</title>
  <author>
    <name>yohei</name>
  </author>
  <link rel="self" href="http://d.hatena.ne.jp/yohei/atom/blog?page=2"/>
  <link rel="next" href="http://d.hatena.ne.jp/yohei/atom/blog?page=3"/>
  <entry>
...

2ページ目のリソースからは3ページ目にリンクしてますね。ここで惜しいのは、rel=”prev” なリンクがないことです。リンクが片道通行になっちゃってます。

もうひとつ気になったのはフィードの id です。tag スキームを使っているのですが、これに含まれる年がフィードに含まれる最新エントリの年になるようなのです。MT 3.x で同様の問題がありましたが、idは一定の値でないとまずいので、たとえば2008をハードコーディングした方がよいのではないかなと思います。

それから、Content-Type が

application/atom+xml;charset=type=feed

なんですが、これは正しくは

application/atom+xml;charset=utf-8;type=feed

ですね。

ちなみに、コレクションでも HEAD リクエストを送ってみました。

HEAD http://d.hatena.ne.jp/yohei/atom/draft HTTP/1.1
Accept: */*
Host: d.hatena.ne.jp
HTTP/1.0 405 Method Not Allowed
Date: Fri, 19 Sep 2008 05:16:12 GMT
Server: Apache/2.2.3 (CentOS)
Allow: GET, POST
Content-Length: 22
Content-Type: application/atom+xml;charset=type=entry
Vary: Accept-Encoding
X-Cache: MISS from unknown

nil

おおー、405 が返ってきました。しかもちゃんと Allow ヘッダもあります。ただ、Content-Type のパラメータが charset=type=entry なのはおかしいですね。entry ではないですし、charset=utf-8;type=feed であるべきです。

エントリのPOST

ではエントリを POST してみましょう。まずは draft コレクションに POST してみました。

POST http://d.hatena.ne.jp/yohei/atom/draft HTTP/1.1
Accept: */*
Content-Type: application/atom+xml;type=entry;charset=utf-8
Content-Length: 131
Host: d.hatena.ne.jp

<entry xmlns="http://www.w3.org/2005/Atom">
<title>test</title>
<content type="text">- test
-てすと</content>
</entry>
HTTP/1.0 201 Created
Date: Fri, 19 Sep 2008 07:45:24 GMT
Server: Apache/2.2.3 (CentOS)
Location: http://d.hatena.ne.jp/yohei/atom/draft/1221810325
Content-Length: 549
Content-Type: application/atom+xml;charset=type=entry
Vary: Accept-Encoding

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
  <id>tag:d.hatena.ne.jp,2008:diary-draft-yohei-1221810325</id>
  <link rel="edit" href="http://d.hatena.ne.jp/yohei/atom/draft/1221810325"/>
  <author>
    <name>yohei</name>
  </author>
  <title>test</title>
  <updated>2008-09-19T16:45:25+09:00</updated>
  <published>2008-09-19T16:45:25+09:00</published>
  <app:edited xmlns:app="http://www.w3.org/2007/app">2008-09-19T16:45:25+09:00</app:edited>
  <content type="text/plain">- test
-てすと</content>
</entry>

無事エントリが作成されます。ちなみに、content 要素の内容は draft コレクションでははてな記法の文字列が text/plain 形式で入り、blog コレクションではhtmlに変換済みの文字列が text/html で入ります。そして、blog コレクションでのはてな記法の文字列は hatena:syntax 要素に入ってくるそうです。これは最初わからず、ちょっとはまりました…

ただ、ちょっと気になるのは content 要素の type 属性の値です。RFC 4287 では text/html/xhtml or MIME タイプを取ることになっており、ここでは text/plain ではなく text が、そして text/html ではなく html が望ましいですね。

その他の操作

とりあえず仕様書どおりに実装すれば PUT/DELETE はできました。blog コレクションでも PUT するときは content 要素にはてな記法を入れるというのにははまりましたが…。これについては設計編で詳しく触れたいと思います。

それから、ちょっとした不正 URI のテスト(全てGET)をしてみましたが、以下のような結果でした。

  • http://d.hatena.ne.jp/{hatena-id}/ato -> 302
  • http://d.hatena.ne.jp/{hatena-id}/atom/blo -> 404
  • http://d.hatena.ne.jp/{hatena-id}/atom/blog/20 -> 404
  • http://d.hatena.ne.jp/{hatena-id}/atom/blog/{yyymmdd}/foo -> 403

これらは全て 404 が望ましいと思います。

また、全体的に Content-Type が正しく設定されていない場面がみうけられます。

たとえば DELETE のレスポンスが空なのに text/html;charset=utf-8 だったり、400/403 が返るときにプレーンテキストなのにapplication/atom+xml;type=entry が返ってきたりなどです。

この辺はテストケースの不足だと思いますが、AtomPub には Tim Bray が作った Ape や Joe Gregorio の Atom Publishing Protocol Test Suite など、テストケースが用意されているので、これらを使ってテストするといいと思います。

2008-08-27

ペルソナ/シナリオ法セミナーに参加してきました

はてなブックマーク   livedoor clip

昔、とあるデータベース研究の偉い先生に、研究は上位レベルレイヤーか下位レベルレイヤーかどちらかに偏っている方が面白い、というような話を聞きました。とくにそれを意識していたわけではないのですが、最近の自分の興味がその言葉のとおり、一番上と一番下の両極端になってきているなと感じている山本です。

上位レベルレイヤーの方では情報アーキテクチャや情報デザインがとても気になっています。いわゆる REST 的な文脈のリソースモデリングに関ってくると考えているからです。そんな折に、社内のとあるUI系研究者の同僚から「ペルソナ/シナリオ法による商品・サービス開発」というセミナーがあるよ、と教えてもらいました。

プログラムを見ると、いつもブログを拝読している(でも情報量が多くてなかなか読みきれていない)千葉工業大学の山崎先生DESIGN IT! w/LOVE の棚橋さんのご講演ではないですか。さっそく嬉々として申し込みを行いました。

当日は中野坂上の会場いっぱいに100名くらいの参加者があり、たいへん盛況なセミナーでした。
以下では、汚い字で書いた下手なマインドマップとともにレポートしてみます。

まず、山崎先生から「ここちよい体験のためのデザイン ペルソナ・シナリオ法とは」と題して講演がありました。

高度成長期が典型的ですが、これまではモノが不足していたためモノそのものに価値があり、必要な機能を満たすモノさえ提供すれば買ってもらえる時代でした。しかし、今はモノがあるのは当たり前の時代です。モノがあることを前提としてどのような商品やサービスを提供していくか、が重要になっているという背景があり、デザインがモノ中心(色やかたち)から人間中心(お客様の満足)に移ってきたそうです。これは自分の時代感覚とマッチしてナルホドと思いました。

棚橋さんの話(ペルソナ/シナリオの作成のための具体的な実践テクニック)は、ペルソナを作っていく過程のデザイン作業を具体的に、そして実践的に解説するものでした。印象的だったのは、ペルソナをグループワークの共同作業で作ることで、ドキュメント配布によるメンバー間の認識のずれや、大量な情報を読み込むオーバーヘッドを回避することができる、という内容でした。

セミナー全体を通してよく理解できたのは、ペルソナは単なるデザイン手法やマーケティング手法なのではなく、製品やサービスを企画・設計・開発・販売するプロジェクトチームの中でのコミュニケーションツールなのだ、ということです。たとえば Web サイトの色合いを決めるときに、普通の議論だと「自分が赤が好きだから」とか「コーポレートカラーが青だから」、「競合サービスが赤だからうちは青」などという議論をしがちですが、「メンバー全員で作ったペルソナは何色を求めているのか」を議論するとプロジェクトチーム内で合意も得やすいし結果も妥当なものになります。

本当は大和ハウスの山口さんの事例紹介もあったのですが(これも面白かった)、長くなりそうなのでこれくらいで…

このような有益なセミナーを無料で開催してくださるイードさんは太っ腹ですね。参加して本当によかったです。ありがとうございました。

2008-06-19

CouchDB について Erlang 分散システム勉強会で紹介してきました

はてなブックマーク   livedoor clip

先日、Erlang分散システム勉強会で、最近私が追っている CouchDB というオープンソースのドキュメント指向分散データベースについて紹介してきました。発表資料をおいておきます。

ドキュメント指向データベース CouchDB(PDF)

分散システムでしかも Erlang というニッチっぽい勉強会でしたが、30人の参加者が一瞬で集まり、非常に熱い会でした。私も自分の好きな話を好きなように話してしまい、参加者のみなさんはドン引きだったかもしれません…

これだけでは何なので、ついでに手元の Fedora7 on coLinux に CouchDB の trunk を入れたときの手順と、簡単な couchdb の使い方をご紹介しておきます。

基本的にはオフィシャルwikiにあるとおり、yum でモジュールを入れていけば問題ありません。素の coLinux からの場合、以下のモジュールが必要でした。

sudo yum install automake gcc libtool erlang \
  help2man icu libicu-devel js js-devel

次に svn から最新の trunk を取ってきます。

svn co http://svn.apache.org/repos/asf/incubator/couchdb/trunk couchdb

普通にコンパイルします。最初に ./bootstrap が必要です。

cd couchdb; ./bootstrap; ./configure; make

ちゃんとコンパイルできたら、あとはインストールするだけです。

sudo make install

オフィシャルの説明では couchdb ユーザを作って /usr/local/etc/couchdb/couch.ini を編集するようになっていますが、いろいろいじるときは自分のアカウントで動かした方が楽です。

cd ~mkdir couchdbcd couchdb
mkdir databases
cp /usr/local/etc/couchdb/couch.ini .
vi couch.ini

/usr/local/etc/couchdb/couch.ini が雛形なので、それをコピーし、

  • データベースファイル(.couch)を格納するディレクトリ(DbRootDir)
  • 他のマシンからアクセスするためのIPアドレス(BindAddress)
  • ログファイルの位置(LogFile)
  • ログレベル(LogLevel)

を書きかえます。ログレベルは debug にしておく方が、いろいろ情報が出てわかりやすくなります。

; etc/couchdb/couch.ini.tpl.  Generated from couch.ini.tpl.in by configure.

[Couch]

ConsoleStartupMsg=Apache CouchDB is starting.

DbRootDir=/home/yohei/couchdb/databases

Port=5984

BindAddress=192.168.0.2

DocumentRoot=/usr/local/share/couchdb/www

LogFile=/home/yohei/couchdb/couch.log

UtilDriverDir=/usr/local/lib/couchdb/erlang/lib/couch-0.8.0-incubating/priv/lib

LogLevel=debug

[Couch Query Servers]

javascript=/usr/local/bin/couchjs /usr/local/share/couchdb/server/main.js

あとは -c オプションで設定ファイルを指定して couchdb を起動するだけです。

% /usr/local/bin/couchdb -c couch.ini
Apache CouchDB 0.8.0-incubating (LogLevel=debug)
Apache CouchDB is starting.

Apache CouchDB has started. Time to relax.
[debug] [<0.1.0>] Config Info /home/yohei/couchdb/couch.ini:
        CurrentWorkingDir=/home/yohei/couchdb
        DbRootDir=/home/yohei/couchdb/databases
        BindAddress="192.168.0.2"
        Port="5984"
        DocumentRoot=/usr/local/share/couchdb/www
        LogFile=/home/yohei/couchdb/couch.log
        UtilDriverDir=/usr/local/lib/couchdb/erlang/lib/couch-0.8.0-incubating/priv/lib
        DbUpdateNotificationProcesses=
        FullTextSearchQueryServer=
        javascript=/usr/local/bin/couchjs /usr/local/share/couchdb/server/main.js

このようなログが流れれば起動は成功です。

curl でアクセスしてみましょう(curl が入っていない場合は yum で入れてください)。

colinux% curl -v http://192.168.0.2:5984
* About to connect() to 192.168.0.2 port 5984 (#0)
* Trying 192.168.0.2... connected
* Connected to 192.168.0.2 (192.168.0.2) port 5984 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.16.4 (i386-redhat-linux-gnu) libcurl/7.16.4 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.8
> Host: 192.168.0.2:5984
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: MochiWeb/1.0 (Any of you quaids got a smint?)
< Date: Thu, 19 Jun 2008 05:42:35 GMT
< Content-Type: text/plain;charset=utf-8
< Content-Length: 50
<
* Connection #0 to host 192.168.0.2 left intact
* Closing connection #0
{"couchdb":"Welcome","version":"0.8.0-incubating"}

ルートにアクセスすると Welcome メッセージとバージョン番号が入った JSON ドキュメントが取得できます。

データベースは PUT で作成できるので、PUT /test というリクエストを送ってみます。

colinux% curl -v -X PUT http://192.168.0.2:5984/test
* About to connect() to 192.168.0.2 port 5984 (#0)
* Trying 192.168.0.2... connected
* Connected to 192.168.0.2 (192.168.0.2) port 5984 (#0)
> PUT /test HTTP/1.1
> User-Agent: curl/7.16.4 (i386-redhat-linux-gnu) libcurl/7.16.4 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.8
> Host: 192.168.0.2:5984
> Accept: */*
>
< HTTP/1.1 201 Created
< Server: MochiWeb/1.0 (Any of you quaids got a smint?)
< Date: Thu, 19 Jun 2008 05:44:58 GMT
< Content-Type: text/plain;charset=utf-8
< Content-Length: 11
<
* Connection #0 to host 192.168.0.2 left intact
* Closing connection #0
{"ok":true}

ちゃんと 201 Created が返ってきましたね。今度は ドキュメントを追加してみます。ドキュメントの名前(id)がURIに入るので、PUTでドキュメントを作成できます。

colinux% curl -v -X PUT -H "Content-Type: application/json" \
--data "{\"hoge\": \"fuga\"}" http://192.168.0.2:5984/test/testdoc
* About to connect() to 192.168.0.2 port 5984 (#0)
*   Trying 192.168.0.2... connected
* Connected to 192.168.0.2 (192.168.0.2) port 5984 (#0)
> PUT /test/testdoc HTTP/1.1
> User-Agent: curl/7.16.4 (i386-redhat-linux-gnu) libcurl/7.16.4 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.8
> Host: 192.168.0.2:5984
> Accept: */*
> Content-Type: application/json
> Content-Length: 16
>
< HTTP/1.1 201 Created
< Server: MochiWeb/1.0 (Any of you quaids got a smint?)
< Etag: "1623090219"
< Date: Thu, 19 Jun 2008 05:55:33 GMT
< Content-Type: text/plain;charset=utf-8
< Content-Length: 45
<
* Connection #0 to host 192.168.0.2 left intact
* Closing connection #0
{"ok":true,"id":"testdoc","rev":"1623090219"}

ドキュメントの追加はデータベースURIへの POST でもできます。この場合はドキュメント名を指定する必要はありません。サーバ側で適当な名前を付けてくれます。

colinux% curl -v -X POST -H "Content-Type: application/json" \
--data "{\"hoge\": \"piyo\"}" http://192.168.0.2:5984/test;
* About to connect() to 192.168.0.2 port 5984 (#0)
*   Trying 192.168.0.2... connected
* Connected to 192.168.0.2 (192.168.0.2) port 5984 (#0)
> POST /test HTTP/1.1
> User-Agent: curl/7.16.4 (i386-redhat-linux-gnu) libcurl/7.16.4 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.8
> Host: 192.168.0.2:5984
> Accept: */*
> Content-Type: application/json
> Content-Length: 16
>
< HTTP/1.1 201 Created
< Server: MochiWeb/1.0 (Any of you quaids got a smint?)
< Date: Thu, 19 Jun 2008 05:57:47 GMT
< Content-Type: text/plain;charset=utf-8
< Content-Length: 70
<
* Connection #0 to host 192.168.0.2 left intact
* Closing connection #0
{"ok":true,"id":"2366ffde9e795d8e9c9047f1303cfbcd","rev":"4244254602"}

POST の場合は Location ヘッダが返ってきた方がいいような気もしますが、ありませんね。今ソースを確認したところ、POST の場合でも Location を返さないようです。couchdbのバグっぽいですね。

ドキュメントができたらやっと GET できます。これは簡単です。

colinux% curl -v -X GET http://192.168.0.2:5984/test/testdoc
* About to connect() to 192.168.0.2 port 5984 (#0)
* Trying 192.168.0.2... connected
* Connected to 192.168.0.2 (192.168.0.2) port 5984 (#0)
> GET /test/testdoc HTTP/1.1
> User-Agent: curl/7.16.4 (i386-redhat-linux-gnu) libcurl/7.16.4 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.8
> Host: 192.168.0.2:5984
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: MochiWeb/1.0 (Any of you quaids got a smint?)
< Etag: "1623090219"
< Date: Thu, 19 Jun 2008 06:10:04 GMT
< Content-Type: text/plain;charset=utf-8
< Content-Length: 51
<
* Connection #0 to host 192.168.0.2 left intact
* Closing connection #0
{"_id":"testdoc","_rev":"1623090219","hoge":"fuga"}

レスポンスの Content-Type が text/plain なのがちょっと気に入りませんが、これはリクエストの Accept ヘッダに application/json がない場合は text/plain を返すように couchdb が実装されているからです。ブラウザなんかで見るときは、text/plain の方がなにかとありがたいからでしょう。Accept ヘッダを付けた場合の例も示しておきます。

colinux% curl -v -X GET -H "Accept: application/json" http://192.168.0.2:5984/test/testdoc
* About to connect() to 192.168.0.2 port 5984 (#0)
* Trying 192.168.0.2... connected
* Connected to 192.168.0.2 (192.168.0.2) port 5984 (#0)
> GET /test/testdoc HTTP/1.1
> User-Agent: curl/7.16.4 (i386-redhat-linux-gnu) libcurl/7.16.4 OpenSSL/0.9.8b zlib/1.2.3 libidn/0.6.8
> Host: 192.168.0.2:5984
> Accept: application/json
>
< HTTP/1.1 200 OK
< Server: MochiWeb/1.0 (Any of you quaids got a smint?)
< Etag: "1623090219"
< Date: Thu, 19 Jun 2008 06:11:48 GMT
< Content-Type: application/json
< Content-Length: 51
<
* Connection #0 to host 192.168.0.2 left intact
* Closing connection #0
{"_id":"testdoc","_rev":"1623090219","hoge":"fuga"}

こんな感じで、curl のような HTTP クライアントがあれば、すぐに CouchDB で遊んでみることができます。各言語用のライブラリもいろいろ出てきていますので、それを使うのもいいでしょう。HTTP ライブラリさえあれば、ラッパーを作るのは簡単なのでぜひお試しください。

2008-05-28

RESTful Web サービス読書会

はてなブックマーク   livedoor clip

少し前の話ですが、RESTful Web サービス読書会に参加してきた山本です。

この会は kunitさんが企画・運営されている読書会で、毎月第二土曜日の午後をほぼフルに使って、田町のキャンパスイノベーションセンターで開催されています。

私は初回に監訳者としてリアルで参加し、第2回はリモートで ustream の中継を見ながら lingr 等のチャットで観戦・参戦しました。

こういった読書会に参加するのは初めてだったのですが、普段なかなかお会いできない異業種の方(今回の場合はPHP)と、直接話すことができて大変有意義な会でした。土曜日の午後、田町に出かけられる人にはぜひお勧めの勉強会です。

また、リモートでもかなりきちんと試聴できるので、関東近県以外にお住まいの方も参加可能です。さすがにリモートでの発表はちょっと難しそうですが、チャットでの議論もかなり役に立つと感じています。

こういうことを書くと年寄りくさくなりますが、自分が若いころはこのように色々な人と出会える勉強会はなかなかありませんでした。自分の経験で言うと、隣の研究室と輪講したり、学会や研究会に出た程度でした。

それが今ではその道のプロとすぐに情報交換できてしまうのですから、まさに高速道路が用意されているという気がします。

若い人たちにはぜひ、こういう機会(別に勉強会じゃなくてもいいですけど)を有効活用してもらえればと、なんだかオッサンのようなまとめになっちゃいました。

2008-04-30

WEB+DB PRESS Vol. 44

はてなブックマーク   livedoor clip

こんにちは。山本です。

先週発売になった WEB+DB PRESS Vol.44 で、先日公開した郵便番号検索サービスの設計の解説を書きました。

郵便番号の検索はかなりシンプルな機能ですが、仕様書として書き下すとそれなりの分量になります。この仕様書は私がスクラッチから作成したのですが、今回の記事ではこの仕様書を書くにあたって行った設計作業をそのまま文章として書き出してみました。

リソース設計の基本は RESTful Web サービスで紹介されているリソース指向アーキテクチャ(ROA)の設計手順に従っています。この手法の良いところは、フレームワークに依存せずにリソースを考えられるところです。

最初にリソースとその URI を設計し、URI Templates を使って機械的に各リソースとそれに対する操作をコントローラやモデルに振り分けていくという流れがRESTful な Web システムの設計の王道になっていくのではないかと思っています。

その点で、規約によって URI が固定されてしまう Rails 2.0 は不利なんじゃないかと考えています。ROA を主眼に置いた Web システムの設計はまだまだやることがありそうですね。

2008-04-04

RESTアーキテクチャスタイル入門の記事をすべて公開しました

はてなブックマーク   livedoor clip

1月に三分の一を公開して以来、ずるずると遅れていた残りの記事の公開をやっと行いました。

出版は2006年なので2年前の記事です。内容が一部古くなっている部分もあったため、現時点での最新情報に少しだけアップデートしました。

2008-03-24

RESTとROAの社内技術交流会資料と映像を公開します

はてなブックマーク   livedoor clip

こんにちは。山本です。

先月 NTT コミュニケーションズ様で講演した内容を報告しましたがその後なんで社内でやらないのだとツッコミをうけたので、社内の技術交流会でほぼ同じ内容で発表しました。

せっかくなので、このブログでも報告できるようにと映像を撮ってあります。リコーの MPMeister という動画とパワーポイントスライドを同期させたWebコンテンツを簡単に(本当に簡単です)作成できるツールを使って作成しました。MPMeister は残念ながら Windows/IE 限定のツールなのでそれ以外のブラウザの方用に Flash のコンテンツも用意しました。

当日使った資料も PDF で置いておきます。

 


プレゼン当日に社内の一部で妙にニコニコメソッドが話題になっていたため、実施してみました。講演者はスライドばかり見ているわけではないので、何が起っているのかわからないことも多いのですが、参加者の皆さんには大変好評でした。途中でソフトが落ちてしまったので、一次中断していますが、後半また復活しています。

ちなみにこれに影響を受けた日野原は、Air 版をいろいろいじって遊んでいるようです。