REST リソースに対するサーバー駆動型コンテンツ・ネゴシエーションの実装とテストを WebSphere sMash を使って行う

コンテンツ・ネゴシエーションは RESTful な設計にとっての重要な側面です。ここではコンテンツ・ネゴシエーションに使用できるいくつかの手法と、そうした手法を IBM® WebSphere® sMash を使って実装する方法を説明します。

はじめに

コンテンツ・ネゴシエーションとは、1 つのリソースに対して複数のデータ表現が可能であるという考えに基づくものです。コンテンツ・ネゴシエーションでは、(HTTP 仕様の定義にあるとおり) クライアントは、名前が「Accept」で始まるいくつかのヘッダー・フィールドを使用して、レスポンスとして返されるリソースの表現形式を指定します。コンテンツ・ネゴシエーションに最も頻繁に使用されるヘッダーは以下のとおりです。

  • Accept: Accept リクエスト・ヘッダー・フィールドは、クライアントがレスポンスとして受け付け可能なメディア・タイプを指定するために使われます。Accept ヘッダーを使用すると、リクエストでの要求が (インライン画像に対するリクエストの場合のように) いくつかの希望のメディア・タイプに明確に限定されていることを伝えることができます。このフィールドの例には、application/jsonapplication/atom+xmltext/html などがあります (メディア・タイプの一覧はこちらを参照してください)。
  • Accept-Charset: Accept-Charset リクエスト・ヘッダー・フィールドは、クライアントがレスポンスとして受け付け可能な文字セットのうち、どの文字セットを使ってレスポンスを表現する必要があるかを伝えるために使われます。このフィールドを利用すると、広範にわたる文字セットまたは特殊用途の文字セットに対応可能なクライアントは、それらの文字セットを使って文書を表現できるサーバーに対して、クライアント側で対応可能な文字セットを知らせることができます。
  • Accept-Encoding: Accept-Encoding リクエスト・ヘッダー・フィールドは Accept フィールドに似ていますが、Accept-Encoding フィールドではコンテンツのエンコード方法を制限します。例えば、このフィールドを利用すると対応可能な圧縮方法を伝えることができます。このフィールドの値の例には、compress;q=0.5gzip;q=1.0 などがあります。
  • Accept-Language: Accept-Language リクエスト・ヘッダー・フィールドも Accept フィールドに似ていますが、Accept-Language フィールドではリクエストに対するレスポンスとして推奨される自然言語を制限します。このフィールドの値の例には、英語を推奨する場合の en やスペイン語を推奨する場合の es などがあります。

「Accept」という単語で始まる、もう 1 つのヘッダー・フィールドとして Accept-Ranges フィールドがありますが、このフィールドはレスポンスの中で使われるもので、リクエスト・ヘッダーではありません。サーバーは Accept-Ranges を使うことによって、どの範囲が受け付け可能なのかをクライアントに伝えます。

その他に、コンテンツ・ネゴシエーションの機能を高めるために下記のヘッダーがよく使われます。

  • User-Agent ヘッダーもコンテンツ・ネゴシエーションに使われます。User-Agent リクエスト・ヘッダー・フィールドには、そのリクエストを発行したユーザー・エージェントに関する情報が含まれています。サーバーはこの情報を統計用、プロトコル違反の追跡用、ユーザー・エージェントの自動認識用に利用することで、ユーザー・エージェント特有の制約を回避したレスポンスを返すことが可能になります。
  • q 値 (qvalue) も使われます。HTTP のコンテンツ・ネゴシエーションでは、短い浮動小数点数を使うことによって、ネゴシエーション対象のさまざまなパラメーターの相対的な重要性、つまり重み付けを表現します。重みは、0 を最小値、1 を最大値として、0 から 1 の範囲の実数に正規化された値です。あるパラメーターの q 値が 0 であるとすると、このパラメーターで指定されたコンテンツはクライアントにとって「not acceptable (受け付け不可)」です。

HTTP 仕様ではコンテンツ・ネゴシエーションの手法がいくつか定義されています。この記事では、サーバー駆動型コンテンツ・ネゴシエーションと呼ばれる手法を扱います。また HTTP 仕様では、他にも下記の 2 つのタイプのコンテンツ・ネゴシエーションが規定されています。

  • エージェント駆動型ネゴシエーションでは、ユーザー・エージェントがサーバーから最初のレスポンスを受信した後、レスポンスの表現に最適なものを選択します。この選択の元となるのは、最初のレスポンスのヘッダー・フィールドまたはエンティティー・ボディーに含まれる、利用可能なレスポンス表現のリストです (それぞれの表現は独自の URI によって識別されます)。レスポンス表現の選択は自動的に行われる場合 (ユーザー・エージェントが自動選択に対応している場合) も、ユーザーが手動で行う場合 (生成されたメニュー (おそらく HTML のメニュー) からユーザーが選択します) もあります。HTTP/1.1 では、サーバーがサーバー駆動型ネゴシエーションを使って、さまざまなレスポンス表現から 1 つを指定することが不可能な場合に、エージェント駆動型ネゴシエーションを実現するためのステータス・コードとして 300 (Multiple Choices: 複数から選択) と 406 (Not Acceptable: 受け付け不可) が定義されています。
  • 透過ネゴシエーションは、サーバー駆動型ネゴシエーションとエージェント駆動型ネゴシエーションの両方を組み合わせたものです。あるサーバーのリソースへのリクエストに対して、利用可能なレスポンス表現のリスト (エージェント駆動型ネゴシエーションで提供されたのと同様のもの) がキャッシュにあり、しかも各リクエスト・ヘッダー・フィールドの候補に対応するレスポンスもキャッシュにある場合、このリソースに送られるリクエストには、サーバーに代わってキャッシュが対応することでサーバー駆動型ネゴシエーションを行うことができます。

この記事では、Accept ヘッダーを使用してメディア・タイプのネゴシエーションを行うサーバー駆動型コンテンツ・ネゴシエーションに焦点を絞りますが、ここで説明する問題や手法は実際には他の Accept ヘッダーにも当てはまります。またメディア・タイプは、ユーザーが最もよく扱うアプリケーション・タイプとそのサブ・タイプの組み合わせを使用します。

クライアントは、RESTful なリソースに対して Accept ヘッダーを使って、その要求を伝えます。その一方でレスポンス・ヘッダーには、実際のエンティティー・ボディーのメディア・タイプを伝えるための Content-Type ヘッダーがあります。例えば、ある HTTP GET リクエストは、そのクライアントがサポートするメディア・タイプのリストを Accept ヘッダーに含めているかもしれません。するとサーバーは、(可能であれば) 指定されたタイプのリソース表現を返し、そのクライアントに提供される実際のメディア・タイプをレスポンスの Content-Type ヘッダーで指定します。


Accept ヘッダーと URI パラメーター

コンテンツ・ネゴシエーションは、標準に従わない方法で URI を使って行われる場合があるということは、説明しておく必要があります。リソースの URI を変更することでリソース表現のネゴシエーションをする方法に関しては、仕様や標準がありません。URI を使うとブラウザーの中でテストをできるようになることが多いので、URI は便利です。例えば、/document?format=atom は Atom フィードを返します。他にも下記のように便利な慣習として登場した手法がいくつかあります。

  • ドット表記を使う方法 (例えば /document.html/document.json など)
  • クエリー・パラメーターを使う方法 (例えば /myResource?format=json など)

ドット表記は静的なリソースに対して有効です。例えば document.html と document.pdf が Web サーバーに保存されている場合、この 2 つは異なるリソースです。

しかし同じリソースを別フォーマットで表現したものを要求する手段として、ドット表記を使うと、リソースが動的な場合には混乱を招く可能性があります。これは、静的な場合にはドット表記が 2 つの異なるリソースを意味しても、動的な場合には同じリソースにすぎないためです。また、メディア・タイプに関するコンテンツ・ネゴシエーションはコンテンツ・ネゴシエーションの 1 つのタイプにすぎません。メディア・タイプに関するコンテンツ・ネゴシエーションを先ほど説明した他のタイプのコンテンツ・ネゴシエーションと組み合わせると、見た目が変な URL になってしまいます。例えば、あるリソースの isso-8859 JSON バージョンを英語で要求したとすると、/document.json.en.iso-8859-5 のような URI を指定する羽目になります。

クエリー・パラメーターはコンテンツ・ネゴシエーションによく使われます。その理由は、ブラウザーを使って素早くテストをして結果を知りたいことが多いからです。クエリー・パラメーターを追加すると、そうしたことを手軽に行うことができます。クエリー・パラメーターを使う場合には、異なるリソースを扱っているかのような感覚に陥ることはありません。

しかし通常、クライアント・アプリケーションから利用するために構築された動的な RESTful サービスは、Ajax を使ってブラウザーから利用するか、あるいは別のサーバー・アプリケーションから利用します。コンテンツ・ネゴシエーションを行うために URI を変更すると、特定のリソース表現へのリンクを共有する場合やブラウザーの中でテストする場合には便利です。しかしアプリケーションは、最初は標準的な HTTP ヘッダーを利用し、それから、指定された URI の慣習に従うべきです。クエリー・パラメーターは通常、サービスに対する入力 (フィルタリング基準、ソート、その他ビジネス・レベルの詳細など) を提供するために使われます。コンテンツ・ネゴシエーションのヘッダーを使えば、IT の関心事とビジネスの関心事を切り分けることができます。また、リクエストは通常、ファイアーウォールやプロキシーなどのサーバーを通過します。これらの HTTP プロキシーは多くの場合、標準的な HTTP ヘッダーを認識することができ、またキャッシングなどの非機能要件を提供する場合もあります。

妥協案として、開発時にはクエリー・パラメーターを使用し、そのアプリケーションをデプロイする際にはクエリー・パラメーターを無効にすることも可能です。

一般的に、アプリケーションのクライアントは最も単純な手法を使う必要があります。HTTP ヘッダーの多くはネットワークの動作を念頭に置いて設計されており、こうしたヘッダーを利用することでブラウザーや中間プロキシーは情報交換を自動化することができます。ビジネス・アプリケーションでは、そこまで高度なレベルは必要としないことが多いものです。


WebSphere sMash を使ってコンテンツ・ネゴシエーションを実装する

コンテンツ・ネゴシエーションに関する背景を少し説明したので、IBM WebSphere sMash を使ってコンテンツ・ネゴシエーションを実装する例を見てみましょう。そのためにはいくつかの方法がありますが、そのうちの 2 つを後ほど説明します。

ここで使用するサンプルは「document」という単純なリソースです。このリソースはハードコーディングされたデータ構造になっており、いくつかのフィールドを含んでいます。もちろんポイントは、コンテンツ・ネゴシエーションのデモをすることにあります。ここでは Accept ヘッダーを使う例を紹介します。クライアントは、Accept ヘッダーを使って、要求するフォーマットを渡します。すると RESTful サービスは、ハードコーディングされた文書を、最も要求に一致するフォーマットで描画します。一致するものが見つからない場合には、アプリケーションは適切な HTTP レスポンス・コード 406 Not Acceptable を返します。これはビジネス・アプリケーションでは極めて一般的な方法です。

WebSphere sMash をダウンロードし、App Builder を実行する

この記事には、ダウンロードしてテストできる WebSphere sMash アプリケーションが含まれています。このアプリケーションを表示、実行するためには、以下のものをダウンロードする必要があります。

WebSphere sMash Developer Edition V1.1 には App Builder が含まれています。App Builder は WebSphere sMash アプリケーションを開発するための Web ベースの統合ツールであると同時に、アプリケーションの開発、テスト、制限付きデプロイメントなどをサポートする安定したランタイムでもあります。コマンド行インターフェース (CLI) には、アプリケーションを開発、実行するための基本となるサポートが含まれています。追加のランタイム・ライブラリーは必要に応じて ProjectZero.org のモジュール・リポジトリーから取得することができます。

CLI の設定は以下の手順で行います。

  1. zero.zip ファイルをダウンロードし、任意のディレクトリーに解凍します。すると「zero」というサブディレクトリーが作成され、その中には CLI を実行するためのコマンドが含まれています。
  2. zero ディレクトリーをユーザーの PATH 環境変数に追加します。
  3. JDK のインストール・ディレクトリーの下にある bin ディレクトリーをユーザーの PATH 環境変数に追加します。

WebSphere sMash の App Builder を使うとアプリケーションを検証することができます。App Builder を起動するためには、(CLI がインストールされた) zero ディレクトリーまでナビゲートして、appbuilder open を実行します。

図 1. App Builder を実行する
App Builder を実行する

サンプルをロードする

サンプル・アプリケーションは、この記事の中に .zip ファイルとして含まれています。このファイルをダウンロードし、任意の空のディレクトリーに解凍します。次に、このアプリケーションを App Builder を使って開きます。Open existing application (既存のアプリケーションを開く) をクリックし (図 2)、ダウンロード・ファイルを解凍した DocumentsSMashApp アプリケーションまでナビゲートし、Open をクリックします (図 3)。すると、このアプリケーションがアプリケーション・リストに表示されるはずです (図 4)。

図 2. サンプル・アプリケーションを開く
サンプル・アプリケーションを開く
図 3. サンプル・アプリケーションの場所を指定する
サンプル・アプリケーションの場所を指定する
図 4. アプリケーション・リスト
アプリケーション・リスト

zero.test を使って JUnit テストを検証し、実行する

これでアプリケーションがロードされたので、テスト・ケースを検証することができます。テスト駆動開発は、サービスを実装する前にテスト・ケースをコーディングするアジャイル開発手法です。この手法には多くの利点があります。この手法で私が気に入っている点は、この手法を使うと問題点を理解することができ、サービスがどのように使われるかを最初に考えるようになることです。

WebSphere sMash V1.1 では zero.test というモジュールが導入されており、このモジュールを利用すると、WebSphere sMash アプリケーションの中でのユニット・テストの実行や、独自のテスト・ハーネス内での WebSphere sMash の実行が容易になります。またこのモジュールには、追加のテスト・ユーティリティーも用意されています。(zero.test モジュールの詳細は Developer’s Guide の Unit Testing セクションを参照してください。)

ここでは下記のテスト・ケースを検証します。まず、アプリケーション・リストの中の DocumentsSMashApp リンクをクリックし (図 4)、次に Explorer リンクをクリックします (図 5)。

図 5. さまざまなアプリケーション
さまざまなアプリケーション

Explorer ビュー (図 6) を利用すると、プロジェクトの構造を調べたり成果物を検証したりすることができます。/app/scripts までナビゲートし (図 6)、ContentNegotiationTest.groovy というスクリプトを開きます。

図 6. Explorer ビュー
Explorer ビュー

このテスト・ケースの最初のセクションは以下のとおりです。これは Groovy で作成された単純な JUnit テストです。

  • リスト 1 に示すクラスは、いくつかのクラス変数を定義します。Abdera メンバーと Parser メンバーは WebSphere sMash に同梱されている Apache Abdera API の一部です。このコードはこれらのクラスを使うことによって、適切な Atom が返されたかどうかを検証します。
  • 次の変数は URI のリストです。この 2 つの URI は、この記事で説明する 2 つの異なる手法を表しています。どちらの URL も同じ UnitTest を使います。
  • 次に、「callResource」というタイトルの特別なユーティリティー・メソッドがあります。このメソッドは渡される URL に対して、WebSphere sMash API を使って HTTP GET を実行します。またこのメソッドは、HTTP Accept ヘッダーがある場合には、このヘッダーに値を設定します。
リスト 1
public class ContentNegotiationTest 
{ 
    Abdera abdera = new Abdera(); 
    Parser parser = abdera.getParser(); 

    def URIS = ["http://localhost:8080/resources/document",
                "http://localhost:8080/custom/document"];      

    private Connection.Response callResource(uri,acceptHeader)throws  Exception 
    { 
Connection conn = new    Connection(uri,Connection.Operation.GET); 
      if (acceptHeader) conn.addRequestHeader("Accept", acceptHeader); 
      return conn.getResponse(); 
      }

ここには 3 つのテスト・メソッドがあります。最初の 2 つのメソッドは必要な 2 つのメディア・タイプ (Atom と JSON) をテストします。3 番目のメソッドは 406 エラーの条件をテストします。以下に示すのは Atom に対するテスト・ケースです。

  • Accept ヘッダーのメディア・タイプの値のリストがあり、これらの値の場合には Atom が返されるはずです。Accept ヘッダーにはクライアントが要求するタイプをカンマ区切りリストとして含められるため、さまざまなタイプをテストすることが重要です。この例は、Atom が返される例です。

  • テスト・ケース (リスト 2) はまず、このサービスを実装する各 URI をループします。

    リスト 2
    	@Test 
    void testAtomXml() 
     { 
         try { 
             def acceptHeaders = ["application/xml", 
                                  "application/atom+xml", 
                                  "application/xml,application/json", 
                                  "application/atom+xml,application/json", 
                                   null, //Default 
                                  "application/atom+xml;type=feed", 
                                  "text/html,application/atom+xml"];             
             for(uri in URIS) 
             { 
                     
                 for(acceptHeader in acceptHeaders) 
                 { 
                     System.out.println(); 
                     System.out.println(uri + " with Accept set to " + acceptHeader); 
                     Connection.Response resp = callResource(uri,acceptHeader); 
                     Document<Feed> xmlDoc = parser.parse(resp.getResponseBodyReader()); 
                     Feed feed = xmlDoc.getRoot(); 
                     assertNotNull(feed); 
                     assertEquals(resp.getResponseHeader
    				("Content-Type")[0],"application/atom+xml"); 
                     Writer xmlAtomWriter = 
    				abdera.getWriterFactory().getWriter("prettyxml"); 
                     StringWriter stringWriter = new StringWriter(); 
                     feed.writeTo(xmlAtomWriter, stringWriter); 
                     System.out.println(stringWriter.toString()); 
                     System.out.println(); 
                 } 
             } 
         } catch (Exception e) { 
             System.out.println(e.getMessage()); 
             System.out.println(); 
             fail(e.getMessage()); 
         } 
     }
  • 内側のループは各メディア・タイプを調べ、callResource ユーティリティー・メソッドを呼び出します。

同様に、JSON に対するテスト・ケース (リスト 3) では、JSON (つまり application/json) を返すはずの一連のメディア・タイプをリストにしています。このリストには JSON と Atom の両方をサポートするものも含まれていますが、ここでは JSON が最初に記載されています。このテスト・ケースでも先ほどと同様に、このサービスを実装する URI と、サポートされているメディア・タイプをループします。次に、適切な JSON タイプがあること、そして content-type レスポンス・ヘッダーが JSON であることを検証します。

リスト 3
@Test 
    void testJSON() 
    { 
        try { 
            def acceptHeaders = ["application/json", 
                                   "application/json,application/xml", 
                                   "application/json,application/atom+xml", 
                                   "text/html,application/json,text/xhtml"]; 
            for(uri in URIS) 
            { 
                for(acceptHeader in acceptHeaders) 
                { 
                    System.out.println(); 
                    System.out.println(uri + " with Accept set to " + acceptHeader); 
                    Connection.Response resp = callResource(uri,acceptHeader); 
                    def json = Json.decode(resp.getResponseBodyInputStream()); 
                    assertNotNull(json); 
                    assertEquals(resp.getResponseHeader
				("Content-Type")[0],"application/json"); 
                    System.out.println(json); 
                    System.out.println(); 
                } 
            } 
        } catch (Exception e) { 
            System.out.println(e.getMessage()); 
           System.out.println(); 
            fail(e.getMessage()); 
        } 
        }

コードの中でエラー条件をテストすることは非常に重要です。最後のテスト・ケース (リスト 4) では、無効なメディア・タイプのリストを作成します。無効なメディア・タイプの場合には HTTP レスポンス・コードとして 406 を返す必要があります。このテスト・ケースの場合も先ほどと同様、URI とメディア・タイプをループしてサービスをテストします。

リスト 4
    @Test 
    void test406() 
    {    
        try { 
            def acceptHeaders = ["application/ftp", 
                                   "text/html", 
                                   "application/atom+xml;type=entry", 
                                   "junk", 
                                   "text/json"]; 
            for(uri in URIS) 
            { 
                for(acceptHeader in acceptHeaders) 
                { 
                    System.out.println(); 
                    System.out.println(uri + " with Accept set to " + acceptHeader); 
                    Connection.Response resp = callResource(uri,acceptHeader); 
                    assertEquals("406",resp.getResponseStatus()); 
                    System.out.println(resp.getResponseStatus()); 
                    System.out.println(); 
                } 
            } 
        } catch (Exception e) { 
            
           System.out.println(e.getMessage()); 
           System.out.println(); 
           fail(e.getMessage()); 
        } 
    } 
     
     }

サービスそのものを検証する前に、テスト・ケースを実行します。WebSphere sMash アプリケーションの中でユニット・テストを実行するためには、そのアプリケーションに zero.test モジュールを追加する必要があります。また Atom 用に Abdera API が必要なので、zero.atom モジュールも追加する必要があります。この記事に提供されているサンプル・アプリケーションでは、これらの依存関係が既に宣言されています。Dependencies タブをクリックすると (図 7)、core、atom、test という各モジュールが追加されていることがわかります。

図 7. アプリケーションの依存関係
アプリケーションの依存関係

パネルの右上隅にある Start ボタンをクリックしてアプリケーションを起動します (図 8)。

図 8. アプリケーションを起動する
アプリケーションを起動する

WebSphere sMash の App Builder を使うと WebSphere sMash の CLI をブラウザーの中で直接実行することができます。Console タブをクリックし (図 9)、zero update と入力します。これにより、確実にすべての依存関係がローカル・リポジトリーの中にロードされます。次に、zero test と入力します。すると、テスト・タスクは /app/scripts の中にあるスクリプト、または「Test」という名前で終わる Java™ クラスを検索し、それを JUnit テストとして実行します。その結果、図 10 のような成功メッセージが表示されるはずです。

図 9. CLI (コマンド行インターフェース)
CLI (コマンド行インターフェース)
図 10. 成功メッセージ
成功メッセージ

システムから出力される表示内容を上にスクロールし、サービスが実行されていること、またネゴシエーションによるレスポンスを確認します。

図 11. システムから出力されるメッセージ
システムから出力されるメッセージ

これでサービスをテストできたので、2 つの異なる実装を調べることにしましょう。

リソース・ハンドラーとスクリプトを使ったコンテンツ・ネゴシエーション

1 番目の方法では、RESTful なリソース・ハンドラーを使います。WebSphere sMash の目標の 1 つは、規約を提供することによってアプリケーションの実装を容易にすることです。その規約の 1 つがリソース・ハンドラーを使うことです。リソース・ハンドラーによって、コレクションとメンバーというパラダイムに従ったリソースを実装します。コレクションには、追加、取得、更新、そして削除が可能なメンバー項目があります。またコレクションのメンバーのリストを取得することもできます。この例を表 1 に示します。

表 1
HTTP メソッドURI説明
GET/document文書のリストを返します
POST/documentメンバー文書を作成します
GET/document/myDocmyDoc というメンバー文書を取得します
PUT/document/myDocmyDoc というメンバー文書を更新します
DELETE/document/myDocmyDoc というメンバー文書を削除します

WebSphere sMash は <apphome>/app/resources という仮想ディレクトリーの中でコレクション・モデルをネイティブでサポートしています。resources ディレクトリーの中の各スクリプトはコレクションとメンバーの操作を実装するリソース・ハンドラーを表します。リソース・ハンドラーにアクセスするためには下記のパターンに従う単純な URL 表現を使います。

/resources/<collection name>[/<member identifier>[/<path info>]]

例えば、コレクション名が「people」で、メンバー ID が「1」の /resources/people/1 に対するリクエストを考えてみてください。コレクション名はリソース・ハンドラーを特定します。この場合のリソース・ハンドラーは <apphome>/app/resources/people.groovy です。また、メンバー ID の値は <collection name>Id という名前を持つリクエスト・パラメーターの値として提供されます。したがって、zget("/request/params/peopleId") == 1 です。リソース・ハンドラーはリソースの CRUD (create, retrieve, update, and delete) イベントを処理するために設計された、WebSphere sMash のイベント・ハンドラーです。CRUD はデータ (この場合は HTTP リソース) に対して一般的に実行される操作です。

HTTP メソッドはコレクションとメンバーのイベントにマッピングされます (表 2)。

表 2
リソースGETPUTPOSTDELETE
コレクションlistputCollectioncreatedeleteCollection
メンバーretrieveupdatepostMemberdelete

つまり、/resources/document という URI を持つ文書のコレクションを一覧表示するハンドラーを作成するためには、document.groovy という名前のスクリプトをアプリケーションの /app/resources ディレクトリーに作成します。この単純なサンプルでは文書のコレクションに対する HTTP GET メソッドに応答するだけでよいため、スクリプトに必要なものは onList() メソッドのみです。(WebSphere sMash で RESTful なリソース・ハンドラーを作成する方法の詳細については、Developer’s Guide の REST プログラミングに関するセクションと、チュートリアル「Web アプリケーションのための RESTful なサービスを作成する」を参照してください。)

リソース・ハンドラーを表示するためには、Explorer タブに戻ります。/app/resources フォルダーを展開し、document.groovy を開きます (図 12)。このコードをリスト 5 に示します。

図 12. リソース・ハンドラー
リソース・ハンドラー
リスト 5
def onList() 
{ 
    def mediaType = request.headers.in.Accept[]; 
     
    /* if Accept Header is missing, HTTP Spec says 
     * you should assume client supports all types of media types. 
     */ 
    if(!mediaType) mediaType = config.defaultMediaType[]; 
     
    // Media types can be a comma separated list 
    def acceptTypeHeaders = mediaType.split(","); 
    for (acceptTypeHeader in acceptTypeHeaders) 
    { 
        //Media types can have profiles such as application/atom+xml;type=entry 
        def profile = acceptTypeHeader.split(";"); 
        System.out.println (profile) 
        def contentType = profile[0]; 
        if(contentType == "application/atom+xml" || contentType == "application/xml") 
        { 
            if(profile.size() > 1) 
            { 
                /*Checking of client is asking for atom entry only,  
                but this resource can only render feeds*/ 
                def nameValue = profile[1].split("="); 
                def key = nameValue[0]; 
                def value = nameValue[1]; 
                 
 
                if(key == "type") if (value != "feed") continue; 
            } 
            request.status = HttpURLConnection.HTTP_OK; 
            request.view = 'atom' 
            request.atom.output = DocData.dummyPayload 
            render() 
            return; 
        } 
        else if (contentType == "application/json") 
        { 
            request.status = HttpURLConnection.HTTP_OK; 
            request.view = 'JSON' 
            request.json.output = DocData.dummyPayload 
            render() 
            return; 
        } 
    } 
 
    request.status = HttpURLConnection.HTTP_NOT_ACCEPTABLE; 
    request.view = 'error'; 
    render(); 
    return; 
}

WebSphere sMash を利用すると、HTTP ヘッダー、クエリー・パラメーター、ペイロード、その他の HTTP 成果物に対して、自動的に値が設定されるデフォルトのスクリプト変数を使って容易にアクセスすることができます。リスト 5 で行っているのは以下の内容です。

  • Accept ヘッダーへのアクセスはグローバル・コンテキストのリクエスト・ゾーンから行います。グローバル・コンテキストは WebSphere sMash アプリケーションのすべての状態を維持しています。グローバル・コンテキストにはいくつかのゾーンがあり、それぞれのゾーンがデータに対して異なるライフサイクルを実装します。例えばリクエスト・ゾーンには、現在の HTTP リクエストに対するすべてのデータが含まれています。ここでは Accept ヘッダーにアクセスするのに、request.headers.in.Accept[] のようにしました。これはグローバル・コンテキストにアクセスするための 1 つの方法です。(グローバル・コンテキストに関する詳細は「参考文献」を参照してください。)
  • Accept ヘッダーがヌルの場合には、クライアントは任意のタイプを受け付けることになります。そこで、デフォルトの値を設定しています。デフォルトにアクセスするためには、グローバル・コンテキストの config ゾーンから config.defaultMediaType[] のようにします。構成は config ゾーンに含まれています。config ゾーンには、アプリケーションの config/zero.config ファイルの構成が追加されます。(Developer's Guide の構成に関するセクションを参照してください。)
    リスト 6
    #Resource Handler Default Media Type 
    /config/defaultMediaType="application/atom+xml"
  • このコードは次に、Groovy のストリング操作を使って Accept ヘッダーを解析します。コードはカンマ区切りリストを調べ、最初に一致するものを見つけ、そのプロファイルを抽出します。一致するものが見つかると、そのデータ構造を取得し、デフォルトの WebSphere sMash レンダリング・フレームワークを使ってそのデータ構造をレンダリングします。例えば application/json が見つかった場合には、リスト 7 のコードが実行されます。
    リスト 7
    request.status = HttpURLConnection.HTTP_OK;
    request.view = 'JSON' 
    request.json.output = DocData.dummyPayload 
    render()
  • これを見ると、Atom のレンダリング・セクションもあることに気付くと思います。一致するものが見つからない場合、このコードはレスポンス・ステータスに NOT_ACCEPTABLE 変数を設定します。

この手法の利点は、非常に正確な操作を行うことができ、要求どおりのものが得られることです。しかしこの手法では、管理不能なコードを大量に生成してしまう可能性があります。皆さん自身がスクリプトを作成する場合には、おそらく再利用可能なライブラリーを作成してコードを外部化することでしょう。

カスタム・ハンドラーと正規表現を使ったコンテンツ・ネゴシエーション

2 番目の方法はカスタム・ハンドラーを作成する方法です。カスタム・ハンドラーを作成するためには zero.config ファイル内のルールを構成します。すべてのコンテンツ・ネゴシエーション・ロジックを 1 つのハンドラーに処理させるのではなく、zero.config ファイルが Java の正規表現を使って最適なハンドラーを見つけます。app/scripts ディレクトリー配下には、3 つのハンドラーがあることがわかります (図 13)。それぞれのハンドラーは、要求されるメディア・タイプ用の特定のレンダラーを実装します。

図 13. カスタム・ハンドラー
カスタム・ハンドラー

図 14 は onGET メソッドを使って GET イベントを取得するハンドラーを示しています。このハンドラーはデータを Atom としてレンダリングします。

図 14. データを Atom としてレンダリングするカスタム・ハンドラー
データを Atom としてレンダリングするカスタム・ハンドラー

同様に、customJSONDocument.groovy ファイルは JSON をレンダリングします (図 15)。

図 15. データを JSON としてレンダリングするカスタム・ハンドラー
データを JSON としてレンダリングするカスタム・ハンドラー

最後に、406 レスポンス・コードを送信するレンダラーがあります (図 16)

図 16. エラー・コードをレンダリングするカスタム・ハンドラー
エラー・コードをレンダリングするカスタム・ハンドラー

適切なハンドラーを呼び出すためには、zero.config ファイルにマッチング・ルールが記述されている必要があります。config ディレクトリーの下にある zero.config ファイルを開きます (図 17)。

図 17. zero.config を開く
zero.config を開く

zero.config ファイルには各ハンドラー用に 1 つずつ、3 つの構成が含まれており、ハンドラーの構成は Handlers の下にある config ゾーンの下で行われます。これらのハンドラーに対して、イベント (GET など)、ハンドラー、条件を定義します。それぞれのハンドラーの条件の定義では、パスと HTTP GET メソッドに対して正確に突き合わせを行い、=~ と Java の正規表現を使ってマッチする条件を表現します。

WebSphere sMash では行頭 (^) と行末 ($) での行境界の正規表現が追加されています。(境界でのマッチについては、「Boundary Matchers」チュートリアルを参照。) Atom 表現の場合には表現がもっと複雑です。WebSphere sMash は application/xml または application/atom+xml (後に ;type=entry が続いていないもの) を検索し、application/xml または application/atom+xml の前に application/json がないかどうかを、否定後読みを使ってチェックします。また、Accept ヘッダーがない (つまりデフォルト・ケースのヌル) かどうかもチェックします。WebSphere sMash は 1 つの HTTP メソッドに対して 1 つのイベントしか出力しないので、Atom に一致するものが見つかれば Atom を出力し、Atom が見つからない場合には JSON を探します。この場合、前に Atom のエントリーがないかどうかのチェックは必要ありません。Atom に一致するものが見つからなかったということは、ストリングに Atom が含まれていないということだからです。そして最後のチェックでは、Accept ヘッダーにデータが入っているかどうかを調べます。もし入っていれば、そのレスポンスは無効であり、406 が返されます。

リスト 8
#ATOM
/config/handlers += [ 
{ 
    "events" : "GET", 
    "handler" : "customATOMDocument.groovy", 
    "conditions": "(/request/path == /custom/document)  &&
      (/request/method == GET) && (/request/headers/in/Accept =~ 
	(.*)(?<!(application/json,))application/(atom\\+)??xml(?!;type=entry)
	(.*)) ||  !(/request/headers/in/Accept)" 
}] 
 
#JSON
/config/handlers += [ 
{ 
    "events" : "GET", 
    "handler" : "customJSONDocument.groovy", 
    "conditions": "(/request/path == /custom/document)  && 
	(/request/method == GET) && (/request/headers/in/Accept =~ 
	(.*)application/json(.*))" 
}] 
 
#406
/config/handlers += [ 
{ 
    "events" : "GET", 
    "handler" : "notAcceptCustomDocument.groovy", 
    "conditions": "(/request/path == /custom/document)  && 
	(/request/method == GET) &&  (/request/headers/in/Accept) " 
}]

カスタム・ハンドラーを使用するとコンテンツ・ネゴシエーションのロジックを単純化することができますが、大量の構成と格闘する羽目になる可能性があり、またコレクションとメンバーのようなパターンを独自に処理する必要があるかもしれません。さらに、望みどおりの正確さが得られないことがあります。絶対に正確な方法は否定後読みを使う方法のみかもしれません。例えば application/json,application/xml は適切に処理されて JSON を返しますが、application/json,application/html,application/xml は JSON を返さずに Atom を返します。なぜなら JSON が application/xml または application/atom+xml の直前にはないからです。ヘッダーに入れられる内容を指定できる場合には、これは問題にならないかもしれません。


まとめ

コンテンツ・ネゴシエーションは RESTful なアーキテクチャーの重要な一部分です。この記事では、WebSphere sMash アプリケーションの中にコンテンツ・ネゴシエーションの機能を組み込む方法を説明しました。


謝辞

この記事を校閲してくださった Karl Bishop 氏と Brandon Smith 氏に感謝いたします。


ダウンロード

内容ファイル名サイズ
Code sampleDocumentsSMashApp.zip76 KB

参考文献

学ぶために

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

コメント

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=WebSphere, Web development
ArticleID=370320
ArticleTitle=REST リソースに対するサーバー駆動型コンテンツ・ネゴシエーションの実装とテストを WebSphere sMash を使って行う
publish-date=01212009