第1章 Seam チュートリアル

1.1. サンプルを試そう

このチュートリアルでは、 JBoss AS 4.0.4 がダウンロードされ、 EJB 3.0 プロファイルがインストールされたものとして話を進めます。 (JBoss AS インストールを使用して) また、Seam のコピーもダウンロードして、 作業ディレクトリに展開されているものとします。

各 Seam サンプルのディレクトリーは、以下の要領で構成されています。

  • Web ページ、イメージあるいはスタイルシートは、 examples/registration/view にあります。

  • 配備記述子やデータインポートスクリプトなどのリソースは、 examples/registration/resources にあります。

  • Java ソースコードは、 examples/registration/src にあります。

  • Ant ビルドスクリプトは、 examples/registration/build.xml にあります。

1.1.1. サンプルの実行

最初に、$ANT_HOME$JAVA_HOME が正しく設定され、 Ant が正しくインストールされたことを確認してください。 次に Seam をインストールしたルートフォルダにある build.properties ファイルに JBoss AS 4.0.4 のロケーションを設定してください。 まだ起動していなければ、 JBoss のルートディレクトリから bin/run.sh もしくは、bin/run.bat とタイプして JBoss アプリケーションサーバを起動してください。

さあ、examples/registration ディレクトリから、 ant deploy とタイプしてサンプルのビルド、デプロイを行いましょう。

ここまでうまくいったら、サンプルアプリケーションは JBoss にデプロイされています。 ブラウザから、 http://localhost:8080/seam-registration/ に、 アクセスしてみましょう。

1.1.2. サンプルのテスト実行

ほとんどのサンプルは TestNG 統合テストスイートに対応しています。 一番簡単なテスト実行は、 examples/registration ディレクトリから、 ant testexample として起動させてください。 また、お使いの IDE から TestNG プラグインを利用してテスト実行することも可能です。

1.2. 初めての Seam アプリケーション: ユーザ登録サンプル

ユーザ登録サンプルは、データベースに新規ユーザの、ユーザ名、実名、パスワードをデータベースに保存できる、簡単なアプリケーションです。 このサンプルは、Seam のイケテル機能の全てを見せることはできませんが、 JSF アクションリスナとして EJB3 セッション Bean を使用する方法や、 基本的な Seam の設定方法を見せてくれます。

EJB 3.0 には、まだ不慣れであると思いますが、あせらずにいきましょう。

最初のページは 3 つの入力フィールドを持つ基本的なフォームを表示します。 試しに、項目を入力してフォームをサブミットしてください。 これでユーザオブジェクトはデータベースに保存されます。

1.2.1. コードの理解

このサンプルは、2 つの JSP ページ、1 つのエンティティ Bean と、1 つのステートレスセッション Bean で実装されています。

基本から始めるために、コードを見てみましょう。

1.2.1.1. エンティティ Bean: User.java

ユーザデータのために、EJBエンティティ Beanが必要です。 このクラスでは、アノテーションによって 永続性バリデーション (データ妥当性検証) を、宣言的に定義しています。 Seam コンポーネントとしてのクラスを定義するために、別にいくつかのアノテーションも必要です。

例 1.1.

@Entity                                                                                  (1)
@Name("user")                                                                            (2)
@Scope(SESSION)                                                                          (3)
@Table(name="users")                                                                     (4)
public class User implements Serializable
{
   private static final long serialVersionUID = 1881413500711441951L;
   
   private String username;                                                              (5)
   private String password;
   private String name;
   
   public User(String name, String password, String username)
   {
      this.name = name;
      this.password = password;
      this.username = username;
   }
   
   public User() {}                                                                      (6)
   
   @NotNull @Length(min=5, max=15)                                                       (7)
   public String getPassword()
   {
      return password;
   }

   public void setPassword(String password)
   {
      this.password = password;
   }
   
   @NotNull
   public String getName()
   {
      return name;
   }

   public void setName(String name)
   {
      this.name = name;
   }
   
   @Id @NotNull @Length(min=5, max=15)                                                   (8)
   public String getUsername()
   {
      return username;
   }

   public void setUsername(String username)
   {
      this.username = username;
   }

}
(1)

EJB3 標準 @Entity アノテーションは、 User クラスがエンティティ Bean であることを示しています。

(2)

Seam コンポーネントは、 @Name アノテーションで指定される コンポーネント名 が必要です。 この名前は Seam アプリケーション中でユニークである必要があります。 JSF が Seam に Seam コンポーネント名と同じコンテキスト変数の解決を求める時に、 コンテキスト変数が現在、未定義 (null) であれば、 Seam はインスタンスを生成し、 新しいインスタンスをコンテキスト変数にバインドします。 このサンプルでは、 JSF が初めて、user という変数と出会うときに、 Seam は、User をインスタンス化します。

(3)

Seam がインスタンスを生成する時には、 必ずコンポーネントの デフォルトコンテキスト にあるコンテキスト変数に、 新しいインスタンスをバインドします。 デフォルトコンテキストは、 @Scope アノテーションを使用して定義されます。 User Beanは、セッションスコープのコンポーネントです。

(4)

EJB 標準 @Table アノテーションは、 User クラスが users テーブルにマッピングされることを示しています。

(5)

namepassword そして、 username は、 エンティティ Bean の永続属性です。 すべての永続属性には、アクセスメソッドが定義されています。 これらは、JSF がレスポンス生成 (Render Response) フェーズおよびモデル値の更新 (update model values) フェーズで必要となります。

(6)

空コンストラクタは、EJB と Seam の両方の仕様から必要となります。

(7)

@NotNull@Length アノテーションは、 Hibernate バリデータ (データ妥当性検証) フレームワークの一部です。 Seam は、 Hibernate のバリデータと統合されており、データの妥当性検証に利用できます。 (たとえ永続性として Hiberenate を使用しなくても)

(8)

EJB 標準 @Id アノテーションは、 エンティティ Bean の主キーであることを示しています。

このサンプルで、もっとも注目してほしい重要なものは、 @Name@Scope アノテーションです。 このアノテーションは、このクラスがSeamコンポーネントであることを規定しています。

User クラスのプロパティは、直接JSFコンポーネントにバインドされ、 モデル値の変更フェーズの期間に、JSFによって生成されたことが以降の説明からわかります。 JSP ページとエンティティ Bean のドメインモデルとの間で、 行き来するデータをコピーするために、 退屈な接着コード (glue code) は不要です。

しかし、エンティティ Bean はトランザクション管理やデータベースアクセスをすべきではありません。 従って、このコンポーネントを JSF のアクションリスナとして使用できません。 これが、セッション Bean が必要な理由です。

1.2.1.2. ステートレスセッション Bean クラス: RegisterAction.java

ほとんどの Seam アプリケーションは、セッション Bean を JSF アクションリスナとして使用します。 (好みに応じて JavaBean を使うことも可能)

このアプリケーションには、たった 1 つの JSF アクションしかなく、 1 つのセッション Bean のメソッドは、それにリンクしています。 このサンプルでは、ステートレスセッション Bean を使用しています。 なぜなら、アクションに関連するすべての状態は、User Bean によって保持されているからです。

サンプルの中で、これだけが本当に興味深いコードです。

例 1.2.

@Stateless                                                                               (1)
@Name("register")
@Interceptors(SeamInterceptor.class)                                                     (2)
public class RegisterAction implements Register
{

   @In @Valid                                                                            (3)(4)
   private User user;
   
   @PersistenceContext                                                                   (5)
   private EntityManager em;
   
   @In
   private FacesContext facesContext;                                                    (6)
   
   @IfInvalid(outcome=Outcome.REDISPLAY)
   public String register()                                                              (8)
   {                                                                                     (7)
      List existing = em.createQuery("select username from User where username=:username")
         .setParameter("username", user.getUsername())
         .getResultList();
         
      if (existing.size()==0)
      {
         em.persist(user);
         return "success";
      }
      else
      {
         facesContext.addMessage(null, new FacesMessage("username already exists"));
         return null;
      }
   }

}
(1)

EJB 標準 @Stateless アノテーションは、 このクラスをステートレスセッション Bean としてマークしています。

(2)

SeamInterceptor EJB インターセプタは、 Seam コンポーネントであるすべてのセッション Bean のために、有効にする必要があります。

(3)

@In アノテーションは、 Seam によってインジェクトされる Bean の属性にマークします。 このサンプルでは、この属性は、 user (インスタンス変数名) と名付けられたコンテキスト変数から、 インジェクトされています。

(4)

@Valid アノテーションは、 Hibernate バリデータにから提供されています。 それは関連するオブジェクトにおいて繰り返し発生するデータ妥当性検証をしています。

(5)

EJB 標準 @PersistenceContext アノテーションは、 EJB3 エンティティ Entity Manager にインジェクトするために使用されます。

(6)

Seam はさまざまな JSF、Seam や jBPM コンテキストオブジェクトが、インジェクトされることも可能です。 現在の FacesContext インスタンスを、 Seam コンポーネントにインジェクトするために、 facesContext と名付けられたインスタンス変数に、 @In アノテーションをマークしています。

(7)

@IfInvalid アノテーションは、Seam にHibernate バリデータを使用してコンポーネントの状態を データ妥当性検証を行うよう指示しています。 妥当性検証は、アクションリスナメソッドの前に呼ばれ、 状態が正しくない場合、異なった JSF 結果 (outcome) が返されます。 このサンプルでは、register() メソッドが呼ばれ、 user がデータ妥当性検証されます。 データ妥当性検証が失敗した場合、メッセージと共にフォームが再表示されます。

(8)

アクションリスナメソッドは、データベースとやり取りするために、 標準 EJB3 EntityManager API を使用し、JSF 結果 (outcome) を返します。 これはセッションBeanなので、 register() メソッドが呼ばれたときに、 トランザクションは自動的に開始された、終了したときにコミットされることに、 注意してください。

ここで、 @Scope を明示的に指定していないことに注意してください。 それぞれの Seam コンポーネントタイプは、明示的にスコープが指定されない場合、 デフォルトのスコープが適用されます。 ステートレスセッション Bean では、デフォルトスコープはステートレスコンテキストです。 実際、すべてのステートレスセッション Bean はステートレスコンテキストに属します。

このセッション Bean のアクションリスナは、この小さなアプリケーションのために、 ビジネスと永続ロジックを提供しています。 さらに複雑なアプリケーションでは、 コードを階層化し、永続ロジックが、専門のデータアクセスコンポーネントとなるように、 リファクタリングする必要があるかもしれません。 これをするのは簡単ではありますが、 Seam はアプリケーションの階層化のために、 特殊な方法を強要してはいないことに、ご注意ください。

さらに、このセッション Bean は、 Web リクエスト (例えば、FacesContext オブジェクト) に関連するコンテキストにアクセスすると同時に、 トランザクションリソース ( EntityManager オブジェクト) で保持される状態にもアクセスします。 これは古典的なJ2EEアーキテクチャからの新たな始まりです。 もし、典型的な J2EE の階層化が好きであれば、 Seam アプリケーションでも実装は可能です。 しかし、多くのアプリケーションにとって、それが有用なわけではありません。

1.2.1.3. セッション Bean ローカルインタフェース: Register.java

当然、セッション Bean には、ローカルインタフェースが必要です。

例 1.3.

@Local
public interface Register
{
   public String register();
}

Javaコードは以上です。続いて配備記述子です。

1.2.1.4. WEB 配備記述子: web.xml

この小さなアプリケーションのプレゼンテーション層はWARにデプロイされます。 従って、WEB 配備記述子が必要です。

例 1.4.

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


    <!-- Seam -->

    <listener>
        <listener-class>org.jboss.seam.servlet.SeamListener</listener-class>
    </listener>

    <!-- Global JNDI name pattern for JBoss EJB3 (change for other servers) -->
    <context-param>
        <param-name>org.jboss.seam.core.init.jndiPattern</param-name>
        <param-value>jboss-seam-registration/#{ejbName}/local</param-value>
    </context-param>

    <!-- MyFaces -->

    <listener>
        <listener-class>
            org.apache.myfaces.webapp.StartupServletContextListener
        </listener-class>
    </listener>

    <context-param>
        <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
        <param-value>client</param-value>
    </context-param>

    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Faces Servlet Mapping -->
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.seam</url-pattern>
    </servlet-mapping>

</web-app>

この web.xml ファイルは、Seam と MyFaces を設定します。 ここで見る設定は、Seam アプリケーションではいつも同じです。

1.2.1.5. JSF 設定: faces-config.xml

すべての Seam アプリケーションはプレゼンテーション層として JSF ビューを使用します。 従って、faces-config.xml が必要です。

例 1.5.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE faces-config 
PUBLIC "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
                            "http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
<faces-config>

    <navigation-rule>
        <navigation-case>
            <from-outcome>success</from-outcome>
            <to-view-id>/registered.jsp</to-view-id>
        </navigation-case>
    </navigation-rule>

    <!-- A phase listener is needed by all Seam applications -->
    
    <lifecycle>
        <phase-listener>org.jboss.seam.jsf.SeamPhaseListener</phase-listener>
    </lifecycle>

</faces-config>

jsf-config.xml ファイルは、Seam を JSF に統合させ、 ユーザ登録アプリケーションにナビゲーション規則を定義しています。 Managed Bean 宣言がないことに注意してください。(Seam では不要) Managed Bean は Seam コンポーネントです。 Seam アプリケーションでは、 jsf-config.xml は、 論理的な JSF 結果 (outcome) からビューページにマッピングする、 ページフローを定義しています。

実際、基本的な記述子の設定だけあれば、 新しい機能を Seam アプリケーションに追加するときに、 必要となる XML は、 ナビゲーション規則とたぶんjBPM プロセス定義 だけです。 Seam は XML に記された、 プロセスフロー設定データから、 ビューを取得します。

1.2.1.6. EJB 永続配備記述子: persistence.xml

persistence.xml ファイルは、EJB 永続プロバイダに、 データベースの場所、ベンダー特有の設定を含みます。 このサンプルで、それはHibernateにどのデータベースを使用するかを知らせ、 スタートアップ時に、自動的にスキーマをエキスポートすることを可能にしています。

<persistence>
    <persistence-unit name="userDatabase">
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <jta-data-source>java:/DefaultDS</jta-data-source>
      <properties>
         <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
         <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
      </properties>
    </persistence-unit>
</persistence>

1.2.1.7. ビュー: register.jspregistered.jsp

Seam アプリケーションのビューページは、 JSF をサポートする多くの技術を使用して実装されています。 JSP は多くの開発者にとって知られていること、ここでは最小限の要件しかないため、 このサンプルでは、JSP を使用しています。 (でも、私たち (JBoss) のアドバイスを受ければ、アプリケーションで Facelet を使うことも可能)

例 1.6.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
 <head>
  <title>Register New User</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <table border="0">
       <tr>
         <td>Username</td>
         <td><h:inputText value="#{user.username}"/></td>
       </tr>
       <tr>
         <td>Real Name</td>
         <td><h:inputText value="#{user.name}"/></td>
       </tr>
       <tr>
         <td>Password</td>
         <td><h:inputSecret value="#{user.password}"/></td>
       </tr>
     </table>
     <h:messages/>
     <h:commandButton type="submit" value="Register" action="#{register.register}"/>
   </h:form>
  </f:view>
 </body>
</html>

例 1.7.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
 <head>
  <title>Successfully Registered New User</title>
 </head>
 <body>
  <f:view>
    Welcome, <h:outputText value="#{user.name}"/>, 
    you are successfully registered as <h:outputText value="#{user.username}"/>.
  </f:view>
 </body>
</html>

これらは退屈ないつものJSPページです。 Seam 独特のものはここにありません。

1.2.1.8. EAR 配備記述子: application.xml

最後に、EARとして アプリケーションがデプロイされるために、配備記述子も必要です。

例 1.8.

<application>
    <display-name>Seam</display-name>

    <module>
        <web>
            <web-uri>jboss-seam-registration.war</web-uri>
            <context-root>/seam-registration</context-root>
        </web>
    </module>
    <module>
        <ejb>jboss-seam-registration.jar</ejb>
    </module>
    
</application>

この配備記述子はエンタープライズアーカイブのモジュールとリンクし、 WEBアプリケーションをコンテキストルート /seam-registration にバインドします。

エンタープライズアプリケーション中のすべてのファイルを見終わりました。

1.2.2. 動作内容

フォームがサブミットされたとき、 JSF は Seam に変数名 user の解決を要求します。 この名前にリンクした変数は、(どの Seam コンテキスト中にも) まだ存在しないため、 Seam は user コンポーネントをインスタンス化し、 Seam セッションコンテキストにそれを保管した後、 User エンティティBean インスタンスを JSF に、返り値として返します。 JSF はフォームに入力された値を User エンティティ のプロパティにバインドします。

次に、JSF は Seam に変数名 register の解決を要求します。 Seam は、ステートレスコンテキスト中の RegisterAction ステートレスセッション Bean を見つけ、 それを返します。 JSF は register() アクションリスナメソッドを呼び出します。

Seam はメソッド呼び出しをインターセプトし、 Hibernate バリデータにセッション Bean インスタンス (そして、繰り返し、User エンティティ Bean インスタンス) のデータ妥当性検証を要求する前に、 セッションコンテキストから User エンティティと、 現在の FacesContext インスタンスを、 インジェクトします。 状態が有効であれば、 呼び出しは進み、 register() メソッドが呼び出されます。 そうでない場合、 Seam は null 結果 (outcome) を返し、 JSF はページを再表示します。

JSF が次の JSP ページのレンダリングフェーズに達するとき、 JSF は Seam に変数名 user の解決を要求し、 その結果としてSeam のセッション スコープから返された User エンティティのプロパティ値を使います。

1.3. Seam クリック可能リスト: 掲示板サンプル

データベースの検索結果をクリック可能リストとすることは、 多くのオンラインアプリケーションにおいて重要な部分であり、 Seamは、JSF 上に、 EJB-QL あるいは、HQL を使用してデータをクエリーし、 JSF <h:dataTable> を使って、 クリック可能なリストとしてそれを表示することを、 より容易する特別な機能性を提供します。 この掲示板サンプルはこの機能をデモンストレーションしています。

1.3.1. コードの理解

この掲示板サンプルは、 1 つのエンティティ Bean である Message、 1 つのセッション Bean である MessageListBean、 そして 1 つの JSP から構成されています。

1.3.1.1. エンティティ Bean: Message.java

Message エンティティ Bean は、 タイトル、 テキスト、 掲示メッセージの日付および時間、 そして、メッセージが既読か否かを示すフラグを 定義しています。

例 1.9.

@Entity
@Name("message")
@Scope(EVENT)
public class Message implements Serializable
{
   private Long id;
   private String title;
   private String text;
   private boolean read;
   private Date datetime;
   
   @Id @GeneratedValue
   public Long getId() {
      return id;
   }
   public void setId(Long id) {
      this.id = id;
   }
   
   @NotNull @Length(max=100)
   public String getTitle() {
      return title;
   }
   public void setTitle(String title) {
      this.title = title;
   }
   
   @NotNull @Lob
   public String getText() {
      return text;
   }
   public void setText(String text) {
      this.text = text;
   }
   
   @NotNull
   public boolean isRead() {
      return read;
   }
   public void setRead(boolean read) {
      this.read = read;
   }
   
   @NotNull 
   @Basic @Temporal(TemporalType.TIMESTAMP)
   public Date getDatetime() {
      return datetime;
   }
   public void setDatetime(Date datetime) {
      this.datetime = datetime;
   }
   
}

1.3.1.2. ステートフルセッション Bean: MessageListBean.java

前述のサンプル同様、 フォーム上の 2 つのボタンのために、 アクションリスナメソッドを定義したセッション Bean MessageListBean が 1 つあります。 ボタンの 1 つはリストからメッセージを選択し、 そのメッセージを表示します。 もう 1 つのボタンはメッセージを削除します。 この点において、前述のサンプルとそれほど違いません。

しかし、 初めて掲示板ページに画面遷移するとき、 MessageListBean はメッセージのリストを検索する責務を持ちます。 ユーザが画面を遷移させる方法はさまざまありますが、 これらのすべてが JSF アクションによって進められるわけではありません —例えば、ユーザはブックマークしているかもしれません。 従って、メッセージリストを検索する作業は、 アクションリスナメソッドの代わりに Seam factory method で起こります。

サーバとリクエストの間でメモリ中にメッセージのリストをキャッシュしたいので、 ステートフルセッション Bean にこれを行わせます。

例 1.10.

@Stateful
@Scope(SESSION)
@Name("messageList")
@Interceptors(SeamInterceptor.class)
public class MessageListBean implements Serializable, MessageList
{

   @DataModel                                                                            (1)
   private List<Message> messages;
   
   @DataModelSelection                                                                   (2)
   @Out(required=false)                                                                  (3)
   private Message message;
   
   @PersistenceContext(type=EXTENDED)                                                    (4)
   private EntityManager em;
   
   @Factory("messages")                                                                  (5)
   public void findMessages()
   {
      messages = em.createQuery("from Message msg order by msg.datetime desc").getResultList();
   }
   
   public String select()                                                                (6)
   {
      message.setRead(true);
      return "selected";
   }
   
   public String delete()                                                                (7)
   {
      messages.remove(message);
      em.remove(message);
      message=null;
      return "deleted";
   }
   
   @Remove @Destory                                                                      (8)
   public void destroy() {}

}
(1)

@DataModel アノテーションは、 java.util.List タイプの属性を、 javax.faces.model.DataModel インスタンスとして JSF ページに表現します。 これにより各行に対しクリック可能なリンクを持つ JSF <h:dataTable> が使用できます。 このサンプルでは、 DataModelmessages の名前のセッションコンテキスト中で利用可能になります

(2)

@DataModelSelection アノテーションは、 Seam にクリックされたリンクと関連した List 要素をインジェクトするよう指示しています。

(3)

@Outアノテーションは次に、選択された値を直接ページに表現します。 従って、クリック可能なリストの行が選択されるごとに Message はステートフル Bean の属性にインジェクト (inject) され、 続いて message の名前のイベントコンテキストに アウトジェクト (outject) されます。

(4)

このステートフル Bean は EJB3 拡張永続コンテキスト (extended persistence context) を持っています。 この Bean が存在する限り、 クエリーで検索された messages は管理された状態に保持されます。 従って、 明示的な EntityManager 呼び出しの必要はなくても、 ステートフル Bean に対して続いて起こる、 どのようなメソッド呼び出しでもそれらを更新することが可能です。

(5)

初めて JSP ページを画面遷移させるとき、 messages コンテキスト変数中に値を持っていません。 @Factory アノテーションは Seam に MessageListBean インスタンスの生成を指示し、 値の初期化のために findMessages() メソッドを呼び出します。 findMessages()messagesファクトリーメソッド (factory method) と呼びます。

(6)

select() アクションリスナメソッドは、 選択された Messageに 読み込み (read) マークを付け、 データベース中のそれを更新します。

(7)

delete() アクションリスナメソッドは、 選択された Message をデータベースから削除します。

(8)

Seam コンテキストが終わり、サーバサイドのあらゆる状態をクリーンアップする場合に、 Seam がステートフル Bean を確実に削除するために、 すべてのステートフルセッション Bean の Seam コンポーネントは、 @Remove @Destroy とマークされたメソッドを持つことが 必須 (must) です。

これがセッションスコープの Seam コンポーネントであることに注意してください。 ユーザログインセッションと関連し、 ログインセッションからのすべてのリクエストは同じコンポーネントのインスタンスを共有しています。 (Seam アプリケーションではいつもセッションスコープのコンポーネントは控えめに使用します。)

1.3.1.3. セッション Bean ローカルインタフェース: MessageList.java

もちろん、すべてのセッション Bean はインタフェースを持ちます。

@Local
public interface MessageList
{
   public void findMessages();
   public String select();
   public String delete();
   public void destroy();
}

ここからは、サンプルコード中のローカルインタフェースの掲載を省略します。

persistence.xmlweb.xmlfaces-config.xml、 そして application.xml は前述までのサンプルとほぼ同じなので、 スキップして、 JSP に進みましょう。

1.3.1.4. ビュー: messages.jsp

このJSPページは JSF <h:dataTable> コンポーネントを使用した簡単なものです。 Seam として特別なものはありません。

例 1.11.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<html>
 <head>
  <title>Messages</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <h2>Message List</h2>
     <h:outputText value="No messages to display" rendered="#{messages.rowCount==0}"/>
     <h:dataTable var="msg" value="#{messages}" rendered="#{messages.rowCount>0}">
        <h:column>
           <f:facet name="header">
              <h:outputText value="Read"/>
           </f:facet>
           <h:selectBooleanCheckbox value="#{msg.read}" disabled="true"/>
        </h:column>
        <h:column>
           <f:facet name="header">
              <h:outputText value="Title"/>
           </f:facet>
           <h:commandLink value="#{msg.title}" action="#{messageList.select}"/>
        </h:column>
        <h:column>
           <f:facet name="header">
              <h:outputText value="Date/Time"/>
           </f:facet>
           <h:outputText value="#{msg.datetime}">
              <f:convertDateTime type="both" dateStyle="medium" timeStyle="short"/>
           </h:outputText>
        </h:column>
        <h:column>
           <h:commandButton value="Delete" action="#{messageList.delete}"/>
        </h:column>
     </h:dataTable>
     <h3><h:outputText value="#{message.title}"/></h3>
     <div><h:outputText value="#{message.text}"/></div>
   </h:form>
  </f:view>
 </body>
</html>

1.3.2. 動作内容

最初に、 messages.jsp ページに画面遷移させるとき、 JSFポストバック (faces リクエスト) であってもあるいは、 直接的なブラウザからの GET リクエスト (non-faces リクエスト) であっても、 ページは messages コンテキスト変数を解決しようと試みます。 このコンテキスト変数は初期化されていなために、 Seam はファクトリーメソッド findMessages()を呼び出します。 このメソッドはデータベースへのクエリー発行や、 アウトジェクト (outject)された DataModel 中の結果の取得を行います。 DataModel<h:dataTable> をレンダリングするのに必要な行データを提供します。

ユーザが <h:commandLink> をクリックしたとき、 JSF は select() アクションリスナを呼び出します。 Seam はこの呼び出しをインターセプトし 選択された行データを messageList コンポーネントの message 属性にインジェクトします。 選択された Message が読み込みでマークされたとき、アクションリスナは発火します。 アクションリスナが発火したとき、選択された Message をマークします。 呼び出しの終わりに、 Seam は選択された Message をコンテキスト変数名 message にアウトジェクトします。 次に、EJB コンテナはトランザクションをコミットし、 そして Message の変更はデータベースにフラッシュされます。 最後に、ページは再びレンダリングされ、掲示板の再表示と選択されたメッセージが下部に表示されます。

ユーザが <h:commandButton> をクリックしたとき、 JSF は delete() アクションリスナを呼び出します。 Seam はこの呼び出しをインターセプトし、 選択された行データを messageList コンポーネントの message 属性にインジェクトします。 アクションリスナが発火したとき、選択された Message はリストから削除され、 また、EntityManagerremove() も呼び出します。 選択された Message をリストから削除し、 EntityManager の remove() を呼び出します。 呼び出しの終わりに、 Seam は messages コンテキスト変数をリフレッシュし、 message の名前のコンテキスト変数をクリアします。 EJB コ ンテナはトランザクションをコミットし、 データベースから Message を削除します。 最後に、ページが再びレンダリングされ、掲示板は再表示されます。

1.4. Seam と jBPM : TO-DO リストサンプル

jBPM はワークフローやタスク管理の優れた機能を提供します。 どのように jBPM が Seam と統合されているかを知るために、 簡単な To-Do リストアプリケーションをお見せしましょう。 タスクのリストを管理することは jBPM の中心的な機能であるため、 このサンプルでは Java コードはほとんどありません。

1.4.1. コードの理解

このサンプルの中心は jBPM のプロセス定義です。 2 つの JSP と 2 つのちょっとした JavaBean もあります。 (データベースアクセスやトランザクション特性がないので、 セッション Bean を使用する理由はありません。) それではプロセス定義から始めましょう。

例 1.12.

<process-definition name="todo">
   
   <start-state name="start">                                                            (1)
      <transition to="todo"/>
   </start-state>
   
   <task-node name="todo">                                                               (2)
      <task name="todo" description="#{todoList.description}">                           (3)
         <assignment actor-id="#{actor.id}"/>                                            (4)
      </task>
      <transition to="done"/>
   </task-node>
   
   <end-state name="done"/>                                                              (5)
   
</process-definition>
(1)

<start-state> ノードはプロセスの論理的な開始を表します。 プロセスが開始したとき、 それはすぐに todo に遷移します。

(2)

<task-node> ノードは 待ち状態 (wait state) を表します。 ここでは、1つ以上のタスクが行われるのを待って、ビジネスプロセスは休止します。

(3)

<task> 要素はユーザによって実行されるタスクを定義します。 このノードには 1 つのタスクしか定義されていないので、 それが完了したとき、 実行は再開され終了状態 (end state) に遷移します。 このタスクは、todoList の名前の Seam コンポーネントから (1 つの JavaBean) から description を取得します。

(4)

タスクが生成されたとき、タスクにはユーザあるいはユーザのグループを割り当てる必要があります。 このサンプルでは、タスクは現在のユーザに割り当てられています。 これは actor と呼ばれる組み込み Seam コンポーネントから取得します。 どのような Seam コンポーネントでもタスク割り当てを実行するために使用される可能性があります。

(5)

<end-state>ノードはビジネスプロセスの論理的な終了を定義します。 実行がこのノードに到達したとき、 プロセスインスタンスは破棄されます。

このドキュメントはノードのグラフとしてビジネスプロセスを定義します。 これはささいな現実にあり得るビジネスプロセスです。 実行されなければならないタスクは 1 つだけです。 タスクが完了したとき ビジネスプロセスは終了します。

最初の JavaBean はログイン画面 login.jsp を扱います。 その仕事は actor コンポーネントを使用して jBPM actor id を初期化するだけです。 (実際のアプリケーションではユーザ認証も必要です。)

例 1.13.

@Name("login")
public class Login {
   
   @In(create=true) 
   private Actor actor;
   
   private String user;

   public String getUser() {
      return user;
   }

   public void setUser(String user) {
      this.user = user;
   }
   
   public String login()
   {
      actor.setId(user);
      return "success";
   }
}

@In(create=true) が使用されていることがわかります。 これは Seam に、もし現在のコンテキストに何も存在しない場合、 Seam にコンポーネントのインスタンスを生成することを指示しています。 このサンプルでは actor と呼ばれるコンポーネントです。

次のJSPは取るに足らないものです。

例 1.14.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<f:view>
    <h:form>
      <div>
        <h:inputText value="#{login.user}"/>
        <h:commandButton value="Login" action="#{login.login}"/>
      </div>
    </h:form>
</f:view>
</body>
</html>

2 つめの JavaBean はビジネスプロセスインスタンスの開始とタスクの終了を担当します。

例 1.15.

@Name("todoList")
public class TodoList {
   
   private String description;
   
   public String getDescription()                                                        (1)
   {
      return description;
   }

   public void setDescription(String description) {
      this.description = description;
   }
   
   @CreateProcess(definition="todo")                                                     (2)
   public void createTodo() {}
   
   @StartTask @EndTask                                                                   (3)
   public void done() {}

}
(1)

description プロパティは JSP ページからユーザ入力を受け取り、 タスク内容 (description) が設定されるように、それをプロセス定義に公開します。

(2)

Seam @CreateProcess アノテーションは名前つきプロセス定義のために jBPM プロセスインスタンスを生成します。

(3)

Seam @StartTask アノテーションはタスク上で作業を開始します。 @EndTask はタスクを終了し、 ビジネスプロセスの再開を可能にします。

より現実的なサンプルでは、 @StartTask@EndTask は同じメソッドには登場しません。 なぜなら、通常、タスクを完了するためにアプリケーションを使用して仕事が行われるからです。

最後に、このアプリケーションのポイントは todo.jsp にあります。

例 1.16.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>Todo List</title>
</head>
<body>
<h1>Todo List</h1>
<f:view>
   <h:form id="list">
      <div>
         <h:outputText value="There are no todo items." rendered="#{empty taskInstanceList}"/>
         <h:dataTable value="#{taskInstanceList}" var="task" rendered="#{not empty taskInstanceList}">
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Description"/>
                </f:facet>
                <h:inputText value="#{task.description}"/>
            </h:column>
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Created"/>
                </f:facet>
                <h:outputText value="#{task.taskMgmtInstance.processInstance.start}">
                    <f:convertDateTime type="date"/>
                </h:outputText>
            </h:column>
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Priority"/>
                </f:facet>
                <h:inputText value="#{task.priority}" style="width: 30"/>
            </h:column>
            <h:column>
                <f:facet name="header">
                    <h:outputText value="Due Date"/>
                </f:facet>
                <h:inputText value="#{task.dueDate}" style="width: 100">
                    <f:convertDateTime type="date" dateStyle="short"/>
                </h:inputText>
            </h:column>
            <h:column>
                <h:commandLink action="#{todoList.done}"> 
                    <h:commandButton value="Done"/>
                    <f:param name="taskId" value="#{task.id}"/>
                </h:commandLink>
            </h:column>
         </h:dataTable>
      </div>
      <div>
      <h:messages/>
      </div>
      <div>
         <h:commandButton value="Update Items" action="update"/>
      </div>
   </h:form>
   <h:form id="new">
      <div>
         <h:inputText value="#{todoList.description}"/>
         <h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>
      </div>
   </h:form>
</f:view>
</body>
</html>

1 つずつ取り出しましょう。

ページはタスクリストをレンダリングしています。 これは、taskInstanceList と呼ばれる Seam 組み込みコンポーネントから取得します。 このリストはJSFフォームの中に定義されています。

<h:form id="list">
   <div>
      <h:outputText value="There are no todo items." rendered="#{empty taskInstanceList}"/>
      <h:dataTable value="#{taskInstanceList}" var="task" rendered="#{not empty taskInstanceList}">
         ...
      </h:dataTable>
   </div>
</h:form>

リストの各要素は jBPM クラス TaskInstance のインスタンスです。 以下のコードは単に、リスト中の各タスクに関連するプロパティを表示しています。 description (記述内容)、優先順や、納期の値を変更可能にするために、 JSF 入力制御 (inputText, etc.) を使用します。

<h:column>
    <f:facet name="header">
       <h:outputText value="Description"/>
    </f:facet>
    <h:inputText value="#{task.description}"/>
</h:column>
<h:column>
    <f:facet name="header">
        <h:outputText value="Created"/>
    </f:facet>
    <h:outputText value="#{task.taskMgmtInstance.processInstance.start}">
        <f:convertDateTime type="date"/>
    </h:outputText>
</h:column>
<h:column>
    <f:facet name="header">
        <h:outputText value="Priority"/>
    </f:facet>
    <h:inputText value="#{task.priority}" style="width: 30"/>
</h:column>
<h:column>
    <f:facet name="header">
        <h:outputText value="Due Date"/>
    </f:facet>
    <h:inputText value="#{task.dueDate}" style="width: 100">
        <f:convertDateTime type="date" dateStyle="short"/>
    </h:inputText>
</h:column>

このボタンは、@StartTask @EndTaskとアノテーション付きのアクションメソッドが、呼び出されることで終了します。 それは、task id をリクエストパラメータとして Seam に渡します。

<h:column>
    <h:commandLink action="#{todoList.done}"> 
        <h:commandButton value="Done"/>
        <f:param name="taskId" value="#{task.id}"/>
    </h:commandLink>
</h:column>

このボタンはタスクのプロパティを変更するために使用されます。 フォームがサブミットされたとき、Seam と jBPM はタスクに対するどのような変化も永続化します。 アクションリスナメソッドは不要です。

<h:commandButton value="Update Items" action="update"/>

ページの 2 つ目のフォームは新しいアイテムを作成するために使用されます。 @CreateProcessアノテーション付きアクションメソッドから呼び出されることにより行われます。

<h:form id="new">
    <div>
        <h:inputText value="#{todoList.description}"/>
        <h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>
    </div>
</h:form>

このサンプルには、その他いくつか必要なファイルがありますが、 それらは 標準的な jBPM や Seam の設定であり特殊なものはありません。

1.4.2. 動作内容

TODO

1.5. Seam ページフロー: 数字当てゲームサンプル

比較的自由な (アドホック) 画面遷移を持つ Seam アプリケーションでは、 JSF ナビゲーション規則はページフローを定義するのに最適な方法です。 さらに制約の多い画面遷移を持つアプリケーション、 特によりステートフルなユーザインタフェースでは、 ナビゲーション規則がシステムの流れを理解することは容易ではありません。 フローを理解するために、ビューページ、アクション、およびナビゲーション規則からそれをかき集める必要があります。

Seam は jPDL プロセス定義を使うことでページフロー定義を可能にします。 この簡単な数字当てゲームサンプルから、どのようにこれが実現されているかがわかります。

1.5.1. コードの理解

このサンプルは 1 つのJavaBean、3 つの JSP ページ、それと jPDL プロセスフロー定義で実装されています。 ページフローから見始めましょう。

例 1.17.

<pageflow-definition name="numberGuess">
   
   <start-state name="start">
      <transition to="displayGuess"/>
   </start-state>
   
   <page name="displayGuess" view-id="/numberGuess.jsp" redirect="true">                 (1)
      <transition name="guess" to="evaluateGuess">                                       (2)
          <action expression="#{numberGuess.guess}" />                                   (3)
      </transition>
   </page>
   
   <decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">              (4)
      <transition name="true" to="win"/>
      <transition name="false" to="evaluateRemainingGuesses"/>
   </decision>
   
   <decision name="evaluateRemainingGuesses" expression="#{numberGuess.lastGuess}">
      <transition name="true" to="lose"/>
      <transition name="false" to="displayGuess"/>
   </decision>
   
   <page name="win" view-id="/win.jsp" redirect="true">
      <end-conversation />
   </page>
   
   <page name="lose" view-id="/lose.jsp" redirect="true">
      <end-conversation />
   </page>
   
</pageflow-definition>
(1)

<page> 要素は待ち状態 (wait state) を定義しています。 ここではシステムは特定の JSF ビューを表示し、ユーザ入力を待っています。 view-id は普通の JSF ナビゲーション規則で使用されている JSF view id と同じものです。 ページが画面遷移するときに、 redirect 属性は Seam に post-then-redirect の使用を指示しています。 (この結果がブラウザ URL に表示されます。)

(2)

<transition> 要素は JSF 結果 (outcome) に名前を付けます。 JSF アクションが結果 (outcome) を出力するとき、 transition はトリガーされます。 jBPM transition action の呼び出しの後、 実行はページフローグラフの次のノードに進められます。

(3)

transition の <action> は、 jBPM の transitionでそれが起こることを除けば、 JSF action と同じです。 transition action はどのような Seam コンポーネントを呼び出すことが可能です。

(4)

<decision> ノードはページフローを分岐させ、 JSF EL 式を評価することによって次に実行されるノードを決定します。

ページフローを見終わりました、 アプリケーションの残りの部分を理解することはもう簡単です。

これはアプリケーションの主要なページ numberGuess.jsp です。

例 1.18.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>Guess a number...</title>
</head>
<body>
<h1>Guess a number...</h1>
<f:view>
    <h:form>
        <h:outputText value="Higher!" rendered="#{numberGuess.randomNumber>numberGuess.currentGuess}" />
        <h:outputText value="Lower!" rendered="#{numberGuess.randomNumber<numberGuess.currentGuess}" />
        <br />
        I'm thinking of a number between <h:outputText value="#{numberGuess.smallest}" /> and 
        <h:outputText value="#{numberGuess.biggest}" />. You have 
        <h:outputText value="#{numberGuess.remainingGuesses}" /> guesses.
        <br />
        Your guess: 
        <h:inputText value="#{numberGuess.currentGuess}" id="guess" required="true">
            <f:validateLongRange
                maximum="#{numberGuess.biggest}" 
                minimum="#{numberGuess.smallest}"/>
        </h:inputText>
        <h:commandButton type="submit" value="Guess" action="guess" />
        <br/>
        <h:message for="guess" style="color: red"/>
    </h:form>
</f:view>
</body>
</html>

アクションを直接呼び出す代わりに、 どのようにコマンドボタンはguess transitionを指定しているかに着目してください。

win.jsp ページはごく普通のものです。

例 1.19.

<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<html>
<head>
<title>You won!</title>
</head>
<body>
<h1>You won!</h1>
<f:view>
    Yes, the answer was <h:outputText value="#{numberGuess.currentGuess}" />.
    It took you <h:outputText value="#{numberGuess.guessCount}" /> guesses.
    Would you like to <a href="numberGuess.seam">play again</a>?
  </f:view>
</body>
</html>

lose.jsp も同様です。 (コピー&ペーストができないは困りますが) 最後は、JavaBean Seam コンポーネントです。

例 1.20.

@Name("numberGuess")
@Scope(ScopeType.CONVERSATION)
public class NumberGuess {
   
   private int randomNumber;
   private Integer currentGuess;
   private int biggest;
   private int smallest;
   private int guessCount;
   private int maxGuesses;
   
   @Create                                                                               (1)
   @Begin(pageflow="numberGuess")                                                        (2)
   public void begin()
   {
      randomNumber = new Random().nextInt(100);
      guessCount = 0;
      biggest = 100;
      smallest = 1;
   }
   
   public void setCurrentGuess(Integer guess)
   {
      this.currentGuess = guess;
   }
   
   public Integer getCurrentGuess()
   {
      return currentGuess;
   }
   
   public void guess()
   {
      if (currentGuess>randomNumber)
      {
         biggest = currentGuess - 1;
      }
      if (currentGuess<randomNumber)
      {
         smallest = currentGuess + 1;
      }
      guessCount ++;
   }
   
   public boolean isCorrectGuess()
   {
      return currentGuess==randomNumber;
   }
   
   public int getBiggest()
   {
      return biggest;
   }
   
   public int getSmallest()
   {
      return smallest;
   }
   
   public int getGuessCount()
   {
      return guessCount;
   }
   
   public boolean isLastGuess()
   {
      return guessCount==maxGuesses;
   }

   public int getRemainingGuesses() {
      return maxGuesses-guessCount;
   }

   public void setMaxGuesses(int maxGuesses) {
      this.maxGuesses = maxGuesses;
   }

   public int getMaxGuesses() {
      return maxGuesses;
   }

   public int getRandomNumber() {
      return randomNumber;
   }
}
(1)

最初に、JSP ページが numberGuess コンポーネントを要求するとき、 Seam は新しいコンポーネントを生成します。 そして、@Create メソッドが呼ばれ、 コンポーネント自身の初期化が可能になります。

(2)

@Begin アノテーションは、 Seam の対話を開始し(詳細は後述)、 対話のページフローを使用するためのページフロー定義を指定します。

お分かりの通り、この Seam コンポーネントは純粋なビジネスロジックです。 ユーザインタラクションのフローについてまったく知る必要がありません。 これは本質的にコンポーネントの再利用性を向上させます。

1.5.2. 動作内容

TODO

1.6. 本格的 Seam アプリケーション: ホテル予約サンプル

1.6.1. はじめに

この予約アプリケーションは以下の特徴を持つ本格的なホテルの部屋予約システムです。

  • ユーザ登録

  • ログイン

  • ログアウト

  • パスワード設定

  • ホテル検索

  • ホテル選択

  • 部屋予約

  • 予約確認

  • 現状の予約リスト

この予約アプリケーションは JSF、EJB 3.0、Seam とともにビューとして Facelet を使用しています。 JSF、Facelets、Seam、JavaBeans そして、Hibernate3 のアプリケーションの移植版もあります

このアプリケーションをある程度の期間、 いじってわかることの 1 つは、 それがとても 堅牢 (robust) であることです。 バックボタンをいじっても、 ブラウザのリフレッシュをしても、 複数のウィンドを開いても、 無意味なデータを好きなだけ入力しても、 アプリケーションをクラッシュさせることがとても困難であることがわかります。 これを達成するためにテストやバグ取りに何週間も掛かったと思われるかもしれません。 現実的にはそんなことありません。 Seam は、堅牢な WEB アプリケーションを簡単に構築できるように設計されています。 そして、これまでコーディングそのものによって得られていた堅牢性は、 Seam を使用することで自然かつ自動的に得られます。

サンプルアプリケーションのコードを見れば、 どのようにアプリケーションが動作しているか習得できます。 そして、この堅牢性の達成するために、 どのように宣言的状態管理や統合されたデータ妥当性検証が使用されているかを見ることができます。

1.6.2. 予約サンプルの概要

プロジェクトの構成は以前のものと同じです。 項1.1. 「サンプルを試そう」 を参照してください。 うまくアプリケーションが起動したならば、 ブラウザから指定して http://localhost:8080/seam-booking/ をアクセスすることが可能です。

ちょうど 10 のクラス (加えて、6 つのセッション Bean のインタフェースと 1 つのアノテーションのインタフェース)が、 このアプリケーションの実装のために使われています。 6 つのセッション Bean アクションリスナはリストに記載された特徴のためにすべてのビジネスロジックを含んでいます。

  • BookingListAction は、その時のログインユーザのために現状の予約を取得します。
  • ChangePasswordAction は、その時のログインユーザのパスワードを変更します。
  • HotelBookingActionは、アプリケーションの中核的機能を実装します。 ホテル部屋検索、選択、予約、予約確認。 この機能は、対話 として実装されており、 このアプリケーションではもっとも関心を引くクラスです。
  • LoginAction は、ログイン詳細のデータ妥当性チェックとログインユーザの取得を行います。
  • LogoutAction は、ログインセッションを終了させます。
  • RegisterAction は新しいシステムユーザを登録します。

3 つのエンティティ Bean がアプリケーションの永続ドメインモデルを実装しています。

  • Hotel は、ホテルを表すエンティティ Bean です。
  • Booking は、現状の予約を表すエンティティ Bean です。
  • User は、ホテル予約ができるユーザを表すエンティティ Bean です。

最後に、LoggedIn アノテーション と LoggedInInterceptor は、 ログインユーザを必要するアクションを保護するために使用されます。

1.6.3. Seam 対話 (conversations) の理解

気が向けばソースコードを読まれることをお勧めします。 このチュートリアルの中では、機能の特定の 1 つに集中します。 ホテル検索、選択、予約と確認。 ユーザの視点から見ると、 これは 1 つの連続的な仕事の単位、 つまり 対話 (conversations) です。

ほとんどの WEB アプリケーションのアーキテクチャは、対話を公開するための構造を持っていません。 これは対話に関連した状態を管理に関する大きな問題を引き起こします。 通常、Java WEB アプリケーションは 2 つの技術を組み合わせて使用します。 最初に、状態は HttpSession に投げられ、 2 番目に、永続可能状態は各リクエストの後に、データベースに書き込みがされ、 そして、各リクエストの始めに、データベースから再構築がされます。

データベースは最もスケーラビリティに乏しい層 (Tier) なので、 これはしばしば受けいれがたいスケーラビリティの不足を招きます。 加えて、リクエストごとにデータベースとやり取りする余分な転送量による待ち時間も問題です。 この冗長な転送量を減少させるために、 Java アプリケーションではしばしばリクエスト間で普通にアクセスされるデータを保管するデータキャッシュ (2 次レベル) を導入します。 このキャッシュは必ずしも効率ではありません。 なぜなら データが無効化どうかの判断を、 ユーザがデータの操作を終了したかどうかをベースとする代わりに LRU ポリシーをベースとしているからです。 さらに、 キャッシュは多くの現在のトランザクションによって共有されているので、 キャッシュされた状態とデータベース間の一貫性を保つことに関連する多くの問題に対策を講じています。

さて、HttpSession に保管された状態を考察してみましょう。 注意深いプログラムによりセッションデータのサイズをコントロールできるかもしれません。 これは想像するよりずっと困難です。 なぜならば、WEB ブラウザはアドホックで非線形な画面操作を許しているからです。 しかし、もしシステムの開発途中に、 ユーザに複数の並列の対話 (multiple concurrent conversations) を許可するというシステム要件を、 突然見つけたとします (私にはありますけど) 。 セッションステートを関連する別の並行する対話から分離するための機構を開発することと ユーザがブラウザのウィンドウやタブを閉じることで対話の一つを異常終了させた場合に、 その対話状態を破棄することを保証するフェールセーフ機構を組み込むことは、 気が弱い者の仕事ではありません。 (私は 2 度ほど実装したことがあります。 1 つはクライアントアプリケーション、1 つはSeamです。 だけど、私は病的なことで有名です。)

もっとよい方法があります。

Seam はファーストクラスの構造として 対話コンテキスト (conversation context) を導入しています。 コンテキスト中に安全に対話状態を維持すること、 そして十分に定義されたライフサイクルで保証されることが可能です さらに、アプリケーションサーバとデータベースの間でデータを連続的に行き来させる必要はありません。 なぜなら、対話コンテキストは、ユーザが現在作業しているデータの自然なキャッシュだからです。

通常、対話コンテキスト中で保持するコンポーネントはステートフルセッション Bean です。 (対話コンテキスト中でエンティティ Bean や JavaBean も保持できます。) Java コミュニティでは、ステートフルセッション Bean は、 スケーラビリティの殺し屋という古くからの作り話があります。 1988年、WebFoobar 1.0 がリリースされたときに、これは真実だったかもしれません。 こんにち、もはや真実ではありません。 JBoss 4.0 のようなアプリケーションサーバは、 ステートフルセッション Bean の レプリケーションのためのとても優れたメカニズムを持っています。 (例えば、JBoss EJB3 コンテナはきめ細かくレプリケーションを行い、 実際に属性値が変化した Bean のみレプリケーションを行います。) なぜステートフル Bean が非効率的かという、 すべての古典的な技術の議論は HttpSession にも等しく当てはまります。 従って、パフォーマンスを改善するために、 ビジネス層 (Tier) のステートフルセッション Bean から、 WEB セッションに変更する慣習は、 信じられないほど誤って指導されていますことに注意してください。 ステートフル Bean の誤った使用、あるいは不適切なものに使用することにより、 ステートフルセッション Bean を使用したスケーラブルでないアプリケーションを書く可能性も確かにあります。 しかし、これはそれらの使用を禁止していることを意味しません。 とにかく、Seam は安全な使用モデルに向かって誘導します。 Welcome to 2005.

それでは、くどくど言うのは止めてチュートリアルに戻りましょう。

対話と関係する永続データを自然にキャッシュするために、 どのように予約サンプルのアプリケーションが対話スコープのステートフル Bean を利用しているか見てみましょう。   以下のコードサンプルはちょっと長いです。 しかし、対話のステップを実装したアクションのリストであると考えると、 理解しやすくなります。 クラスを物語のように徹底的に読みましょう。

例 1.21.

@Stateful                                                                                (1)
@Name("hotelBooking")
@Conversational(ifNotBegunOutcome="main")
@LoggedIn                                                                                (2)
public class HotelBookingAction implements HotelBooking, Serializable                    (3)
{
   private static final Logger log = Logger.getLogger(HotelBooking.class);

   @PersistenceContext(type=EXTENDED)
   private EntityManager bookingDatabase;                                                (4)

   private String searchString;

   @DataModel
   private List<Hotel> hotels;                                                           (5)
   @DataModelSelectionIndex
   private int hotelIndex;                                                               (6)

   @Out(required=false)
   private Hotel hotel;                                                                  (7)

   @In(required=false)
   @Out(required=false)
   @Valid
   private Booking booking;

   @In
   private User user;

   @In
   private transient FacesContext facesContext;

   @Begin
   public String find()                                                                  (8)
   {
      hotel = null;
      String searchPattern = searchString==null ? "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
      String searchQuery = "from Hotel where lower(city) like :search" +
                           " or lower(zip) like :search" +
                           " or lower(address) like :search";
      hotels = bookingDatabase.createQuery(searchQuery)
            .setParameter("search", searchPattern)
            .setMaxResults(50)
            .getResultList();

      log.info(hotels.size() + " hotels found");

      return "main";
   }

   public String getSearchString()
   {
      return searchString;
   }

   public void setSearchString(String searchString)
   {
      this.searchString = searchString;
   }

   public String selectHotel()
   {
      if ( hotels==null ) return "main";
      setHotel();
      return "selected";
   }

   public String nextHotel()
   {
      if ( hotelIndex<hotels.size()-1 )
      {
         ++hotelIndex;
         setHotel();
      }
      return null;
   }

   public String lastHotel()
   {
      if (hotelIndex>0)
      {
         --hotelIndex;
         setHotel();
      }
      return null;
   }

   private void setHotel()
   {
      hotel = hotels.get(hotelIndex);
      log.info( hotelIndex + "=>" + hotel );
   }

   public String bookHotel()
   {
      if (hotel==null) return "main";
      booking = new Booking(hotel, user);
      booking.setCheckinDate( new Date() );
      booking.setCheckoutDate( new Date() );
      return "book";
   }

   @IfInvalid(outcome=REDISPLAY)
   public String setBookingDetails()
   {
      if (booking==null || hotel==null) return "main";
      if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
      {
         log.info("invalid booking dates");
         FacesMessage facesMessage =
           new FacesMessage("Check out date must be later than check in date");
         facesContext.addMessage(null, facesMessage);
         return null;
      }
      else
      {
         log.info("valid booking");
         return "success";
      }                                                                                  (9)
   }

   @End
   public String confirm()
   {
      if (booking==null || hotel==null) return "main";
      bookingDatabase.persist(booking);
      log.info("booking confirmed");
      return "confirmed";                                                                (10)
   }

   @Destroy @Remove
   public void destroy() {
      log.info("destroyed");
   }

}
(1)

EJB 標準 @Stateful アノテーション は、 このクラスをステートフルセッション Bean に指定しています。 ステートフルセッション Bean はデフォルトで対話コンテキストのスコープを持ちます。

(2)

Seam @Conversational は、 対話コンポーネントであることを宣言しています。 これは@Begin メソッドを呼ぶことで開始され、 長期の対話の外では呼び出すことはできません。 もし、そのような呼び出しが起これば、 Seam は JSFに ifNotBegunOutcome を返します。

(3)

@LoggedIn アノテーションはカスタム Seam インターセプタをコンポーネントに適用します。 @LoggedIn アノテーションに、 @Interceptor メタアノテーションがマークされていることによって、これは動作します。

(4)

この Bean は EJB3 の拡張永続コンテキストを利用し、 その結果、エンティティインスタンスはステートフルセッション Bean のライフサイクル全体で管理され続けます。

(5)

@DataModel アノテーションは、 JSF ListDataModel として List に公開します。 これは検索画面のためにクリック可能なリストを実装することを容易にします。 このサンプルでは、 ホテルのリストが hotels の名前の対話変数中に ListDataModel としてページに公開します。

(6)

@DataModelSelectionIndex アノテーションは、 @DataModel プロパティの行インデックスを保持するためのフィールドもしくは get/set のペアを定義します。

(7)

@Out アノテーションは、 メソッド呼び出しの後に、属性の値がコンテキスト変数にアウトジェクトされることを宣言します。 このサンプルでは、すべてのアクションリスナの呼び出しが完了した後に、 hotel の名前のコンテキスト変数は hotel インスタンス変数の値に設定されます。

(8)

@Begin アノテーションは、 アノテーション付きメソッドが長期対話を開始することを定義しています。 従って、リクエストの終了では現在の対話コンテキストは破棄されません。 その代わりに、 現在のウインドからのすべてのリクエストに再び関連し、 対話の非活動によるタイムアウトあるいは一致する @End メソッドの呼び出しにより破棄されます。

(9)

@End アノテーションは、 アノテーション付きメソッドが現在の長期対話を終了することを定義しています。 従って、リクエストの終わりで現在の対話コンテキストは破棄されます。

(10)

EJB標準の @Removeアノテーションは、 アノテーション付きメソッドが呼ばれた後に、 ステートフルセッション Bean が取り除かれ、 状態が破棄されることを定義します。 Seam では、 すべてのステートフルセッション Bean は @Destroy @Remove とマークされたメソッドが定義される必要があります。 これは EJB の remove メソッドで、Seam が対話コンテキストを破棄するときに呼ばれます。 実際、@Destroy は、 一般的に有用です、 なぜなら Seam コンテキストが終了したとき、 発生するさまざまな種類のクリーンアップに使用されるからです。 もし、@Destroy @Remove メソッドを使わなければ、 状態はリークしてパフォーマンスの問題に苦しむでしょう。

HotelBookingAction は、すべてのアクションリスナを持っており、 ホテル検索、選択、予約、予約確認が実装され、この操作に関連する状態をインスタンス変数中に保持しています。 このコードが、 HttpSession の属性から get/set するものと比較して、 よりクリーンで簡単なコードであることに同意してもらえると考えます。

さらに良くするために、ユーザはログインセッション毎に複数の分離された対話を持つことができます。 試してみてください。 ログインして、複数のブラウザのタブからホテルの検索ページを開いてください。 生成した 2 つの異なるホテル予約から同時に作業することができます。 対話を長時間、非活動にすると、Seam は最終的に対話をタイムアウトし、状態を破棄します。

1.6.4. Seam デバッグページ

examples/booking/view ディレクトリに、 debug.xhtml と呼ばれる facelets ページがあります。 このページで現在のログインセッションに関する Seam コンテキスト中の Seam コンポーネントを見たり検査したりすることができます。 ブラウザに、http://localhost:8080/seam-booking/debug.seam. と入力してください。

このページをあなたのアプリケーションに含めることができます。

1.6.5. Tomcat デプロイ

このサンプルは、 tomcat.home に Tomcat のインストールディレクトリの設定し、 examples/booking ディレクトリから ant deploy.tomcat とタイプすることで、 Tomcat にデプロイされます。 それでは、Tomcat を起動してブラウザから http://localhost:8080/jboss-seam-booking/ と入力しましょう。

Tomcat にアプリケーションをデプロイするとき、 EJB 3.0 コンポーネントは JBoss の 組み込み可能 EJB3 コンテナ (完全なスタンドアローン EJB3 コンテナ環境) の内部で動作します。

1.7. Seam と jBPM を使った本格的アプリケーション: DVD ストアサンプル

DVD ストアのデモアプリケーションは、 タスク管理とページフローのための jBPM の実践的な使用法を見せてくれます。

ユーザ画面は検索やショッピングカート機能の実装のために jPDL ページフローを利用しています。

管理画面はオーダの承認やショッピングサイクルを管理するために jBPM を利用します。 ビジネスプロセスは異なるプロセス定義を選択することにより動的に変更されるかもしれません。

TODO

dvdstore ディレクトリの中をご覧ください。

1.8. Seam ワークスペース管理を使った本格的アプリケーション: 問題追跡システムサンプル

問題追跡デモは Seam のワークスペース管理の機能をよく披露しています。 対話スイッチャ (conversation switcher)、対話リスト (conversation list)、ブレッドクラム (breadcrumbs)

TODO

issues ディレクトリの中をご覧ください。

1.9. Hibernate を使った Seam サンプル: Hibernate 予約システムサンプル

Hibernate 予約デモは、 永続性のために Hibernate、 セッション Bean の代わりに JavaBean を使用した簡単な予約デモの移植版です。

TODO

hibernate ディレクトリの中をご覧ください。