Grails をマスターする: JSON と Ajax による非同期 Grails

Grails での Google Maps のマッシュアップ

Web 2.0 開発には、JSON (JavaScript Object Notation) と Ajax (Asynchronous JavaScript + XML) が不可欠です。連載「Grails をマスターする」では今回、Scott Davis がこの Web フレームワークに JSON と Ajax ならではの機能を組み込む方法を紹介します。

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年 11月 18日

この記事では相補的な技術、JSON と Ajax に対する Grails のサポートを取り上げます。これまで「Grails をマスターする」ではサポート的な役割を果たしてきたこの 2 つの技術が、今回は話題の中心となります。今回の記事では、組み込み Prototype ライブラリーならびに Grails の <formRemote> タグを使用して Ajax リクエストを実行する方法を説明するとともに、ローカルの JSON を扱う例と、Web 経由でリモートからの JSON を動的に取り入れる例も紹介します。

以上の内容を実際の動作による例を使って見ていくために、旅行プランを立てるためのページを組み立てます。このページではユーザーが出発地と到着地の空港を入力し、Google Maps に空港が表示されたら、ユーザーはリンクを使って到着地の空港周辺にあるホテルを検索することができます。図 1 に、このページを実際に使用している様子を示します。

図 1. 旅行プラン用ページ
旅行プラン用ページ

この機能は、約 150 行のコードを単一の GSP ファイルと 3 つのコントローラーに散りばめることで、完成させることができます。

Ajax およびJSON 小史

この連載について

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

1990年代中頃、初めて Web に人気が出てきた当時、ブラウザーでは簡単な HTTP リクエストしか実行することができませんでした。ハイパーリンクやフォームの送信ボタンをクリックすると、ページ全体が消去され、新しい結果がそこに置き換わるといった具合です。ページを中心としたナビゲーションにはこれで問題ありませんでしたが、ページ上の個々のコンポーネント自体を単独で更新することはできませんでした。

1999年になると、Microsoft® が Internet Explorer 5.0 のリリースを機に XMLHTTP オブジェクトを導入しました。この新しいオブジェクトによって、開発者は HTML ページの構成要素のごく一部の要素に関する『マイクロ』HTTP リクエストを行えるようになり、しかもそれ以外の要素は HTML ページに表示したまま行えるようになりました。この機能は W3C (World Wide Web Consortium) 標準に基づくものではなかったものの、Mozilla チームはその可能性を認識し、2002年の Mozilla 1.0 のリリースに XMLHttpRequest (XHR) オブジェクトを追加しました。それ以来、このオブジェクトはデファクト・スタンダードとなり、代表的なすべての Web ブラウザーがサポートするようになりました。

2005年に一般公開された Google Maps は、非同期の HTTP リクエストをフルに活用している点で、当時 Web に公開されていた他の地図サイトとは明らかな対照を成していました。Google Maps をパンするときには、クリックしてからページ全体がリロードされるのを待たずに、そのまま続けてマウスを使ってマップを上下左右にスクロールします。Jesse James Garrett がブログの投稿のなかで、Google Maps に採用されている一連の技術を Ajax というキャッチーな簡略表現で表したことから、それ以来、この名前が使用されるようになっています (「参考文献」を参照)。

最近では、Ajax は特定の技術の集合を表す用語というよりは、「Web 2.0」アプリケーション全般を表すようになってきています。Ajax リクエストは非同期で、JavaScript によって行われるのが通常ですが、そのレスポンスは必ずしも XML であるとは限りません。ブラウザー・ベースのアプリケーション開発における XML の問題は、使いやすいネイティブ JavaScript パーサーがないことです。もちろん JavaScript DOM API を使用すれば XML を構文解析することはできますが、慣れていない人にとっては簡単なことではありません。このような理由から、Ajax Web サービスでは結果をプレーン・テキスト、HTML スニペット、あるいは JSON で返す場合がよくあります。

2006年7月には、Douglas Crockford が IETF (Internet Engineering Task Force) に JSON を記述する RFC 4627 を提出し、その年の終わりまでには、Yahoo! や Google などの主要なサービス・プロバイダーは XML の代わりとして JSON 出力を提供するようになりました (「参考文献」を参照)。ちなみに、この記事では後で Yahoo! の JSON Web サービスを利用します。


JSON の利点

JSON を Web 開発に使用した場合には、2 つの大きな利点があります。1 つめの利点は何よりもまず、JSON は XML よりも簡潔であるということです。JSON オブジェクトは、カンマで区切られた単なる name:value のペアが中括弧でラップされているだけですが、それとは対照的に、XML は開始タグと終了タグの 2 つのタグを使ってデータ値をラップします。そのため、同じものを JSON で表現した場合と比べると、XML ではメタデータのオーバーヘッドが 2 倍になります。これが、Crockford に JSON を「the fat-free alternative to XML (XML に代わる無駄のない表現手段)」と言わしめた理由です (「参考文献」を参照)。Web 開発で「シン・パイプ」(細い回線) を扱っている場合には、バイト数のあらゆる削減がそのままパフォーマンスの向上として反映されてきます。

JSON と XML では、同じ情報をそれぞれどのように編成するかをリスト 1 に示します。

リスト 1. JSON と XML の比較
{"city":"Denver", "state":"CO", "country":"US"}

<result>
  <city>Denver</city>
  <state>CO</state>
  <country>US</country>
</result>

JSON オブジェクトは、Groovy プログラマーにとっては馴染みがあるように見えるはずです。中括弧を大括弧に置き換えてみると、Groovy で HashMap を定義しているのと同じになります。大括弧について言えば、JSON オブジェクトの配列を定義する方法は、Groovy オブジェクトの配列を定義する方法とまったく同じです。JSON 配列はリスト 2 に示すように、カンマで区切られた一連の値を単に大括弧でラップしたものです。

リスト 2. JSON オブジェクトのリスト
[{"city":"Denver", "state":"CO", "country":"US"},
 {"city":"Chicago", "state":"IL", "country":"US"}]

JSON のもう 1 つの利点は、JSON オブジェクトを構文解析して作業すると明らかになります。JSON をメモリーにロードするには、eval() を 1 回呼び出せばよいだけのことです。いったん JSON がロードされると、どのフィールドにでも、その名前を使って直接アクセスすることができます (リスト 3 を参照)。

リスト 3. JSON のロードとフィールドの呼び出し
var json = '{"city":"Denver", state:"CO", country:"US"}'
var result = eval( '(' + json + ')' )
alert(result.city)

Groovy の XmlSlurper でも、同じように XML 要素に直接アクセスすることができます (XmlSlurper の操作は、「Grails サービスと Google Maps」で経験済みです)。最近の Web ブラウザーがクライアント・サイドの Groovy をサポートしているとしたら、私はそれほど JSON には興味を持たなかったと思います。しかし残念ながら、Groovy は完全にサーバー・サイドのソリューションなので、クライアント・サイドの開発となると JavaScript を選択せざるを得ません。そこで、私はサーバー・サイドでは Groovy で XML を操作し、クライアント・サイドでは JavaScript で JSON を操作するようにしています。どちらの操作を行う場合でも、最小限の作業でデータを扱うことができています。

これで JSON についてざっとは把握してもらえたと思うので、いよいよ Grails アプリケーションに独自の JSON を生成させる作業に取り掛かります。


Grails コントローラーでの JSON のレンダリング

Grails コントローラーから JSON を返したのは、「Ajax をほんの少し加えた多対多の関係」が最初でした。リスト 4 のクロージャーは、その時に作成したクロージャーと似ていますが、このクロージャーにアクセスするには、お馴染みの URI (Uniform Resource Identifier) を使用するという点が異なります (この手法については、「RESTful な Grails」で説明しています)。また、「Grails アプリケーションのテスト」で取り上げた Elvis 演算子も使用しています。

iata という名前のクロージャーを、「Grails とレガシー・データベース」で作成した grails-app/controllers/AirportMappingController.groovy クラスに追加してください。その際に、ファイルの先頭で grails.converters パッケージをインポートすることも忘れないでください (リスト 4 を参照)。

リスト 4. Groovy オブジェクトから JSON への変換
import grails.converters.*
class AirportMappingController {
    def iata = {
      def iata = params.id?.toUpperCase() ?: "NO IATA"
      def airport = AirportMapping.findByIata(iata)
      if(!airport){
        airport = new AirportMapping(iata:iata, name:"Not found")
      }
      render airport as JSON
    }
}

ブラウザーに http://localhost:9090/trip/airportMapping/iata/den と入力すると、リスト 5 に記載する JSON 形式での結果が表示されるはずです。

リスト 5. JSON 形式での有効な AirportMapping オブジェクト
{"id":328,
"class":"AirportMapping",
"iata":"DEN",
"lat":"39.858409881591797",
"lng":"-104.666999816894531",
"name":"Denver International",
"state":"CO"}

さらに、http://localhost:9090/trip/airportMapping/iatahttp://localhost:9090/trip/airportMapping/iata/foo と入力することで、「Not Found」が返されることも確認することができます。その場合の無効な JSON オブジェクトをリスト 6 に示します。

リスト 6. JSON 形式での無効な AirportMapping オブジェクト
{"id":null,
"class":"AirportMapping",
"iata":"FOO",
"lat":null,
"lng":null,
"name":"Not found",
"state":null}

もちろん、この「肝試し」(簡単なテスト) は実際のテスト・セットに代わるものではありません。


コントローラーのテスト

test/integration に AirportMappingControllerTests.groovy を作成し、リスト 7 に記載する 2 つのテストを追加してください。

リスト 7. Grails コントローラーのテスト
class AirportMappingControllerTests extends GroovyTestCase{
  void testWithBadIata(){
    def controller = new AirportMappingController()
    controller.metaClass.getParams = {->
      return ["id":"foo"]
    }
    controller.iata()
    def response = controller.response.contentAsString
    assertTrue response.contains("\"name\":\"Not found\"")
    println "Response for airport/iata/foo: ${response}"
  }
  void testWithGoodIata(){
    def controller = new AirportMappingController()
    controller.metaClass.getParams = {->
      return ["id":"den"]
    }
    controller.iata()
    def response = controller.response.contentAsString
    assertTrue response.contains("Denver")
    println "Response for airport/iata/den: ${response}"
  }
}

テストを実行するには、$grails test-app と入力します。すると JUnit HTML レポートには、図 2 のようにテストにパスしたことが表示されるはずです (Grails アプリケーションのテストが初めての方は、「Grails アプリケーションのテスト」を参照してください)。

図 2. JUnit でのテストの合格結果
JUnit でのテストの合格結果

リスト 7testWithBadIata() で何が行われているのかを説明すると、最初の行では (当然のことながら) AirportMappingController のインスタンスを作成します。このインスタンスを作成するのは、後で controller.iata() を呼び出して、最終的に生成される JSON に対してアサーションを書き込むためです。この呼び出しを失敗 (この場合に該当) または成功 (estWithGoodIata() の場合) に終わらせるには、params ハッシュマップに id エントリーを提供する必要があります。通常、params に保管されるのは構文解析されたクエリー・ストリングですが、この例では構文解析対象の HTTP リクエストがありません。そのため、この例ではその代わりとして、Groovy メタプログラミングを使用して getParams メソッドを直接オーバーライドし、返される HashMap に期待される値が含まれるようにしています (Groovy でのメタプログラミングについての詳細は、「参考文献」を参照してください)。

これで、JSON 生成プログラムを動作させてテストし終わったので、今度は Web ページから JSON を使用する場合についての説明に焦点を絞ります。


Google Maps の初期セットアップ

この旅行プラン用ページには、http://localhost:9090/trip/trip/plan でアクセスできるようにしたいので、plan クロージャーを grails-app/controllers/TripController.groovy に追加します (リスト 8 を参照)。

リスト 8. コントローラーのセットアップ
class TripController {
  def scaffold = Trip
  def plan = {}
}

plan()render() または redirect() で終わっていないため、Convention over Configuration の原則により、grails-app/views/trip/plan.gsp が表示されることになります。次に、リスト 9 の HTML コードを使用してファイルを作成します (この Google Maps の基礎について復習するには、「Grails サービスと Google Maps」を参照してください)。

リスト 9. Google Map の初期セットアップ
<html>
  <head>
    <title>Plan</title>
    <script src="http://maps.google.com/maps?file=api&v=2&key=YourKeyHere"
      type="text/javascript"></script>
    <script type="text/javascript">
    var map
    var usCenterPoint = new GLatLng(39.833333, -98.583333)
    var usZoom = 4
    function load() {
      if (GBrowserIsCompatible()) {
        map = new GMap2(document.getElementById("map"))
        map.setCenter(usCenterPoint, usZoom)
        map.addControl(new GLargeMapControl());
        map.addControl(new GMapTypeControl());
      }
    }
    </script>
  </head>
  <body onload="load()" onunload="GUnload()">
    <div class="body">
      <div id="search" style="width:25%; float:left">
      <h1>Where to?</h1>
      </div>
      <div id="map" style="width:75%; height:100%; float:right"></div>
    </div>
  </body>
</html>

何も問題がなければ、ブラウザーで http://localhost:9090/trip/trip/plan にアクセスすると、図 3 のようなページが表示されます。

図 3. 基本となる Google Map
基本となる Google Map

これで基本となる地図が用意できました。次は、出発地と到着地の空港を入力するためのフィールドを追加します。


フォーム・フィールドの追加

Ajax をほんの少し加えた多対多の関係」では、Prototype の Ajax.Request オブジェクトを使用しました。このオブジェクトは、今回も後でリモート・ソースから JSON を取得する際に使用しますが、とりあえずは <g:formRemote> タグを使用します。grails-app/views/trip/plan.gsp に、リスト 10 の HTML を追加してください。

リスト 10. <g:formRemote> の使用
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form"
              url="[controller:'airportMapping', action:'iata']"
              onSuccess="addAirport(e, 0)">
  From:<br/>
  <input type="text" name="id" size="3"/>
  <input type="submit" value="Search" />
</g:formRemote>
<div id="airport_0"></div>
<g:formRemote name="to_form"
              url="[controller:'airportMapping', action:'iata']"
              onSuccess="addAirport(e, 1)">
  To: <br/>
  <input type="text" name="id" size="3"/>
  <input type="submit" value="Search" />
</g:formRemote>
<div id="airport_1"></div>
</div>

Web ブラウザーの更新ボタンをクリックして、ページがどのように変更されているかを確認してください (図 4 を参照)。

図 4. フォーム・フィールドの追加
フォーム・フィールドの追加

通常の <g:form> を使用すると、ユーザーがフォームを送信した場合にページ全体が更新されることになりますが、<g:formRemote> を使用することによって、Ajax.Request がフォームの送信を裏で非同期に実行するようにしています。入力テキスト・フィールドには id という名前を付けて、コントローラーで確実に params.id に値が入るようにします。<g:formRemote> には url 属性があることから、ユーザーが送信ボタンをクリックすると AirportMappingController.iata() が呼び出されることは明らかです。

Ajax をほんの少し加えた多対多の関係」で <g:formRemote> を利用できなかった理由は、HTML フォームを別の HTML フォームの中にネストできないからです。しかし今回は、2 つの個別のフォームを作成することができるため、Prototype コードを独自に作成する必要はありません。非同期 JSON リクエストの結果は、addAirport() JavaScript 関数に渡されます。

次のタスクは、addAirport() を作成することです。


JSON を処理するための JavaScript の追加

これから作成する addAirport() 関数は単純な 2 つのことを実行します。まず JSON オブジェクトをメモリーにロードし、それからさまざまな目的でフィールドを使用します。この場合は、緯度と経度の値を使って GMarker を作成し、これを地図に追加します。

<g:formRemote> を機能させるためには、head セクションの先頭に必ず Prototype ライブラリーを組み込んでください (リスト 11 を参照)。

リスト 11. GSP での Prototype の組み込み
<g:javascript library="prototype" />

init() 関数の後には、リスト 12 の JavaScript を追加します。

リスト 12. addAirport および drawLine の実装
<script type="text/javascript">
var airportMarkers = []
var line
function addAirport(response, position) {      
  var airport = eval('(' + response.responseText + ')')
  var label = airport.iata + " -- " + airport.name
  var marker = new GMarker(new GLatLng(airport.lat, airport.lng), {title:label})
  marker.bindInfoWindowHtml(label)
  if(airportMarkers[position] != null){
    map.removeOverlay(airportMarkers[position])
  }
  if(airport.name != "Not found"){
    airportMarkers[position] = marker
    map.addOverlay(marker)           
  }
  document.getElementById("airport_" + position).innerHTML = airport.name
  drawLine()
}
function drawLine(){
  if(line != null){
    map.removeOverlay(line)
  }
  
  if(airportMarkers.length == 2){
    line = new GPolyline([airportMarkers[0].getLatLng(), airportMarkers[1].getLatLng()])
    map.addOverlay(line)
  }
}    
</script>

リスト 12 のコードでは、最初に新しい変数を 2 つ宣言しています。1 つは行を保持するためのもの、そしてもう 1 つは 2 つの空港マーカーを保持するための配列です。入力された JSON に対して eval() を実行した後に、airport.iataairport.nameairport.latairport.lng などのフィールドを直接呼び出します (JSON オブジェクトがどのようなものかを思い出すには、リスト 5 を参照してください)。

airport オブジェクトに対するハンドルを取得したら、新しい GMarker を作成します。これは Google Maps で見慣れている、お馴染みの「赤いマーカー」です。title 属性は、ユーザーがマーカー上にマウス・ホバーしたときにツールチップに表示する内容を API に指示します。そしてユーザーがマーカーをクリックしたときに表示する内容を API に指示するのは、bindInfoWindowHtml() メソッドです。マーカーがオーバーレイとしてマップに追加されると、今度は drawLine() 関数が呼び出されます。名前からわかるように、この関数は両方の空港マーカーが存在する場合、その 2 つを結ぶ線を描画します。

GMarkerGLatLngGPolyline などの Google Maps API オブジェクトについての詳細は、オンライン・マニュアルを参照してください (「参考文献」を参照)。

出発地と到着地の空港を入力すると、ページは図 5 のような表示になります。

図 5. 線で結ばれた 2 つの空港の表示
線で結ばれた 2 つの空港の表示

GSP ファイルを変更するたびに、忘れずに Web ブラウザーで表示の更新を行ってください。

これで、Grails アプリケーションからローカルに返される JSON を使用するサンプルが用意できたので、今度はもう少し範囲を広げ、次のセクションではリモート Web サービスから JSON を動的に取得します。もちろん JSON を取得した後の操作は上記の例とまったく同じで、メモリーにロードしてから各種の属性に直接アクセスします。


リモートの JSON にするか、それともローカルの JSON にするか

次のタスクは、到着地の空港に最も近いホテルを 10 件表示することです。それにはほぼ間違いなく、データをリモートで取得する必要が出てきます。

データをローカルに持つべきか、あるいは要求に応じてリモートから取得すべきかという質問に対する標準的な答えはありません。この空港データ・セットの場合には、ローカルで持つことになるはずです。そうすれば、データは自由に使用できるようになり、取り込むのも簡単だからです (米国にある空港の数はわずか 901 で、そのうち主要な空港の数はほとんど変わらないため、リストがすぐに時代遅れになってしまう可能性はありません)。

空港データ・セットが変わりやすかったり、ローカルで保管するにはかなりの量がある場合、あるいはただ単にダウンロードでは入手できないという場合には、リモートで要求するという方法を選んでいたでしょう。「Grails サービスと Google Maps」で使用した geonames.org のジオコーディング・サービスでは、XML だけでなく JSON でも出力することができます (「参考文献」を参照)。Web ブラウザーで http://ws.geonames.org/search?name_equals=den&fcode=airp&style=full&type=json と入力すると、リスト 13 に記載する JSON による結果が表示されるはずです。

リスト 13. GeoNames からの JSON での結果
{"totalResultsCount":1,
"geonames":[
  {"alternateNames":[
    {"name":"DEN","lang":"iata"},
    {"name":"KDEN","lang":"icao"}],
  "adminCode2":"031",
  "countryName":"United States",
  "adminCode1":"CO",
  "fclName":"spot, building, farm",
  "elevation":1655,
  "countryCode":"US",
  "lng":-104.6674674,
  "adminName2":"Denver County",
  "adminName3":"",
  "fcodeName":"airport",
  "adminName4":"",
  "timezone":{
    "dstOffset":-6,
    "gmtOffset":-7,
    "timeZoneId":"America/Denver"},
  "fcl":"S",
  "name":"Denver International Airport",
  "fcode":"AIRP",
  "geonameId":5419401,
  "lat":39.8583188,
  "population":0,
  "adminName1":"Colorado"}]
}

ご覧のように、GeoNames サービスが提供する空港情報は、「Grails とレガシー・データベース」でインポートした USGS のデータ・セットよりも豊富です。そのため、空港のタイムゾーンやメートル単位の高度を知る必要があるといった新しいユーザー要件が出てきた場合には、GeoNames が強力な代替案となります。また、提供される情報にはロンドンのヒースロー空港 (LHR) やフランクフォート空港 (FRA) などの国際空港も含まれます。AirportMapping.iata() が GeoNames を使用するように内部で変換する方法については、追加の演習項目として読者に残しておくので自分で調べてください。

一方、ここで到着地の空港近辺のホテルのリストを表示するために選択できる唯一の方法は、リモート Web サービスを利用する方法です。数千件のホテルが含まれるリストは絶えず内容が変わるため、このリストは他の誰かに管理を任せるしかありません。

Yahoo! では、住所、郵便番号、あるいは緯度と経度の地点さえも基準にして、その近辺で営業しているサービスを検索できるローカル検索サービスを提供しています (「参考文献」を参照)。「RESTful な Grails」ですでに開発者キーを登録していれば、今回もこのサービスを利用することができます。当然のことながら、包括的検索 URI のフォーマットは、前回使用したものと、今回使用するものとではほとんど同じです。前回は、Web サービスがデフォルトで XML を返すようにしましたが、もう 1 つ name=value のペア (output=json) を追加することで、XML の代わりに JSON を取得することが可能になります。

ブラウザーに以下の URL を (改行は入れずに) 入力して、デンバー国際空港近くのホテルのリストが JSON 形式で得られることを確認してください。

http://local.yahooapis.com/LocalSearchService/V3/localSearch?appid=
   YahooDemo&query=hotel&latitude=39.858409881591797&longitude=
   -104.666999816894531&sort=distance

リスト 14 に、JSON の結果を (省略して) 記載します。

リスト 14. Yahoo! による JSON 結果
{"ResultSet":
  {"totalResultsAvailable":"803",
  "totalResultsReturned":"10",
  "firstResultPosition":"1",
  "ResultSetMapUrl":"http:\/\/maps.yahoo.com\/broadband\/?tt=hotel&tp=1",
  "Result":[
    {"id":"42712564",
    "Title":"Springhill Suites-Denver Arprt",
    "Address":"18350 E 68th Ave",
    "City":"Denver",
    "State":"CO",
    "Phone":"(303) 371-9400",
    "Latitude":"39.82076",
    "Longitude":"-104.673719",
    "Distance":"2.63",
    [SNIP]

有望なホテルのリストを用意できたところで、次は、AirportMapping.iata() でも行ったようにコントローラー・メソッドを作成する必要があります。


リモート JSON リクエストを実行するためのコントローラー・メソッドの作成

HotelController は以前の記事によって、所定の場所にすでに用意されているはずです。このコントローラーにリスト 15 の near クロージャーを追加します (「Grails サービスと Google Maps」でも同じようなコードを使用しました)。

リスト 15. HotelController
class HotelController {
  def scaffold = Hotel
  def near = {
    def addr = "http://local.yahooapis.com/LocalSearchService/V3/localSearch?"
    def qs = []
    qs << "appid=YahooDemo"
    qs << "query=hotel"
    qs << "sort=distance"
    qs << "output=json"
    qs << "latitude=${params.lat}"
    qs << "longitude=${params.lng}"
    def url = new URL(addr + qs.join("&"))
    render(contentType:"application/json", text:"${url.text}")
  }
}

最後の 2 つ、latitudelongitude を除いたすべてのクエリー・ストリング・パラメーターはハードコーディングされています。最後から 2 番目の行が新しい java.net.URLをインスタンス化し、最後の行でサービス (url.text) を呼び出して結果をレンダリングします。JSON コンバーターは使用していないため、MIME タイプを明示的に application/json に設定する必要があります。render は特に指定しない限り、text/plain を返します。

ブラウザーに以下の URL を (改行は入れずに) 入力してください。

http://localhost:9090/trip/hotel/near?lat=
   39.858409881591797&lng=-104.666999816894531

上記の結果を先ほど http://local.yahooapis.com を直接呼び出した場合と比較してください。2 つの結果はまったく同じになるはずです。

ブラウザーから直接リモート Web サービスを呼び出せない理由

local.yahooapis.com のURL を Ajax.Request に組み込むと、何の反応もなく失敗します。この URL はブラウザーのアドレス・バーに入力すると機能しますが、JavaScript からプログラムによって呼び出すと失敗に終わります。真面目な話、これは 1 つの特徴であって、バグではありません。

具体的に説明すると、Ajax リクエストは同一ソースまたは同一送信元というルールに制約されているからです。つまり Ajax リクエストへのレスポンスは、ソース HTML ページを送信したドメインと同じドメインにしか戻ることができません。この記事の例で言うと、http://localhost にはあらゆる呼び出しを行うことができても、http://local.yahooapis.com や他の URL にはできないということです。

このような制約が設けられているのは、セキュリティー上の理由からです。例えばクレジット・カードの番号を http://amazon.com に入力する際には、番号の数字が秘密裏に http://hackers.r.us に送信されることが絶対にないようにしたいはずです (正式には XSS (クロスサイト・スクリプティング) として知られています)。

同一送信元のルールは、クライアント・サイドの JavaScript にだけ適用され、サーバー・サイドの Groovy には適用されません。それが理由で、私はプロキシーにコントローラーから http://local.yahooapis.com の呼び出しを行わせ、ブラウザーにトランスペアレントに渡されるようにしたのです。

どうしても、ブラウザーから直接 Yahoo! または Google Web サービスを呼び出したいという場合、この 2 つのサービスでは、同一ソースのルールを回避するちょっとした裏技として、コールバック・オプションを設定することができます。JSON コールバックについての詳細は、「参考文献」に記載されている該当する資料へのリンクを参照してください。

コントローラー・メソッドにリモート JSON リクエストを行わせるという方法には、2 つの利点があります。1 つは、同一ソースという Ajax の制約 (囲み記事「ブラウザーから直接リモート Web サービスを呼び出せない理由」を参照) に対する次善策になることですが、それよりも重要なもう 1 つの利点は、カプセル化が行われることです。つまり、コントローラーは実質上、DAO (Data Access Object) のようなものになります。

リモート Web サービスへの URL をハードコーディングしたくないのと同様に、ビューにそのままの SQL は残したくないものです。そこで、ローカル・コントローラーを呼び出すことによって、ダウンストリームのクライアントで実装を変更しなくても済むようにします。テーブル名やフィールド名を変更すると組み込み SQL 文が壊れてしまいます。かと言って URL を変更するとなると、今度は組み込み Ajax 呼び出しが壊れることになります。AirportMapping.iata() を呼び出すことで、データ・ソースをローカル・テーブルからリモート GeoNames サービスに自由に変更できるようになるとともに、クライアント・サイドのインターフェースを維持することにもなります。長期的なパフォーマンスのためには、リモート・サービスへの呼び出しをローカル・データベースのキャッシュに入れ、リクエストが行われるごとにローカル・キャッシュを構築していくという方法を採ることも可能です。

これで、サービスは単独で機能するようになったので、Web ページからサービスを呼び出すことができます。

ShowHotels リンクの追加

ユーザーが到着地の空港を指定するまでは、Show Nearby Hotels (近くのホテルを表示) ハイパーリンクを表示しても意味がありません。それと同じく、ユーザーが実際にホテルのリストを表示しようとしていると確信するまでは、リモート・リクエストを行っても意味がありません。そこでまず始めに行うことは、showHotelsLink() 関数を plan.gsp のスクリプト・ブロックに追加し、さらに showHotelsLink() の呼び出しを addAirport() の最後の行に追加することです (リスト 16 を参照)。

リスト 16. showHotelsLink() の実装
function addAirport(response, position) {
  ...
  drawLine()
  showHotelsLink()
}
function showHotelsLink(){
  if(airportMarkers[1] != null){
    var hotels_link = document.getElementById("hotels_link")
    hotels_link.innerHTML = "<a href='#' onClick='loadHotels()'>Show Nearby Hotels...</a>"
  }
}

Grails では、(非同期でフォームをサブミットするのは <g:formRemote> ですが) 非同期ハイパーリンクを作成する <g:remoteLink> タグを用意しています。しかしライフサイクルの問題から、ここではこの 2 つのタグを使用することができません。g: タグのレンダリングは、サーバー上で行われるからです。このリンクはクライアント・サイドで動的に追加されるため、ここでは純粋な JavaScript ソリューションに頼るしかありません。

おそらくお気付きだと思いますが、上記のリストでは document.getElementById("hotels_link") が呼び出されています。そこで、search <div> の最後に以下の新規 <div> を追加してください (リスト 17 を参照)。

リスト 17. hotels_link <div> の追加
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form" ... >
<g:formRemote name="to_form" ...>
<div id="hotels_link"></div>
</div>

到着地の空港を指定した後、ブラウザーに表示を更新するとハイパーリンクが表示されることを確認してください (図 6 を参照)。

図 6. Show Nearby Hotels ハイパーリンクの表示
Show Nearby Hotels ハイパーリンクの表示

次に必要な作業は、loadHotels() 関数の作成です。


Ajax.Remote の呼び出し

plan.gsp のスクリプト・ブロックにリスト 18 に記載する新しい関数を追加します。

リスト 18. loadHotels() の実装
function loadHotels(){
  var url = "${createLink(controller:'hotel', action:'near')}"
  url += "?lat=" + airportMarkers[1].getLatLng().lat()
  url += "&lng=" + airportMarkers[1].getLatLng().lng()
  new Ajax.Request(url,{
    onSuccess: function(req) { showHotels(req) },
    onFailure: function(req) { displayError(req) }
  })
}

Hotel.near() への URL を構成する基本部分は、サーバー・サイドでページがレンダリングされても変更されないため、ここで確実な方法となるのは、Grails の createLink メソッドを使用することです。クライアント・サイドの JavaScript を使用して、この URL の動的な部分を付加した後、今ではもうお馴染みの Prototype 呼び出しを使って Ajax リクエストを行います。


エラーの処理

簡単のため、<g:formRemote> の呼び出しではエラーの処理を無視しましたが、今回は (ローカル・コントローラーのプロキシーを経由しているものの) リモート・サーバーを呼び出しているため、何の反応もなしに失敗するのではなく、何らかのフィードバックを行うのが賢明です。そこで、plan.gsp のスクリプト・ブロックに displayError() 関数を追加します (リスト 19 を参照)。

リスト 19. displayError() の実装
function displayError(response){
  var html = "response.status=" + response.status + "<br />"
  html += "response.responseText=" + response.responseText + "<br />"
  var hotels = document.getElementById("hotels")
  hotels.innerHTML = html
}

確かにこの関数はユーザーに対し、Show Nearby Hotels (近くのホテルを表示) リンクの下にある hotels <div> (通常結果が表示される部分) にエラーを表示するだけのことしかしません。リモート呼び出しはサーバー・サイドのコントローラーにカプセル化しているため、お望みであれば、これよりも高度なエラー修正を行うことも可能です。

前に追加した hotels_link <div> の下にhotels <div> を追加してください (リスト 20 を参照)。

リスト 20. hotels <div> の追加
<div id="search" style="width:25%; float:left">
<h1>Where to?</h1>
<g:formRemote name="from_form" ... >
<g:formRemote name="to_form" ...>
<div id="hotels_link"></div>
<div id="hotels"></div>
</div>

これで、完成まであと一歩のところまでたどり着きました。最後に必要なのは、成功した JSON リクエストをロードして、hotels <div> にデータを設定する関数を追加することだけです。


成功の処理

リスト 21 に記載するこの最後の関数が、JSON レスポンスをローカル Yahoo! サービスから取得して HTML リストを作成し、そのリストを hotels <div> に書き込みます。

リスト 21. showHotels() の実装
function showHotels(response){
  var results = eval( '(' + response.responseText + ')')
  var resultCount = 1 * results.ResultSet.totalResultsReturned
  var html = "<ul>"
  for(var i=0; i < resultCount; i++){
    html += "<li>" + results.ResultSet.Result[i].Title + "<br />"
    html += "Distance: " + results.ResultSet.Result[i].Distance + "<br />"
    html += "<hr />"
    html += "</li>"
  }
  html += "</ul>"
  var hotels = document.getElementById("hotels")
  hotels.innerHTML = html
}

最後にもう一度ブラウザーの表示を更新して、出発地と到着地の空港を入力してください。すると、図 1 のような画面が表示されます。

この記事での例はこれで完了ですが、読者の皆さんはこれで終わりにしないで、自分でいろいろと試してみてください。例えば、別の GMarker の配列を使って地図上にホテルをプロットしてみたり、あるいは Yahoo! の結果から電話番号や住所などのフィールドを追加するなど、無限の可能性があります。


まとめ

約 150 行のコードとしては上出来だと思いませんか? この記事では、Ajax リクエストを行う際に XML の代わりに JSON を使うことがいかに有効な手段となりうるかを説明しました。この記事で説明したように、Grails アプリケーションからローカルに JSON を返すのは簡単なことです。さらに、リモート Web サービスから JSON を返すとしても、それほど厄介な作業にはなりません。<g:formRemote><g:linkRemote> などの Grails タグは、HTML がサーバー・サイドでレンダリングされているときに使用することができますが、Prototype が行う基礎の Ajax.Request 呼び出しを使用する方法を理解することが、真の動的 Web 2.0 アプリケーションには重要です。

次回の記事では、Grails のネイティブ JMX (Java Management Extensions) 機能を実例で紹介します。それまでは、Grails を楽しみながらマスターしてください。

参考文献

学ぶために

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

  • Grails: Grails の最新リリースをダウンロードしてください。

議論するために

コメント

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=358721
ArticleTitle=Grails をマスターする: JSON と Ajax による非同期 Grails
publish-date=11182008