2011年8月2日火曜日

Deploying to Google App Engine

http://code.google.com/intl/ja/webtoolkit/doc/latest/tutorial/appengine.html


 Google App Engineへデプロイ(配置)する

この時点でクライアントサイドのコードで株価データをシミュレートする
ストックウォッチャー(株価監視)アプリケーションの最初の実装を作成済です。

このセクションでは、Google App Engine上へこのアプリケーションをデプロイ(配置)します。
さらにいくつかのApp Engine service APIとユーザーが自身のGoogleアカウントへログインし、自身の株のリストを取得できるようにStockWatcherアプリケーションをパーソナライズするためにこれらのAPIの使用についても学びます。 
  1. App Engineの前準備
  2. App Engineへアプリケーションをデプロイする
  3. User Serviceを使用したアプリケーションのパーソナライズ
  4. データストアへのデータの格納
Note: デプロイについての幅広いガイドはDeploy a GWT Applicationを見てください。
このチュートリアルはGWTの概念とBuild a Sample GWT Application チュートリアルで作成した
StockWatcherアプリケーションを基にしています。

更にGWT RPCチュートリアルで説明されているテクニックも使用しています。もしこれらのチュートリアルを終えておらず、GWTの概念がある程度わかっているなら(慣れ親しんでいるなら)、以下で指示されるように、ここでStockWatcherプロジェクトをコードとしてインポートすることが出来ます。

 1. App Engine の前準備

App Engine アカウントのサインアップ(登録)する

App Engineアカウントに登録します。 アカウントがアクティベイトされた後、サインインしてアプリケーションを作成します。StockWatcherプロジェクトを設定する時に必要になるので、選択したアプリケーションIDはメモしておいてください。このチュートリアルを終えた後、このアプリケーションIDは他のアプリケーションで再利用することが出来ます。
アクティベイトされる…大雑把に言うとユーザーやアカウントがシステム側からソフトウェア、ハードウェア等への利用許可を受けること。

App Engine SDKをダウンロードする

Eclipseを使うつもりならば、 Google Plugin for Eclipseと一緒にApp Engine SDKをダウンロードします。または別個にApp Engine SDK for Javaをダウンロードします。

プロジェクトをセットアップする

プロジェクトをセットアップする(Eclipseで)

もし初めにGWTとGoogle App Engineの両方を有効にしてあるGoogle Plugin for Eclipseを使用してStockWatcher Eclipseプロジェクトを作成した場合は、プロジェクトは既にApp Engineで走らせる準備が出来ています。もしそうでない場合は:
  1. まだGWTとApp Engine SDKの両方を含んだGoogle Plugin for Eclipse をインストールしていない場合はインストールをしてEclipse を再起動します。
  2. GWTとGoogle App Engineの両方が有効にしてあるプロジェクトを作成したことを確認して、 Build a Sample GWT Application チュートリアルを完了してください。或いは、 Build a Sample GWT Applicationチュートリアルをスキップしたい場合は、StockWatcher Eclipseプロジェクトをダウンロードして、解凍、インポートしてください。
    プロジェクトをインポートするには:
    1. [ファイル(F)]メニューの[インポート(I)]メニューオプションを選択します。
    2. 「インポート・ソースの選択(S)」で"一般"-"既存プロジェクトをワークスペースへ"を選択します。「次へ(N)」ボタンをクリックします。
    3. 「ルート・ディレクトリーの選択(T)」で、(解凍したファイルのから)StockWatcherのディレクトリへ移動して選択します。「完了(F)」ボタンをクリックします。
    4. Google Web ToolkitとApp Engineの機能を新しく作成したプロジェクトへ追加します。
      (プロジェクトを右クリックして[Google]-[Web ツールキット設定]/[App エンジン設定])
      これはGoogle Pluginの機能をプロジェクトに追加するだけでなく、プロジェクトの WEB-INF/lib ディレクトリへ必須ライブラリを自動的に追加します。

      ※[Web ツールキット設定]では「Google Web ツールキットを使用」にチェック済み?
      ※ubuntu上のeclipseだと「App エンジン設定」で「Google App エンジンを使用する」にチェックを入れて「OK」ボタンをクリックすると"The appengine-web.xml file is missing"というエラーが発生する。(我が家の環境が安定していないので、環境由来のエラーかも)
      以下の3.の手順で作成する?

プロジェクトをセットアップする(Eclipseなしで)

  1. まだApp Engine SDK for Javaをインストールしていない場合はダウンロードします。
  2. GWTアプリケーションを作成するためにwebAppCreatorを使用してBuild a Sample GWT Applicationチュートリアルを完了してください。或いは、 Build a Sample GWT Applicationチュートリアルをスキップしたい場合は、 このファイルをダウンロードして解凍してください。 
    StockWatcher/build.xmlのgwt.sdkプロパティを編集してから、以下の修正を続行してください。
  3. App Engineは自身のwebアプリケーション配備記述子(appengine-web.xml)を必要とします。 以下の内容を含むStockWatcher/war/WEB-INF/appengine-web.xmlを作成します:
    <?xml version="1.0" encoding="utf-8"?>
    <appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
      <application><!-- Your App Engine application ID goes here --></application>
      <version>1</version>
    </appengine-web-app>
    
    二行目のハイライト部分をあなたのApp Engineのapplication IDに代えてください。詳細はappengine-web.xmlを読んでください。
  4. 後でデータを格納するために Java Data Objects (JDO)を使用するので、 以下の内容を含むStockWatcher/src/META-INF/jdoconfig.xmlファイルを作成します:
    <?xml version="1.0" encoding="utf-8"?>
    <jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">
      <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass" value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
      </persistence-manager-factory>
    </jdoconfig> 
    後で"transactions-optional"(ハイライト部分)という名前によって設定を参照します。詳細は jdoconfig.xmlを読んでください。
  5. DataNucleus JDOのコンパイルとApp Engine 開発サーバの使用をサポートするためにGWT ant ビルドファイルを 修正する必要があります。 StockWatcher/build.xmlを編集し以下の追加を行います:
    1. App Engine SDKディレクトリ用のプロパティを追加します。 
      <!-- Configure path to GWT SDK -->
        <property name="gwt.sdk" location="Path to GWT" />
        <!-- Configure path to App Engine SDK -->
        <property name="appengine.sdk" location="Path to App Engine SDK" />
      
    2. App Engine tools class path用のプロパティを追加します。
      <path id="project.class.path">
          <pathelement location="war/WEB-INF/classes"/>
          <pathelement location="${gwt.sdk}/gwt-user.jar"/>
          <fileset dir="${gwt.sdk}" includes="gwt-dev*.jar"/>
          <!-- Add any additional non-server libs (such as JUnit) -->
          <fileset dir="war/WEB-INF/lib" includes="**/*.jar"/>
        </path>
      
        <path id="tools.class.path">
          <path refid="project.class.path"/>
          <pathelement location="${appengine.sdk}/lib/appengine-tools-api.jar"/>
          <fileset dir="${appengine.sdk}/lib/tools">
            <include name="**/asm-*.jar"/>
            <include name="**/datanucleus-enhancer-*.jar"/>
          </fileset>
        </path>
      
    3. 必要なJarファイルがWEB-INF/libにコピーされるように、"libs" ant targetを修正します。
      <target name="libs" description="Copy libs to WEB-INF/lib">
          <mkdir dir="war/WEB-INF/lib" />
          <copy todir="war/WEB-INF/lib" file="${gwt.sdk}/gwt-servlet.jar" />
          <!-- Add any additional server libs that need to be copied -->
          <copy todir="war/WEB-INF/lib" flatten="true">
            <fileset dir="${appengine.sdk}/lib/user" includes="**/*.jar"/>
          </copy>
        </target>
      
    4. JDOはDataNucleus Javaバイトコード拡張で実装されます。バイトコード拡張を追加するために、"javac" ant targetを修正します。
      <target name="javac" depends="libs" description="Compile java source">
          <mkdir dir="war/WEB-INF/classes"/>
          <javac srcdir="src" includes="**" encoding="utf-8"
              destdir="war/WEB-INF/classes"
              source="1.5" target="1.5" nowarn="true"
              debug="true" debuglevel="lines,vars,source">
            <classpath refid="project.class.path"/>
          </javac>
          <copy todir="war/WEB-INF/classes">
            <fileset dir="src" excludes="**/*.java"/>
          </copy>
          <taskdef
             name="datanucleusenhancer"
             classpathref="tools.class.path"
             classname="org.datanucleus.enhancer.tools.EnhancerTask" />
          <datanucleusenhancer
             classpathref="tools.class.path"
             failonerror="true">
            <fileset dir="war/WEB-INF/classes" includes="**/*.class" />
          </datanucleusenhancer>
        </target>
      
    5. GWTに伴うサーブレットコンテナの代わりにApp Engine開発サーバを使用するために"devmode" ant targetを修正します。
      <target name="devmode" depends="javac" description="Run development mode"">
          <java failonerror="true" fork="true" classname="com.google.gwt.dev.DevMode"">
            <classpath>
              <pathelement location="src"/>
              <path refid="project.class.path"/>
              <path refid="tools.class.path"/>
            </classpath>
            <jvmarg value="-Xmx256M"/>
            <arg value="-startupUrl"/>
            <arg value="StockWatcher.html"/>
            <!-- Additional arguments like -style PRETTY or -logLevel DEBUG -->
            <arg value="-server"/>
            <arg value="com.google.appengine.tools.development.gwt.AppEngineLauncher"/>
            <arg value="com.google.gwt.sample.stockwatcher.StockWatcher"/>
          </java>
        </target>
      

ローカルでテストする

プロジェクトがうまく設定されたことを確認するためにGWT開発モードでアプリケーションを走らせます。しかしながら、GWTに伴うサーブレットコンテナを使用する代わりに、アプリケーションをApp Engine開発サーバ内(App Engine SDKに伴うサーブレットコンテナ)で走らせます。何が違うのか?
App Engine開発サーバはApp Engineプロダクション環境を真似るように設定されています。 

開発モードでアプリケーションを走らせる(Eclipseで)

  1. パッケージエクスプローラビューで、StockWatcherプロジェクトを選択します。
  2. ツールバーの"実行"ボタンをクリックします。("Web アプリケーションの実行")

開発モードでアプリケーションを走らせる(Eclipseなしで)

  1. コマンドラインでStockWatcher ディレクトリへ移動します。
  2. 以下のコマンドを実行します:
    ant devmode

 2. App Engineへアプリケーションをデプロイする

StockWatcherプロジェクトがGWT開発モード及びApp Engine開発サーバでローカルに走ることを確認した今、App Engineでアプリケーションを走らせることが出来ます。

App Engineへアプリケーションをデプロイする(Eclipseで)

  1. パッケージエクスプローラビューで、StockWatcherプロジェクトを選択します。
  2. ツールバーで"App エンジン・プロジェクトのデプロイ" iconボタンをクリックします。
  3. (初回時のみ) アプリケーションIDを指定するために"App Engine project settings..."のリンクをクリックします。入力し終えたら"OK"ボタンをクリックします。
  4. Google Accounts emailアドレスとパスワードを入力します。"配置"ボタンをクリックします。Eclipseのコンソールで配置の進行状況を見ることが出来ます。

App Engineへアプリケーションをデプロイする(Eclipseなしで)

  1. コマンドラインでStockWatcher ディレクトリへ移動します。
  2. 以下のコマンドを実行することによってアプリケーションをコンパイルします:
    ant build
    Tip: antでフルパスを指定しなければならないことを避けるには、ant binディレクトリを環境パスに追加します。
  3. appcfgはApp Engine SDKs付属のコマンドラインツールです。以下のコマンドを実行することによってアプリケーションをアップロードします:
    appcfg.sh update war
    Windowsのコマンドプロントでは、このコマンドはappcfg update warです。最初のパラメータは実行したいアクションです。二番目のパラメータは更新内容があるディレクトリです。この場合で言うとディレクトリは静的ファイルとGWTコンパイラからの出力物を含んだ相対ディレクトリです。 Google Accounts emailアドレスとパスワードの入力を促すダイアログが表示されるので、入力します。
    Tip: appcfg.shでフルパスを指定しなければならないことを避けるには、App Engine SDK binディレクトリを環境パスに追加します。

 App Engineでテストする

http://application-id.appspot.com/(application-id は先に作成したApp EngineアプリケーションID)を開くことによってアップロードしたアプリケーションをテストします。現在StockWatcher アプリケーションはApp EngineのアプリケーションID下で動作しています。

 3. User Serviceを使用したアプリケーションのパーソナライズ

概要

StockWactherをApp Engineを配置した今、アプリケーションを高機能にするために利用可能なサービスを使い始めることが出来ます。各ユーザーに基づく株価速報リストを永続化することから始めます。これはデータベースサービス(このサービスはアプリケーションデータの保存を可能にします)及びUser Service(このサービスはユーザーログインとユーザー毎の株価速報リストを保存することを可能にします)によって可能です。永続化の為には、App Engine SDKによって提供されるJava Data Objects(JDO)を使用します。
ログイン機能を実装するためにUser Serviceを使用します。適切にこのサービスを使用すれば、Googleアカウントを持つユーザーならStockWatcherアプリケーションにアクセスするために自身のアカウントを使用してログインすることが出来ます。このセクションではアプリケーションにユーザーログインを追加するためにApp Engine User APIを使用します。

App Engine User Serviceは非常に簡単に使えます。最初に以下に示すコードのようにUserServiceをインスタンス化する必要があります:
UserService userService = UserServiceFactory.getUserService();
次に、StockWatcherアプリケーションへアクセスしている現在のユーザーを取得する必要があります:
User user = userService.getCurrentUser();
アプリケーションにアクセスしている現在のユーザーが自身のGoogleアカウントにログイン済みの場合、UserServiceはインスタンス化されたユーザーオブジェクトを返します。ユーザーオブジェクトはアカウントに紐付けされたeメールアドレス及びアカウントニックネームのような役に立つ情報を含んでいます。 アプリケーションにアクセスしているユーザーが自身のアカウントにログインしていない、またはGoogleアカウントを持っていない場合、返されたユーザーオブジェクトはnullです。
この場合、どのように私達が状況を処理したいかで利用出来る幾つかの選択肢がありますが、StockWatcherアプリケーションの目的のためにユーザーに自身のGoogleアカウントにログイン出来るログインURLを示します。
User APIはログインURLを生成する為の簡単な方法を提供します。単にUserServiceの createLoginURL(String requestUri) メソッドを呼びます。このメソッドはユーザーをGoogleアカウントログイン画面へ送るためのリダイレクトログインURLをあなたに提供します。 一旦、ユーザーがログインしたら、App EngineコンテナはcreateLoginURL() メソッドを呼んだ時にあなたが提供したrequestUri に基づいたユーザーをリダイレクトすべき場所を知ります。

 ログインRPCサービスを定義する

これをより具体的にするためにStockWatcherアプリケーション用のログインRPCサービスを作成しましょう。あなたがGWT RPCに慣れ親しんでないのなら、前のチュートリアルを見てください。
初めに、User serviceからのログイン情報を包含するLoginInfoオブジェクトを作成します。

LoginInfo.java:

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

import java.io.Serializable;

public class LoginInfo implements Serializable {

  private boolean loggedIn = false;
  private String loginUrl;
  private String logoutUrl;
  private String emailAddress;
  private String nickname;

  public boolean isLoggedIn() {
    return loggedIn;
  }

  public void setLoggedIn(boolean loggedIn) {
    this.loggedIn = loggedIn;
  }

  public String getLoginUrl() {
    return loginUrl;
  }

  public void setLoginUrl(String loginUrl) {
    this.loginUrl = loginUrl;
  }

  public String getLogoutUrl() {
    return logoutUrl;
  }

  public void setLogoutUrl(String logoutUrl) {
    this.logoutUrl = logoutUrl;
  }

  public String getEmailAddress() {
    return emailAddress;
  }

  public void setEmailAddress(String emailAddress) {
    this.emailAddress = emailAddress;
  }

  public String getNickname() {
    return nickname;
  }

  public void setNickname(String nickname) {
    this.nickname = nickname;
  }
}
LoginInfoはRPCメソッドの戻り値なのでシリアライズ可能です。
次に、LoginService と LoginServiceAsync インターフェイスを作成します。

LoginService.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("login")
public interface LoginService extends RemoteService {
  public LoginInfo login(String requestUri);
}
パスのアノテーション"login"は後で設定されます。

LoginServiceAsync.java:

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

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

public interface LoginServiceAsync {
  public void login(String requestUri, AsyncCallback<LoginInfo> async);
}
以下のようにcom.google.gwt.sample.stockwatcher.serverパッケージ内にLoginServiceImplクラスを作成します:

LoginServiceImpl.java:

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

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.sample.stockwatcher.client.LoginInfo;
import com.google.gwt.sample.stockwatcher.client.LoginService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class LoginServiceImpl extends RemoteServiceServlet implements
    LoginService {

  public LoginInfo login(String requestUri) {
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    LoginInfo loginInfo = new LoginInfo();

    if (user != null) {
      loginInfo.setLoggedIn(true);
      loginInfo.setEmailAddress(user.getEmail());
      loginInfo.setNickname(user.getNickname());
      loginInfo.setLogoutUrl(userService.createLogoutURL(requestUri));
    } else {
      loginInfo.setLoggedIn(false);
      loginInfo.setLoginUrl(userService.createLoginURL(requestUri));
    }
    return loginInfo;
  }

}
最後に、web.xmlファイル内のサーブレットを設定します。マッピングはGWTモジュール定義内のrename-to属性(stockwatcher)とRemoteServiceRelativePathアノテーション(login)とで構成されます。更に、greetServletはこのアプリケーションでは必要がないので、その設定は削除することも出来ます。

web.xml:

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

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

  <!-- Servlets -->
  <servlet>
    <servlet-name>loginService</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.LoginServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>loginService</servlet-name>
    <url-pattern>/stockwatcher/login</url-pattern>
  </servlet-mapping>

</web-app>

 StockWatcherユーザーインターフェイスを更新する

ログインRPCサービスが適切に動作する今、最後に為すべきことはStockWatcherエントリポイントクラスからサービスを呼び出すことです。しかしながら、ログイン機能を追加した今、アプリケーションフローがどのように変わるかを考慮しなければなりません。前のバージョンのアプリケーションでは、
いかなるログインも必要とされなかったので、無条件でStockWatcherをロード出来ました。ユーザーログインを必要としている今、ローディングロジックを少し変更しなければなりません。
一例として、ユーザーが既にログインしている場合、アプリケーションは処理を続行し、StockWatcherをロードします。しかしながらユーザーがログインしていない場合は、ユーザーをログインページへ転送しなければなりません。一旦ログインしたら、ユーザーが本当に認証されたことをもう一度確認する必要があるStockWatcherホストページへユーザーを転送し返します。認証チェックをパスしたら、stockwatcherをロードすることが出来ます。
stock watcherのロードはログインの結果次第ということが注意すべきキーポイントです。これはStockWatcherをロードするロジックはログインが成功した時にだけ呼び出されなければならないということを意味します。 これはリファクタリングを少し必要とします。Eclipseを使用している場合、これを行うのは簡単です。単に、StockWatcherのonModuleLoad() メソッド内のコードを選択し"リファクタリング(T)"メニューを選択、そして"メソッドの抽出(X)"機能をクリックします。 そこから、抽出したメソッドを private void loadStockWatcher()のような適当な名前で宣言することが出来ます。
以下と似たような感じになるでしょう:

StockWatcher.java:

public void onModuleLoad() {
    loadStockWatcher();
  }

  private void loadStockWatcher() {
    // Create table for stock data.
    stocksFlexTable.setText(0, 0, "Symbol");
    stocksFlexTable.setText(0, 1, "Price");
    stocksFlexTable.setText(0, 2, "Change");
    stocksFlexTable.setText(0, 3, "Remove");
    ...
  }
StockWatcherローディングロジックを呼び出し可能なメソッドにリファクタリングした今、 onModuleLoad() メソッド内でログインRPCサービスを呼び出し、ログインに成功した場合にloadStockWatcher() メソッドを呼び出すことが出来ます。しかしながら、ユーザーがログインしていない場合、作業を続行するためにはログインする必要があるという何らかの指示をユーザーに提供する必要があります。このために、ユーザーにログインの続行を指示するための付随するラベルとボタンを載せたログインパネルを使用することが理に適っています。
これら全てを考慮して、StockWatcherエントリポイントクラスへ以下のようなコードを追加しなければなりません:

StockWatcher.java

import com.google.gwt.user.client.ui.Anchor;

...

  private LoginInfo loginInfo = null;
  private VerticalPanel loginPanel = new VerticalPanel();
  private Label loginLabel = new Label("Please sign in to your Google Account to access the StockWatcher application.");
  private Anchor signInLink = new Anchor("Sign In");

  public void onModuleLoad() {
    // Check login status using login service.
    LoginServiceAsync loginService = GWT.create(LoginService.class);
    loginService.login(GWT.getHostPageBaseURL(), new AsyncCallback<LoginInfo>() {
      public void onFailure(Throwable error) {
      }

      public void onSuccess(LoginInfo result) {
        loginInfo = result;
        if(loginInfo.isLoggedIn()) {
          loadStockWatcher();
        } else {
          loadLogin();
        }
      }
    });
  }

  private void loadLogin() {
    // Assemble login panel.
    signInLink.setHref(loginInfo.getLoginUrl());
    loginPanel.add(loginLabel);
    loginPanel.add(signInLink);
    RootPanel.get("stockList").add(loginPanel);
  }
ログイン機能についてもう一つの重要な点はアプリケーションのサインアウトが出来ることです。これはログイン機能と同様にStockWatcherアプリケーションに追加しなければならない機能です。 幸いにも、User ServiceはcreateLoginURL(String requestUri)メソッドと似た呼び出しを通してログアウトURLを提供します。以下のコードを追加することによって、これをStockWatcherアプリケーションに追加することが出来ます:

StockWatcher.java

private Anchor signInLink = new Anchor("Sign In");
  private Anchor signOutLink = new Anchor("Sign Out");

...

  private void loadStockWatcher() {
    // Set up sign out hyperlink.
    signOutLink.setHref(loginInfo.getLogoutUrl());

    // Create table for stock data.
    stocksFlexTable.setText(0, 0, "Symbol");
    stocksFlexTable.setText(0, 1, "Price");
    stocksFlexTable.setText(0, 2, "Change");
    stocksFlexTable.setText(0, 3, "Remove");

  ...

    // Assemble Main panel.
    mainPanel.add(signOutLink);
    mainPanel.add(stocksFlexTable);
    mainPanel.add(addPanel);
    mainPanel.add(lastUpdatedLabel);

 User Serviceの機能をテストする

上述の手順を繰り返してローカルまたはApp Engine上でアプリケーションを走らせることが出来ます。
App Engine開発サーバを使用して開発モードでアプリケーションを走らせる場合、サインインページは(テストを容易にするために)どのようなeメールアドレスを入力してもサインインが出来ます。アプリケーションをApp Engineへデプロイしている場合、サインインページはアプリケーションにアクセスするためにユーザーにGoogleアカウントへサインインすることを要求します。

 4. データストアへのデータの格納

概要

App Engine Javaランタイムで利用出来るDatastore serviceはPythonランタイムで利用出来るサービスと同じです。Javaでこのサービスにアクセスする為に、 低レベルdatastore APIJava Data Objects (JDO)、 Java Persistence API (JPA)を使用できます。このサンプルではJDOを使用します。

Stock RPC サービスを定義する

ユーザーの株の永続化を処理する基本のStockServiceを作成します。更にこのサービスをGWT RPCサービスとして公開します。

StockService.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("stock")
public interface StockService extends RemoteService {
  public void addStock(String symbol) throws NotLoggedInException;
  public void removeStock(String symbol) throws NotLoggedInException;
  public String[] getStocks() throws NotLoggedInException;
}

StockServiceAsync.java

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

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

public interface StockServiceAsync {
  public void addStock(String symbol, AsyncCallback<Void> async);
  public void removeStock(String symbol, AsyncCallback<Void> async);
  public void getStocks(AsyncCallback<String[]> async);
}

StockWatcher.java

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("Add");
  private Label lastUpdatedLabel = new Label();
  private ArrayList stocks = new ArrayList();
  private LoginInfo loginInfo = null;
  private VerticalPanel loginPanel = new VerticalPanel();
  private Label loginLabel = new Label("Please sign in to your Google Account to access the StockWatcher application.");
  private Anchor signInLink = new Anchor("Sign In");
  private final StockServiceAsync stockService = GWT.create(StockService.class);
チェックされる例外はユーザーがまだログインしていないことを指し示します。たとえ現在のユーザーが存在しなくてもStockServiceによってRPCコールを受け取れるので、そのようなシナリオはありえます。 このクラスはAsyncCallbackのonFailure(Throwable error)メソッド経由のRPCコールによって返されるのでシリアライズ可能です。更に、サーブレットフィルタまたはSpringセキュリティを実装出来ます。

NotLoggedInException.java

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

import java.io.Serializable;

public class NotLoggedInException extends Exception implements Serializable {

  public NotLoggedInException() {
    super();
  }

  public NotLoggedInException(String message) {
    super(message);
  }

}
StockクラスはJDOを使用して永続化したものです。JDOアノテーションによってそれがどのように永続化されるかの詳細を規定しています。特に:
  • PersistenceCapableアノテーションはこのクラスを処理するためのDataNucleusバイトコード拡張を示します。
  • PrimaryKeyアノテーションは自身のプライマリーキーを格納するためにid属性を指定します。 
  • このクラスでは、各属性は永続化されます。しかしながら、NotPersistentアノテーションを使用して 属性を永続化しないように指定出来ます。
  • User属性はeメールアドレス変更をまたがってユーザーを識別出来る特別なApp Engineの型です?

Stock.java

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

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.users.User;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Stock {

  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Long id;
  @Persistent
  private User user;
  @Persistent
  private String symbol;
  @Persistent
  private Date createDate;

  public Stock() {
    this.createDate = new Date();
  }

  public Stock(User user, String symbol) {
    this();
    this.user = user;
    this.symbol = symbol;
  }

  public Long getId() {
    return this.id;
  }

  public User getUser() {
    return this.user;
  }

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

  public Date getCreateDate() {
    return this.createDate;
  }

  public void setUser(User user) {
    this.user = user;
  }

  public void setSymbol(String symbol) {
    this.symbol = symbol;
  }
}
このクラスはStockServiceをimplementsし株データを永続化するJDO APIへの呼び出しを含みます。注意すべき点:
  • ロガーによって記録されたメッセージは App Engine Administrationコンソールであなたのアプリケーションを調査する時に見ることが出来ます。
  • PersistenceManagerFactoryシングルトンは上述のjdoconfig.xml内の"transactions-optional"という名のプロパティから作成されます。
  • checkedLoggedInメソッドはユーザーがログインしているかどうか確認したい時に呼び出されます。
  • getUserメソッドはUserServiceを使用します。

StockServiceImpl.java

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

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.gwt.sample.stockwatcher.client.NotLoggedInException;
import com.google.gwt.sample.stockwatcher.client.StockService;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;

public class StockServiceImpl extends RemoteServiceServlet implements
StockService {

  private static final Logger LOG = Logger.getLogger(StockServiceImpl.class.getName());
  private static final PersistenceManagerFactory PMF =
      JDOHelper.getPersistenceManagerFactory("transactions-optional");

  public void addStock(String symbol) throws NotLoggedInException {
    checkLoggedIn();
    PersistenceManager pm = getPersistenceManager();
    try {
      pm.makePersistent(new Stock(getUser(), symbol));
    } finally {
      pm.close();
    }
  }

  public void removeStock(String symbol) throws NotLoggedInException {
    checkLoggedIn();
    PersistenceManager pm = getPersistenceManager();
    try {
      long deleteCount = 0;
      Query q = pm.newQuery(Stock.class, "user == u");
      q.declareParameters("com.google.appengine.api.users.User u");
      List<Stock> stocks = (List<Stock>) q.execute(getUser());
      for (Stock stock : stocks) {
        if (symbol.equals(stock.getSymbol())) {
          deleteCount++;
          pm.deletePersistent(stock);
        }
      }
      if (deleteCount != 1) {
        LOG.log(Level.WARNING, "removeStock deleted "+deleteCount+" Stocks");
      }
    } finally {
      pm.close();
    }
  }

  public String[] getStocks() throws NotLoggedInException {
    checkLoggedIn();
    PersistenceManager pm = getPersistenceManager();
    List<String> symbols = new ArrayList<String>();
    try {
      Query q = pm.newQuery(Stock.class, "user == u");
      q.declareParameters("com.google.appengine.api.users.User u");
      q.setOrdering("createDate");
      List<Stock> stocks = (List<Stock>) q.execute(getUser());
      for (Stock stock : stocks) {
        symbols.add(stock.getSymbol());
      }
    } finally {
      pm.close();
    }
    return (String[]) symbols.toArray(new String[0]);
  }

  private void checkLoggedIn() throws NotLoggedInException {
    if (getUser() == null) {
      throw new NotLoggedInException("Not logged in.");
    }
  }

  private User getUser() {
    UserService userService = UserServiceFactory.getUserService();
    return userService.getCurrentUser();
  }

  private PersistenceManager getPersistenceManager() {
    return PMF.getPersistenceManager();
  }
}
GWT RPCサービスが実装された今、サーブレットコンテナがそれについて知っているか確認しましょう。 マッピング /stockwatcher/stock はGWTモジュール定義内のrename-to属性(stockwatcher)とRemoteServiceRelativePathアノテーション(stock)とで構成されます。

web.xml

<?xml version="1.0" encoding="UTF-8"?>

<web-app>

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

  <!-- Servlets -->
  <servlet>
    <servlet-name>loginService</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.LoginServiceImpl</servlet-class>
  </servlet>

  <servlet>
    <servlet-name>stockService</servlet-name>
    <servlet-class>com.google.gwt.sample.stockwatcher.server.StockServiceImpl</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>loginService</servlet-name>
    <url-pattern>/stockwatcher/login</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>stockService</servlet-name>
    <url-pattern>/stockwatcher/stock</url-pattern>
  </servlet-mapping>

</web-app>

 StockWatcherユーザーインターフェイスを更新する

株データを取得する

StockWatcherアプリケーションがロードされた時、ユーザーの株データが予め取得されていなければなりません。株の表示をする既存のコードを再利用するためには、新しい株データを表示するロジックが新しい displayStock(String symbol) メソッドなのでStockWatcherの addStock() をリファクタリングします。
private void addStock() {
    final String symbol = newSymbolTextBox.getText().toUpperCase().trim();
    newSymbolTextBox.setFocus(true);

    // Stock code must be between 1 and 10 chars that are numbers, letters, or dots.
    if (!symbol.matches("^[0-9a-zA-Z\\.]{1,10}$")) {
      Window.alert("'" + symbol + "' is not a valid symbol.");
      newSymbolTextBox.selectAll();
      return;
    }

    newSymbolTextBox.setText("");

    // Don't add the stock if it's already in the table.
    if (stocks.contains(symbol))
      return;

    displayStock(symbol);
  }

  private void displayStock(final String symbol) {
    // Add the stock to the table.
    int row = stocksFlexTable.getRowCount();
    stocks.add(symbol);
    stocksFlexTable.setText(row, 0, symbol);
    stocksFlexTable.setWidget(row, 2, new Label());
    stocksFlexTable.getCellFormatter().addStyleName(row, 1, "watchListNumericCol
umn");
    stocksFlexTable.getCellFormatter().addStyleName(row, 2, "watchListNumericCol
umn");
    stocksFlexTable.getCellFormatter().addStyleName(row, 3, "watchListRemoveColu
mn");

    // Add a button to remove this stock from the table.
    Button removeStockButton = new Button("x");
    removeStockButton.addStyleDependentName("remove");
    removeStockButton.addClickHandler(new ClickHandler() {
      public void onClick(ClickEvent event) {
        int removedIndex = stocks.indexOf(symbol);
        stocks.remove(removedIndex);
        stocksFlexTable.removeRow(removedIndex + 1);
      }
    });
    stocksFlexTable.setWidget(row, 3, removeStockButton);

    // Get the stock price.
    refreshWatchList();

  }
株テーブルをセットアップした後が株データをロードするのに適切な時です。
private void loadStockWatcher() {

  ...

    stocksFlexTable.setCellPadding(5);
    stocksFlexTable.addStyleName("watchList");
    stocksFlexTable.getRowFormatter().addStyleName(0, "watchListHeader");
    stocksFlexTable.getCellFormatter().addStyleName(0, 1, "watchListNumericColumn");
    stocksFlexTable.getCellFormatter().addStyleName(0, 2, "watchListNumericColumn");
    stocksFlexTable.getCellFormatter().addStyleName(0, 3, "watchListRemoveColumn");

    loadStocks();

  ...

  }
loadStocks() メソッドは前に定義したStockServiceを呼び出します。RPCは株シンボルの配列を返します。この配列はdisplayStock(String symbol) メソッドを使用して独立して表示されます。

  private void loadStocks() {
    stockService.getStocks(new AsyncCallback<String[]>() {
      public void onFailure(Throwable error) {
      }
      public void onSuccess(String[] symbols) {
        displayStocks(symbols);
      }
    });
  }

  private void displayStocks(String[] symbols) {
    for (String symbol : symbols) {
      displayStock(symbol);
    }
  }

 株データを追加する

株データが追加された時、それらを表示する代わりに、データストアへ新しい株シンボルを保存するためにStockServiceを呼び出します。
private void addStock() {
    final String symbol = newSymbolTextBox.getText().toUpperCase().trim();
    newSymbolTextBox.setFocus(true);

    // Stock code must be between 1 and 10 chars that are numbers, letters, or dots.
    if (!symbol.matches("^[0-9a-zA-Z\\.]{1,10}$")) {
      Window.alert("'" + symbol + "' is not a valid symbol.");
      newSymbolTextBox.selectAll();
      return;
    }

    newSymbolTextBox.setText("");

    // Don't add the stock if it's already in the table.
    if (stocks.contains(symbol))
      return;

    displayStock(symbol);
    addStock(symbol);
  }

  private void addStock(final String symbol) {
    stockService.addStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
      }
      public void onSuccess(Void ignore) {
        displayStock(symbol);
      }
    });
  }

  private void displayStock(final String symbol) {
    // Add the stock to the table.
    int row = stocksFlexTable.getRowCount();
    stocks.add(symbol);

...

  }

株データを除去する

そして単に表示から株データを除去する代わりにデータストアから株データを除去するためにStockServiceを呼び出します。
private void displayStock(final String symbol) {

  ...

    // Add a button to remove this stock from the table.
    Button removeStock = new Button("x");
    removeStock.addStyleDependentName("remove");

    removeStock.addClickHandler(new ClickHandler(){
      public void onClick(ClickEvent event) {
        removeStock(symbol);
      }
    });
    stocksFlexTable.setWidget(row, 3, removeStock);

    // Get the stock price.
    refreshWatchList();

  }

  private void removeStock(final String symbol) {
    stockService.removeStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
      }
      public void onSuccess(Void ignore) {
        undisplayStock(symbol);
      }
    });
  }

  private void undisplayStock(String symbol) {
    int removedIndex = stocks.indexOf(symbol);
    stocks.remove(removedIndex);
    stocksFlexTable.removeRow(removedIndex+1);
  }

 エラー処理

RPCコールが結果としてエラーを生じさせた時、私たちはユーザーへメッセージを表示したい。
さらにまた、何らかの理由でユーザーが既に自身のGoogleアカウントにログインしていない場合、 StockServiceがNotLoggedInExceptionをスローすることを思い出してください:
private void checkLoggedIn() throws NotLoggedInException {
    if (getUser() == null) {
      throw new NotLoggedInException("Not logged in.");
    }
  }
このエラーを受け取った場合、ユーザーをログアウトURLへ転送します。
ここにこれら二つのエラー処理を遂行するためのヘルパーメソッドがあります。
  private void handleError(Throwable error) {
    Window.alert(error.getMessage());
    if (error instanceof NotLoggedInException) {
      Window.Location.replace(loginInfo.getLogoutUrl());
    }
  }
各AsyncCallbackのonFailure(Throwable error)メソッドへこれを追加することが出来ます。
loginService.login(GWT.getHostPageBaseURL(), new AsyncCallback<LoginInfo>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    }
stockService.getStocks(new AsyncCallback<String[]>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    });
stockService.addStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    });
stockService.removeStock(symbol, new AsyncCallback<Void>() {
      public void onFailure(Throwable error) {
        handleError(error);
      }

    ...

    });

データストア機能をテストする

上述の手順を繰り返してローカルまたはApp Engine上でアプリケーションを走らせることが出来ます。
ランタイムエラーに遭遇した場合は、App Engine Administration コンソールでログを調査してください。

 その他の情報

更なる実習

ユーザーは今、GoogleアカウントへのサインインとApp Engine上で動作するStockWatcherアプリケーションで自身の株リストを管理できます。
ここに実習としてあなたが挑戦出来る拡張の提案が幾つか有ります。
  • 株リストのロードは目立った遅れがあります。株リストがロード中であることを示すUI要素を加えてください。
  • Stockクラスにより多くの属性を追加してください。これらの属性が加えられる前に保存されたデータに何が起こりますか? 
  • あるユーザーがサインアウトし、別のユーザーがサインインした時、StockServiceはこれを感知しません。 この稀な事例を処理するためにどのようにアプリケーションを修正しますか?

App Engineについてより詳しく学ぶ

App Engine Java Getting Started tutorial はゼロからプロジェクトを作成する、JSPを使用する、異なるアプリケーションバージョンを管理する、web配備記述子ファイルのより多くの詳細というようなトピックを含んだApp Engineアプリケーションの構築についてのより多くの詳細を提供します。 
App Engine Java documentationはUser serviceとDatastore serviceをより詳細にカバーします。特に、datastore serviceにアクセスするためにJPAを使用する方法を詳細に記載しています。 Memcache、HTTPクライアント、Java Mailを含む他のサービスも詳細に記載しています。(※原文、過去形でtooもalsoもない?)  App Engine Java ランタイムの制限も項目別に挙げられています。