目次


Grails をマスターする

RESTful な Grails

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

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: Grails をマスターする

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:Grails をマスターする

このシリーズの続きに乞うご期待。

今月は、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 で表示されます。

仮説として、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 サービス

それではサービスを真のリソース指向サービスにするには何が必要なのでしょうか。それは突き詰めるところ、有効な 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 出力
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 出力
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
    }
  }
}

それぞれのフォーマット・ブロックの最後の行は、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 マッピングが配置されていていることを確認してください。

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 の Web サイトにアクセスしてください。
  • Grails Framework Reference Documentation: Grails のバイブルとなります。
  • Architectural Styles and the Design of Network-based Software Architectures」(Roy Fielding 著、カリフォルニア大学アービン校、2000年): REST について説明している Fielding の博士論文です。
  • Representational State Transfer: ウィキペディアの REST に関する記事です。
  • Creating a REST Request for Yahoo! Search Web Services: Yahoo! Web サービスを正式に使用するには、YahooDemo ではなく、自分自身のアプリケーション・キーを取得して使用する必要があります。
  • RESTful Web サービス』(Leonard Richardson、Sam Ruby 共著、O'Reilly Media、2007年): この本の著者たちは、INSERT としての PUT の使用を支持しています。
  • Creating XML using Groovy's MarkupBuilder および Reading XML using Groovy's XmlSlurper: Groovy で XML を作成および使用してください。
  • コンテキスト・ネゴシエーション: サーバー・サイドのコンテキスト・ネゴシエーションの詳細を学んでください。
  • Status Code Definitions: HTTP ステータス・コードの適用範囲がわかります。
  • Groovy Recipes』: Scott Davis の最新の著作で Groovy と Grails の詳細を学んでください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。
  • Grails: Grails の最新リリースをダウンロードしてください。
  • XFire および Apache Axis2 プラグイン: SOAP インターフェースを公開するための Grails プラグインです。
  • cURL: ほとんどの UNIX®、Linux®、および Mac OS X システムでは、cURL がデフォルトでインストールされます。Windows® をはじめ、実質的にすべての OS それぞれに対応したバージョンをダウンロードできます。

コメント

コメントを登録するにはサインインあるいは登録してください。

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