Grails をマスターする: RESTful な Grails

リソース指向アーキテクチャーを構築する

私たちは今、マッシュアップの時代に生きています。手始めとしてはユーザーが必要とする情報を提供する Web ページを作成するのも有効ですが、それよりも好ましいのは、加工していないデータを提供して、他の Web 開発者たちがそれぞれのアプリケーションに簡単に組み込めるようにすることです。今回の「Grails をマスターする」では、Scott Davis が Grails にいつもの HTML ではなく、XML を生成させるさまざまな方法を紹介します。

Scott Davis, Editor in Chief, AboutGroovy.com

Scott DavisScott Davis は国際的に知られた著者、講演者、そしてソフトウェア開発者です。彼の著書には、『Groovy Recipes: Greasing the Wheels of Java』、『GIS for Web Developers: Adding Where to Your Application』、『The Google Maps API』、『JBoss At Work』などがあります。



2008年 9月 16日

今月は、Grails アプリケーションを他の Web アプリケーションが利用できる加工していないデータのソース、つまり具体的には XML のソースにする方法を説明します。普段はこれを、Grails アプリケーションの Web サービスをセットアップする、と言い表すところですが、最近では Web サービスという言葉にはいろいろな意味が隠されています。多くの人々は Web サービスを、SOAP や本格的なサービス指向アーキテクチャー (SOA) と関連付けます。お好みであれば、Grails の 2 つのプラグインを使用して SOAP インターフェースをアプリケーションに公開するという方法もありますが (「参考文献」を参照)、今回説明する方法では SOAP のような特定の実装を扱うことはしません。この方法では、REST (Representational State Transfer) ベースのインターフェースを使って POX (Plain Old XML) を返します。

RESTful な Web サービスに関して言えば、その方法を理解するのと同じく、その根拠を理解することも重要です。REST という頭字語を生み出した Roy Fielding の博士論文 (「参考文献」を参照) では Web サービスの 2 つの手法として、サービス指向とリソース指向について要点をまとめています。この記事では、独自の RESTful なリソース指向アーキテクチャー (ROA) を実装するコードを紹介する前に、サービス指向とリソース指向の設計理念の違いを明確にし、一般的な使い方でのこの 2 つの競合する REST 定義を説明することにします。記事の前半で説明する内容を頭に入れてもらった上で、その後に説明した内容を実装した Grails コードの数々を紹介します。

REST の簡単な説明

開発者たちが RESTful な Web サービスの提供について語るとき、通常彼らが意味しているのは、アプリケーションから単純で手軽な方法で XML を取得できるようにするということです。RESTful な Web サービスには一般に、HTTP GET リクエストに応答して XML を返す URL が用意されています (この後すぐ、より正式な REST の定義を説明するなかで、この定義の微妙でありながら重要な点について詳しく説明します)。

Yahoo! で提供している多数の RESTful な Web サービス (「参考文献」を参照) は、単純な HTTP の GET リクエストに応答して POX を返します。例えば、Web ブラウザーのロケーション・フィールドに http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=beatles と入力してみてください。Yahoo! ホーム・ページの検索ボックスに beatles と入力した場合には通常、HTML で Web 検索結果が表示されますが、それと同じ結果が XML で表示されます。

この連載について

Grails は、Spring や Hibernate などのよく知られた Java™ 技術に「Convention over Configuration (設定より規約)」といった現代のプラクティスを盛り込んだ最新の Web 開発フレームワークです。Groovy で作成された Grails は既存の Java コードをシームレスに統合するだけでなく、スクリプト言語ならではの柔軟性と動的機能を与えてくれます。Grails を学んだら、Web 開発に対する今までの見方がまったく違ってくるはずです。

仮説として、Yahoo! が SOAP インターフェースをサポートするとしたら (実際にはサポートしていません)、SOAP リクエストを実行してもこれと同じデータが返されるはずです。しかし、これには開発者がリクエストを実行するという部分で多少厄介な作業が必要になってきます。この場合、リクエスターがサブミットしなければならないのは、単純な名前と値のペアを指定したクエリー・ストリングではなく、SOAP ヘッダーと本文セクションを持つ明確に定義された XML 文書です。さらに、このリクエストは HTTP の GET ではなく POST を使用してサブミットします。このような追加の作業を行った上で初めて、レスポンスが正式な XML 文書として返ってきます。このレスポンスにはリクエストと同様に SOAP ヘッダーと本文セクションがあるため、クエリーの結果にするためには、この SOAP ヘッダーおよび本文セクションを取り除かなければなりません。このような SOAP の複雑さに代わる「控えめな儀式」として一般に受け入れられているのが、RESTful な方法による Web サービスです。

RESTful な Web サービスの人気が高まっていることは、いくつかの傾向が示しています。例えば Amazon.com は RESTful なサービスと SOAP ベースのサービスの両方を提供していますが、実際の使用パターンには、ユーザーが十中八九、RESTful インターフェースのほうを好むことが現れています。もう 1 つの注目すべき事例として、Google は 2006年12月、正式に SOAP ベースの Web サービスを廃止しました。Google Data API の下にまとめられたデータ・サービスはすべて、より RESTful な方法を採用しています。


サービス指向 Web サービス

REST と SOAP との違いを GETPOST の優劣に要約できたとしたら、その違いは単純だったことでしょう。どの HTTP メソッドを使用するかは重要ですが、それは最初に頭に浮かぶような理由とは異なる理由からです。REST と SOAP との違いを十分に理解するには、この 2 つのストラテジーが持つセマンティクスをより深く認識する必要があります。SOAP が具現化しているのは、サービス指向の Web サービスです。サービス指向の Web サービスでは、サービスを利用するための第一の手段となるのはメソッド (あるいは動詞) です。一方、REST で採用しているのはリソース指向の手法で、この場合にはオブジェクト (あるいは名詞) が主役となります。

SOA でのサービス呼び出しは、リモート・プロシージャー・コール (RPC) に似ています。例えば Java の Weather という仮定のクラスに getForecast(String zipcode) というメソッドがあるとします。そしてこのメソッドは簡単に Web サービスとして公開できるとします。実際、Yahoo! には、まさにこれと同じことをする Web サービスがあります。試しにブラウザーに http://weather.yahooapis.com/forecastrss?p=94089 と入力してください。ここで、p パラメーターには、自分の郵便番号を入力します。この Yahoo! サービスはもう 1 つのパラメーター、u をサポートします。u パラメーターが受け入れるのは、華氏の f、または摂氏の c です。2 番目のパラメーターを受け入れるために、この仮定のクラスにメソッド・シグニチャーを詰め込む方法は簡単に想像がつくように、getForecast("94089", "f") となります。

前に行った Yahoo! 検索クエリーをメソッド呼び出しに書き換えるとしても、その方法は同じく簡単に想像がつきます。http://api.search.yahoo.com/WebSearchService /V1/webSearch?appid=YahooDemo&query=beatles を WebSearchService.webSearch("YahooDemo", "beatles") に変換すればよいだけの話です。

このように、Yahoo! 呼び出しが実際には RPC 呼び出しだとすると、Yahoo! サービスは RESTful なサービスであるという前の説明に矛盾しないでしょうか? 残念ながら、その通りです。しかし、間違えているのは私だけではありません。Yahoo! も同じく、これらのサービスを RESTful なサービスと呼んでいます。とは言うものの、Yahoo! では厳密な REST なサービスの定義には当てはまらないことを認識しています。Yahoo! Web Services FAQ の「REST とは」というエントリーの答えは、「REST とは Representational State Transfer の略です。大多数の Yahoo! Web Services では、「REST のような」RPC スタイルの操作を HTTP の GET または POST で使用します...」となっています。

この点については、REST コミュニティーで議論されている最中です。何が問題かと言えば、「HTTP の POST より GETを優先し、XML リクエストより単純な URL リクエストを優先する RPC ベースの Web サービス」という説明を簡潔に表す一般的なキャッチフレーズがないことです。HTTP/POX サービス、あるいは REST/RPC サービスと呼んでいる開発者もいれば、High REST Web サービス (Fielding による元来のリソース指向アーキテクチャーの定義に近いサービス) と対比させて Low REST と呼んでいる開発者もいます。

非公式にですが、私は Yahoo! のようなサービスを GETful なサービスと呼んでいます。これは軽蔑した呼び方ではありません。逆に、Yahoo! は形式張らない一連の Web サービスを 1 つのコレクションにまとめるという偉大な仕事をしたと私は思っています。GETful という言葉が捉えているのは、Yahoo! の RPC スタイルのサービスが持つ主な利点です。つまり、Fielding による当初の定義を誤用することなく、単純な HTTP の GET リクエストを行うことで XML の結果を取得できるという利点です。


リソース指向 Web サービス

POSTPUT の比較

REST コミュニティーでは、新しいリソースを挿入する際の POSTPUT の役割について議論されています。当初の HTTP 1.1 の RFC (Fielding がリーダーとして作成) による PUT の定義は、該当するリソースが存在しない場合、サーバーがそのリソースを作成できるとあります。また、既存のリソースがある場合には、「...そのなかに含まれるエンティティーを、起点サーバーに置かれたリソースの変更バージョンとして見なすべき」とも述べています。したがって、リソースが存在しない場合には、PUT は INSERT に相当し、リソースが存在する場合には、PUT は UPDATE に相当するということです。これだけなら話は単純ですが、一方の POST の定義は以下のようになっています。

POST は、以下の機能を統一したメソッドで扱えるように設計されています。

  • 既存のリソースにアノテーションを付ける。
  • 掲示板、ニュースグループ、メーリング・リスト、あるいは同じような記事のグループにメッセージを投稿する。
  • データ・ブロック (フォームをサブミットした結果など) をデータ処理プロセスに提供する。
  • データベースを付加操作によって拡張する。」

「既存のリソースにアノテーションを付ける」という機能は UPDATE を意味するように思えます。また、「掲示板にメッセージを投稿する」、そして「データベースを拡張する」という機能は INSERT と同じように思えます。

すべての主要なブラウザーでは、HTML フォーム・データをサブミットする際に PUT メソッドをサポートしない (サポートするのは GET および POST のみ) という事実を考えると、どの場合にどのメソッドを使用するべきかについて群集の意見は分かれるはずです。

Atom 出版プロトコル (Atom Publishing Protocol) はよく使用される配信フォーマットで、RESTful な原則に従っています。Atom の RFC の作成者たちは以下のようにして、POSTPUT の議論に決定的な終止符を打とうとしています。

「Atom 出版プロトコル では HTTP メソッドを使用して、以下のようにメンバー・リソースを作成します。

  • GET は、既知のリソース表現を取得するために使用する。
  • POST は、動的に名前が付けられた新しいリソースを作成するために使用する。
  • PUT は、既知のリソースを編集するために使用する。リソースの作成には使用しない。
  • DELETE は、既知のリソースを削除するために使用する」。

この記事では Atom をガイドとして、INSERT には POST を、UPDATE には PUT を使用しています。ただし、自分のアプリケーションではこれらのメソッドを逆にして使いたいという方にも心強い仲間がいます。『RESTful Web サービス』という本 (「参考文献」を参照) では、INSERT に PUT を使用することを支持しています。

それではサービスを真のリソース指向サービスにするには何が必要なのでしょうか。それは突き詰めるところ、有効な URI (Uniform Resource Identifier) を作成すること、そして 1 つの HTTP 動詞 (GET) をカスタム・メソッド呼び出しと併せて使用するのではなく、4 つの HTTP 動詞 (GETPOSTPUT、および DELETE) を標準化した方法で使用することです。

Beatles のクエリーに戻ると、RESTful なインターフェースの形式に近づけるための最初のステップは、URI を調整することです。つまり、BeatleswebSearch メソッドへの引数として渡すのではなく、Beatles を URI の中心リソースにするということです。例えばウィキペディアでは、ビートルズに関する記事の URI は http://en.wikipedia.org/wiki/Beatles となっています。

しかし、何が本当に GETful な理念を RESTful な理念と差別化しているかと言えば、それはリソースの表現を返すために使用するメソッドです。Yahoo! RPC インターフェースは数々のカスタム・メソッド (webSearchalbumSearchnewsSearch など) を定義しています。メソッド呼び出しの名前を知るには、資料を読む他に手段はありません。Yahoo! の場合、パターンに従えば songSearchimageSearchvideoSearch メソッド呼び出しもあると推測できますが、確実な保証はありません。さらに、他の Web サイトでは findSongsongQuery といった異なる命名規則を使用していることも考えられます。Grails の場合には、aiport/listairport/show などのカスタム・アクションがアプリケーション全体を通して標準となりますが、これらのメソッド名が他の Web フレームワークでも標準となることは決してありません。

それとは対照的に、RESTful な手法では、常に HTTP の GET を使用して該当するリソースの表現を返します。したがって、ウィキペディアではどのリソース (http://en.wikipedia.org/wiki/Beatles、http://en.wikipedia.org/wiki/United_Airlines、あるいは http://en.wikipedia.org/wiki/Peanut_butter_and_jelly_sandwich) を取得するにも、標準となる方法は当然、GET です。

標準化されたメソッド呼び出しの威力は、リソースの完全な CRUD (Create/Retrieve/Update/Delete ) ライフサイクルを扱っていると、一層明白になってきます。 RPC インターフェースでは、新しいリソースを作成するための方法が標準化されていません。カスタム・メソッド呼び出しは createnewinsertadd など、ほとんど何にでもなる可能性があります。RESTful なインターフェースでは、POSTリクエストを URI に送信すると新しいリソースが挿入されます。そして PUT はリソースを更新し、DELETE はリソースを削除します (囲み記事「POSTPUT の比較」を参照)。

以上の説明で、GETful な Web サービスと RESTful な Web サービスとの違いが理解できてきたはずです。ここからはこの 2 つの Web サービスを実際に Grails で作成する作業に取り掛かります。この記事では両方の例を取り上げますが、まずは単純な POX の例から始めます。


Grails での GETful な Web サービスの実装

Grails アプリケーションから POX を取り出すのに最も手っ取り早い方法は、grails.converters.* パッケージをインポートして新しいクロージャーをいくつか追加することです (リスト 1 を参照)。

リスト 1. 単純な XML 出力
import grails.converters.*

class AirportController{
  def xmlList = {
    render Airport.list() as XML
  }

  def xmlShow = {
    render Airport.get(params.id) as XML
  }
  
  //... the rest of the controller
}

grails.converters パッケージは、「Ajax をほんの少し加えた多対多の関係」で実際の機能を説明したとおり、信じ難いほど単純な JSON (JavaScript Object Notation) および XML 出力サポートを提供します。図 1 は、xmlList アクションを呼び出した結果です。

図 1. Grails によるデフォルトの XML 出力
Grails によるデフォルトの XML 出力

デフォルトの XML 出力でもデバッグするには十分ですが、フォーマットを多少カスタマイズしたいという場合もあります。そのような場合には、ありがたいことに render() メソッドが提供する Groovy の MarkupBuilder を使用してカスタム XML をオンザフライで定義することができます (MarkupBuilderについての詳細は、「参考文献」に記載するリンクを参照してください)。リスト 2 は、カスタム XML 出力の作成例です。

リスト 2. カスタム XML 出力
def customXmlList = {
  def list = Airport.list()
  render(contentType:"text/xml"){
    airports{
      for(a in list){
        airport(id:a.id, iata:a.iata){
          "official-name"(a.name)
          city(a.city)
          state(a.state)
          country(a.country)
          location(latitude:a.lat, longitude:a.lng)
        }
      }        
    }
  }
}

上記による出力は、図 2 のようになります。

図 2. Groovy の MarkupBuilder を使用したカスタム XML 出力
Groovy の MarkupBuilder を使用したカスタム XML 出力

ソース・コードと XML 出力がかなり密接に対応していることに注目してください。要素名には、クラスの実際のフィールド名と一致しているかどうかに関わらず、任意の名前 (airports、airport、city) を定義することができます。ハイフンでつなげた要素名 (official-name など) を指定したり、名前空間のサポートを追加したりするにも、要素名を引用符で囲めばよいだけのことです。属性 (idiata など) は、Groovy ハッシュマップの key:value 構文を使用して定義します。要素の本文を取り込むには、key: の部分を取り除いた値を指定します。

コンテンツ・ネゴシエーションと Accept ヘッダー

データをそれぞれ HTML 表現と XML 表現で返す別個のクロージャーは簡単に作成することができますが、1 つのクロージャーで両方とも処理することは可能でしょうか。それは、HTTP リクエストに組み込まれた Accept ヘッダーのおかげで可能になります。リクエスト内のこの小さなメタデータは、サーバーに対して「この URI にあるリソースには複数の表現が考えられるけれども、この表現を優先したい」という嗜好を知らせるからです。

cURL は重宝なオープンソースのコマンドライン HTTP ツールです (「参考文献」を参照)。例えばコマンドラインに curl http://localhost:9090/trip/airport/list と入力して、空港のリストに対するブラウザー・リクエストをシミュレートしてみてください。画面には、スクロール可能な HTML レスポンスが表示されるはずです。

ここで、リクエストに 2 つのちょっとした調整を加えます。今回は GET ではなく、HEAD リクエストを使用します。HEAD は標準の HTTP メソッドで、レスポンスの本文ではなくメタデータだけを返します (ここで行っているデバッグとまったく同じタイプのデバッグについては、HTTP 仕様を参照してください)。また、cURLverbose モードに設定して、リクエストのメタデータも確認できるようにしてください (リスト 3 を参照)。

リスト 3. cURL を使用した HTTP デバッグ
$ curl --request HEAD --verbose http://localhost:9090/trip/airport/list
* About to connect() to localhost port 9090 (#0)
*   Trying ::1... connected
* Connected to localhost (::1) port 9090 (#0)
> HEAD /trip/airport/list HTTP/1.1
> User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) 
        libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
> Host: localhost:9090
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Language: en-US
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Server: Jetty(6.1.4)
< 
* Connection #0 to host localhost left intact
* Closing connection #0

上記のリクエストに含まれる Accept ヘッダーに注目してください。クライアントが */* をサブミットするということは、基本的には「どのフォーマットで戻ってきても構わない。何でも受け入れる」と言っていることです。

cURL では、この値を --header パラメーターで上書きすることができます。curl --request HEAD --verbose --header Accept:text/xml http://localhost:9090/trip/airport/list と入力して、Accept ヘッダーが text/xml を要求することを確認してください。これは、リソースの MIME タイプです。

今度はサーバー・サイドで Grails がどのように Accept ヘッダーに応答するのかを見ていきましょう。まずはリスト 4 に記載するもう 1 つのクロージャーを AirportController に追加します。

リスト 4. debugAccept アクション action
def debugAccept = {
  def clientRequest = request.getHeader("accept")
  def serverResponse = request.format
  render "Client: ${clientRequest}\nServer: ${serverResponse}\n"    
}

リスト 4 のメソッドの最初の行では、リクエストからヘッダーを取得しています。2 行目では、リクエスト、そして送信するレスポンスを Grails がどのように解釈するかが示されています。

ここで cURL を使用して、ある調査を行います (リスト 5 を参照)。

リスト 5. cURL での Accept ヘッダーの調整
$ curl  http://localhost:9090/trip/airport/debugAccept
Client: */*
Server: all

$ curl  --header Accept:text/xml http://localhost:9090/trip/airport/debugAccept
Client: text/xml
Server: xml

allxml の値がどこから来ているのかというと、grails-app/conf/Config.groovy を調べると、ファイルの先頭近くにハッシュマップがあるはずです。このハッシュマップは、キーには単純な名前 (allxml などの名前) を使用し、キーの値には名前に対応する MIME タイプを使用します。リスト 6 に、この grails.mime.types ハッシュマップを記載します。

リスト 6. Config.groovy に含まれる grails.mime.types ハッシュマップ
grails.mime.types = [ html: ['text/html','application/xhtml+xml'],
                      xml: ['text/xml', 'application/xml'],
                      text: 'text-plain',
                      js: 'text/javascript',
                      rss: 'application/rss+xml',
                      atom: 'application/atom+xml',
                      css: 'text/css',
                      csv: 'text/csv',
                      all: '*/*',
                      json: ['application/json','text/json'],
                      form: 'application/x-www-form-urlencoded',
                      multipartForm: 'multipart/form-data'
                    ]

コンテンツ・ネゴシエーションについて多少の知識を身につけたところで、withFormat ブロックを list アクションに追加してください (リスト 7 を参照)。これで、リクエストに含まれる Accept ヘッダーに応じて適切なデータ・タイプが返されるようになります。

リスト 7. withFormat ブロックの実際の使用方法
def list = {
  if(!params.max) params.max = 10
  def list = Airport.list(params)
  withFormat{
    html{
      return [airportList:list]
    }
    xml{
      render list as XML
    }
  }
}

高度なコンテンツ・ネゴシエーション

標準 Web ブラウザーが指定する Accept ヘッダーは、cURL で使用する単純なものよりも多少複雑になります。例えば、Mac OS X 10.5.4 上の Firefox 3.0.1 は、以下のような Accept ヘッダーを指定します。

text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

これはカンマで区切られたリストで、オプションの q 属性によって他に優先する特定の MIME タイプを指定します (「quality」の略である q の値は、0.0 から 1.0 までの浮動小数点値です)。上記の application/xml には 0.9 の q 値が指定されているため、Firefox は XML データを第一に優先するということになります。

以下は、Mac OS X 10.5.4 上の Safari バージョン 3.1.2 による Accept ヘッダーです。

text/xml,application/xml,application/xhtml+xml,
text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

上記では、text/htmlMIME に 0.9 の q 値が指定されています。つまり、HTML が優先される出力タイプで、次に優先されるのは値が 0.8 の text/plain、続いて値が 0.5 の */* ということになります。

サーバー・サイドでのコンテンツ・ネゴシエーションについての詳細は、「参考文献」を参照してください。

それぞれのフォーマット・ブロックの最後の行は、renderreturn、または redirect にする必要があります。これは、通常のアクションと何ら変わりありません。Accept ヘッダーが「すべて (all)」 (*/*) となっている場合には、ブロック内の最初のエントリーが使用されます。

cURL での Accept ヘッダーの調整はこれで問題ありませんが、URI を調整してテストすることもできます。Accept ヘッダーを明示的に上書きする手段としては、http://localhost:8080/trip/airport/list.xml や http://localhost:8080/trip/airport/list?format=xml のように URI を指定する方法があります。さまざまなコンテンツ・タイプの URI になるように URI を上書きし、cURL を操作することで、withFormat ブロックが期待通りに機能することを確認してください。

この振る舞いを Grails での標準にする場合は、grails install-templates と入力して、/src/templates 内のファイルを編集するという方法があることを忘れないでください。

基本的なすべてのビルディング・ブロックは、これで用意できました。パズルを完成させるための最後のピースは、GETful なインターフェースから、真の RESTful なインターフェースへの移行です。


Grails での RESTful な Web サービスの実装

まず始めに必要な作業として、コントローラーが確実に 4 つの基本 HTTP メソッドに応答するようにします。ユーザーが listshow などのカスタム・アクションを指定しない場合、index クロージャーがコントローラーへのエントリー・ポイントになることを思い出してください。デフォルトでは、indexlist アクションにリダイレクトします。このデフォルト・コード、def index = { redirect(action:list,params:params) } をリスト 8 のコードに置き換えます。

リスト 8. HTTP メソッドへの切り替え
def index = {       
  switch(request.method){
    case "POST":
      render "Create\n"
      break
    case "GET":
      render "Retrieve\n"
      break
    case "PUT":
      render "Update\n"
      break
    case "DELETE":
      render "Delete\n"
      break
  }   
}

リスト 9 の cURL を使用して、switch 文が正しく機能することを確認してください。

リスト 9. 4 つすべての HTTP メソッドに使用する cURL
$ curl --request POST http://localhost:9090/trip/airport
Create
$ curl --request GET http://localhost:9090/trip/airport
Retrieve
$ curl --request PUT http://localhost:9090/trip/airport
Update
$ curl --request DELETE http://localhost:9090/trip/airport
Delete

GET の実装

XML を返す方法がわかっていれば、GET メソッドはごく簡単に実装できます。ただし注意を要する点が 1 つあります。それは、http://localhost:9090/trip/airport に対する GET リクエストの場合には空港のリストを返し、http://localhost:9090/trip/airport/den に対する GET リクエストの場合には IATA コードが den に設定された空港のインスタンスを返さなければならないことです。そのためには、カスタム URL マッピングをセットアップする必要があります。

grails-app/conf/UrlMappings.groovy をテキスト・エディターで開いてください。デフォルトの /$controller/$action?/$id? マッピングはお馴染みのはずです。URL の http://localhost:9090/trip/airport/show/1 は AiportControllershow アクションにマッピングし、params.id の値は 1 に設定されます。アクションと ID に続く末尾の疑問符は、この URL 要素がオプションであることを意味します。

リスト 10 に示すように、static mappings ブロックに、RESTful なリクエストを AirportController にマッピングする行を追加します。とりあえず、ここではコントローラーをハードコーディングしていますが、その理由は、まだ他のコントローラーに REST サポートを実装していないからです。後でおそらく、この URL の airport の部分を $controller に置き換えることになります。

リスト 10. カスタム URL マッピングの作成
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
         constraints { // apply constraints here
         }
        }		  
        "/rest/airport/$iata?"(controller:"airport",action:"index")
     "500"(view:'/error')
   }
}

このマッピングにより、/rest で始まるすべての URI が確実に index アクションにルーティングされることになります (こうすることで、コンテンツ・ネゴシエーションを処理する必要がなくなります)。また、params.iata の有無をチェックすれば、リストを返すのか、あるいはインスタンスを返すのかを決定できることにもなります。

index アクションは、リスト 11 のように変更します。

リスト 11. HTTP GET からの XML のリターン
def index = {       
  switch(request.method){
    case "POST":   //...
    case "GET":
      if(params.iata){render Airport.findByIata(params.iata) as XML}
      else{render Airport.list() as XML}          
      break
    case "PUT":    //...
    case "DELETE": //...
  }      
}

Web ブラウザーに http://localhost:9090/trip/rest/airport および http://localhost:9090/trip/rest/airport/den と入力して、カスタム URL マッピングが配置されていていることを確認してください。

HTTP メソッドによるカスタム URL マッピング

RESTful な URL マッピングをセットアップするには、多少異なる手法を使えます。それは、リクエストを HTTP リクエストに応じて特定のアクションにルーティングするという手法です。例えば、GETPUTPOST、および DELETE を対応する既存の Grails アクションにマッピングするには、以下のようにします。

static mappings = {
"/airport/$id"(controller:"airport"){
action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]
}
}

DELETE の実装

DELETE のサポートを追加する方法は、GET の場合とそれほど変わりません。ただしこの例で必要なのは、IATA コードを基準に個々の空港を削除できるようにすればよいだけです。ユーザーが IATA コードを指定しないで HTTP DELETE をサブミットした場合には、400 HTTP ステータス・コード「Bad Request」を返すことにします。ユーザーがサブミットした IATA コードが見つからない場合に返すのは、お馴染みの 404 ステータス・コード「Not Found」です。削除が成功した場合にだけ、標準の 「200 OK」を返します (HTTP ステータス・コードについての詳細は、「参考文献」を参照してください)。

リスト 12 のコードを、index アクションの DELETE case に追加します。

リスト 12. HTTP DELETE に対する反応
def index = {       
  switch(request.method){
    case "POST": //...
    case "GET":  //...
    case "PUT":  //...
    case "DELETE":
      if(params.iata){
        def airport = Airport.findByIata(params.iata)
        if(airport){
          airport.delete()
          render "Successfully Deleted."
        }
        else{
          response.status = 404 //Not Found
          render "${params.iata} not found."
        }
      }
      else{
        response.status = 400 //Bad Request
        render """DELETE request must include the IATA code
                  Example: /rest/airport/iata
        """
      }
      break
  }
}

まず、存在することがわかっている空港を削除してみてください (リスト 13 を参照)。

リスト 13. 既存の空港の削除
Deleting a Good Airport</heading>
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/lga
> DELETE /trip/rest/airport/lga HTTP/1.1 
< HTTP/1.1 200 OK
Successfully Deleted.

今度は、存在するかわからない空港を削除してみます (リスト 14 を参照)。

リスト 14. 存在しない空港を DELETE しようとした場合
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/foo
> DELETE /trip/rest/airport/foo HTTP/1.1
< HTTP/1.1 404 Not Found
foo not found.

最後に、IATA コードを指定しないで DELETE リクエストを実行してください (リスト 15 を参照)。

リスト 15. すべての空港を同時に DELETE しようとした場合
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport
> DELETE /trip/rest/airport HTTP/1.1
< HTTP/1.1 400 Bad Request
DELETE request must include the IATA code
Example: /rest/airport/iata

POST の実装

次の目標は、新しい Airport を挿入することです。それにはまず、simpleAirport.xml という名前のファイルを作成します (リスト 16 を参照)。

リスト 16. simpleAirport.xml
<airport>
  <iata>oma</iata>
  <name>Eppley Airfield</name>
  <city>Omaha</city>
  <state>NE</state>
  <country>US</country>
  <lat>41.3019419</lat>
  <lng>-95.8939015</lng>
</airport>

リソースの XML 表現がフラット (要素が深くネストされていないこと) で、それぞれの要素名がクラスのフィールド名と一致していれば、Grails は XML から直接新しいクラスを作成することができます。XML 文書のルート要素は、params を使用してアドレス指定することができます (リスト 17 を参照)。

リスト 17. HTTP POST に対する反応
def index = {       
  switch(request.method){
    case "POST":
      def airport = new Airport(params.airport)
      if(airport.save()){
        response.status = 201 // Created
        render airport as XML
      }
      else{
        response.status = 500 //Internal Server Error
        render "Could not create new Airport due to errors:\n ${airport.errors}"
      }
      break
    case "GET":    //...
    case "PUT":    //...
    case "DELETE": //...
  }      
}

XML はフラットでなければなりません。それは、params.airport は実際にはハッシュマップだからです (Grails は裏で XML とハッシュマップ間の変換を行います)。これは実質的に、Airport には名前付き引数のコンストラクターを使用するということです (def airport = new Airport(iata:"oma", city:"Omaha", state:"NE"))。

新しいコードをテストするには、cURL を使用して simpleAirport.xml ファイルの POST 操作を実行します (リスト 18 を参照)。

リスト 18. cURL による HTTP POST の実行
$ curl --verbose --request POST --header "Content-Type: text/xml" --data 
      @simpleAirport.xml http://localhost:9090/trip/rest/airport
> POST /trip/rest/airport HTTP/1.1
> Content-Type: text/xml
> Content-Length: 176
> 
< HTTP/1.1 201 Created
< Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?><airport id="14">
  <arrivals>
    <null/>
  </arrivals>
  <city>Omaha</city>
  <country>US</country>
  <departures>
    <null/>
  </departures>
  <iata>oma</iata>
  <lat>41.3019419</lat>
  <lng>-95.8939015</lng>
  <name>Eppley Airfield</name>
  <state>NE</state>
</airport>

XML が複雑な場合には、手作業で XML をアンマーシャルする必要があります。例えば、前に定義したカスタム XML フォーマットを思い出してください。newAirport.xml という名前のファイルを作成します (リスト 19 を参照)。

リスト 19. newAirport.xml
<airport iata="oma">
  <official-name>Eppley Airfield</official-name>
  <city>Omaha</city>
  <state>NE</state>
  <country>US</country>
  <location latitude="41.3019419" longitude="-95.8939015"/>
</airport>

次は index アクションで、def airport = new Airport(params.airport) という 1 行をリスト 20 のコードに置き換えます。

リスト 20. 複雑な XML の構文解析
def airport = new Airport()
airport.iata = request.XML.@iata
airport.name = request.XML."official-name"
airport.city = request.XML.city
airport.state = request.XML.state
airport.country = request.XML.country
airport.lat = request.XML.location.@latitude
airport.lng = request.XML.location.@longitude

request.XML オブジェクトは、加工していない XML を保持する groovy.util.XmlSlurper です。このオブジェクトは実質的にはルート要素なので、子要素は名前で要求することができます (request.XML.city)。名前がハイフンで結ばれていたり、名前空間が設定されていたりする場合には、引用符で囲んでください (request.XML."official-name")。要素の属性にアクセスするには、request.XML.location.@latitude のように @ 記号を使用します (XmlSlurperについての詳細は、「参考文献」に記載されているリンクを参照してください)。

最後にcURL でテストするため、curl --request POST --header "Content-Type: text/xml" --data @newAirport.xml http://localhost:9090/trip/rest/airport と入力します。

PUT の実装

サポートする必要のある最後の HTTP メソッドは PUT です。POST の説明でおわかりのように、この場合のコードも基本的には同じです。唯一の違いは、XML からクラスを直接作成する代わりに、GORM に既存のクラスを問い合わせなければならないという点だけです。その上で、airport.properties = params.airport という行が既存のフィールド・データを XML からの新しいデータに置き換えます (リスト 21 を参照)。

リスト 21. HTTP PUT に対する反応
def index = {       
  switch(request.method){
    case "POST":  //... 
    case "GET":   //...
    case "PUT":   
      def airport = Airport.findByIata(params.airport.iata)
      airport.properties = params.airport
      if(airport.save()){
        response.status = 200 // OK
        render airport as XML
      }
      else{
        response.status = 500 //Internal Server Error
        render "Could not create new Airport due to errors:\n ${airport.errors}"
      }
      break
    case "DELETE": //...
  }      
}

editAirport.xml という名前のファイルを作成します (リスト 22 を参照)。

リスト 22. editAirport.xml
<airport>
  <iata>oma</iata>
  <name>xxxEppley Airfield</name>
  <city>Omaha</city>
  <state>NE</state>
  <country>US</country>
  <lat>41.3019419</lat>
  <lng>-95.8939015</lng>
</airport>

仕上げは cURL によるテストです。それには、curl --verbose --request PUT --header "Content-Type: text/xml" --data @editAirport.xml http://localhost:9090/trip/rest/airport と入力してください。


まとめ

短い時間でさまざまな基礎を説明しましたが、これで、SOA と ROA の違いを理解してもらえたと思います。さらに、すべての RESTful な Web サービスが同じというわけではないことも認識できたはずです。HTTP の GET リクエストを使用して RPC のようなメソッド呼び出しを行う GETful な手法もあれば、URI がリソースにアクセスする鍵となり、HTTP の標準の GETPOSTPUT、そして DELETE メソッドが完全な CRUD 機能を実現する、真のリソース指向の手法もあります。GETful な手法と RESTful な手法のどちらを選ぶにしても、XML を出力して、これを再び簡単に取り込むための堅固なサポートは Grails が提供します。

連載「Grails をマスターする」の次回の記事では、テストに目を向けます。Grails には、そのまま使用できる優れたテスト機能が備わっています。提供されていない機能については、後からプラグインとして追加することもできます。今までに Grails 開発に費やしたすべての時間を無駄にしないためには、Gails アプリケーションにバグが一切なく、アプリケーションの存続期間中、その状態が維持されることを確実にしなければなりません。次回の記事まで、Grails を楽しみながらマスターしてください。

参考文献

学ぶために

製品や技術を入手するために

  • Grails: Grails の最新リリースをダウンロードしてください。
  • XFire および Apache Axis2 プラグイン: SOAP インターフェースを公開するための Grails プラグインです。
  • cURL: ほとんどの UNIX®、Linux®、および Mac OS X システムでは、cURL がデフォルトでインストールされます。Windows® をはじめ、実質的にすべての OS それぞれに対応したバージョンをダウンロードできます。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Web development, Open source
ArticleID=345949
ArticleTitle=Grails をマスターする: RESTful な Grails
publish-date=09162008