SeamFramework.org Community Documentation

- 前のページ
- 次のページ

## 第1章 Seam チュートリアル

## 1.1. Seam サンプルを使用する

Seam は Seam のさまざまな機能の利用方法を実演する多くのサンプルアプリケーションを提供しています。 このチュートリアルはこれらのサンプルを通してあなたが Seam を学び始めるための案内をします。Seam サンプルは Seam ディストリビューションの `examples` サブディレクトリに置かれています。 初めて見るユーザー登録サンプルは、`examples/registration` ディレクトリにあります。

各サンプルは同じディレクトリの構造をしています。

- The `view` ディレクトリにはWebページテンプレート、イメージ、スタイルシートなどビュー関連のファイルが入っています。

- `resources` ディレクトリにはデプロイメント記述子やその他の構成ファイルが入っています。

- `src` ディレクトリにはアプリケーションソースコードが入っています。

サンプルアプリケーションは追加設定することなく JBoss AS と Tomcat で動作します。 これからの章では両方のケースの手順を説明します。 Ant `build.xml` によりビルドと起動を行いますから、始める前に最新の Ant をインストールする必要があることに留意してください。

### 1.1.1. JBoss AS 上でのサンプルの実行

サンプルは JBoss AS 4.2 または 5.0用に構成されています。 Seam のインストレーションのルートフォルダ内 `build.properties` ファイル中の `jboss.home` に JBoss AS のインストレーションの場所を設定する必要があります。

一度設定を完了してアプリケーションサーバーを起動すれば、サンプル用のディレクトリの `ant deploy` をタイプすることでどのサンプルでもビルドとでデプロイが可能です。 サンプルはすべて `/seam-`example`` のような URL にデプロイされます。 ここで、一つの例外を除いては `example` はこのサンプルのフォルダ名になっています。サンプルフォルダがseamで始まるなら、プレフィックス "seam" は省略されます。例えば、JBoss ASがポート8080上で実行しているなら、ユーザー登録サンプルの URL は `http://localhost:8080/seam-registration/`になります。一方で、seamspace サンプルの URL は `http://localhost:8080/seam-space/` になります。

一方、もしそのサンプルが WAR にパッケージされるなら、それは `/jboss-seam-`example`` のような URL へデプロイされます。サンプルのほとんどは `ant tomcat.deploy` とタイプすることでEmbedded JBossを使って WAR として Tomcat へデプロイできます。いくつかのサンプルは WAR だけでデプロイ可能です。そのようなサンプルは groovybooking、hibernate、jpa、そして spring です。

### 1.1.2. Tomcat 上でのサンプル実行

サンプルは Tomcat 6.0 用にも構成されています。Tomcat 6.0 組み込み JBossへのインストールは 項30.6.1. 「Embedded JBoss をインストールする」 のインストラクションに従う必要があります。 組み込み JBoss は Tomcat 上で EJB3コンポーネントを利用する Seam デモを動作させるためだけに必要です。 組み込み JBoss を利用しない Tomcat 上で動作可能な non-EJB3 サンプルもあります。

Seam のインストレーションのルートフォルダ内 `build.properties` ファイル中の `tomcat.home` に Tomcat のインストレーションの場所を設定する必要があります。 Tomcat の場所を設定したことを確かめてください。

Tomcat を利用する場合には異なった Ant ターゲットを使用する必要があります。 Tomcat 用のサンプルのビルドとデプロイで example サブディレクトリ内の `ant tomcat.deploy` を使用してください。

Tomcatの場合、サンプルは `/jboss-seam-`example`` のような URL にデプロイされます。 ユーザー登録サンプルの URL は `http://localhost:8080/jboss-seam-registration/` のようになります。前のセクションで述べたように、WARとしてデプロイされる他のサンプルでも同様です。

### 1.1.3. サンプルのテスト起動

ほとんどのサンプルは TestNG 統合テストスイートに対応しています。 一番簡単なテスト実行は、 `ant test`起動させることです。 また、お使いの IDE から TestNG プラグインを利用してテスト実行することも可能です。詳細情報は Seam ディストリビューションの examples ディレクトリ内の readme.txt をご覧下さい。

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

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

EJB 3.0 にまだ不慣れな方もいらっしゃるかもしれませんので、 ゆっくり進めていきます。

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

### 1.2.1. コードの理解

このサンプルは、二つの Facelets テンプレート、一つのエンティティ Bean と、一つのステートレスセッション Bean で実装されています。 "bottom" からコードを見てみましょう。

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

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

例 1.1. User.java

    @Entity

    @Name("user")

    @Scope(SESSION)

    @Table(name="users")

    public class User implements Serializable

    {

       private static final long serialVersionUID = 1881413500711441951L;

       private String username;

       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() {}

       @NotNull @Length(min=5, max=15)

       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)

       public String getUsername()

       {

          return username;

       }

       public void setUsername(String username)

       {

          this.username = username;

       }

    }

| EJB3 標準 `@Entity` アノテーションは、 `User` クラスがエンティティ Bean であることを示しています。
| Seam コンポーネントは `@Name` アノテーションで指定される コンポーネント名 が必要です。 この名前は Seam アプリケーション中でユニークである必要があります。 JSF が Seam に Seam コンポーネント名と同じコンテキスト変数の解決を求める、コンテキスト変数がそのとき未定義 (null) の場合、 Seam はそのコンポーネントをインスタンス化し、その新しいインスタンスをコンテキスト変数にバインドします。 このサンプルでは、 JSF が初めて `user` という変数と出会うときに Seam は `User` をインスタンス化します。
| Seam がコンポーネントをインスタンス化する時には、 必ずコンポーネントの デフォルトコンテキストにあるコンテキスト変数に新しいインスタンスをバインドします。 デフォルトコンテキストは `@Scope` アノテーションを使用して定義されます。 `User` はセッションスコープのコンポーネントです。
| EJB 標準 `@Table` アノテーションは、 `User` クラスが `users` テーブルにマッピングされることを示しています。
| `name`、`password`、 `username` は、 エンティティ Bean の永続属性です。 すべての永続属性はアクセスメソッドを定義しています。 応答のレンダリングフェーズおよびモデル値の更新フェーズで JSF によりこのコンポーネントが使用されるときに必要となります。
| 空コンストラクタは、EJB と Seam の両方の仕様から必要となります。
| `@NotNull` と `@Length` アノテーションは、 Hibernate Validator フレームワークの一部です。 Seam は Hibernate Validator を統合しているため、 データの妥当性検証にこれを使用することができます (永続性に Hiberenate を使用していない場合でも使用できる)。
| EJB 標準 `@Id` アノテーションは、 エンティティ Bean の主キーであることを示しています。

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

以下では、`User` クラスのプロパティは 直接 JSF コンポーネントにバインドされ、 モデル値の変更フェーズで JSF によって生成されたことがわかります。 JSP ページとエンティティ Bean ドメインモデル間を行き来するデータコピーの面倒なコードは必要ありません。

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

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

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

アプリケーション内の JSF アクションはちょうど一つだけで、 これにセッションBean メソッドが 一つリンクしています。 このサンプルでは、 アクションに関連する状態はすべて `User` Bean によって保持されるため、 ステートレスセッション Bean を使用しています。

サンプルの中で、本当に注意すべきコードは以下のみです。

例 1.2. RegisterAction.java

    @Stateless
    @Name("register")
    public class RegisterAction implements Register
    {
       @In
       private User user;

       @PersistenceContext
       private EntityManager em;

       @Logger
       private Log log;

       public String register()
       {
          List existing = em.createQuery(
             "select username from User where username = #{user.username}")
             .getResultList();

          if (existing.size()==0)
          {
             em.persist(user);
             log.info("Registered new user #{user.username}");
             return "/registered.xhtml";
          }
          else
          {
             FacesMessages.instance().add("User #{user.username} already exists");
             return null;
          }
       }

    }

| EJB `@Stateless` アノテーションはこのクラスがステートレスセッション Bean であることを示しています。
| `@In` アノテーションは、 Seam によってインジェクトされる Bean の属性としてマークしています。 ここで、この属性は、`user` (インスタンス変数名) という名前のコンテキスト変数からインジェクトされます。
| EJB 標準 `@PersistenceContext` アノテーションは、 EJB3 エンティティ Entity Manager にインジェクトするために使用されます。
| Seam `@Logger` アノテーションはコンポーネントの `Log` インスタンスをインジェクトするために使用されています。
| アクションリスナーメソッドは、データベースとやり取りするために、 標準 EJB3 `EntityManager` API を使用し、JSF 結果 (outcome) を返します。 これはセッション Bean なので、 `register()` メソッドが呼ばれたときに、 トランザクションは自動的に開始され、終了したときにコミットされることに留意してください。
| Seam では EJB-QL 内で JSF EL 式を使用することができます。 バックグラウンドで行われるため見えませんが、 これにより普通の JPA `setParameter()` が標準 JPA `Query` オブジェクトを呼び出すことになります。 便利でしょう?
| `Log` API は、テンプレート化されたログメッセージを容易に表示可能です。ログメッセージは JSF EL 式も利用可能です。
| JSF アクションリスナーメソッドは、次にどのページを表示するかを決定するストリング値の結果 (outcome) を返します。 null 結果 (outcome) (あるいは、void アクションリスナーメソッド) は、 前のページを再表示します。 普通の JSF では、 結果 (outcome) から JSF ビュー id を決定するために、 常に JSF ナビゲーション規則 を使用することが普通です。 複雑なアプリケーションにとって、この間接的方法は、実用的な良い慣行です。 しかし、このようなとても簡単なサンプルのために、 Seam は、結果 (outcome) として JSF ビュー id の使用を可能とし、 ナビゲーション規則の必要性を取り除きました。 結果 (outcome) としてビュー id を使用する場合、 Seam は、常にブラウザリダイレクトを行うことに留意してください。
| Seam は、共通な問題の解決を支援するために多くの 組み込みコンポーネントを提供しています。 `FacesMessages` コンポーネントは、 テンプレート化されたエラーや成功メッセージを容易に表示可能です (Seam 2.1から、JSF へのセマンティック上の依存を取り除くために代わりに `StatusMessages` を利用可能です)。 組み込み Seam コンポーネントは、 インジェクションによって、あるいは組み込みコンポーネントのクラス上の`instance()` メソッド呼び出しによって取得可能です。

ここで、`@Scope` を明示的に指定していないことに留意してください。 各 Seam コンポーネントタイプは明示的にスコープが指定されない場合デフォルトのスコープが適用されます。 ステートレスセッション Bean のデフォルトスコープはステートレスコンテキストです。 これは単に理にかなった値です。

このセッション Bean のアクションリスナーは、この小さなアプリケーションのビジネスロジックと永続ロジックを提供しています。 さらに複雑なアプリケーションでは、個別のサービスレイヤが必要かもしれません。 Seamで、これをするのは簡単ですが、 ほとんどの Web アプリケーションでは過剰です。 Seam は、アプリケーションのレイヤ化のために特殊な方法を強要しているのではなく、アプリケーションをより簡単に、また望むならばより複雑にすることを可能にしています。

このアプリケーションについて、私たちはこれまで実際に必要とされるよりはるかに複雑にしてきました。 Seam アプリケーションコントローラを利用していたならば、アプリケーションコードのほとんどを排除できたかもしれない。しかしながら、 当時、説明する多くのアプリケーションがありませんでした。

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

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

例 1.3. Register.java

    @Local

    public interface Register

    {

       public String register();

    }

Java コードは以上です。 次は ビューを見ましょう。

#### 1.2.1.4. ビュー : `register.xhtml` と `registered.xhtml`

Seam アプリケーションのビューページは、 JSF をサポートする多くの技術を使用して実装されています。 このサンプルでは、JSP より優れていると考えている Facelets を使用しています。

例 1.4. register.xhtml

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

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml"

        xmlns:s="http://jboss.com/products/seam/taglib"

        xmlns:h="http://java.sun.com/jsf/html"

        xmlns:f="http://java.sun.com/jsf/core">

       <head>

          <title

    >Register New User</title>

       </head>

       <body>

          <f:view>

             <h:form>

                <s:validateAll>

                   <h:panelGrid columns="2">

                      Username: <h:inputText value="#{user.username}" required="true"/>

                      Real Name: <h:inputText value="#{user.name}" required="true"/>

                      Password: <h:inputSecret value="#{user.password}" required="true"/>

                   </h:panelGrid>

                </s:validateAll>

                <h:messages/>

                <h:commandButton value="Register" action="#{register.register}"/>

             </h:form>

          </f:view>

       </body>

    </html

    >

ここで Seam 固有となるのは `<s:validateAll>` タグのみです。 この JSF コンポーネントは 含まれるすべての入力フィールドをエンティティ Bean で指定される Hibernate Validator アノテーションに対して検証するよう JSF に指示しています。

例 1.5. registered.xhtml

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

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"

        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <html xmlns="http://www.w3.org/1999/xhtml"

        xmlns:f="http://java.sun.com/jsf/core">

        <head>

            <title

    >Successfully Registered New User</title>

        </head>

        <body>

            <f:view>

                Welcome, #{user.name}, you are successfully registered as #{user.username}.

            </f:view>

        </body>

    </html>

これは、組み込み EL 式を使用した簡単な Facelets ページです。 Seam 固有のものはありません。

#### 1.2.1.5. Seam コンポーネントデプロイメント記述子 : `components.xml`

はじめて Seam アプリを見るので、デプロイメント記述子も見てみます。 その話の前に、Seam が最小限の設定であることは注目に値します。 これらの設定ファイルは、Seamアプリケーションが作成されるときに生成されるものです。 あなたはほとんどこれらのファイルに触れる必要はないでしょう。 サンプルにおいてすべての要素が何をしているかを理解すること助けるためだけにこれらを提示しています。

既に多くの Java フレームワークを使用した経験がある方なら、 プロジェクトが成熟するにつれ徐々に大きくなり管理し難くなる XML ファイルにコンポーネントクラスをすべてを宣言することにもそのうち慣れていくことでしょう。 Seam ではアプリケーションコンポーネントに XML を付随する必要がないこと知ったら、 きっとほっとすることでしょう。 大部分の Seam アプリケーションは、ほんの少しの XML しか必要としません。 また、 この XMLはプロジェクトが大きくなっていっても、 あまり大きくなりません。

それにもかかわらず、あるコンポーネントのある外部設定の規定が可能であることは、 多くの場合、有用です (特に、Seam に組み込まれたコンポーネント)。 ここでは二つの選択があります。 しかし、最も柔軟性のある選択は `WEB-INF` ディレクトリに位置する `components.xml` と呼ばれるファイルに設定を規定することです。 Seam に JNDI で EJB コンポーネントの見つけ方を指示するためには `components.xml` ファイルを使用します。

例 1.6. components.xml

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

    <components xmlns="http://jboss.com/products/seam/components"

        xmlns:core="http://jboss.com/products/seam/core"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="

            http://jboss.com/products/seam/core

            http://jboss.com/products/seam/core-2.2.xsd

            http://jboss.com/products/seam/components

            http://jboss.com/products/seam/components-2.2.xsd">

        <core:init jndi-pattern="@jndiPattern@"/>

    </components

    >

このコードは、`org.jboss.seam.core.init` という名前の Seam コンポーネントの `jndiPattern` という名前のプロパティを設定します。 Ant build スクリプトがアプリケーションをデプロイするときに正しく JNDI pattern を配置するために、奇妙な `@` シンボルがあります。Ant スクリプトはそれをcomponents.properties ファイルから読み取ります。このプロセスの動作の仕組みについては 項5.2. 「`components.xml`によるコンポーネントの構成」 で学びましょう。

#### 1.2.1.6. WEB デプロイメント記述子 : `web.xml`

ミニアプリケーションのプレゼンテーションレイヤは WAR にデプロイされます。 したがって、Web デプロイメント記述子が必要です。

例 1.7. web.xml

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

    <web-app xmlns="http://java.sun.com/xml/ns/javaee"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="

            http://java.sun.com/xml/ns/javaee

            http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

        version="2.5">

        <listener>

            <listener-class

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

        </listener>

        <context-param>

            <param-name

    >javax.faces.DEFAULT_SUFFIX</param-name>

            <param-value

    >.xhtml</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>

        <servlet-mapping>

            <servlet-name

    >Faces Servlet</servlet-name>

            <url-pattern

    >*.seam</url-pattern>

        </servlet-mapping>

        <session-config>

            <session-timeout

    >10</session-timeout>

        </session-config>

    </web-app

    >

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

#### 1.2.1.7. JSF 設定 : `faces-config.xml`

ほとんどの Seam アプリケーションはプレゼンテーション層として JSF ビューを使用します。 従って通常 `faces-config.xml` が必要です。 この場合ビュー定義に Facelets を使用しますので、JSF にテンプレートエンジンとして Faceles を使用することを指定する必要があります。

例 1.8. faces-config.xml

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

    <faces-config xmlns="http://java.sun.com/xml/ns/javaee"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="

            http://java.sun.com/xml/ns/javaee

            http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"

        version="1.2">

        <application>

            <view-handler

    >com.sun.facelets.FaceletViewHandler</view-handler>

        </application>

    </faces-config

    >

JSF 管理 Bean 宣言が不要であることに留意してください。 マネージド Bean は Seam コンポーネントとしてアノテーションが付けられています。 Seam アプリケーションでは、`faces-config.xml` は普通の JSF と比べてあまり使用されません。ここで、JSP の代わりに view handler として Facelets を有効にするため単純にそれを使います。

実際、基本的な記述子の設定だけ行えば、Seam アプリケーションとして新たに機能追加として記述する必要のあるXMLは、オーケストレーション だけ です(つまり、ナビゲーションルールまたはjBPMビジネス定義)。Seam では プロセスフロー と 設定データ だけが XML として存在すべきものであるという見方をします。

この簡単なサンプルでは、 ビュー id をアクションコードに埋め込んだため、 ナビゲーション規則さえ不要です。

#### 1.2.1.8. EJB デプロイメント記述子 : `ejb-jar.xml`

`ejb-jar.xml` ファイルは、 アーカイブ中のすべてのセッション Bean に `SeamInterceptor` を付加することによって EJB3 と統合します。

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

    <ejb-jar xmlns="http://java.sun.com/xml/ns/javaee"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="

            http://java.sun.com/xml/ns/javaee

            http://java.sun.com/xml/ns/javaee/ejb-jar_3_0.xsd"

        version="3.0">

        <interceptors>

            <interceptor>

                <interceptor-class

    >org.jboss.seam.ejb.SeamInterceptor</interceptor-class>

            </interceptor>

        </interceptors>

        <assembly-descriptor>

            <interceptor-binding>

                <ejb-name

    >*</ejb-name>

                <interceptor-class

    >org.jboss.seam.ejb.SeamInterceptor</interceptor-class>

            </interceptor-binding>

        </assembly-descriptor>

    </ejb-jar

    >

#### 1.2.1.9. EJB 永続デプロイメント記述子 : `persistence.xml`

`persistence.xml` ファイルは、EJB 永続プロバイダに、 データソースの場所を指示します。また、ベンダー固有の設定を含んでいます。 このサンプルでは起動時に自動スキーマエキスポートを可能としています。

    <?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="userDatabase">

            <provider

    >org.hibernate.ejb.HibernatePersistence</provider>

            <jta-data-source

    >java:/DefaultDS</jta-data-source>

            <properties>

                <property name="hibernate.hbm2ddl.auto" value="create-drop"/>

            </properties>

        </persistence-unit>

    </persistence

    >

#### 1.2.1.10. EAR デプロイメント記述子 : `application.xml`

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

例 1.9. ユーザー登録アプリケーション

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

    <application xmlns="http://java.sun.com/xml/ns/javaee"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="

            http://java.sun.com/xml/ns/javaee

            http://java.sun.com/xml/ns/javaee/application_5.xsd"

        version="5">

        <display-name

    >Seam Registration</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>

        <module>

            <ejb

    >jboss-seam.jar</ejb>

        </module>

        <module>

            <java

    >jboss-el.jar</java>

        </module>

    </application

    >

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

これで私たちはアプリケーションにあるすべてのファイルを見ました！

### 1.2.2. 動作内容

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

フォームの入力値は、 `User` エンティティで指定された Hibernate Validator 制約に対してデータ整合性検証が行われるようになります。 制約に違反していると JSF はそのページを再表示します。 これ以外は、 フォームの入力値を `User` エンティティ Bean のプロパティにバインドします。

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

しかし、Seam はまだやり終えていません。Seam は、メソッド呼び出しをインタセプトし、呼び出しが進む前に、 Seam セッションコンテキストから `User` エンティティをインジェクトします。

`register()` メソッドは入力されたユーザー名が既に存在するかどうかを調べます。 存在した場合、 エラーメッセージは `FacesMessages` コンポーネントでキューイングされ、 null 結果 (outcome) が返されてページが再表示されることになります。 `FacesMessages` コンポーネントはメッセージ文字列に組み込まれた JSF 式を補完し、 ビュー に JSF `FacesMessage` を追加します。

そのユーザー名のユーザーが存在しない場合、`"/registered.xhtml"`" 結果 (outcome) により `registered.xhtml` ページへのブラウザリダイレクトが発生します。 JSF がページのレンダリングに到達すると、 Seam に `user` という名前の変数の解決を要求し、 Seam のセッションスコープから返される `User` エンティティのプロパティ値を使用します。

## 1.3. Seam でクリックが可能な一覧: 掲示板サンプル

データベースの検索結果をクリック可能一覧とすることは、 多くのオンラインアプリケーションにおいてたいへん重要な部分です。Seam は、EJB-QL またはHQL を使用してデータの問合せを行うことと、 その結果をJSF `<h:dataTable>` を使用してクリック可能な一覧として表示することを容易にするために、 JSF 上に特別な機能を提供します。 この掲示板サンプルは、この機能を実演しています。

### 1.3.1. コードの理解

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

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

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

例 1.10. Message.java

    @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 : `MessageManagerBean.java`

前述のサンプル同様、 一つのセッション Bean `MessageManagerBean` があります。 それは、フォームにある二つのボタンに対応するアクションリスナーメソッドを定義しています。 ボタンの一つは、一覧からメッセージを選択し、 もう一つのボタンは、メッセージを削除します。 この点において、前述のサンプルと大きな違いはありません。

しかし、 初めて掲示板ページに画面遷移するとき、 `MessageManagerBean` は、メッセージの一覧取得も行います。 ユーザーが画面を遷移させる方法はさまざまありますが、 これらのすべてが JSF アクションによって進められるわけではありません。 例えば、 ユーザーがそのページをブックマークしているかもしれません。 そこでメッセージの一覧を取得する作業は、 アクションリスナーメソッドの代わりに、Seam ファクトリメソッド で行われます。

メッセージの一覧をサーバ要求にまたがってメモリにキャッシュしたいので、 ステートフルセッション Bean でこれを行います。

例 1.11. MessageManagerBean.java

    @Stateful
    @Scope(SESSION)
    @Name("messageManager")
    public class MessageManagerBean implements Serializable, MessageManager
    {
       @DataModel
       private List<Message
    > messageList;

       @DataModelSelection
       @Out(required=false)
       private Message message;

       @PersistenceContext(type=EXTENDED)
       private EntityManager em;

       @Factory("messageList")
       public void findMessages()
       {
          messageList = em.createQuery("select msg from Message msg order by msg.datetime desc")
                          .getResultList();
       }

       public void select()
       {
          message.setRead(true);
       }

       public void delete()
       {
          messageList.remove(message);
          em.remove(message);
          message=null;
       }

       @Remove
       public void destroy() {}

    }

| `@DataModel` アノテーションは、 `java.util.List` タイプの属性を、 `javax.faces.model.DataModel` インスタンスとして JSF ページに公開します。 これは、各行に対してクリック可能なリンクを持つ JSF `<h:dataTable>` 中の一覧を使用可能とします。 このサンプルでは、 `DataModel` は、 `messageList` という名前のセッションコンテキスト中で利用可能になります。
| `@DataModelSelection` アノテーションは、 Seam にクリックされたリンクと関連した `List` 要素をインジェクトするよう指示しています。
| `@Out`アノテーションは、次に選択された値を直接ページに公開します。 従って、クリック可能一覧の行が選択されるたびに、 `Message` は、ステートフル Bean の属性にインジェクト (inject) され、 続いて `message` という名前のイベントコンテキスト変数に アウトジェクト (outject) されます。
| このステートフル Bean は、EJB3 拡張永続コンテキスト を持っています。 この Bean が存在する限り、 クエリー検索された messages は、管理された状態に保持されます。 従って、 それに続くステートフル Bean へのメソッド呼び出しは、 明示的に `EntityManager` を呼び出すことなく、 それらの更新が可能です。
| 初めて JSP ページに画面遷移するとき、 `messageList` コンテキスト変数中に値を持っていません。 `@Factory` アノテーションは、Seam に `MessageManagerBean` インスタンスの生成を指示し、 初期値を設定するために `findMessages()` メソッドを呼び出します。 `findMessages()` を `messages` の ファクトリメソッドと呼びます。
| `select()` アクションリスナーメソッドは、 選択された `Message`に 既読 マークを付け、 データベース中のそれを更新します。
| `delete()` アクションリスナーメソッドは、 選択された `Message` をデータベースから削除します。
| すべてのステートフルセッション Bean の Seam コンポーネントは、`@Remove` とマークされたパラメータを持たないメソッドを持つことが必須です。 これによりSeam コンテキストが終わり、サーバサイドの状態をクリーンアップするときに、 Seam はステートフル Bean の削除を行います。

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

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

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

例 1.12. MessageManager.java

    @Local

    public interface MessageManager

    {

       public void findMessages();

       public void select();

       public void delete();

       public void destroy();

    }

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

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

#### 1.3.1.4. ビュー: `messages.jsp`

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

例 1.13. messages.jsp

    <%@ 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="#{messageList.rowCount==0}"/>

         <h:dataTable var="msg" value="#{messageList}"

                      rendered="#{messageList.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="#{messageManager.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="#{messageManager.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` ページに画面遷移させるとき、ページは `messageList` コンテキスト変数を解決することを試みます。 このコンテキスト変数は初期化されていないため、 Seam はファクトリメソッド `findMessages()`を呼び出します。 それはデータベースにクエリー発行や、 アウトジェクト (outject) された `DataModel` の結果取得を行います。 この `DataModel` は `<h:dataTable>` をレンダリングするために必要な行データを提供します。

ユーザーが `<h:commandLink>` をクリックすると、 JSF は `select()` アクションリスナーを呼び出します。 Seam はこの呼び出しをインタセプトして選択された行データを `messageManager` コンポーネントの `message` 属性にインジェクトします。 アクションリスナーが実行されて、 選択 `Message` に既読マークを付けます。 呼び出しの終わりに、 Seam は、選択 `Message` を `message` という名前のコンテキスト変数にアウトジェクトします。 次に、 EJB コンテナはトランザクションをコミットし、 `Message` に対する変更がデータベースにフラッシュされます。 最後に、 このページが再度レンダリングされてメッセージ一覧を再表示、 その下に選択メッセージが表示されます。

ユーザーが `<h:commandButton>` をクリックすると、 JSF は、`delete()` アクションリスナーを呼び出します。 Seam はこの呼び出しをインタセプトし、 選択された行データを `messageList` コンポーネントの `message` 属性にインジェクトします。 アクションリスナーが起動し、 選択 `Message` が一覧から削除され、 `EntityManager` の `remove()` が呼び出されます。 呼び出しの終わりに、 Seam は `messageList` コンテキスト変数を更新し、 `message` という名前のコンテキスト変数を消去します。 EJB コ ンテナはトランザクションをコミットし、 データベースから `Message` を削除します。 最後に、 このページが再度レンダリングされ、 メッセージ一覧を再表示します。

## 1.4. Seam と jBPM : TO-DO 一覧サンプル

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

### 1.4.1. コードの理解

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

例 1.14. todo.jpdl.xml

    <process-definition name="todo">

       <start-state name="start">
          <transition to="todo"/>
       </start-state>

       <task-node name="todo">
          <task name="todo" description="#{todoList.description}">
             <assignment actor-id="#{actor.id}"/>
          </task>
          <transition to="done"/>
       </task-node>

       <end-state name="done"/>

    </process-definition
    >

| `<start-state>` ノードはプロセスの論理的な開始を表します。 プロセスが開始したとき、 それはすぐに `todo` に遷移します。
| `<task-node>` ノードは、待ち状態 を表します。 ビジネスプロセスの実行が一時停止され、一つまたは複数のタスクが行われるのを待機します。
| `<task>` 要素は、ユーザーによって実行されるタスクを定義します。 このノードには一つのタスクしか定義されていないので、 それが完了すると実行が再開され、 終了状態に遷移していきます。 このタスクは、 `todoList` という名前の Seam コンポーネント (JavaBean の 一種) から description を取得します。
| タスクを生成するとき、タスクはユーザーあるいはユーザーグループに割り当てる必要があります。 このサンプルでは、タスクは、現在のユーザーに割り当てられています。 現在のユーザーは `actor` という名前の組み込み Seam コンポーネントから取得します。 どのような Seam コンポーネントもタスク割り当てを実行するために使用される可能性があります。
| `<end-state>`ノードは、ビジネスプロセスの論理的な終了を定義します。 実行がこのノードに到達したとき、 プロセスインスタンスは、破棄されます。

JBossIDE に提供されたプロセス定義エディタを使用してプロセス定義を見た場合、 以下のようになります。

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

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

例 1.15. Login.java

    @Name("login")

    public class Login

    {

       @In

       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 "/todo.jsp";

       }

    }

ここでは、組み込み `Actor` コンポーネントをインジェクトするために、 `@In` を使用しているのがわかります。

次の JSP 自体は重要ではありません。

例 1.16. login.jsp

    <%@ 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

    >

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

例 1.17. TodoList.java

    @Name("todoList")
    public class TodoList
    {
       private String description;

       public String getDescription()
       {
          return description;
       }

       public void setDescription(String description)
       {
          this.description = description;
       }

       @CreateProcess(definition="todo")
       public void createTodo() {}

       @StartTask @EndTask
       public void done() {}

    }

| description プロパティは、JSP ページからユーザー入力を受け取り、 タスク内容 (description) が設定されるように、それをプロセス定義に公開します。
| Seam `@CreateProcess` アノテーションは、名前付きプロセス定義のために jBPM プロセスインスタンスを生成します。
| Seam `@StartTask` アノテーションは、タスク上で作業を開始します。 `@EndTask` は、タスクを終了し、ビジネスプロセスの再開を可能にします。

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

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

例 1.18. todo.jsp

    <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>

    <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>

    <%@ taglib uri="http://jboss.com/products/seam/taglib" prefix="s" %>

    <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>

                    <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/>

                </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

    >

一つづつ見ていきましょう。

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

例 1.19. todo.jsp

    <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) 、 優先順 (Priority) や、 納期の値 (Due Date) については、 ユーザーがこれらの値を更新できるよう入力コントロールを使用します。

    <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

    >

## 注意

Seam は文字列を日付 (時刻ではありません) へ変換するためのデフォルト JSF date コンバータを提供します。したがって、そのコンバータは `#{task.dueDate}` にバインドしたフィールドのためには不要です。

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

    <h:column>

        <s:button value="Done" action="#{todoList.done}" taskInstance="#{task}"/>

    </h:column

    >

これは `seam-ui.jar` パッケージから Seam `<s:button>` JSF コントロールを使用していることに留意してください。 このボタンはタスクのプロパティを更新するために使われます。 フォームがサブミットされるとき、Seam と jBPM はタスク永続に変化も起こします。 アクションリスナーメソッドには何の必要もありません。

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

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

    <h:form id="new">

        <div>

            <h:inputText value="#{todoList.description}"/>

            <h:commandButton value="Create New Item" action="#{todoList.createTodo}"/>

        </div>

    </h:form

    >

### 1.4.2. 動作内容

ログイン後、todo.jsp は現在のユーザーのための未解決の To-Do 項目を表示するために `taskInstanceList` を使用します。最初は何もありません。新しいエントリを登録するフォームが表示されます。ユーザーが todo 項目をタイプし、"Create New Item" ボタンを押下するとき、`#{todoList.createTodo}` は呼ばれます。これは `todo.jpdl.xml` で定義したプロセスを開始します。

プロセスインスタンスが生成されると、start 状態が開始されすぐに `todo` 状態に遷移します。そこで新しいタスクが生成されます。 タスク記述はユーザーの入力にしたがって設定されます。それは `#{todoList.description}` に保存されます。 そして、タスクは現在のユーザーに割り当てられます。それは Seam の actor コンポーネントに保管されます。 このサンプルにおいて、プロセスは追加のプロセス状態を持っていないことに留意してください。 このサンプルにおけるすべての状態はタスク定義に保管されています。 プロセスとタスク情報は要求の最後でデータベースに保管されます。

`todo.jsp` が再表示されるとき、`taskInstanceList` はちょうど作成されたタスクを見つけます。 タスクは `h:dataTable` に表示されます。 タスクの内部状態は `#{task.description}`、 `#{task.priority}`、 `#{task.dueDate}` などのカラムに表示されます。 これらのフィールドはすべて編集やデータベースに保管可能です。

各To-Do項目は "Done" ボタンを持っていて、それは `#{todoList.done}` を呼び出します。 `todoList` コンポーネントは、各 s:button が `taskInstance="#{task}"` を指定しているために、どのタスクボタンがテーブルの特定行のためのタスクを参照するかを知っています。 `@StartTast` と `@EndTask` アノテーションは タスクをアクティブにさせるものと、すぐにタスクを完了させるものです。 プロセス定義にしたがってオリジナルのプロセスが `done` 状態に遷移します。 そこでそれは終了します。 タスクとプロセスの状態はともにデータベースにアップデートされます。

`todo.jsp` が再表示されるとき、いま完了したタスクはもう `taskInstanceList` には表示されません。 なぜならコンポーネントはユーザーにとってアクティブなタスクだけを表示するからです。

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

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

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

### 1.5.1. コードの理解

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

例 1.20. pageflow.jpdl.xml

    <pageflow-definition
            xmlns="http://jboss.com/products/seam/pageflow"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://jboss.com/products/seam/pageflow
                                http://jboss.com/products/seam/pageflow-2.2.xsd"
            name="numberGuess">

       <start-page name="displayGuess" view-id="/numberGuess.jspx">
          <redirect/>
          <transition name="guess" to="evaluateGuess">
             <action expression="#{numberGuess.guess}"/>
          </transition>
          <transition name="giveup" to="giveup"/>
          <transition name="cheat" to="cheat"/>
       </start-page>

       <decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
          <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="giveup" view-id="/giveup.jspx">
          <redirect/>
          <transition name="yes" to="lose"/>
          <transition name="no" to="displayGuess"/>
       </page>

       <process-state name="cheat">
          <sub-process name="cheat"/>
          <transition to="displayGuess"/>
       </process-state>

       <page name="win" view-id="/win.jspx">
          <redirect/>
          <end-conversation/>
       </page>

       <page name="lose" view-id="/lose.jspx">
          <redirect/>
          <end-conversation/>
       </page>

    </pageflow-definition
    >

| `<page>` 要素は、待ち状態を定義しています。 ここでは、システムは特定の JSF ビューを表示し、ユーザー入力を待っています。 `view-id` は普通の JSF ナビゲーション規則で使用されている JSF ビュー と同じものです。 ページが画面遷移するときに、 `redirect` 属性は、Seam に post-then-redirect の使用を指示しています。 (この結果がブラウザ URL に表示されます。)
| `<transition>` 要素は JSF 結果 (outcome) に名前を付けます。 JSF アクションがその結果 (outcome) となる場合に、 transition が起動されます。 jBPM 遷移アクションが呼び出された後、 実行はページフローグラフの次のノードに進みます。
| transition の `<action>` は、 jBPM の transitionでそれが起こることを除けば、 JSF アクションと同じです。 遷移アクションはどのような Seam コンポーネントでも呼び出すことが可能です。
| `<decision>` ノードはページフローを分岐させ、 JSF EL 式を評価することによって次に実行されるノードを決定します。

以下は JBoss Developer Studio ページフローエディタでどのように表示するかを示しています。

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

これがアプリケーションの中心のページ `numberGuess.jspx` です。

例 1.21. numberGuess.jspx

    <<?xml version="1.0"?>

    <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"

              xmlns:h="http://java.sun.com/jsf/html"

              xmlns:f="http://java.sun.com/jsf/core"

              xmlns:s="http://jboss.com/products/seam/taglib"

              xmlns="http://www.w3.org/1999/xhtml"

              version="2.0">

      <jsp:output doctype-root-element="html"

                  doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"

                  doctype-system="http://www.w3c.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>

      <jsp:directive.page contentType="text/html"/>

      <html>

      <head>

        <title

    >Guess a number...</title>

        <link href="niceforms.css" rel="stylesheet" type="text/css" />

        <script language="javascript" type="text/javascript" src="niceforms.js" />

      </head>

      <body>

        <h1

    >Guess a number...</h1>

        <f:view>

          <h:form styleClass="niceform">

            <div>

            <h:messages globalOnly="true"/>

            <h:outputText value="Higher!"

                   rendered="#{numberGuess.randomNumber gt numberGuess.currentGuess}"/>

            <h:outputText value="Lower!"

                   rendered="#{numberGuess.randomNumber lt numberGuess.currentGuess}"/>

            </div>

            <div>

            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.

            </div>

            <div>

            Your guess:

            <h:inputText value="#{numberGuess.currentGuess}" id="inputGuess"

                         required="true" size="3"

                         rendered="#{(numberGuess.biggest-numberGuess.smallest) gt 20}">

              <f:validateLongRange maximum="#{numberGuess.biggest}"

                                   minimum="#{numberGuess.smallest}"/>

            </h:inputText>

            <h:selectOneMenu value="#{numberGuess.currentGuess}"

                             id="selectGuessMenu" required="true"

                             rendered="#{(numberGuess.biggest-numberGuess.smallest) le 20 and

                                         (numberGuess.biggest-numberGuess.smallest) gt 4}">

              <s:selectItems value="#{numberGuess.possibilities}" var="i" label="#{i}"/>

            </h:selectOneMenu>

            <h:selectOneRadio value="#{numberGuess.currentGuess}" id="selectGuessRadio"

                              required="true"

                              rendered="#{(numberGuess.biggest-numberGuess.smallest) le 4}">

              <s:selectItems value="#{numberGuess.possibilities}" var="i" label="#{i}"/>

            </h:selectOneRadio>

            <h:commandButton value="Guess" action="guess"/>

            <s:button value="Cheat" view="/confirm.jspx"/>

            <s:button value="Give up" action="giveup"/>

            </div>

            <div>

            <h:message for="inputGuess" style="color: red"/>

            </div>

          </h:form>

        </f:view>

      </body>

      </html>

    </jsp:root

    >

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

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

例 1.22. win.jspx

    <jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
              xmlns:h="http://java.sun.com/jsf/html"
              xmlns:f="http://java.sun.com/jsf/core"
              xmlns="http://www.w3.org/1999/xhtml"
              version="2.0">
      <jsp:output doctype-root-element="html"
                  doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
                  doctype-system="http://www.w3c.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>
      <jsp:directive.page contentType="text/html"/>
      <html>
      <head>
        <title
    >You won!</title>
        <link href="niceforms.css" rel="stylesheet" type="text/css" />
      </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.
          <h:outputText value="But you cheated, so it doesn't count!"
                        rendered="#{numberGuess.cheat}"/>
          Would you like to <a href="numberGuess.seam"
    >play again</a
    >?
        </f:view>
      </body>
      </html>
    </jsp:root>

`lose.jspx` はほぼ同じです。 説明は省略します。

最後に、実際のアプリケーションコードを見ましょう。

例 1.23. NumberGuess.java

    @Name("numberGuess")
    @Scope(ScopeType.CONVERSATION)
    public class NumberGuess implements Serializable {

       private int randomNumber;
       private Integer currentGuess;
       private int biggest;
       private int smallest;
       private int guessCount;
       private int maxGuesses;
       private boolean cheated;

       @Create
       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;
       }

       public void cheated()
       {
          cheated = true;
       }

       public boolean isCheat() {
          return cheated;
       }

       public List<Integer
    > getPossibilities()
       {
          List<Integer
    > result = new ArrayList<Integer
    >();
          for(int i=smallest; i<=biggest; i++) result.add(i);
          return result;
       }

    }

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

`pages.xml` ファイルは Seam 対話 (conversation) を開始し ( 詳細は後述 )、対話のページフローを使用するためのページフロー定義を規定します。

例 1.24. pages.xml

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

    <pages xmlns="http://jboss.com/products/seam/pages"

           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

           xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.2.xsd">

      <page view-id="/numberGuess.jspx">

        <begin-conversation join="true" pageflow="numberGuess"/>

      </page>

    </pages

    >

見てわかるように、この Seam コンポーネントは純粋なビジネスロジックです! ユーザーインタラクションのフローについて理解する必要はまったくありません。 これによりコンポーネント再利用性を本当に向上させます。

### 1.5.2. 動作内容

アプリケーションの基本的なフローを見てみましょう。 ゲームは `numberGuess.jspx` から始まります。 始めてページが表示されたとき、`pages.xml` 設定は対話を開始させ `numberGuess` ページフローを対話と関連付けます。 ページフローは 待機状態である`start-page` から開始されるので `numberGuess.xhtml` が表示されます。

ビューは `numberGuess` コンポーネントを参照します。 その結果新しいインスタンスが生成され対話に保管されます。 `@Create` メソッドが呼ばれゲームの状態が初期化されます。 ビューはユーザーが `#{numberGuess.currentGuess}` を編集可能な `h:form` を表示します。

"Guess" ボタンは `guess` アクションを呼び起こします。 Seam はアクションを処理するためにページフローに従います。それはページフローが `evaluateGuess` 状態に遷移することを命じます。 最初の呼び出し `#{numberGuess.guess}` は guess count と `numberGuess` コンポーネント中の highest/lowest suggestions を更新します。

`evaluateGuess` 状態は `#{numberGuess.correctGuess}` の値をチェックし `win` または `evaluatingRemainingGuesses` 状態に遷移させます。 数字が間違っていたとすると、その場合ページフローは `evaluatingRemainingGuesses` に遷移します。 それは decision 状態であり、 ユーザーがまだ数字当てをするか否かを決定するために `#{numberGuess.lastGuess}` をテストします。 まだ数字あてをするならば ( `lastGuess` が `false` )、最初の `displayGuess` 状態に遷移させます。 最後に、page 状態に達し、関連するページ `/numberGuess.jspx` が表示されます。ページは redirect 要素を持っているので、Seam はユーザーのブラウザにリダイレクトを送信しプロセスを再始動させます。

以降の要求により `win` または `lose` に遷移する状態以外これ以上状態については説明しません。つまりユーザーが `/win.jspx` または `/lose.jspx` を取得することについてです。 両方の状態は Seam が対話を終了し、ユーザーに最終ページをリダイレクトする前に、ゲームの状態やページフローの状態を破棄することを規定しています。

数字当てサンプルは Giveup と Cheat ボタンも持っています。 二つのアクションについて比較的容易にページフローをトレースすることも可能です。 `cheat` 遷移に注目しましょう。 それはフローを処理するためにサブプロセスをロードします。 このアプリケーションではやり過ぎですが、理解を容易にするために複雑なページフローが小さな部分に分解可能であることを実演しています。

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

### 1.6.1. はじめに

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

- ユーザー登録

- ログイン

- ログアウト

- パスワード設定

- ホテル検索

- ホテル選択

- 部屋予約

- 予約確認

- 現状の予約一覧

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

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

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

### 1.6.2. 予約サンプルの概要

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

このアプリケーションは以下の機能を実装するビジネスロジックのために 6 つのセッション Bean を使用しています。

- `AuthenticatorAction` はログイン認証ロジックを提供します。

- `BookingListAction` は、その時のログインユーザーのために現状の予約を取得します。

- `ChangePasswordAction` は、その時のログインユーザーのパスワードを変更します。

- `HotelBookingAction` は、アプリケーションの中核的機能を実装します。 この機能は 対話 として実装されるため、 このアプリケーションの中でもっとも興味を引くクラスです。

- `HotelSearchingAction` はホテル検索を実装しています。

- `RegisterAction` は、新しいシステムユーザーを登録します。

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

- `Hotel` はホテルを表現するエンティティ Bean です。

- `Booking` は、現状の予約を表すエンティティ Bean です。

- `User` は、ホテル予約ができるユーザーを表すエンティティ Bean です。

### 1.6.3. Seam 対話の理解

気が向いたならばソースコードを読まれることをお勧めします。 このチュートリアルでは、特定の機能つまりホテル検索、選択、予約と確認を集中して説明します。 ユーザーの視点から見ると、 ホテルの選択から予約確認までのすべては、一つの連続した仕事の単位、 つまり対話です。 しかし、検索は対話の一部ではありません。 ユーザーは異なるブラウザタブで同じ検索結果のページから複数のホテルを選択可能です。

ほとんどの WEB アプリケーションのアーキテクチャは対話を扱うためのファーストクラスの構造を持っていません。 これは対話の状態を管理するために重大な問題となります。通常、Java WEB アプリケーションは二つの技術を組み合わせて使用します。 ある状態は URL に変換可能です。 不可能なものはすべての要求の後に `HttpSession` に投げられるかあるいはデータベースにフラッシュされます。そしてすべての新しい要求の最初にデータベースから再構築されます。

データベースは最もスケーラビリティに乏しい層なので、 許容不能なほどスケーラビリティに乏しい結果となることがよくあります。 要求ごとデータベースを行き来する転送量が増加すると、追加される待ち時間も問題となります。この冗長な転送量を減少させるために、Java アプリケーションでは要求間でよくアクセスされるデータを保管するデータキャッシュ (2 次レベル) をしばしば導入します。 このキャッシュは必ずしも効率的ではありません。 なぜならデータが無効かどうかの判断をユーザーがデータの操作を終了したかどうかをもとにして行うのではなく LRU ポリシーをベースとして行うためです。 さらに、 キャッシュは多くの並列トランザクション間で共有されるので、 キャッシュされた状態とデータベース間の一貫性維持に関する多くの問題をも取り入れてしまうことになるためです。

さて `HttpSession` に保管された状態を考察してみましょう。 HttpSession はセッションデータにとってとても便利な場所です。 つまりユーザーがアプリケーションとして持つすべての要求に共通なデータにとって。 しかし、一連の個別の要求に関するデータを保管する場所としては適しません。 戻るボタンや複数のウィンドウを操作するとき、対話的なセッションの使用はすぐに破たんしてしまいます。 それに加えて、慎重なプログラミングがなければ、HTTP セッションのデータはとても大きくなり、HTTP セッションをクラスタに対応させることが困難になる可能性があります。 異なる同時並行的な対話に関連するセッション状態を分離するメカニズムを開発することや、ブラウザウィンドウまたはタブを閉じることでユーザーが対話の一つを中断するときに対話状態が破棄されることを保証するフェイルセーフを組み込むことは簡単な仕事ではありません。 幸いにも Seam にはそんな心配は無用です。

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

このアプリケーションでは、ステートフルセッション Bean を保管するために対話コンテキストを使用します。 Java コミュニティには、ステートフルセッション Bean はスケーラビリティ殺しだというデマが古くからあります。 初期のエンタープライズ Java では真実であったかもしれませんが、今日ではもはや真実ではありません。 今日のアプリケーションサーバーはステートフルセッション Bean の状態を複製するために極めて洗練されたメカニズムを持っています。 例えば、JBoss AS はきめの細かい複製を行い、 実際に変化した bean 属性値のみの複製を行います。 ステートフル Bean が非効率的かという伝統的技術論はすべて `HttpSession` にも等しく当てはまります。 その結果パフォーマンスを改善するためにビジネス層のステートフルセッション Bean から Web セッションに移行する慣習は驚くほど誤解されていることに留意してください。間違ってステートフル Bean を使用することあるいは間違ったもののためにそれらを使うことによって、スケーラブルでないアプリケーションを書く可能性は確かにあります。 しかしそれは使うべきでないということにはなりません。もし納得できなければ、Seam ではセッション Bean の代わりに POJO を使用することも可能です。 Seam では、選択はあなた次第です。

この予約サンプルアプリケーションは、 複雑な振る舞いを実現するために、 異なるスコープを持つステートフルコンポーネントがどのように連携することが可能であるかを示しています。 予約アプリケーションのメインページは、 ユーザーにホテル検索を可能にしています。 検索結果は、Seam セッションスコープに保持されます。 ユーザーがこれらのホテルの一つに遷移するとき、 対話は、開始します。 そして、対話スコープのコンポーネントは、 選択されたホテルを取得するために、 セッションスコープのコンポーネントを呼び返します。

手書きの JavaScript を使用することなくリッチクライアントの振る舞いを実装するためにホテル予約サンプルは RichFaces Ajax の使用を実演しています。

検索機能は、セッションスコープのステートフル Bean を使用して実装されます。 それはメッセージ一覧サンプルに見られるものと同様です。

例 1.25. HotelSearchingAction.java

    @Stateful
    @Name("hotelSearch")
    @Scope(ScopeType.SESSION)
    @Restrict("#{identity.loggedIn}")
    public class HotelSearchingAction implements HotelSearching
    {

       @PersistenceContext
       private EntityManager em;

       private String searchString;
       private int pageSize = 10;
       private int page;

       @DataModel
       private List<Hotel
    > hotels;

       public void find()
       {
          page = 0;
          queryHotels();
       }
       public void nextPage()
       {
          page++;
          queryHotels();
       }

       private void queryHotels()
       {
          hotels =
              em.createQuery("select h from Hotel h where lower(h.name) like #{pattern} " +
                             "or lower(h.city) like #{pattern} " +
                             "or lower(h.zip) like #{pattern} " +
                             "or lower(h.address) like #{pattern}")
                .setMaxResults(pageSize)
                .setFirstResult( page * pageSize )
                .getResultList();
       }

       public boolean isNextPageAvailable()
       {
          return hotels!=null && hotels.size()==pageSize;
       }

       public int getPageSize() {
          return pageSize;
       }

       public void setPageSize(int pageSize) {
          this.pageSize = pageSize;
       }

       @Factory(value="pattern", scope=ScopeType.EVENT)
       public String getSearchPattern()
       {
          return searchString==null ?
                "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
       }

       public String getSearchString()
       {
          return searchString;
       }

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

       @Remove
       public void destroy() {}
    }

| EJB 標準 `@Stateful` アノテーションは、 このクラスがステートフルセッション Bean であることを識別しています。 ステートフルセッション Bean は、 デフォルトで対話コンテキストのスコープを持ちます。
| `@Restrict` アノテーションはコンポーネントへのセキュリティ制限に適用します。 ログインユーザーだけがコンポーネントにアクセスを許されるように制限します。 セキュリティの章では、Seam におけるセキュリティがさらに詳細に説明されています。
| `@DataModel` アノテーションは JSF `ListDataModel` として `List` を公開します。 これは画面検索のためのクリック可能一覧の実装を容易にします。 この場合、ホテルの一覧は `hotels` という名前の対話変数で `ListDataModel` としてページを公開します。
| EJB 標準の `@Remove` アノテーションはアノテーションが付けられたメソッドが呼ばれた後ステートフルセッション Bean が取り除かれることを規定しています。 Seam では、すべてのセッション Bean はパラメータなしの `@Remove` メソッドを定義される必要があります。 Seam がセッションコンテキストを破棄するときメソッドは呼び出されます。

アプリケーションの中心となるページは Facelets ページです。 ホテルを検索に関連する部分を見てみましょう。

例 1.26. main.xhtml

    <div class="section">

        <span class="errors">
           <h:messages globalOnly="true"/>
        </span>

        <h1
    >Search Hotels</h1>

        <h:form id="searchCriteria">
        <fieldset
    >
           <h:inputText id="searchString" value="#{hotelSearch.searchString}"
                        style="width: 165px;">
             <a:support event="onkeyup" actionListener="#{hotelSearch.find}"
                        reRender="searchResults" />
           </h:inputText>
           &#160;
           <a:commandButton id="findHotels" value="Find Hotels" action="#{hotelSearch.find}"
                            reRender="searchResults"/>
           &#160;
           <a:status>
              <f:facet name="start">
                 <h:graphicImage value="/img/spinner.gif"/>
              </f:facet>
           </a:status>
           <br/>
           <h:outputLabel for="pageSize"
    >Maximum results:</h:outputLabel
    >&#160;
           <h:selectOneMenu value="#{hotelSearch.pageSize}" id="pageSize">
              <f:selectItem itemLabel="5" itemValue="5"/>
              <f:selectItem itemLabel="10" itemValue="10"/>
              <f:selectItem itemLabel="20" itemValue="20"/>
           </h:selectOneMenu>
        </fieldset>
        </h:form>

    </div>

    <a:outputPanel id="searchResults">
      <div class="section">
        <h:outputText value="No Hotels Found"
                      rendered="#{hotels != null and hotels.rowCount==0}"/>
        <h:dataTable id="hotels" value="#{hotels}" var="hot"
                     rendered="#{hotels.rowCount
    >0}">
            <h:column>
                <f:facet name="header"
    >Name</f:facet>
                #{hot.name}
            </h:column>
            <h:column>
                <f:facet name="header"
    >Address</f:facet>
                #{hot.address}
            </h:column>
            <h:column>
                <f:facet name="header"
    >City, State</f:facet>
                #{hot.city}, #{hot.state}, #{hot.country}
            </h:column
    >
            <h:column>
                <f:facet name="header"
    >Zip</f:facet>
                #{hot.zip}
            </h:column>
            <h:column>
                <f:facet name="header"
    >Action</f:facet>
                <s:link id="viewHotel" value="View Hotel"
                        action="#{hotelBooking.selectHotel(hot)}"/>
            </h:column>
        </h:dataTable>
        <s:link value="More results" action="#{hotelSearch.nextPage}"
                rendered="#{hotelSearch.nextPageAvailable}"/>
      </div>
    </a:outputPanel
    >

| RichFaces Ajax `<a:support>` タグは `onkeyup` のような JavaScript イベントが発生するとき JSF アクションイベントリスナーが非同期の `XMLHttpRequest` により呼び出されることを可能にしています。 さらに良いことには、`reRender` 属性は JSF ページの一部分だけのレンダリングを可能とし非同期の応答を受信したときに部分的なページ更新を可能にしています。
| RichFaces Ajax `<a:status>` タグは非同期の要求が返されるのを待つ間にアニメーションイメージを表示させます。
| RichFaces Ajax `<a:outputPanel>` タグは非同期要求によって再レンダリング可能なページの領域を定義します。
| Seam `<s:link>` タグは、 JSF アクションリスナーを普通の (非 JavaScript) HTML リンクに付けることができます。 標準 `<h:commandLink>` と比べてこれが有利なのは、 "新しいウィンドウで開く" や "新しいタブで開く"といった操作を損なわないことです。 パラメータ `#{hotelBooking.selectHotel(hot)}` のメソッドバインディングを利用していることにも留意してください。 これは、標準統一された EL式 では不可能ですが、 Seam は、 すべてのメソッドバインディング表現でパラメータ使用できるよう EL式 を拡張しています。 どのようにナビゲーションが起こるかと思うならば、`WEB-INF/pages.xml` にすべてのルールが定義されていることを見つけるでしょう。 これについては 項6.7. 「ナビゲーション」 で議論します。

このページは、タイプしたときに検索結果が動的に表示し、 ホテルの選択をさせ、 `HotelBookingAction` の `selectHotel()` メソッドに選択結果を渡します。 そこでは、かなり興味深いことが起こっています。

対話と関連する永続データを自然にキャッシュするために予約サンプルアプリケーションがどのように対話スコープのステートフル Bean を利用するか見てみましょう。 以下のサンプルコードは結構長いですが、対話の各種ステップを実装するスクリプト化された動作の一覧と考えると理解できます。 ストーリーを読むように徹底的に読んでください。

例 1.27. HotelBookingAction.java

    @Stateful
    @Name("hotelBooking")
    @Restrict("#{identity.loggedIn}")
    public class HotelBookingAction implements HotelBooking
    {

       @PersistenceContext(type=EXTENDED)
       private EntityManager em;

       @In
       private User user;

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

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

       @In
       private FacesMessages facesMessages;

       @In
       private Events events;

       @Logger
       private Log log;

       private boolean bookingValid;

       @Begin
       public void selectHotel(Hotel selectedHotel)
       {
          hotel = em.merge(selectedHotel);
       }

       public void bookHotel()
       {
          booking = new Booking(hotel, user);
          Calendar calendar = Calendar.getInstance();
          booking.setCheckinDate( calendar.getTime() );
          calendar.add(Calendar.DAY_OF_MONTH, 1);
          booking.setCheckoutDate( calendar.getTime() );
       }

       public void setBookingDetails()
       {
          Calendar calendar = Calendar.getInstance();
          calendar.add(Calendar.DAY_OF_MONTH, -1);
          if ( booking.getCheckinDate().before( calendar.getTime() ) )
          {
             facesMessages.addToControl("checkinDate", "Check in date must be a future date");
             bookingValid=false;
          }
          else if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
          {
             facesMessages.addToControl("checkoutDate",
                                        "Check out date must be later than check in date");
             bookingValid=false;
          }
          else
          {
             bookingValid=true;
          }
       }

       public boolean isBookingValid()
       {
          return bookingValid;
       }

       @End
       public void confirm()
       {
          em.persist(booking);
          facesMessages.add("Thank you, #{user.name}, your confimation number " +
                            " for #{hotel.name} is #{booki g.id}");
          log.info("New booking: #{booking.id} for #{user.username}");
          events.raiseTransactionSuccessEvent("bookingConfirmed");
       }

       @End
       public void cancel() {}

       @Remove
       public void destroy() {}

| この Bean は、EJB3 拡張永続コンテキスト を使用します。 その結果、エンティティインスタンスは、 ステートフルセッション Bean のライフサイクル全体の管理を維持します。
| `@Out` アノテーションはメソッド呼び出しの後に属性の値がコンテキスト変数にアウトジェクト (outject) されることを宣言しています。 このサンプルではすべてのアクションリスナーの呼び出しが完了した後に `hotel` の名前のコンテキスト変数は `hotel` インスタンス変数の値に設定されます。
| `@Begin` アノテーションは アノテーション付きメソッドが長期対話 (long-running conversation) を開始することを定義しています。 したがって要求の終了で現在の対話コンテキストは破棄されません。 その代わりに、 現在のウインドからのすべての要求に再び関連する、対話の非活動によるタイムアウトあるいは `@End` とマッチするメソッドの呼び出しにより破棄されるでしょう。
| `@End` アノテーションはアノテーション付きメソッドが現在の長期対話を終了することを定義しています。 したがって要求の終わりで現在の対話コンテキストは破棄されるでしょう。
| Seam は、対話コンテキストを破棄するときこの EJB remove メソッドは呼び出されるでしょう。 このメソッドを定義することを忘れないでください。

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

さらに良いことに、ユーザーは、ログインセッション毎に複数の分離された対話を持つことが可能です。 試してみてください。 ログインして、検索して、複数のブラウザタブに異なるホテルのページを表示させてください。 同時に二つの異なるホテル予約を作成することが可能です。 対話を長時間放置した場合、 Seam は最終的に対話をタイムアウトし状態を破棄します。 対話が終了した後に、その対話ページに戻るボタンを押し処理実行を試みた場合、 Seam は対話が既に終了したことを検出し検索ページにリダイレクトします。

### 1.6.4. Seam デバッグページ

WAR は `seam-debug.jar` も含みます。 Seam デバッグページは `WEB-INF/lib` に Facelets と合わせてこの jar がデプロイされ、`init` コンポーネントの debug プロパティが以下のように設定された場合に有効になります。

    <core:init jndi-pattern="@jndiPattern@" debug="true"/>

このページで現在ログインしているセッションに関連するすべての Seam コンテキスト中の Seam コンポーネントを見たり検査することができます。 使い方は単にブラウザから `http://localhost:8080/seam-booking/debug.seam` を指定するだけです。

## 1.7. ネストされた対話 : ホテル予約サンプルの拡張

### 1.7.1. はじめに

長期対話はマルチウインドウ操作やも戻るボタンに直面してもアプリケーションの状態の一貫性を維持することを容易にします。 残念なことに、長期対話の開始や終了は通常十分ではありません。 アプリケーション要件に応じて、ユーザーの期待するもとのアプリケーションの状態の現実の間の矛盾は結果としてまだ生じます。

ネストされたホテル予約アプリケーションは部屋の選択を関連づけるためホテル予約アプリケーションの機能を拡張しています。 それぞれのホテルはユーザーが選択するために宿泊可能な部屋を説明付きで持っています。 これはホテルの予約の流れにおいて部屋選択ページの機能追加を必要とします。

ユーザーはその時予約に含まれるべき宿泊可能な部屋のオプションを持っています。 これまで見たホテルの予約アプリケーションと同様に、これは状態の一貫性の問題を引き起こす可能性があります。 `HTTPSession` に状態を保管するように、対話変数が変更すると同じ対話コンテキストの中で動作しているすべてのウィンドウに影響します。

これを実演するために、ユーザーが一つの新しいウィンドウでの中でまったく同じの選択画面を表示させたとします。 そしてユーザーは Wonderful Room を選択して確認画面に進みます。 上流の生活を過ごすのにどれだけかかるかを見るために、ユーザーはもともとの画面を戻して、予約のために Fantastic Suite を選択し、再び確認に進みます。 総費用を見直した後に、ユーザーは実用性を重視して確認のために Wonderful Room を表示するウィンドウに戻ることを決めました。

このシナリオでは、単純にすべての状態を対話に保管するならば、同じ対話なかにある複数ウィンドウの操作を保護できません。 ネストされた対話は同じ対話中でコンテキストが変更するときでさえユーザーの正しい振る舞いを達成可能にしています。

### 1.7.2. ネストされた対話の理解

さあ、ネストされたホテル予約サンプルがネストされた対話を使用することでどのようにホテル予約アプリケーション機能拡張させているか見てみましょう。 繰り返しになりますが、物語のようにクラスを徹底的に読むことができます。

例 1.28. RoomPreferenceAction.java

    @Stateful
    @Name("roomPreference")
    @Restrict("#{identity.loggedIn}")
    public class RoomPreferenceAction implements RoomPreference
    {

       @Logger
       private Log log;

       @In private Hotel hotel;

       @In private Booking booking;

       @DataModel(value="availableRooms")
       private List<Room
    > availableRooms;

       @DataModelSelection(value="availableRooms")
       private Room roomSelection;

       @In(required=false, value="roomSelection")
       @Out(required=false, value="roomSelection")
       private Room room;

       @Factory("availableRooms")
       public void loadAvailableRooms()
       {
          availableRooms = hotel.getAvailableRooms(booking.getCheckinDate(), booking.getCheckoutDate());
          log.info("Retrieved #0 available rooms", availableRooms.size());
       }

       public BigDecimal getExpectedPrice()
       {
          log.info("Retrieving price for room #0", roomSelection.getName());

          return booking.getTotal(roomSelection);
       }

       @Begin(nested=true)
       public String selectPreference()
       {
          log.info("Room selected");

          this.room = this.roomSelection;

          return "payment";
       }

       public String requestConfirmation()
       {
          // all validations are performed through the s:validateAll, so checks are already
          // performed
          log.info("Request confirmation from user");

          return "confirm";
       }

       @End(beforeRedirect=true)
       public String cancel()
       {
          log.info("ending conversation");

          return "cancel";
       }

       @Destroy @Remove
       public void destroy() {}
    }

| `hotel` インスタンスは対話コンテキストからインジェクトされます。 エンティティは対話の期間中に管理され続けるように hotel は拡張永続コンテキストからロードされます。 このことは関連の参照先を単純にアクセスすることにより `@Factory` メソッドを通して `availableRooms` を遅延ロードすることを可能にしています。
| `@Begin(nested=true)` に出会うとき、ネストされた対話は対話スタックにプッシュされます。 ネストされた対話中で実行するとき、コンポーネントはそれでも対話の外のすべての状態にアクセスできますが、ネストされた対話の状態コンテナに値を設定することは対話の外側には影響しません。 加えて、ネストされた対話は同じ外側の対話に対して同時並行的にスタックされて存在することが可能で、それぞれの状態は独立しています。
| `roomSelection` は `@DataModelSelection` に基づいた対話にアウトジェクトされます。 ネストされた対話は独立したコンテキストを持つので、`roomSelection` は新たなネストされた対話にのみ設定されることに留意してください。 ユーザーが別のウインドウまたはタブで違う好みを選ぶならば新たなネストされた対話が開始されます。
| `@End` アノテーションは対話スタックからポップし外側の対話を再開します。 対話コンテキストとともに `roomSelection` は破棄されます。

ネストされた対話にあるとき対話スタックにプッシュされます。 `nestedbooking` サンプルでは、対話スタックは外側の長期対話 (booking) とそれぞれのネストされた対話 (room selections) から構成されます。

例 1.29. rooms.xhtml

    <div class="section">
        <h1
    >Room Preference</h1>
    </div>

    <div class="section">
        <h:form id="room_selections_form">
            <div class="section">
                <h:outputText styleClass="output"
                    value="No rooms available for the dates selected: "
                    rendered="#{availableRooms != null and availableRooms.rowCount == 0}"/>
                <h:outputText styleClass="output"
                    value="Rooms available for the dates selected: "
                    rendered="#{availableRooms != null and availableRooms.rowCount
    > 0}"/>

                <h:outputText styleClass="output" value="#{booking.checkinDate}"/> -
                <h:outputText styleClass="output" value="#{booking.checkoutDate}"/>

                <br/><br/>

                <h:dataTable value="#{availableRooms}" var="room"
                        rendered="#{availableRooms.rowCount
    > 0}">
                    <h:column>
                        <f:facet name="header"
    >Name</f:facet>
                        #{room.name}
                    </h:column>
                    <h:column>
                        <f:facet name="header"
    >Description</f:facet>
                        #{room.description}
                    </h:column>
                    <h:column>
                        <f:facet name="header"
    >Per Night</f:facet>
                        <h:outputText value="#{room.price}">
                            <f:convertNumber type="currency" currencySymbol="$"/>
                        </h:outputText>
                    </h:column>
                    <h:column>
                        <f:facet name="header"
    >Action</f:facet>
                        <h:commandLink id="selectRoomPreference"
                            action="#{roomPreference.selectPreference}"
    >Select</h:commandLink>
                    </h:column>
                </h:dataTable>
            </div>
            <div class="entry">
                <div class="label"
    >&#160;</div>
                <div class="input">
                    <s:button id="cancel" value="Revise Dates" view="/book.xhtml"/>
                </div>
            </div
    >
        </h:form>
    </div>

| EL から要求されるとき、`RoomPreferenceAction` に定義された `@Factory` メソッドにより `#{availableRooms}` がロードされます。 `@Factory` メソッドは `@DataModel` インスタンスのような現在のコンテキストに値をロードするときに 1 度だけ実行されます。
| `#{roomPreference.selectPreference}` アクションを呼び出すことにより行が選択され `@DataModelSelection` に値が設定されます。 そして値はネストされた対話コンテキストにアウトジェクトされます。
| 日付の変更は単純に `/book.xhtml` に返されます。 まだネストされた対話ではないこと （ room preference は選ばれていない ）、現在の対話は単純にレジューム可能であることに留意してください。 `<s:button >` コンポーネントは `/book.xhtml` ビューを表示するときに単純に現在の対話を伝播します。

今や対話のネスティングの方法がわかったので、部屋が選ばれたらどのように予約を確認することができるかを見てみましょう。 これは `HotelBookingAction`.の振る舞いを単に拡張することによって達成可能です。

例 1.30. HotelBookingAction.java

    @Stateful
    @Name("hotelBooking")
    @Restrict("#{identity.loggedIn}")
    public class HotelBookingAction implements HotelBooking
    {

       @PersistenceContext(type=EXTENDED)
       private EntityManager em;

       @In
       private User user;

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

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

       @In(required=false)
       private Room roomSelection;

       @In
       private FacesMessages facesMessages;

       @In
       private Events events;

       @Logger
       private Log log;

       @Begin
       public void selectHotel(Hotel selectedHotel)
       {
          log.info("Selected hotel #0", selectedHotel.getName());
          hotel = em.merge(selectedHotel);
       }

       public String setBookingDates()
       {
          // the result will indicate whether or not to begin the nested conversation
          // as well as the navigation.  if a null result is returned, the nested
          // conversation will not begin, and the user will be returned to the current
          // page to fix validation issues
          String result = null;

          Calendar calendar = Calendar.getInstance();
          calendar.add(Calendar.DAY_OF_MONTH, -1);

          // validate what we have received from the user so far
          if ( booking.getCheckinDate().before( calendar.getTime() ) )
          {
             facesMessages.addToControl("checkinDate", "Check in date must be a future date");
          }
          else if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
          {
             facesMessages.addToControl("checkoutDate", "Check out date must be later than check in date");
          }
          else
          {
             result = "rooms";
          }

          return result;
       }

       public void bookHotel()
       {
          booking = new Booking(hotel, user);
          Calendar calendar = Calendar.getInstance();
          booking.setCheckinDate( calendar.getTime() );
          calendar.add(Calendar.DAY_OF_MONTH, 1);
          booking.setCheckoutDate( calendar.getTime() );
       }

       @End(root=true)
       public void confirm()
       {
          // on confirmation we set the room preference in the booking.  the room preference
          // will be injected based on the nested conversation we are in.
          booking.setRoomPreference(roomSelection);

          em.persist(booking);
          facesMessages.add("Thank you, #{user.name}, your confimation number for #{hotel.name} is #{booking.id}");
          log.info("New booking: #{booking.id} for #{user.username}");
          events.raiseTransactionSuccessEvent("bookingConfirmed");
       }

       @End(root=true, beforeRedirect=true)
       public void cancel() {}

       @Destroy @Remove
       public void destroy() {}
    }

| `@End(root=true)` アノテーションを付けることによって効果的に対話スタック全体が破棄され対話のルート (root) が終了します。 どんな対話も終了するときに、ネストされた対話も終了します。 ルートがすべてを開始した対話であるとき、これはが予約が確認されたワークスペースと関連したすべての状態を破壊しリリースする簡単な方法です。
| `roomSelection` はユーザー確認において `booking` とだけ関連します。 ネストされた対話に値をアウトジェクトする間にコンテキストは外側の対話に影響を与えません。外側の対話からインジェクトされるオブジェクトは参照によりインジェクトされます。 これはオブジェクトに対する変更は別ののネストされた対話だけでなく親の対話にも反映されることを意味しています。
| `@End(root=true, beforeRedirect=true)` で簡単にアクションをキャンセルするアノテーションによりユーザーをホテル選択ビューにリダイレクトする前に容易にワークスペースに関連するすべての状態を破棄しリリースすることが可能です。

気軽にアプリケーションをデプロイし、たくさんのウィンドウやタブを開きさまざまな好みの部屋によるさまざまなホテルの組み合わせを試してみて下さい。 予約確認はネストされた対話モデルのおかげで正しいホテルと好みの部屋をもたらします。

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

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

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

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

Seam DVD ストアデモは他のでもアプリケーション同様に `dvdstore` ディレクトリから起動できます。

## 1.9. Blog サンプルのブックマーク可能 URL

Seam はサーバサイドで状態保持するアプリケーションの実装をとても容易にします。 しかし、サーバサイドの状態管理はいつも適切というわけではありません。 (特に、コンテンツ (content) を提供する機能において ) この種の問題のために、ユーザーにページをブックマークさせ、 そして、比較的ステートレスなサーバとする必要がしばしばあります、 その結果、ブックマークを通していつでもどんなページにもアクセス可能になります。 この Blog サンプルは Seam を使用した RESTful アプリケーションの実装方法を見せてくれます。 検索結果ページを含むすべてのアプリケーションのページはブックマークが可能です。

この Blog サンプルは、"引っぱり (PULL) " - スタイル MVC の使用を実演しています。 ここで、ビューのためのデータ取得とデータ準備のアクションメソッドリスナーを使用する代わりに、 ビューは、レンダリングしているコンポーネントからデータを引き出します (PULL) 。

### 1.9.1. "PULL" 型 MVC の使用

`index.xhtml` facelets ページの一部は最新のブログエントリの一覧を表示しています。

例 1.31.

    <h:dataTable value="#{blog.recentBlogEntries}" var="blogEntry" rows="3">

     <h:column>

        <div class="blogEntry">

           <h3

    >#{blogEntry.title}</h3>

           <div>

              <s:formattedText value="#{blogEntry.excerpt==null ? blogEntry.body : blogEntry.excerpt}"/>

           </div>

           <p>

              <s:link view="/entry.xhtml" rendered="#{blogEntry.excerpt!=null}" propagation="none"

                  value="Read more...">

                 <f:param name="blogEntryId" value="#{blogEntry.id}"/>

              </s:link>

           </p>

           <p>

              [Posted on&#160;

              <h:outputText value="#{blogEntry.date}">

                  <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>

              </h:outputText

    >]

              &#160;

              <s:link view="/entry.xhtml" propagation="none" value="[Link]">

                 <f:param name="blogEntryId" value="#{blogEntry.id}"/>

              </s:link>

           </p>

        </div>

     </h:column>

    </h:dataTable

    >

ブックマークからこのページにナビゲートするならば、`<h:dataTable>` により使用された `#{blog.recentBlogEntries}` データは実際にはどのように初期化されるのでしょうか？ `blog` という名前の Seam コンポーネントにより、必要なときに `Blog` はレイジーに—"PULLされ"—取得されます。 これは Struts のような従来のアクションベースの Web フレームワークで使用されるものと制御の流れが逆です。

例 1.32.

    @Name("blog")
    @Scope(ScopeType.STATELESS)
    @AutoCreate
    public class BlogService
    {

       @In EntityManager entityManager;

       @Unwrap
       public Blog getBlog()
       {
          return (Blog) entityManager.createQuery("select distinct b from Blog b left join fetch b.blogEntries")
                .setHint("org.hibernate.cacheable", true)
                .getSingleResult();
       }

    }

| このコンポーネントは Seam 管理永続コンテキスト (seam-managed persistence context) を使用しています。 これまで見てきた他のサンプルとは異なり、この永続コンテキストは、EJB3 コンテナの代わりに Seam により管理されます。 永続コンテキストは Web 要求全体におよび、ビューにおいてフェッチしていない関連にアクセスするときに発生する例外を回避することが可能です。
| `@Unwrap` アノテーションは Seam に クライアントへの実際の `BlogService` コンポーネントを返す代わりに `Blog` メソッドの実行結果を返すよう指示します。 これは Seam マネージャコンポーネントパターン と呼ばれます。

これは、これまでのところ良いですが、 検索結果ページのようなフォームサブミットの結果のブックマークではどうでしょうか？

### 1.9.2. ブックマーク可能検索結果ページ

この Blog サンプルは、 各ページの右上にユーザーの Blog 記事の検索を可能にする小さなフォームを持ちます。 これは、facelet テンプレート、`template.xhtml` に含まれる `menu.xhtml` ファイルに定義されます。

例 1.33.

    <div id="search">

       <h:form>

          <h:inputText value="#{searchAction.searchPattern}"/>

          <h:commandButton value="Search" action="/search.xhtml"/>

       </h:form>

    </div

    >

ブックマーク可能検索結果ページの実装のために、 検索フォームのサブミットを処理した後に、 ブラウザリダイレクトを実行する必要があります。 アクション結果 (outcome) として JSF ビュー ID を使用しているので、 Seam は、フォームがサブミットされたとき、自動的に ビュー ID にリダイレクトします。 別の方法として、以下のようなナビゲーションルールを定義することも可能です。

    <navigation-rule>

       <navigation-case>

          <from-outcome

    >searchResults</from-outcome>

          <to-view-id

    >/search.xhtml</to-view-id>

          <redirect/>

       </navigation-case>

    </navigation-rule

    >

フォームは、以下と似たようなものになるでしょう。

    <div id="search">

       <h:form>

          <h:inputText value="#{searchAction.searchPattern}"/>

          <h:commandButton value="Search" action="searchResults"/>

       </h:form>

    </div

    >

しかし、リダイレクトするとき、 `http://localhost:8080/seam-blog/search.seam?searchPattern=seam` のようなブックマーク URL を取得するために、 要求パラメータとしてフォームによってサブミットされた値を含む必要があります。 JSF はこれを行なう簡単な方法は提供しませんが、Seam は提供します。これを達成するために ページパラメータ と URL書き換え という二つのSeam機能を使います。両方とも `WEB-INF/pages.xml` で定義されます。

例 1.34.

    <pages>

       <page view-id="/search.xhtml">

          <rewrite pattern="/search/{searchPattern}"/>

          <rewrite pattern="/search"/>

          <param name="searchPattern" value="#{searchService.searchPattern}"/>

       </page>

       ...

    </pages

    >

検索ページへの要求があるときや検索ページへのリンクが生成されるときはいつでも、ページパラメータは Seam に `searchPattern` という名前の要求パラメータを `#{searchService.searchPattern}` の値にリンクすることを指示します。 Seam は URL とアプリケーションの状態のリンクについて維持することに責任を持ちます。 私たちや開発者はそれを心配する必要はありません。

URL 書き換えがない場合、`book` に関する検索のための URL は `http://localhost:8080/seam-blog/seam/search.xhtml?searchPattern=book` になります。これは良いですが、Seam は書き換えルールを使用して URL をさらに簡単にすることが可能です。 最初の書き換えルールつまり `/search/{searchPattern}` パターンは searchPattern 要求パラメータ付きの search.xhtml の URL を持つときはいつでも、URL をもっと簡単な URL にまとめることができます。 それで、最初に見た URL `http://localhost:8080/seam-blog/seam/search.xhtml?searchPattern=book` は `http://localhost:8080/seam-blog/search/book`. として書くことが可能です。

ページパラメータと同様に、URL 書き換えは両方向です。 Seam はより簡単な URL の要求を適切なビューにフォーワードすること、そして簡単なビューを自動的に生成することも意味します。 唯一の要件は URL を書き換えするために、書き換えフィルタが `components.xml` において使用可能であることです。

    <web:rewrite-filter view-mapping="/seam/*" />

リダイレクトによって `search.xhtml` ページに移動します。

    <h:dataTable value="#{searchResults}" var="blogEntry">

      <h:column>

         <div>

            <s:link view="/entry.xhtml" propagation="none" value="#{blogEntry.title}">

               <f:param name="blogEntryId" value="#{blogEntry.id}"/>

            </s:link>

            posted on

            <h:outputText value="#{blogEntry.date}">

                <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>

            </h:outputText>

         </div>

      </h:column>

    </h:dataTable

    >

これもまた Hibernate 検索を使用し実際の検索結果を取得するために "PULL" 型 MVC を使用しています。

    @Name("searchService")

    public class SearchService

    {

       @In

       private FullTextEntityManager entityManager;

       private String searchPattern;

       @Factory("searchResults")

       public List<BlogEntry

    > getSearchResults()

       {

          if (searchPattern==null || "".equals(searchPattern) ) {

             searchPattern = null;

             return entityManager.createQuery("select be from BlogEntry be order by date desc").getResultList();

          }

          else

          {

             Map<String,Float

    > boostPerField = new HashMap<String,Float

    >();

             boostPerField.put( "title", 4f );

             boostPerField.put( "body", 1f );

             String[] productFields = {"title", "body"};

             QueryParser parser = new MultiFieldQueryParser(productFields, new StandardAnalyzer(), boostPerField);

             parser.setAllowLeadingWildcard(true);

             org.apache.lucene.search.Query luceneQuery;

             try

             {

                luceneQuery = parser.parse(searchPattern);

             }

             catch (ParseException e)

             {

                return null;

             }

             return entityManager.createFullTextQuery(luceneQuery, BlogEntry.class)

                   .setMaxResults(100)

                   .getResultList();

          }

       }

       public String getSearchPattern()

       {

          return searchPattern;

       }

       public void setSearchPattern(String searchPattern)

       {

          this.searchPattern = searchPattern;

       }

    }

### 1.9.3. RESTful アプリケーションの "PUSH" 型 MVC の使用

ごく希に、RESTful ページ処理のために PUSH 型 MVC を使用することが当然の場合があります。 そこで、Seam は、ページアクション の概念を提供します。 Blog サンプルは、 Blog 記入ページ、 `entry.xhtml` にページアクションを使用しています。 これは、少しわざとらしい感じで、ここでは、PULL 型 MVC を使用する方が容易かもしれません。

`entryAction` コンポーネントは、 Struts のような典型的な PUSH 型 MVC アクション指向フレームワークのように動作します。

    @Name("entryAction")

    @Scope(STATELESS)

    public class EntryAction

    {

       @In Blog blog;

       @Out BlogEntry blogEntry;

       public void loadBlogEntry(String id) throws EntryNotFoundException

       {

          blogEntry = blog.getBlogEntry(id);

          if (blogEntry==null) throw new EntryNotFoundException(id);

       }

    }

ページアクションは、`pages.xml` でも宣言されます。

    <pages>

       ...

        <page view-id="/entry.xhtml"

    >

            <rewrite pattern="/entry/{blogEntryId}" />

            <rewrite pattern="/entry" />

            <param name="blogEntryId"

                   value="#{blogEntry.id}"/>

            <action execute="#{entryAction.loadBlogEntry(blogEntry.id)}"/>

        </page>

        <page view-id="/post.xhtml" login-required="true">

            <rewrite pattern="/post" />

            <action execute="#{postAction.post}"

                    if="#{validation.succeeded}"/>

            <action execute="#{postAction.invalid}"

                    if="#{validation.failed}"/>

            <navigation from-action="#{postAction.post}">

                <redirect view-id="/index.xhtml"/>

            </navigation>

        </page>

        <page view-id="*">

            <action execute="#{blog.hitCount.hit}"/>

        </page>

    </pages

    >

このサンプルはポストバリデーションとページビューカウンタのためにページアクションを使用していことに留意してください。 同様にページアクションメソッドバインディングでのパラメータの使用にも留意してください。 これは標準 JSF EL の機能ではありませんが、Seam はページアクションだけでなく JSF メソッドバインディングでも使用を可能にしています。

`entry.xhtml` ページが要求されたとき、Seam は最初にページパラメータ `blogEntryId` をモデルにバインドします。 URL 書き換えのために、blogEntryId パラメータは URL に表示されないことを覚えておいてください。 Seam はまずページアクションを実行します。 それは必要なデータ— `blogEntry`— を取得し、Seam イベントコンテキストに配置します。 最後に以下がレンダリングされます。

    <div class="blogEntry">

        <h3

    >#{blogEntry.title}</h3>

        <div>

            <s:formattedText value="#{blogEntry.body}"/>

        </div>

        <p>

        [Posted on&#160;

        <h:outputText value="#{blogEntry.date}">

           <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>

        </h:outputText

    >]

        </p>

    </div

    >

blog エントリがデータベースで見つからない場合、 `EntryNotFoundException` 例外がスローされます。 exception is thrown. この例外は 505 エラーではなく 404 であって欲しいので、 例外クラスのアノテーションを付けます。

    @ApplicationException(rollback=true)

    @HttpError(errorCode=HttpServletResponse.SC_NOT_FOUND)

    public class EntryNotFoundException extends Exception

    {

       EntryNotFoundException(String id)

       {

          super("entry not found: " + id);

       }

    }

別実装のサンプルは、メソッドバインディングでパラメータを使用しません。

    @Name("entryAction")

    @Scope(STATELESS)

    public class EntryAction

    {

       @In(create=true)

       private Blog blog;

       @In @Out

       private BlogEntry blogEntry;

       public void loadBlogEntry() throws EntryNotFoundException

       {

          blogEntry = blog.getBlogEntry( blogEntry.getId() );

          if (blogEntry==null) throw new EntryNotFoundException(id);

       }

    }

    <pages>

       ...

       <page view-id="/entry.xhtml" action="#{entryAction.loadBlogEntry}">

          <param name="blogEntryId" value="#{blogEntry.id}"/>

       </page>

       ...

    </pages

    >

どの実装を選択するかは好みの問題です。

ブログデモはまたとても簡単なパスワード認証、ブログのポスト、ページの一部のキャッシュ、 atom フィードの生成も実演しています。

- 前のページJBoss Seam の紹介
- 上に戻る
- ホーム
- 次のページ第2章 seam-gen を使って Seam を始めよう