2009年4月20日月曜日

GAE for Javaをarchetype:createする

Google Codeにmaven2のgae-maven-archetypeというarchetyoe plug-inプロジェクトがあり、それが使える。自分はt-2+Guice+JPA用のものを作って使っているが、gae-maven-archetypeプロジェクトのものが汎用的で使いやすそう。

  • $ mvn archetype:create -DgroupId=com.shin1ogawa -DartifactId=com.shin1ogawa.gae.sample -DarchetypeArtifactId=appengine-quickstart -DarchetypeGroupId=org.mvnsearch.maven.archetypes -DarchetypeVersion=1.0.1 -DremoteRepositories=http://www.mvnsearch.org/maven2

上記のコマンドを使えばプロジェクトが作成できて(斜体部分の2カ所は適宜修正が必要)、公式のEclipse Plug-inなしでもすぐに開発を始める事ができる。ただし、antと併用する形になり、「$ mvn package」でビルド、「$ ant runserver」で実行、「$ ant update」でデプロイできる。

というか、モジュールが「http://www.mvnsearch.org/maven2/com/google/appengine/」というリポジトリに配置されているので、これを使って独自にmaven2との連携をしていく事もできる。

追記

antを実行する際は、appengine_homeパラメータを設定してやる必要がある。例えば「$ ant runserver -Dappengine_home=/opt/appengine-java-sdk-1.2.0/」みたいなカンジ。

2009年4月9日木曜日

GAE/Javaでt2-frameworkを使ってみる

Wicketを試す合間にT2(with Guice+JPA)を試していたのでその手順を。

開発環境の準備

Eclipseを使用している事と、EclipseにGAE用のプラグインをインストールしてある事。

プロジェクトを作ってT2用に設定する

Eclipse上でプロジェクトを作る
新規作成Wizardで[Goolge/Web Application Project]を選択する。なお、JDKは1.6に変更しておく事!
T2関連のモジュールをコピーする
t2-frameworkのサイトから「Core/T2-${version}-ga」と「Core/Lucy-${version}-ga」をダウンロードする。また、今回はついでにGuiceも使いたいので「Samples/T2 + Guice + JPA sample application」もダウンロードしておく。以下をダウンロードしたモジュールから抜き出して${baseDir}/war/WEB-INF/libにコピーして、[Build path][Add to Build Path]しておく。他にはcommons-langも適当に拾って来て同様に配置、ビルドパスに含めておこう。
  • commons-0.5.1-ga.jar
  • guice-1.0.jar
  • guice-servlet-1.0.jar
  • guiceadapter-0.5.1-ga.jar
  • logback-classic-0.9.15.jar
  • logback-core-0.9.15.jar
  • slf4j-api-1.5.6.jar
  • t2-0.5.1-ga.jar

各種設定ファイルの準備をする

web.xmlを修正する
対象ファイルはwar/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 xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
  <context-param>
    <param-name>t2.encoding</param-name>
    <param-value>UTF-8</param-value>
  </context-param>

  <filter>
    <filter-name>Guice Servlet Filter</filter-name>
    <filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
  </filter>
  <filter>
    <filter-name>t2</filter-name>
    <filter-class>org.t2framework.t2.filter.T2Filter</filter-class>
    <init-param>
      <param-name>t2.rootpackage</param-name>
      <param-value>com.shin1ogawa.page</param-value>
    </init-param>
    <init-param>
      <param-name>t2.container.adapter</param-name>
      <param-value>org.t2framework.t2.adapter.GuiceAdapter</param-value>
    </init-param>
    <!--
    <init-param>
      <param-name>t2.eagerload</param-name>
      <param-value>true</param-value>
    </init-param>
    -->
    <init-param>
      <param-name>t2.exclude-resources</param-name>
      <param-value>css, js</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>Guice Servlet Filter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>  
  <filter-mapping>
    <filter-name>t2</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>
log4.propertiesを修正する
src/log4j.propertiesに以下を追記しておく。
log4j.category.DataNucleus.Connection=DEBUG, A1
log4j.category.DataNucleus.Query=DEBUG, A1
log4j.logger.org.t2framework=TRACE, A1
log4j.logger.com.shin1ogawa=TRACE, A1
最後の行は自分のアプリに合わせて適宜変更する事。
セッションを有効にする
GAEの設定ファイルである、war/WEB-INF/appengine-web.xml内のappengine-web-app要素の直下に以下を追加する。
<sessions-enabled>true</sessions-enabled>
persistence.xmlを作成する。

src/META-INFにpersistence.xmlを作成する。内容は以下のとおりで、これはGAE/JavaでJPAを使う時に必要なもの。JDOを使わずJPAを使うのであればGAEプラグインがデフォルトでsrc/META-INF直下に作成するjdoconfig.xmlは削除してもかまわない、と思う。

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

  <persistence-unit name="transactions-optional">
    <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
    <class>com.shin1ogawa.entity.Messages</class>
    <properties>
      <property name="datanucleus.NontransactionalRead" value="true"/>
      <property name="datanucleus.NontransactionalWrite" value="true"/>
      <property name="datanucleus.ConnectionURL" value="appengine"/>
    </properties>
  </persistence-unit>
</persistence>
class要素に記述するのはJPAのEntityクラスなので、自分のアプリに合わせて適宜修正する事。
T2に読ませるServiceProviderの設定ファイルを作成する
src/META-INFにservicesフォルダを作成し、com.google.inject.Moduleというファイルを作成する。内容は以下の一行で、自分のアプリに合わせて適宜修正する事。
com.shin1ogawa.ApplicationModule

Javaのクラスを作成する

JPA用のModule
package com.shin1ogawa;
import javax.persistence.*;
import com.google.inject.*;
public class JpaModule extends AbstractModule {
  @Override protected void configure() {
    bind(EntityManager.class).toProvider(new Provider() {
      public EntityManager get() {
        EntityManager manager = Persistence.createEntityManagerFactory(
            "transactions-optional").createEntityManager();
        manager.setFlushMode(FlushModeType.COMMIT);
        return manager;
      }
    }).in(Singleton.class);
  }
}
JPA用のEntity
今回は超単純なEntityをひとつだけ用意してみる。persistence.xmlのclass要素でJPA的に使えるように登録する必要がある(上記手順で設定済み)。
package com.shin1ogawa.entity;
import java.util.Date;
import javax.persistence.*;
@Entity
public class Messages {
  @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
  private String userName;
  private String message;
  private Date updated;
  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }
  public String getUserName() { return userName; }
  public void setUserName(String userName) { this.userName = userName; }
  public String getMessage() { return message; }
  public void setMessage(String message) { this.message = message; }
  public void setUpdated(Date updated) { this.updated = updated; }
  public Date getUpdated() { return updated; }
}
処理モジュールを作成する
Guiceを使って管理する対象にしたいので、インターフェースと実装クラスを分離している。
package com.shin1ogawa.logic;

import java.util.List;
import com.google.appengine.api.users.User;
import com.shin1ogawa.entity.Messages;

public interface IMessagesLogic {
  public Messages newMessage(User user, String messageBody);
  public List<Messages> selectAll();
}
package com.shin1ogawa.logic;

import java.util.*;
import javax.persistence.*;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.slf4j.*;
import com.google.appengine.api.users.User;
import com.google.inject.Inject;
import com.shin1ogawa.entity.Messages;

public class MessagesLogic implements IMessagesLogic {
  static final Logger logger = LoggerFactory.getLogger(MessagesLogic.class);

  @Inject EntityManager em;

  public Messages newMessage(User user, String messageBody) {
    Messages message = new Messages();
    message.setUserName(user != null ? user.getNickname() : "unknown");
    message.setMessage(messageBody);
    message.setUpdated(new Date(System.currentTimeMillis()));

    EntityTransaction transaction = em.getTransaction();
    transaction.begin();
    em.persist(message);
    transaction.commit();
    return message;
  }

  public List<Messages> selectAll() {
    @SuppressWarnings("unchecked")
    List resultList = em.createQuery(
        "Select m from com.shin1ogawa.entity.Messages m").getResultList();
    return resultList;
  }

  @Override public String toString() {
    return ToStringBuilder.reflectionToString(this);
  }
}
実装クラス中のJPQLでクラス名をFQCNで指定ている。"Select m from Messages m"とか短いクラス名で記述したいのだけど、そうするとORMの実装であるDataNucleusがクラス名を解決できないというエラーを出してくる。oem.xmlを作ってやってもダメ。なのでとりあえずFQCNで記述しておいた。誰か解決方法がわかる人は教えてください!
T2のアプリケーション用のModule
package com.shin1ogawa;
import org.slf4j.*;
import com.google.inject.*;
import com.shin1ogawa.logic.IMessagesLogic;
import com.shin1ogawa.logic.MessagesLogic;
public class ApplicationModule extends AbstractModule {
  @Override protected void configure() {
    install(new ServletModule());
    install(new JpaModule());
    bind(IMessagesLogic.class).to(MessagesLogic.class).in(Scopes.SINGLETON);
  }
}
T2のページクラス
ちなみに、jspは好きではないのでjspは使ってない。データの投稿もGetで行う事が前提で書いた。データの一覧表示用とデータの投稿用のページクラス。
package com.shin1ogawa.page;

import java.util.*;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.t2framework.t2.annotation.core.Default;
import org.t2framework.t2.annotation.core.Page;
import org.t2framework.t2.contexts.WebContext;
import org.t2framework.t2.navigation.SimpleText;
import org.t2framework.t2.spi.Navigation;

import com.google.inject.Inject;
import com.google.inject.servlet.RequestParameters;
import com.google.inject.servlet.RequestScoped;
import com.shin1ogawa.entity.Messages;
import com.shin1ogawa.logic.IMessagesLogic;

@RequestScoped
@Page("messageList")
public class MessageList {
  @Inject @RequestParameters Map params;

  @Inject IMessagesLogic logic;
  
  @Default public Navigation index(WebContext context) {
    StringBuilder b = new StringBuilder();
    b.append(ToStringBuilder.reflectionToString(this));
    
    List<Messages> messageList = logic.selectAll();
    if (messageList != null) {
      b.append("messages count=").append(messageList.size()).append("\n");
      for (Messages messages : messageList) {
        b.append(ToStringBuilder.reflectionToString(messages)).append("\n");
      }
    } else {
      b.append("messages count=0").append("\n");
    }
    return new SimpleText(b.toString());
  }
}
package com.shin1ogawa.page;

import java.util.Map;

import org.apache.commons.lang.StringUtils;
import org.t2framework.t2.annotation.core.Default;
import org.t2framework.t2.annotation.core.Page;
import org.t2framework.t2.contexts.WebContext;
import org.t2framework.t2.navigation.Redirect;
import org.t2framework.t2.navigation.SimpleText;
import org.t2framework.t2.spi.Navigation;

import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.inject.Inject;
import com.google.inject.servlet.RequestParameters;
import com.google.inject.servlet.RequestScoped;
import com.shin1ogawa.logic.IMessagesLogic;

@RequestScoped
@Page("addMessage")
public class AddMessage {
  @Inject @RequestParameters Map params;

  @Inject IMessagesLogic logic;

  @Default public Navigation index(WebContext context) {
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();

    final String message = params.get("message") != null ? params.get("message")[0] : null;
    if (StringUtils.isEmpty(message)) {
      return new SimpleText("メッセージが入力されていません。");
    } else {
      logic.newMessage(user, message);
      return new Redirect(MessageList.class);
    }
  }
}
表示がめっちゃくちゃ適当だけど、自分はWebのUIをメインに使う気がない(だからこそT2っていう選択肢でもある)のでこんなもんで十分なのだ。

アプリケーションを実行する

GAEプラグインでプロジェクトを作った場合は、プロジェクトと同時にWeb Applicationの実行の構成も作られているのでそれを実行し、「http://localhost:8080/messageList」を開けばおk!最初は空っぽだが、「http://localhost:8080/addMessage?message=hoge」とかやるとデータが増えていくはずだ。データはアプリケーションを終了しても消えない模様。

GAE/JavaでWicketを使ってみる - その3

Wicketのセッション情報の書き出し処理中にAccessControlExceptionが発生する件は、以下のようにしてHttpSessionStoreを適用(WebApplicationクラスに記述)する事で回避できた…っぽいw

@Override
protected ISessionStore newSessionStore() {
  return new HttpSessionStore(this);
}
「っぽい」って書いたのは、まだ動作していないから。うっかりJPAの処理も含めてしまってデプロイしているせいか、次は以下のようなExceptionに変わった(長いから適当に抜粋した)w
Caused by: java.io.NotSerializableException: org.datanucleus.store.appengine.query.DatastoreQuery$1
  at java.io.ObjectOutputStream.writeObject0(Unknown Source)
  ...(snip)...
  at java.io.ObjectOutputStream.writeObject0(Unknown Source)
  at java.io.ObjectOutputStream.defaultWriteFields(Unknown Source)
  at java.io.ObjectOutputStream.defaultWriteObject(Unknown Source)
  at org.apache.wicket.Component.writeObject(Component.java:4395)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
  at java.lang.reflect.Method.invoke(Unknown Source)
  ...(snip)...
  at java.util.HashMap.writeObject(Unknown Source)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
  at java.lang.reflect.Method.invoke(Unknown Source)
  at java.io.ObjectStreamClass.invokeWriteObject(Unknown Source)
  at java.io.ObjectOutputStream.writeSerialData(Unknown Source)
  at java.io.ObjectOutputStream.writeOrdinaryObject(Unknown Source)
  at java.io.ObjectOutputStream.writeObject0(Unknown Source)
  at java.io.ObjectOutputStream.writeObject(Unknown Source)
  at com.google.apphosting.runtime.jetty.SessionManager.serialize(SessionManager.java:331)
  ... 39 more
JPAをはずしたら動作してるんかなー。

GAE/JavaでWicketを使ってみる - その2

昨晩のエントリ後、承認メールが届いたので実際にデプロイを試したところ、デプロイには成功した。が、以下のようなExceptionが出てしまって動作はしなかった、という報告。

java.lang.SecurityException: Unable to create temporary file
  at java.io.File.checkAndCreate(File.java:1753)
  at java.io.File.createTempFile(File.java:1845)
  at java.io.File.createTempFile(File.java:1882)
  at org.apache.wicket.protocol.http.pagestore.DiskPageStore.getDefaultFileStoreFolder(DiskPageStore.java:583)
  at org.apache.wicket.protocol.http.pagestore.DiskPageStore.(DiskPageStore.java:606)
  at org.apache.wicket.protocol.http.pagestore.DiskPageStore.(DiskPageStore.java:615)
  at org.apache.wicket.protocol.http.WebApplication.newSessionStore(WebApplication.java:624)
  at org.apache.wicket.Application.internalInit(Application.java:982)
  at org.apache.wicket.protocol.http.WebApplication.internalInit(WebApplication.java:521)
  at org.apache.wicket.protocol.http.WicketFilter.init(WicketFilter.java:692)

後でSessionStoreをHttpSessionStoreにしてデプロイしてみる!

2009年4月8日水曜日

GAE/JavaでWicketを使ってみる

本日 Google App Engine for Java が提供開始されていて、中身を見ているとどうやらシンプルなWebAppなカンジ。Wicketも動作しそうだなーと思って試してみたメモを書いておく。去年GAEがリリースされた時もその日に色々エントリを書いたのが懐かしい。

ただし、GAEforJavaのアカウント(?)だか承認のメールが来ていないので、デプロイできておらずローカルでの動作確認しかできていない。デプロイしても動作するかどうかが不明だ…。

開発環境の準備

Eclipseを使用している事と、EclipseにGAE用のプラグインをインストールしてある事。

プロジェクトを作る

wicket-quicksartするか、EclipseのGAEプラグインで作るか、で迷うけどGAEプラグインで作る事にする。というのも、GAEプラグインが「${basedir}/war」の直下を固定で見ている気がするので、quickstartしてからフォルダ構成を変えるよりはGAEで作ったプロジェクトにWicket関連のモジュールをコピーする方がラクそうだから。

Eclipse上でプロジェクトを作る
新規作成Wizardで[Goolge/Web Application Project]を選択する。
Wicket関連のモジュールをコピーする
プロジェクト内の war/WEB-INF/lib直下に以下をコピーする。
  • wicket-1.4-rc2.jar
  • slf4j-api-1.4.2.jar
  • log4j-1.2.4.jar
  • slf4j-log4j12-1.4.2.jar
wicket-quickstartを流した事があれば、全部mavenのローカルリポジトリに存在するはずなので、そこからコピーするとかする。コピーした後は、Eclipse上でwar/WEB-INF/lib配下の対象jarを選択して[Build Path][Add to Build path]しておく。
web.xmlを修正する
普段wicket-quickstartで作られるものをそのまま流用。対象ファイルはwar/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 xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
  <filter>
    <filter-name>wicket.shin1ogawa</filter-name>
     <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
    <init-param>
      <param-name>applicationClassName</param-name>
      <param-value>com.shin1ogawa.WicketApplication</param-value>
     </init-param>
   </filter>
  <filter-mapping>
    <filter-name>wicket.shin1ogawa</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>
com.shin1ogawa.WicketApplicationっていうのが自分のWicketApplicationクラス。ここは適宜変えてくだしあ。
log4.propertiesを修正する
src/log4j.propertiesに以下のWicket用のロガーの設定を追加する。
log4j.logger.org.apache.wicket=INFO, A1
log4j.logger.org.apache.wicket.protocol.http.HttpSessionStore=INFO, A1
log4j.logger.org.apache.wicket.version=INFO, A1
log4j.logger.org.apache.wicket.RequestCycle=INFO, A1
JPAを使う予定の場合は、ついでに以下も追記しておくと良い。
log4j.category.DataNucleus.Connection=DEBUG, A1
log4j.category.DataNucleus.Query=DEBUG, A1
もちろん、自分のアプリケーション用の設定も必要であれば追記する。
Wicketの実行モードをDEPLOYMENTにする
残念ながら、DEVELOPMENTモードでは動作しない。というのも、DEVELOPMENTモードではリソースのチェックか何かにModificationWatcherというクラスが動作して、その先で new Thread() している箇所があり、そこでAccessControlException(java.lang.RuntimePermission modifyThreadGroup)が発生してしまうため。例えば自分ならWicketApplicationを以下のようにした。
package com.shin1ogawa;

import org.apache.wicket.Application;
import org.apache.wicket.protocol.http.WebApplication;

public class WicketApplication extends WebApplication {
  public WicketApplication() {
    System.out.println("WicketApplication constructor()");
  }

  @Override
  public String getConfigurationType() {
    return Application.DEPLOYMENT;
  }

  @Override
  protected void init() {
    super.init();
    mountBookmarkablePage("/home", HomePage.class);
  }

  public Class<HomePage> getHomePage() {
    return HomePage.class;
  }
}
ローカル環境のjava.policyを設定すれば良いと思うが、Macの場合にコイツをどこに配置すればいいのかわからんし、とりあえずWicketが動いてりゃそれでいいし、あんまり困らんので放置しておく。
セッションを有効にする
GAEの設定ファイルである、war/WEB-INF/appengine-web.xml内のappengine-web-app要素の直下に以下を追加する。
<sessions-enabled>true</sessions-enabled>
アプリケーションを実行する
GAEプラグインでプロジェクトを作った場合は、プロジェクトと同時にWeb Applicationの実行の構成も作られているのでそれを実行し、http://localhost:8080/を開けばおk!自分の場合は以下のようにQuickstartで作ったHomePage.javaを少し変えた程度。
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null) {
 add(new Label("message", "Hello, " + user.getNickname()));
} else {
 getRequestCycle().setRequestTarget(
   new RedirectRequestTarget(userService.createLoginURL("/home")));
}
未ログイン時にGoogleのログインページへリダイレクトするため(ログインして返ってくるため)に、WicketApplication中で mountBookmarkablePage("/home", HomePage.class); しています。

困っている

データベースも使いたい&JDOよりもJPAでしょ!と思うのでJPAを使っているが、JPQL中でのEntityクラス名をFQCNにしないと org.datanucleus.exceptions.ClassNotResolvedException とか出てしまう。persistence.xml中でclass要素の指定をしてもダメ、DataNucleusのサイトにあった orm.xml を追加しないとダメか?と試したがそれだともっとダメなカンジ。FQCNで指定すればorm.xmlが無くても動作するし、なんだろなー。