Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その3

Google Web Toolkit 2.1のチュートリアルを試してみています。
Google Web Toolkit に関する記事の一覧


JSONPでデータが取得できるところまで、前回試してみました。
Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その2

今回は、JSONPで取得したデータを全てオブジェクトに格納してみます。


Making cross-site requests
こちらを参考に、プログラムを作成しました。




配列データ(bookmarks)の取得



前回は、一番変換が簡単そうなデータだけ取得してみました。


count(ブックマークしている合計ユーザ数)
url(ブックマークされているURL)
title(タイトル)
eid(エントリーID)
entry_url(はてなブックマークエントリーページのURL)
screenshot(スクリーンショット画像のURL)





今回は、配列で定義されている「bookmarks」のデータを取得してみます。
bookmarks配列の個々の内容はこんな感じになります。


comment(コメント)
timestamp(ブックマークした時刻)
user(ブックマークしたユーザ名)
tags(タグの配列)





かなり手探りでしたが、まずbookmarks配列用のオブジェクトを
定義しました。

パッケージ・エクスプローラーで「com.fc2.blog68.symfoware.hatena.client」を
選択した状態でEclipseの[ファイル]-[新規]-[クラス]を選択。

名前に「HatenaBookmarkData」
スーパークラスに「com.google.gwt.core.client.JavaScriptObject」
と入力して、完了を押下します。

14_001.png



クラスの内容は以下のとおり。


package com.fc2.blog68.symfoware.hatena.client;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArrayString;

public class HatenaBookmarkData extends JavaScriptObject {
    
    protected HatenaBookmarkData() {
    }
    
    // コメント
    public final native String getComment() /*-{ return this.comment; }-*/;
    
    // ブックマークした時刻
    public final native String getTimestamp() /*-{ return this.timestamp; }-*/;
    
    // ブックマークしたユーザ名
    public final native String getUser() /*-{ return this.user; }-*/;
    
    // タグの配列
    public final native JsArrayString getTags() /*-{ return this.tags; }-*/;
}



タグの配列は、文字列の配列となるのですが、この戻り値に
何を指定すればよいかわからずはまりました。

最初、戻り値をString[]としていたのですが、これだと
オブジェクト変換時にエラーになります。

どうやら、「com.google.gwt.core.client.JsArrayString」を
使えばJSONの文字列配列を表現できるようです。


前回作成した「HatenaData」クラスにブックマークの配列を
取得するためのメソッド「getBookmarks()」を追加します。




package com.fc2.blog68.symfoware.hatena.client;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;

public class HatenaData extends JavaScriptObject {
    
    protected HatenaData() {
    }
    
    // ブックマークしている合計ユーザ数
    public final native String getCount() /*-{ return this.count; }-*/;
    
    // ブックマークされているURL
    public final native String getUrl() /*-{ return this.url; }-*/;
    
    // タイトル
    public final native String getTitle() /*-{ return this.title; }-*/;
    
    // エントリーID
    public final native String getEid() /*-{ return this.eid; }-*/;
    
    // はてなブックマークエントリーページのURL
    public final native String getEntryUrl() /*-{ return this.entry_url; }-*/;
    
    // スクリーンショット画像のURL
    public final native String getScreenShot() /*-{ return this.screenshot; }-*/;
    
    // bookmarks配列を取得
    public final native JsArray<HatenaBookmarkData> getBookmarks() /*-{ return this.bookmarks; }-*/;
}






getBookmarks()の戻り値を以下のように定義するのがミソ。


JsArray<HatenaBookmarkData>








テーブルにデータを表示する処理



これでデータの取得と変換が行えるようになりました。
テーブルに取得したデータを表示する処理を追加します。


package com.fc2.blog68.symfoware.hatena.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;


public class Hatena implements EntryPoint {
    
    // 検索するURLを入力するテキストボックス
    private TextBox findText = new TextBox();
    // 検索事項ボタン
    private Button findButton = new Button("検索");
    
    // 検索条件、検索ボタンを配置するパネル
    private HorizontalPanel findPanel = new HorizontalPanel();
    // 検索結果を表示するためのテーブル
    private FlexTable bookmarksFlexTable = new FlexTable();
    
    // はてなAPIのURL
    private static final String JSON_URL = "http://b.hatena.ne.jp/entry/json/?url=";
    
    // デバッグ用のラベル
    private Label debugLabel = new Label();
    
    
    /**
     * HTML描画時に呼び出されるメソッド
     */
    public void onModuleLoad() {
        
        // 検索テキスト、ボタンを水平パネルに配置
        findText.setWidth("300px");
        findPanel.add(findText);
        findPanel.add(findButton);
        
        // 検索するURLを入力するテキストボックスの初期値は、blogのURL
        findText.setText("http://symfoware.blog68.fc2.com/");
        
        // テーブルの作成
        bookmarksFlexTable.setText(0, 0, "ユーザー");
        bookmarksFlexTable.setText(0, 1, "コメント");
        bookmarksFlexTable.setText(0, 2, "ブックマーク時刻");
        bookmarksFlexTable.setText(0, 3, "タグ");
        
        // HTMLに記載したdiv name="rootPanel"の位置に部品を配置
        RootPanel.get("rootPanel").add(findPanel);
        RootPanel.get("rootPanel").add(bookmarksFlexTable);
        RootPanel.get("rootPanel").add(debugLabel);
        

        // 初期フォーカスは、検索URL入力テキストに設定
        findText.setFocus(true);
        
        // クリック時、データの検索を行う
        findButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                findData();
            }
        });
    }
    
    private void findData(){
        
        debugLabel.setText("");
        
        String url = JSON_URL;
        
        // はてなAPI用のURLに、テキストボックスに入力された文字列を連結
        url += findText.getText();
        
        // callback関数名を連結
        url = URL.encode(url) + "&callback=mycall";
        
        getJson(url, this);
    }
    
    public native static void getJson(String url, Hatena handler) /*-{
        var callback = "mycall";
        
        // [1] Create a script element.
        var script = document.createElement("script");
        script.setAttribute("src", url);
        script.setAttribute("type", "text/javascript");
        
        // [2] Define the callback function on the window object.
        window[callback] = function(jsonObj) {
            // [3]
            handler.@com.fc2.blog68.symfoware.hatena.client.Hatena::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(jsonObj);
            window[callback + "done"] = true;
        }
        
        // [4] JSON download has 1-second timeout.
        setTimeout(function() {
            if (!window[callback + "done"]) {
                handler.@com.fc2.blog68.symfoware.hatena.client.Hatena::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(null);
            }
            
            // [5] Cleanup. Remove script and callback elements.
            document.body.removeChild(script);
            delete window[callback];
            delete window[callback + "done"];
        }, 1000);

        // [6] Attach the script element to the document body.
        document.body.appendChild(script);
    }-*/;
    
    
    public void handleJsonResponse(JavaScriptObject jso) {
        if (jso == null) {
            debugLabel.setText("Couldn't retrieve JSON");
            return;
        }

        HatenaData data = asHatenaData(jso);
        
        for (int i = 0; i < data.getBookmarks().length(); i++) {
            HatenaBookmarkData bookmark = data.getBookmarks().get(i);
            
            bookmarksFlexTable.setText(i + 1, 0, bookmark.getUser());
            bookmarksFlexTable.setText(i + 1, 1, bookmark.getComment());
            bookmarksFlexTable.setText(i + 1, 2, bookmark.getTimestamp());
            bookmarksFlexTable.setText(i + 1, 3, bookmark.getTags().join(":"));
        }
    }
    
    private final native HatenaData asHatenaData(JavaScriptObject jso) /*-{
        return jso;
    }-*/;
}






開発用のサーバーを起動して実行してみると

14_002.png


非常に画面が地味ですが、ちゃんと情報を取得できていることが
確認できました。








関連記事



Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その2
Google Web Toolkit に関する記事の一覧






Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その2

Google Web Toolkit 2.1のチュートリアルを試してみています。
Google Web Toolkit に関する記事の一覧


雛形で生成されたプログラムを変更するところまで、前回試してみました。
Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その1

今回は、JSONPでデータを取得するロジックを考えたいと思います。


Making cross-site requests
こちらを参考に、プログラムを作成しました。




JavaScriptObjectを継承したクラスの作成



取得したデータを解析し、オブジェクトに変換するための
JavaScriptObjectを継承したクラスを作成します。

パッケージ・エクスプローラーで「com.fc2.blog68.symfoware.hatena.client」を
選択状態にしておき、[ファイル]-[新規作成]-[クラス]を選択。

クラス名を「HatenaData」
スーパークラスに「com.google.gwt.core.client.JavaScriptObject」
を入力して、完了を押下。

13_001.png



まず、一番上位にある変換が簡単そうなデータから。
以下の情報をJSON形式のデータからマッピングするクラスを作成してみます。


count(ブックマークしている合計ユーザ数)
url(ブックマークされているURL)
title(タイトル)
eid(エントリーID)
entry_url(はてなブックマークエントリーページのURL)
screenshot(スクリーンショット画像のURL)





「HatenaData.java」の内容はこんな感じになりました。


package com.fc2.blog68.symfoware.hatena.client;

import com.google.gwt.core.client.JavaScriptObject;

public class HatenaData extends JavaScriptObject {
    
    protected HatenaData() {
    }
    
    // ブックマークしている合計ユーザ数
    public final native String getCount() /*-{ return this.count; }-*/;
    
    // ブックマークされているURL
    public final native String getUrl() /*-{ return this.url; }-*/;
    
    // タイトル
    public final native String getTitle() /*-{ return this.title; }-*/;
    
    // エントリーID
    public final native String getEid() /*-{ return this.eid; }-*/;
    
    // はてなブックマークエントリーページのURL
    public final native String getEntryUrl() /*-{ return this.entry_url; }-*/;
    
    // スクリーンショット画像のURL
    public final native String getScreenShot() /*-{ return this.screenshot; }-*/;
    
    // TODO
    // bookmarksを配列で宣言
}








データ取得と変換部分の作成



入力されたURLをパラメーターに付加し、JSON形式のデータを取得できるようにします。
あまり内容の詳細を理解できていませんが・・・参考サイトに
記載されているソースから試行錯誤して作成したソースがこちら。




package com.fc2.blog68.symfoware.hatena.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;


public class Hatena implements EntryPoint {
    
    // 検索するURLを入力するテキストボックス
    private TextBox findText = new TextBox();
    // 検索事項ボタン
    private Button findButton = new Button("検索");
    
    // 検索条件、検索ボタンを配置するパネル
    private HorizontalPanel findPanel = new HorizontalPanel();
    // 検索結果を表示するためのテーブル
    private FlexTable stocksFlexTable = new FlexTable();
    
    // はてなAPIのURL
    private static final String JSON_URL = "http://b.hatena.ne.jp/entry/json/?url=";
    
    // デバッグ用のラベル
    private Label debugLabel = new Label();
    
    
    /**
     * HTML描画時に呼び出されるメソッド
     */
    public void onModuleLoad() {
        
        // 検索テキスト、ボタンを水平パネルに配置
        findText.setWidth("300px");
        findPanel.add(findText);
        findPanel.add(findButton);
        
        // 検索するURLを入力するテキストボックスの初期値は、blogのURL
        findText.setText("http://symfoware.blog68.fc2.com/");

        // HTMLに記載したdiv name="rootPanel"の位置に部品を配置
        RootPanel.get("rootPanel").add(findPanel);
        RootPanel.get("rootPanel").add(stocksFlexTable);
        RootPanel.get("rootPanel").add(debugLabel);
        

        // 初期フォーカスは、検索URL入力テキストに設定
        findText.setFocus(true);
        
        // クリック時、データの検索を行う
        findButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                findData();
            }
        });
    }
    
    private void findData(){
        
        debugLabel.setText("");
        
        String url = JSON_URL;
        
        // あてなAPI用のURLに、テキストボックスに入力された文字列を連結
        url += findText.getText();
        
        // callback関数名を連結
        url = URL.encode(url) + "&callback=mycall";
        
        getJson(url, this);
    }
    
    public native static void getJson(String url, Hatena handler) /*-{
        var callback = "mycall";
        
        // [1] Create a script element.
        var script = document.createElement("script");
        script.setAttribute("src", url);
        script.setAttribute("type", "text/javascript");
        
        // [2] Define the callback function on the window object.
        window[callback] = function(jsonObj) {
            // [3]
            handler.@com.fc2.blog68.symfoware.hatena.client.Hatena::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(jsonObj);
            window[callback + "done"] = true;
        }
        
        // [4] JSON download has 1-second timeout.
        setTimeout(function() {
            if (!window[callback + "done"]) {
                handler.@com.fc2.blog68.symfoware.hatena.client.Hatena::handleJsonResponse(Lcom/google/gwt/core/client/JavaScriptObject;)(null);
            }
            
            // [5] Cleanup. Remove script and callback elements.
            document.body.removeChild(script);
            delete window[callback];
            delete window[callback + "done"];
        }, 1000);

        // [6] Attach the script element to the document body.
        document.body.appendChild(script);
    }-*/;
    
    
    public void handleJsonResponse(JavaScriptObject jso) {
        if (jso == null) {
            debugLabel.setText("Couldn't retrieve JSON");
            return;
        }

        HatenaData data = asHatenaData(jso);
        debugLabel.setText(data.getEntryUrl() + ":" + data.getTitle() + ":" + data.getCount());
    }
    
    private final native HatenaData asHatenaData(JavaScriptObject jso) /*-{
        return jso;
    }-*/;
}






ちゃんとデータの取得と変換が行われれば、デバッグ用に用意したラベルに
・はてなブックマークエントリーページのURL
・タイトル
・ブックマークされている件数
が表示されるはずです。




実行結果



ここまでで、開発用のサーバーを起動し、動作を確認してみました。
検索ボタンを押すと、狙い通りの情報が表示されました。

13_002.png


次は、配列として格納されているbookmarksを変換する方法について
考えてみることにします。





関連記事



Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その3
Google Web Toolkit に関する記事の一覧





Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その1

Google Web Toolkit 2.1のチュートリアルを試してみています。
Google Web Toolkit に関する記事の一覧


前回はJSONを使用したクライアントとサーバー間の通信を試しました。
Google Web Toolkit 2.1 JSON形式のデータの利用

今回は、JSONPでのデータ取得を試してみます。


Making cross-site requests
こちらを参考に、プログラムを作成しました。




1. Reviewing the requirements and design



参考にしたサイトでは、JSONPで返却する値を検討しています。
今回は、はてなブックマークのエントリー情報を取得するJSONPが
公開されていますので、それにアクセスしデータを取得・表示する
サンプルを作成してみようと思います。

はてなブックマークエントリー情報取得API


こちらのサイトを参考にさせていただきました。
ブラウザからJSONで呼び出せるサービス一覧
JSON with Padding Test


オフィシャルのAPI解説よりもJSON with Padding Testでの実行結果を
見たほうが返却されるデータ形式の理解が早いかと思います。


12_001_20101109224817.png


上位に

count(ブックマークしている合計ユーザ数)
url(ブックマークされているURL)
bookmarks(ユーザがブックマークしたデータの配列)
title(タイトル)
eid(エントリーID)
entry_url(はてなブックマークエントリーページのURL)
screenshot(スクリーンショット画像のURL)


という情報が並びます。


bookmarks配列のないようはこんな感じになるようです。

12_002_20101109224817.png



comment(ブックマークコメント)
timestamp(ブックマークした時刻)
user(ブックマークしたユーザ名)
tags(タグの配列)




受け取り用のオブジェクトを定義するときの参考になりそうです。





2. Creating a data a source



参考にしたサイトでは、仕様を満たすサーバーサイドのソースコードに
ついて解説されていますが、ここでは、はてなAPIに送るリクエスト
について考えて見ます。

といっても簡単で


http://b.hatena.ne.jp/entry/json/?url=[取得したいサイトのURL]&callback=[コールバック関数]



という形式でGETリクエストを送信すればよいようです。






3. Requesting the data from the remote server



これまで、「StockWatcher」というプロジェクトを作成し、徐々に
機能追加を行う形で学習してきました。

復習の意味もこめて、新しくプロジェクトを作成し、プログラムを
作成することにしました。

EclipseにGoogle Web Toolkit 2.1のプラグインをインストールしておきます。
Google Web Toolkit 2.1のEclipseプラグインをインストールする


Eclipseツールバーのアイコンをクリック。

12_003_20101109224817.png


プロジェクト名を「hatena」
パッケージを「com.fc2.blog68.symfoware.hatena」
として、プロジェクトを新規作成します。

12_004_20101109224817.png


雛形のファイルが作成されますが、以下のクラスは不要なので
削除しました。


com.fc2.blog68.symfoware.hatena.client.GreetingService
com.fc2.blog68.symfoware.hatena.client.GreetingServiceAsync
com.fc2.blog68.symfoware.hatena.server.GreetingServiceImpl
com.fc2.blog68.symfoware.hatena.shared.FieldVerifier



パッケージ・エクスプローラーでの表示です。
選択状態にあるソースを削除しました。

12_005_20101109224816.png





Hatena.gwt.xmlの編集



HTTPリクエストを行ううえで後々必要になると思いますので、
Hatena.gwt.xmlに一行「com.google.gwt.http.HTTP」を使用する
設定を追記しておきます。

以下、パッケージ・エクスプローラー上からみた「Hatena.gwt.xml」の
場所と、Hatena.gwt.xmlの記載内容です。

12_006.png



<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='hatena'>
  <!-- Inherit the core Web Toolkit stuff.                        -->
  <inherits name='com.google.gwt.user.User'/>

  <!-- Inherit the default GWT style sheet. You can change     -->
  <!-- the theme of your GWT application by uncommenting         -->
  <!-- any one of the following lines.                            -->
  <inherits name='com.google.gwt.user.theme.standard.Standard'/>
  <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
  <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>     -->

  <!-- Other module inherits                                     -->
  <!--=== 以下の一行を追加 ===-->
  <inherits name="com.google.gwt.http.HTTP" />

  <!-- Specify the app entry point class.                         -->
  <entry-point class='com.fc2.blog68.symfoware.hatena.client.Hatena'/>

  <!-- Specify the paths for translatable code                    -->
  <source path='client'/>
  <source path='shared'/>

</module>







Hatena.htmlの編集



Hatena.htmlを雛形の状態から編集しました。
idが「rootPanel」という名前のdivを追加し、プログラム中から
ここへ部品を配置することにします。

パッケージ・エクスプローラー上での「Hatena.html」の場所と
記載内容は以下のとおり。

12_007.png



<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <link type="text/css" rel="stylesheet" href="Hatena.css">
    <title>はてなブックマークエントリー情報取得サンプル</title>
    <script type="text/javascript" language="javascript" src="hatena/hatena.nocache.js"></script>
  </head>
  <body>

    <h1>はてなブックマークエントリー情報取得</h1>
    <div id="rootPanel" align="center"></div>
    
    <!-- OPTIONAL: include this if you want history support -->
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>
    
    <!-- RECOMMENDED if your web app will not function without JavaScript enabled -->
    <noscript>
     <div style="width: 22em; position: absolute; left: 50%; margin-left: -11em; color: red; background-color: white; border: 1px solid red; padding: 4px; font-family: sans-serif">
        Your web browser must have JavaScript enabled
        in order for this application to display correctly.
     </div>
    </noscript>
  </body>
</html>








web.xmlの編集



雛形として出力されたweb.xmlにはサーブレットに関する記載が
ありますが、先ほどざっくり消しましたので、このまま起動しようと
するとエラーになってしまいます。

web.xmlのサーブレットに関する指定を消しておきます。
パッケージ・エクスプローラー上での場所と内容は以下のとおり。

12_008.png




<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

  <!-- ここから==================
  <servlet>
    <servlet-name>greetServlet</servlet-name>
    <servlet-class>com.fc2.blog68.symfoware.hatena.server.GreetingServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>greetServlet</servlet-name>
    <url-pattern>/hatena/greet</url-pattern>
  </servlet-mapping>
  ==================ここまで削除 -->

  <welcome-file-list>
    <welcome-file>Hatena.html</welcome-file>
  </welcome-file-list>

</web-app>









Hatena.javaの編集



ここまでで下準備は整いました。
いよいよメインのHatena.javaを編集します。

初めてチュートリアルに沿わない状態でプログラムを作成しているので、まずは

・検索するURLを記載するテキストボックス(findText)
・検索実行ボタン(findButton)
・結果を表示するテーブル(stocksFlexTable)



これらを配置するだけのプログラムを書いてみました。

パッケージ・エクスプローラー上での「Hatena.java」の位置と
記載内容は以下のとおり。

12_009.png



package com.fc2.blog68.symfoware.hatena.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;


public class Hatena implements EntryPoint {
    
    // 検索するURLを入力するテキストボックス
    private TextBox findText = new TextBox();
    // 検索事項ボタン
    private Button findButton = new Button("検索");
    
    // 検索条件、検索ボタンを配置するパネル
    private HorizontalPanel findPanel = new HorizontalPanel();
    // 検索結果を表示するためのテーブル
    private FlexTable stocksFlexTable = new FlexTable();
    
    /**
     * HTML描画時に呼び出されるメソッド
     */
    public void onModuleLoad() {
        
        // 検索テキスト、ボタンを水平パネルに配置
        findText.setWidth("300px");
        findPanel.add(findText);
        findPanel.add(findButton);
        
        // 検索するURLを入力するテキストボックスの初期値は、blogのURL
        findText.setText("http://symfoware.blog68.fc2.com/");

        // HTMLに記載したdiv name="rootPanel"の位置に部品を配置
        RootPanel.get("rootPanel").add(findPanel);
        RootPanel.get("rootPanel").add(stocksFlexTable);

        // 初期フォーカスは、検索URL入力テキストに設定
        findText.setFocus(true);

    }
}






ここまでで、デバッグ用のサーバーを起動して動作を確認しました。
起動は、実行ボタンの右側にある下矢印を押すと、「hatena」という
項目が追加されていますので、これを選択します。

12_010.png



サーバー起動後、
http://127.0.0.1:8888/Hatena.html?gwt.codesvr=127.0.0.1:9997
にアクセスすると、こんな表示になりました。

12_011.png



ここまでは狙い通り動いてくれているようです。





関連記事



Google Web Toolkit 2.1 はてなブックマークエントリーをJSONPで取得 その2
Google Web Toolkit に関する記事の一覧








Google Web Toolkit 2.1 JSON形式のデータの利用

Google Web Toolkit 2.1のチュートリアルを試してみています。
Google Web Toolkit に関する記事の一覧


前回はGWT RPCを使用したクライアントとサーバー間の通信を試しました。
Google Web Toolkit 2.1 GWT RPCの使用

今回は、GWT RPCではなく普通のJSON形式のデータを取り扱う方法を試して見ます。


Retrieving JSON Data
こちらを参考に、プログラムを作成しました。




1. Creating a source of JSON data



StockWatcher.javaのrefreshWatchListメソッドで、株価をランダムに生成しています。
Google Web Toolkit 2.1 GWT RPCの使用
この記事では、GWT RPCを使用してサーバー側で株価を生成するように
変更しました。

今回は、株価をJavaのサーブレットで作成し、JSON形式のレスポンスを作成。
クライアント側のJavaScriptで解析し表示するように変更します。


パッケージ・エクスプローラーの
「com.google.gwt.sample.stockwatcher.client」
を選択した状態で、Eclipseのツールバーの[ファイル]-[新規]-[クラス]を選択します。

パッケージ名を「com.google.gwt.sample.stockwatcher.server」に変更
クラス名を「JsonStockData」
として、完了ボタンを押下。
新しいクラスを作成します。

11_001_20101109002730.png



作成した「JsonStockData」を以下の内容に変更します。


package com.google.gwt.sample.stockwatcher.server;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Random;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JsonStockData extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final double MAX_PRICE = 100.0; // $100.00
    private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2%

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        Random rnd = new Random();

        PrintWriter out = resp.getWriter();
        out.println('[');
        String[] stockSymbols = req.getParameter("q").split(" ");
        for (String stockSymbol : stockSymbols) {

            double price = rnd.nextDouble() * MAX_PRICE;
            double change = price * MAX_PRICE_CHANGE
                    * (rnd.nextDouble() * 2f - 1f);

            out.println(" {");
            out.print("    \"symbol\": \"");
            out.print(stockSymbol);
            out.println("\",");
            out.print("    \"price\": ");
            out.print(price);
            out.println(',');
            out.print("    \"change\": ");
            out.println(change);
            out.println(" },");
        }
        out.println(']');
        out.flush();
    }

}





このサーブレットにアクセスできるよう、WEB-INF/web.xmlを編集します。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

  <!-- Servlets -->
  <servlet>
    <!--====== jsonStockDataというサーブレットを定義 ======-->
    <servlet-name>jsonStockData</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.JsonStockData</servlet-class>
  </servlet>

  <servlet-mapping>
    <!--====== /stockwatcher/stockPricesというURLに関連付け ======-->
    <servlet-name>jsonStockData</servlet-name>
    <url-pattern>/stockwatcher/stockPrices</url-pattern>
  </servlet-mapping>

  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>StockWatcher.html</welcome-file>
  </welcome-file-list>

</web-app>





ここまでで、JSONデータの取得が行えるようになっているはずです。
ブラウザで
http://localhost:8888/stockwatcher/stockPrices?q=ABC+DEF
を表示すると、こんな結果が得られると思います。

11_002_20101109002730.png







2. Manipulating JSON data in the client-side code



まず、JSONデータをラップするためのクラスを作成します。

パッケージ・エクスプローラーで、
「com.google.gwt.sample.stockwatcher.client」
を選択した状態で、Eclipseのツールバーの[ファイル]-[新規作成]-[クラス]を選択。

パッケージは「com.google.gwt.sample.stockwatcher.client」
クラス名は「StockData」
スーパークラスは「com.google.gwt.core.client.JavaScriptObject」
として、完了を押下します。

11_003.png


作成した「StockData」を以下の内容で更新します。


package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.core.client.JavaScriptObject;

public class StockData extends JavaScriptObject {
    // Overlay types always have protected, zero argument constructors.
    protected StockData() {
    }
    
    // JSNI methods to get stock data.
    public final native String getSymbol() /*-{ return this.symbol; }-*/;

    public final native double getPrice() /*-{ return this.price; }-*/;

    public final native double getChange() /*-{ return this.change; }-*/;

    // Non-JSNI method to return change percentage. // [4]
    public final double getChangePercent() {
        return 100.0 * getChange() / getPrice();
    }
}





このクラスが、サーバーから取得したデータの

  {
    "symbol": "DEF",
    "price": 43.819500786906815,
    "change": 0.18992536537119453
  }


この部分に割り当てられるイメージでよいかな?と思っています。





3. Making HTTP requests



まず、StockWatcher.gwt.xmlに変更を加えます。


<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='stockwatcher'>
<!-- Inherit the core Web Toolkit stuff.                        -->
<inherits name='com.google.gwt.user.User'/>

<!-- Inherit the default GWT style sheet. You can change     -->
<!-- the theme of your GWT application by uncommenting         -->
<!-- any one of the following lines.                            -->
<inherits name='com.google.gwt.user.theme.standard.Standard'/>
<!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
<!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>     -->

<!-- Other module inherits                                     -->
<!--=== 以下の一行を追加 ===-->
<inherits name="com.google.gwt.http.HTTP" />

<!-- Specify the app entry point class.                         -->
<entry-point class='com.google.gwt.sample.stockwatcher.client.StockWatcher'/>

<!-- Specify the paths for translatable code                    -->
<source path='client'/>
<source path='shared'/>

</module>




「com.google.gwt.http.HTTP」の記載を追加することで、サーバー側との
HTTP通信が行えるようになるようです。
※雛形のJavaScriptにメソッドが追加出力されるようになる?



次に、StockWatcher.javaに以下のimportを追加。


import com.google.gwt.core.client.JsArray
import com.google.gwt.core.client.GWT;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import java.util.Iterator;





データ取得を行うURLを定義


private static final String JSON_URL = GWT.getModuleBaseURL() + "stockPrices?q=";





株価データの取得を行うメソッド「refreshWatchList」を変更します。


    // 価格情報の更新を行う
    private void refreshWatchList() {
        if (stocks.size() == 0) {
            return;
        }

        String url = JSON_URL;

        // Append watch list stock symbols to query URL.
        Iterator iter = stocks.iterator();
        while (iter.hasNext()) {
            url += iter.next();
            if (iter.hasNext()) {
                url += "+";
            }
        }

        url = URL.encode(url);

        // Send request to server and catch any errors.
        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);

        try {
            Request request = builder.sendRequest(null, new RequestCallback() {
                public void onError(Request request, Throwable exception) {
                    displayError("Couldn't retrieve JSON");
                }

                public void onResponseReceived(Request request,
                        Response response) {
                    if (200 == response.getStatusCode()) {
                        updateTable(asArrayOfStockData(response.getText()));
                    } else {
                        displayError("Couldn't retrieve JSON ("
                                + response.getStatusText() + ")");
                    }
                }
            });
        } catch (RequestException e) {
            displayError("Couldn't retrieve JSON");
        }
    }






取得したJSON形式のデータを配列に変換するメソッドを追加。


    private final native JsArray<StockData> asArrayOfStockData(String json) /*-{
        return eval(json);
    }-*/;





updateTableの引数を、「StockPrice[]」から「JsArray<StockData>」に変更します。


    /*
    private void updateTable(StockPrice[] prices) {
        // テーブルの更新処理
        for (int i = 0; i < prices.length; i++) {
            updateTable(prices[i]);
        }
    */
    private void updateTable(JsArray<StockData> prices) {
        // テーブルの更新処理
        for (int i = 0; i < prices.length(); i++) {
            updateTable(prices.get(i));
        }
        
        DateTimeFormat timeFormat = DateTimeFormat.getFormat("yyyy年MM月dd日 HH:mm:ss");
        
        // 最終更新時間を表示
        lastUpdatedLabel.setText("最終更新時間 : "
            + timeFormat.format(new Date(), TimeZone.createTimeZone(-60 * 9)));
        
        // ここまで処理がくれば、エラーは発生していない
        // エラーメッセージ領域を非表示にする
        errorMsgLabel.setVisible(false);
    }





もう一箇所、updateTableの引数「StockPrice」を「StockData」に変更。


    //private void updateTable(StockPrice price) {
    private void updateTable(StockData price) {
        // ArrayListに指定された株式銘柄コードが存在しない場合は処理中断
        if (!stocks.contains(price.getSymbol())) {
            return;
        }








4. Handling GET errors



エラーメッセージ表示用のメソッド「displayError」を定義します。


    // エラー表示
    private void displayError(String error) {
        errorMsgLabel.setText("Error: " + error);
        errorMsgLabel.setVisible(true);
    }






駆け足でしたが、これでGWT RPCからJSONへの変更は完了です。

開発用のサーバーを起動して、ブラウザからアクセスすると従来どおりの
表示が行われているはず。


11_004.png




最終的に、StockWatcher.javaはこうなりました。


package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlexTable;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.user.client.Window;
import java.util.ArrayList;
import com.google.gwt.user.client.Timer;
import com.google.gwt.i18n.client.NumberFormat;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.TimeZone;

import java.util.Date;
import com.google.gwt.core.client.GWT;

import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import java.util.Iterator;

public class StockWatcher implements EntryPoint {
    
    // 更新間隔を指定
    private static final int REFRESH_INTERVAL = 5000; // ms
    private VerticalPanel mainPanel = new VerticalPanel();
    private FlexTable stocksFlexTable = new FlexTable();
    private HorizontalPanel addPanel = new HorizontalPanel();
    private TextBox newSymbolTextBox = new TextBox();
    private Button addStockButton = new Button("追加");
    private Label lastUpdatedLabel = new Label();
    // テーブルに表示しているデータ保存用の変数
    private ArrayList<String> stocks = new ArrayList<String>();
    private StockPriceServiceAsync stockPriceSvc = GWT.create(StockPriceService.class);
    
    private Label errorMsgLabel = new Label();
    
    private static final String JSON_URL = GWT.getModuleBaseURL() + "stockPrices?q=";
    
    /**
     * Entry point method.
     */
    public void onModuleLoad() {
        // テーブルの作成
        stocksFlexTable.setText(0, 0, "銘柄");
        stocksFlexTable.setText(0, 1, "価格");
        stocksFlexTable.setText(0, 2, "変動");
        stocksFlexTable.setText(0, 3, "削除");
        
        // CellPaddingを指定して、間隔を
        stocksFlexTable.setCellPadding(6);
        
        // スタイルの指定を追加
        stocksFlexTable.getRowFormatter().addStyleName(0, "watchListHeader");
        stocksFlexTable.addStyleName("watchList");
        stocksFlexTable.getCellFormatter().addStyleName(0, 1, "watchListNumericColumn");
        stocksFlexTable.getCellFormatter().addStyleName(0, 2, "watchListNumericColumn");
        stocksFlexTable.getCellFormatter().addStyleName(0, 3, "watchListRemoveColumn");
        
        // 部品が横に並ぶパネルに、銘柄入力テキストとボタンを追加
        addPanel.add(newSymbolTextBox);
        addPanel.add(addStockButton);
        // スタイルの指定を追加
        addPanel.addStyleName("addPanel");
        
        // エラーメッセージ表示領域
        errorMsgLabel.setStyleName("errorMessage");
        errorMsgLabel.setVisible(false);
        
        // 部品が縦に並ぶパネルに、エラー表示ラベル、テーブルと上記パネル、
        // 最終更新日ラベルを追加
        mainPanel.add(errorMsgLabel);
        mainPanel.add(stocksFlexTable);
        mainPanel.add(addPanel);
        mainPanel.add(lastUpdatedLabel);
        
        // StockWatcher.htmlのid=stockListのdivを
        // ルートパネルとして取得し、mainPanelを設定する。
        RootPanel.get("stockList").add(mainPanel);
        
        // 起動時、銘柄入力のテキストボックスにフォーカスを設定する
        newSymbolTextBox.setFocus(true);
        
        // 追加ボタンにイベントリスナーを設定
        addStockButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                addStock();
            }
        });
        
        // 銘柄入力テキストにイベントリスナーを設定
        newSymbolTextBox.addKeyPressHandler(new KeyPressHandler() {
            public void onKeyPress(KeyPressEvent event) {
                // 入力されたキーが「Enter」だったら、追加処理を実行する
                if (event.getCharCode() == KeyCodes.KEY_ENTER) {
                    addStock();
                }
            }
        });
        
        // データの自動更新を行うために、onModuleLoadメソッドでタイマーを生成
        Timer refreshTimer = new Timer() {
            @Override
            public void run() {
                refreshWatchList();
            }
        };
        refreshTimer.scheduleRepeating(REFRESH_INTERVAL);
    }
    
    // 株式銘柄の追加処理
    private void addStock() {
        // テキストボックスから入力された文字列を取得
        final String symbol = newSymbolTextBox.getText().toUpperCase().trim();
        newSymbolTextBox.setFocus(true);

        // 証券コードは、1文字以上10文字以下
        // 使える文字は数字・アルファベット・ドット(.)のみ
        if (!symbol.matches("^[0-9A-Z\\.]{1,10}$")) {
            // 入力された文字が証券コードではない場合は、エラーメッセージ表示
            Window.alert("'" + symbol + "' は不正な証券コードです。");
            newSymbolTextBox.selectAll();
            return;
        }

        newSymbolTextBox.setText("");
        
        // 証券コードの重複チェック処理
        // 追加予定の証券コードが既にリストに含まれていたら、追加処理を実行しない。
        if (stocks.contains(symbol)) {
            return;
        }
        // テーブルへの行追加処理
        // 現在のテーブルの行数を取得
        int row = stocksFlexTable.getRowCount();
        // ArrayListに追加した証券コードを退避
        stocks.add(symbol);
        // テーブルへ行の追加実行
        stocksFlexTable.setText(row, 0, symbol);
        // ラベルを追加
        stocksFlexTable.setWidget(row, 2, new Label());
        
        // スタイルの指定を追加
        stocksFlexTable.getCellFormatter().addStyleName(row, 1, "watchListNumericColumn");
        stocksFlexTable.getCellFormatter().addStyleName(row, 2, "watchListNumericColumn");
        stocksFlexTable.getCellFormatter().addStyleName(row, 3, "watchListRemoveColumn");
        
        // 追加した行に削除ボタンを配置
        Button removeStockButton = new Button("x");
        removeStockButton.addStyleDependentName("remove");
        
        // 削除ボタンが押されたときのイベントを定義
        removeStockButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                // 削除する行のインデックスを取得
                int removedIndex = stocks.indexOf(symbol);
                // ArrayListから削除
                stocks.remove(removedIndex);
                // テーブルからも削除
                stocksFlexTable.removeRow(removedIndex + 1);
            }
        });
        
        // 削除ボタンをテーブルに追加
        stocksFlexTable.setWidget(row, 3, removeStockButton);
        
        // 証券の金額を取得
        refreshWatchList();
    }
    
    // 価格情報の更新を行う
    private void refreshWatchList() {
        if (stocks.size() == 0) {
            return;
        }

        String url = JSON_URL;

        // Append watch list stock symbols to query URL.
        Iterator iter = stocks.iterator();
        while (iter.hasNext()) {
            url += iter.next();
            if (iter.hasNext()) {
                url += "+";
            }
        }

        url = URL.encode(url);

        // Send request to server and catch any errors.
        RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url);

        try {
            Request request = builder.sendRequest(null, new RequestCallback() {
                public void onError(Request request, Throwable exception) {
                    displayError("Couldn't retrieve JSON");
                }

                public void onResponseReceived(Request request,
                        Response response) {
                    if (200 == response.getStatusCode()) {
                        updateTable(asArrayOfStockData(response.getText()));
                    } else {
                        displayError("Couldn't retrieve JSON ("
                                + response.getStatusText() + ")");
                    }
                }
            });
        } catch (RequestException e) {
            displayError("Couldn't retrieve JSON");
        }
    }

    private final native JsArray<StockData> asArrayOfStockData(String json) /*-{
        return eval(json);
    }-*/;
    
    /*
    private void updateTable(StockPrice[] prices) {
        // テーブルの更新処理
        for (int i = 0; i < prices.length; i++) {
            updateTable(prices[i]);
        }
    */
    private void updateTable(JsArray<StockData> prices) {
        // テーブルの更新処理
        for (int i = 0; i < prices.length(); i++) {
            updateTable(prices.get(i));
        }
        
        DateTimeFormat timeFormat = DateTimeFormat.getFormat("yyyy年MM月dd日 HH:mm:ss");
        
        // 最終更新時間を表示
        lastUpdatedLabel.setText("最終更新時間 : "
            + timeFormat.format(new Date(), TimeZone.createTimeZone(-60 * 9)));
        
        // ここまで処理がくれば、エラーは発生していない
        // エラーメッセージ領域を非表示にする
        errorMsgLabel.setVisible(false);
    }
    
    //private void updateTable(StockPrice price) {
    private void updateTable(StockData price) {
        // ArrayListに指定された株式銘柄コードが存在しない場合は処理中断
        if (!stocks.contains(price.getSymbol())) {
            return;
        }

        int row = stocks.indexOf(price.getSymbol()) + 1;

        // 価格表示用のフォーマット
        String priceText = NumberFormat.getFormat("#,##0.00").format(
                price.getPrice());
        // 変動率表示用のフォーマット
        NumberFormat changeFormat = NumberFormat
                .getFormat("+#,##0.00;-#,##0.00");
        
        // 価格・変動率をフォーマットした文字列を取得
        String changeText = changeFormat.format(price.getChange());
        String changePercentText = changeFormat
                .format(price.getChangePercent());

        // 価格・変動率のテーブル表示を更新
        stocksFlexTable.setText(row, 1, priceText);
        // ラベルに対してテキストを追加するようにする。
        Label changeWidget = (Label)stocksFlexTable.getWidget(row, 2);
        changeWidget.setText(changeText + " (" + changePercentText + "%)");
        
        // 変動率に応じて、適用するスタイルの名称を変更
        String changeStyleName = "noChange";
        if (price.getChangePercent() < -0.1f) {
         changeStyleName = "negativeChange";
        }
        else if (price.getChangePercent() > 0.1f) {
         changeStyleName = "positiveChange";
        }
        
        changeWidget.setStyleName(changeStyleName);
        
    }
    
    // エラー表示
    private void displayError(String error) {
        errorMsgLabel.setText("Error: " + error);
        errorMsgLabel.setVisible(true);
    }
}









Google Web Toolkit 2.1 GWT RPCの使用

Google Web Toolkit 2.1のチュートリアルを試してみています。
Google Web Toolkit に関する記事の一覧


簡単なアプリケーションを作成しましたが、クライアント側の
JavaScriptの動作で完結する内容でした。

実際は、サーバーと通信してデータの取得や登録を行う
アプリケーションが多いと思いますので、RPCの使用に関する
章も試してみることにします。


Making Remote Procedure Calls
こちらを参考に、プログラムを作成しました。




1. Creating a service



これまでのチュートリアルで作成した「StockWatcher」で、株価の値は
StockWatcherクラスのrefreshWatchListメソッドでランダムに生成していました。

該当する処理の箇所はここ。


    // 価格情報の更新を行う
    private void refreshWatchList() {
        // 価格情報の更新処理
        final double MAX_PRICE = 100.0; // $100.00
        final double MAX_PRICE_CHANGE = 0.02; // +/- 2%

        StockPrice[] prices = new StockPrice[stocks.size()];
        for (int i = 0; i < stocks.size(); i++) {
            double price = Random.nextDouble() * MAX_PRICE;
            double change = price * MAX_PRICE_CHANGE
                    * (Random.nextDouble() * 2.0 - 1.0);

            prices[i] = new StockPrice(stocks.get(i), price, change);
        }

        updateTable(prices);
        
    }





この部分をサーバー側で処理するように変更します。
まずは、「RemoteService」を継承して「StockPriceService」を作成します。

パッケージ・エクスプローラーで「com.google.gwt.sample.stockwatcher.client」を
選択しておきます。

10_001_20101107191601.png


Eclipseツールバーの[ファイル]-[新規]-[インターフェース]を選択。

10_002_20101107191601.png


名前に「StockPriceService」を入力して「完了」を押します。
その他はデフォルトのままでOK

10_003_20101107191601.png


StockPriceService.javaを以下の内容に変更します。
※変更した時点ではエラーが表示されると思いますが、次のソースを
作成することでエラーが解消されますので、一旦無視で。


package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("stockPrices")
public interface StockPriceService extends RemoteService {
    StockPrice[] getPrices(String[] symbols);
}





次に、「RemoteServiceServlet」を継承したクラスを作成します。

Eclipseのツールバーから[ファイル]-[新規]-[クラス]を選択。

10_004_20101107191601.png


パッケージ名を
「com.google.gwt.sample.stockwatcher.client」から
「com.google.gwt.sample.stockwatcher.server」に変更します。

名前は「StockPriceServiceImpl」

スーパークラスに「com.google.gwt.user.server.rpc.RemoteServiceServlet」を
入力します。

10_005.png


インターフェースは右側にある追加ボタンを押して、
「com.google.gwt.sample.stockwatcher.client.StockPriceService」
を入力し候補を表示。OKを押します。

10_006.png


インターフェースの項目に、指定したクラスが追加されたことを確認して、
完了ボタンを押下します。

10_007.png


Eclipseで自動生成された「StockPriceServiceImpl.java」は
以下のようになるはずです。



package com.google.gwt.sample.stockwatcher.server;

import com.google.gwt.sample.stockwatcher.client.StockPrice;
import com.google.gwt.sample.stockwatcher.client.StockPriceService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class StockPriceServiceImpl extends RemoteServiceServlet implements
        StockPriceService {

    @Override
    public StockPrice[] getPrices(String[] symbols) {
        // TODO 自動生成されたメソッド・スタブ
        return null;
    }

}





この「StockPriceServiceImpl」クラスの「getPrices」メソッドに
クライアントのJavaScriptで動作していた株価生成の処理を移動します。

処理記載後のStockPriceServiceImpl.javaはこうなりました。


package com.google.gwt.sample.stockwatcher.server;

import com.google.gwt.sample.stockwatcher.client.StockPrice;
import com.google.gwt.sample.stockwatcher.client.StockPriceService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import java.util.Random;

public class StockPriceServiceImpl extends RemoteServiceServlet implements
        StockPriceService {
    
    private static final long serialVersionUID = 1L;
    private static final double MAX_PRICE = 100.0; // $100.00
    private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2%
    
    @Override
    public StockPrice[] getPrices(String[] symbols) {
        Random rnd = new Random();
        
        StockPrice[] prices = new StockPrice[symbols.length];
        for (int i=0; i<symbols.length; i++) {
            double price = rnd.nextDouble() * MAX_PRICE;
            double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f);
            
            prices[i] = new StockPrice(symbols[i], price, change);
        }
        return prices;
    }

}







この作成したサービスはservletで動作します。
そのため、web.xmlへservletの追加が必要になります。

http://localhost:8888/stockwatcher/stockPrices

というURLでサービスを公開しようと思いますので、
StockWatcher/war/WEB-INF/web.xml
を以下のように編集しておきます。

※雛形としてで出力されている「greetServlet」という定義があるかと
思いますが、ここで消してしまいます。


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

  <!-- Servlets -->
  <servlet>
    <!--====== stockPriceServiceImplというサーブレットを定義 ======-->
    <servlet-name>stockPriceServiceImpl</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.StockPriceServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <!--====== /stockwatcher/stockPricesというURLに関連付け ======-->
    <servlet-name>stockPriceServiceImpl</servlet-name>
    <url-pattern>/stockwatcher/stockPrices</url-pattern>
  </servlet-mapping>

  <!-- Default page to serve -->
  <welcome-file-list>
    <welcome-file>StockWatcher.html</welcome-file>
  </welcome-file-list>

</web-app>









2. Invoking the service from the client



ここまでの操作でサーバー側で実行されるプログラムが作成できたことになります。

これから、クライアント側からサーバー側に通信するための
プログラムを作成します。

前の段落で作成した「StockPriceService」ですが、エラーが表示されて
いる状態だと思います。

ソースの左側にある黄色いアイコンをクリックすると、こんな感じで
修正候補が表示されるはずです。

10_008.png


一番先頭に表示される「Create asynchronous RemoteService interface ...」
を選択すると、ダイアログが表示されます。

そのまま完了を押下して、操作を進めます。

10_009.png


自動生成された「StockPriceServiceAsync」は以下のようになるはずです。



package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface StockPriceServiceAsync {

    void getPrices(String[] symbols, AsyncCallback<StockPrice[]> callback);

}






クライアント側では、このクラスを経由してサーバー側との通信を
実現する模様。

StockWatcherクラスに以下のimportを追加。

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;




また、以下の変数も追加。


private StockPriceServiceAsync stockPriceSvc = GWT.create(StockPriceService.class);




refreshWatchListメソッドの処理内容を
このように変更します。


    // 価格情報の更新を行う
    private void refreshWatchList() {
        // Initialize the service proxy.
        if (stockPriceSvc == null) {
            stockPriceSvc = GWT.create(StockPriceService.class);
        }
        
        // Set up the callback object.
        AsyncCallback<StockPrice[]> callback = new AsyncCallback<StockPrice[]>() {
            public void onFailure(Throwable caught) {
                // TODO: Do something with errors.
            }
            
            public void onSuccess(StockPrice[] result) {
                updateTable(result);
            }
        };
        
        // Make the call to the stock price service.
        stockPriceSvc.getPrices(stocks.toArray(new String[0]), callback);
    }





ここまでできたら、開発用のサーバーを起動し、ブラウザでアクセスしてみます。
すると、こんな感じでエラーが表示されるはずです。

10_010.png



18:37:31.126 [ERROR] [stockwatcher]
subtype com.google.gwt.sample.stockwatcher.client.StockPrice is not
assignable to 'com.google.gwt.user.client.rpc.IsSerializable' or
'java.io.Serializable' nor does it have a custom field serializer
(reached via com.google.gwt.sample.stockwatcher.client.StockPrice[])




クライアントとサーバー間のデータのやり取りに使用している
「StockPrice」をシリアライズできなかったよ。というエラーですね。






3. Serializing Java objects



というわけで、データの通信に使用している「StockPrice」を
シリアライズ可能とするため、「Serializable」をimplementsするよう
変更してやります。

StockPriceで変更した箇所の抜粋はこちら。


package com.google.gwt.sample.stockwatcher.client;

// シリアライズするため、importを追加
import java.io.Serializable;

public class StockPrice implements Serializable {

    private static final long serialVersionUID = 1L;
    private String symbol;
    private double price;
    private double change;







4. Handling Exceptions



最後にエラー処理を追加します。

Eclipseのツールバー[ファイル]-[新規]-[クラス]を選択。

名前「DelistedException」
スーパークラス「java.lang.Exception」
インターフェース「java.io.Serializable」
を入力し、完了を押下。

10_011.png


DelistedExceptionを以下の内容に変更します。


package com.google.gwt.sample.stockwatcher.client;

import java.io.Serializable;

public class DelistedException extends Exception implements Serializable {

    private static final long serialVersionUID = 1L;
    private String symbol;

    public DelistedException() {
    }

    public DelistedException(String symbol) {
        this.symbol = symbol;
    }

    public String getSymbol() {
        return this.symbol;
    }
}






StockPriceServiceのgetPricesメソッドが作成した例外を
投げられるように修正します。


package com.google.gwt.sample.stockwatcher.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("stockPrices")
public interface StockPriceService extends RemoteService {
    StockPrice[] getPrices(String[] symbols) throws DelistedException;;
}







エラー発生のサンプルとして、StockPriceServiceImplの株価を取得する処理で、
株式銘柄コードに「ERR」と入力された場合は、DelistedExceptionを
発生するように修正しました。

修正後の「StockPriceServiceImpl」はこんな感じになります。


package com.google.gwt.sample.stockwatcher.server;

import com.google.gwt.sample.stockwatcher.client.StockPrice;
import com.google.gwt.sample.stockwatcher.client.StockPriceService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

import java.util.Random;
import com.google.gwt.sample.stockwatcher.client.DelistedException;

public class StockPriceServiceImpl extends RemoteServiceServlet implements
        StockPriceService {
    
    private static final long serialVersionUID = 1L;
    private static final double MAX_PRICE = 100.0; // $100.00
    private static final double MAX_PRICE_CHANGE = 0.02; // +/- 2%
    
    @Override
    public StockPrice[] getPrices(String[] symbols) throws DelistedException {
        Random rnd = new Random();
        
        StockPrice[] prices = new StockPrice[symbols.length];
        for (int i=0; i<symbols.length; i++) {
            
            // 動作サンプルとして、銘柄コードに「ERR」と入力されている場合は
            // 例外を発生させる
            if (symbols[i].equals("ERR")) {
                throw new DelistedException("ERR");
            }
            
            double price = rnd.nextDouble() * MAX_PRICE;
            double change = price * MAX_PRICE_CHANGE * (rnd.nextDouble() * 2f - 1f);
            
            prices[i] = new StockPrice(symbols[i], price, change);
        }
        return prices;
    }

}









エラーの表示は赤い文字で行いたいと思いますので、
StockWatcher.cssに以下の記載を追加。


/* エラー色を指定 */
.errorMessage {
  color: red;
}





StockWatcher.javaにエラー表示用のラベルを追加します。


    private Label errorMsgLabel = new Label();




作成したラベルオブジェクトをパネルに配置する処理も追加。


        // 部品が横に並ぶパネルに、銘柄入力テキストとボタンを追加
        addPanel.add(newSymbolTextBox);
        addPanel.add(addStockButton);
        // スタイルの指定を追加
        addPanel.addStyleName("addPanel");
        
        // エラーメッセージ表示領域
        errorMsgLabel.setStyleName("errorMessage");
        errorMsgLabel.setVisible(false);
        
        // 部品が縦に並ぶパネルに、エラー表示ラベル、テーブルと上記パネル、
        // 最終更新日ラベルを追加
        mainPanel.add(errorMsgLabel);
        mainPanel.add(stocksFlexTable);
        mainPanel.add(addPanel);
        mainPanel.add(lastUpdatedLabel);






refreshWatchListメソッドでTODOとして残していた、onFailureの処理を追記。
サーバー側で発生したエラーをラベルに出力します。


    // 価格情報の更新を行う
    private void refreshWatchList() {
        // Initialize the service proxy.
        if (stockPriceSvc == null) {
            stockPriceSvc = GWT.create(StockPriceService.class);
        }
        
        // Set up the callback object.
        AsyncCallback<StockPrice[]> callback = new AsyncCallback<StockPrice[]>() {
            public void onFailure(Throwable caught) {
                String details = caught.getMessage();
                if (caught instanceof DelistedException) {
                    details = "Company '" + ((DelistedException)caught).getSymbol() + "' was delisted";
                }
                errorMsgLabel.setText("Error: " + details);
                errorMsgLabel.setVisible(true);
            }
            
            public void onSuccess(StockPrice[] result) {
                updateTable(result);
            }
        };
        
        // Make the call to the stock price service.
        stockPriceSvc.getPrices(stocks.toArray(new String[0]), callback);
    }






このままだと、一度エラーが発生した後はずっとエラーの表示が
残ってしまうため、エラー表示を消去する処理を追加します。


    private void updateTable(StockPrice[] prices) {
        // テーブルの更新処理
        for (int i = 0; i < prices.length; i++) {
            updateTable(prices[i]);
        }
        
        DateTimeFormat timeFormat = DateTimeFormat.getFormat("yyyy年MM月dd日 HH:mm:ss");
        
        // 最終更新時間を表示
        lastUpdatedLabel.setText("最終更新時間 : "
            + timeFormat.format(new Date(), TimeZone.createTimeZone(-60 * 9)));
        
        // ここまで処理がくれば、エラーは発生していない
        // エラーメッセージ領域を非表示にする
        errorMsgLabel.setVisible(false);
    }






開発用のサーバーを起動し、ブラウザで動作を確認してみると、
狙い通りの動きになっているかと思います。

10_012.png




関連記事



Google Web Toolkit に関する記事の一覧









プロフィール

Author:symfo
blog形式だと探しにくいので、まとめサイト作成中です。
Symfoware まとめ

PR




検索フォーム

月別アーカイブ