第1章 Seam チュートリアル

1.1. サンプルを試そう

このチュートリアルでは、 JBoss AS 4.0.5 がダウンロードされ、 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. JBoss AS 上でのサンプルの実行

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

次に、 examples/registration ディレクトリから ant deploy と入力してサンプルのビルドおよびデプロイを行います。

ブラウザから、 http://localhost:8080/seam-registration/ にアクセスしてみます。

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

最初に、$ANT_HOME$JAVA_HOME が正しく設定され、 Ant が正しくインストールされたことを確認してください。 次に、Seam をインストールしたルートフォルダにある build.properties ファイルに Tomcat 5.5 のロケーションを設定してください。

次に、examples/registration ディレクトリから ant deploy と入力プしてサンプルのビルドおよびデプロイを行います。

最後に、Tomcat を起動してください。

ブラウザから、 http://localhost:8080/jboss-seam-registration/ にアクセスしてみます。

サンプルを Tomcat にデプロイした場合、 EJB3 コンポーネントは、JBoss 組み込み EJB3 コンテナ (完全なスタンドアローン EJB コンテナ環境) 内部で作動します。

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

ほとんどのサンプルは 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)

namepasswordusername は、 エンティティ Bean の永続属性です。 JBoss の永続属性はすべてアクセスメソッドを定義しています。 レスポンスのレンダリングフェーズおよびモデル値の更新フェーズで JSF によりこのコンポーネントが使用されるときに必要となります。

(6)

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

(7)

@NotNull@Length アノテーションは、 Hibernate Validator フレームワークの一部です。 Seam は Hibernate Validator を統合しているため、 データの妥当性検証にこれを使用することができます (永続性に Hiberenate を使用していない場合でも使用できる)。

(8)

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 アクションは正確に 1 つのみで、 これにセッションBean メソッドが 1 つリンクしています。 このサンプルでは、 アクションに関連する状態はすべて User Bean によって保持されるため、 ステートレスセッション Bean を使用しています。

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

例 1.2.

@Stateless                                                                               (1)
@Name("register")
public class RegisterAction implements Register
{

   @In                                                                                   (2)
   private User user;
   
   @PersistenceContext                                                                   (3)
   private EntityManager em;
   
   @Logger                                                                               (4)
   private Log log;
   
   public String register()                                                              (5)
   {
      List existing = em.createQuery(
         "select username from User where username=#{user.username}")                    (6)
         .getResultList();
         
      if (existing.size()==0)
      {
         em.persist(user);
         log.info("Registered new user #{user.username}");                               (7)
         return "/registered.jsp";                                                       (8)
      }
      else
      {
         FacesMessages.instance().add("User #{user.username} already exists");           (9)
         return null;
      }
   }

}
(1)

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

(2)

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

(3)

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

(4)

Seam @Logger アノテーションは、 コンポーネントの Log インスタンスをインジェクトするために使用されています。

(5)

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

(6)

Seam では EJB-QL 内で JSF EL 式を使用することができます。 バックグラウンドで行われるため見えませんが、 これにより普通の JPA setParameter() が標準 JPA Query オブジェクトを呼び出すことになります。 便利でしょう?

(7)

Log API は、テンプレート化されたログメッセージを容易に表示可能です。

(8)

JSF アクションリスナメソッドは、次にどのページを表示するかを決定するストリング値の結果 (outcome) を返します。 null 結果 (outcome) (あるいは、void アクションリスナメソッド) は、 前のページを再表示します。 普通の JSF では、 結果 (outcome) から JSF view id を決定するために、 常に JSF ナビゲーション規則 を使用することが普通です。 複雑なアプリケーションにとって、この間接的命令は、実用的な良い慣行です。 しかし、このようなとても簡単なサンプルのために、 Seam は、結果 (outcome) として JSF view id の使用を可能とし、 ナビゲーション規則の必要性を取り除きました。 結果 (outcome) として view id を使用する場合、 Seam は、常にブラウザリダイレクトを行うことに留意してください。

(9)

Seam は、共通な問題の解決を支援するために多くの 組み込みコンポーネントを提供しています。 FacesMessages コンポーネントは、 テンプレート化されたエラーや成功メッセージを容易に表示可能です。 組み込み Seam コンポーネントは、 インジェクションあるいは、instance() メソッド呼び出しによって取得可能です。

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

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

さらに、このセッション Bean は WEB リクエスト (例えば、 User オブジェクト内のフォームの値) に関連するコンテキストにアクセスすると同時に、 トランザクションリソース (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. Seam コンポーネント配備記述子 : components.xml

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

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

例 1.4.

<components xmlns="http://jboss.com/products/seam/components"
            xmlns:core="http://jboss.com/products/seam/core">
     <core:init jndi-pattern="@jndiPattern@"/>
</components>

このコードは、org.jboss.seam.core.init という名前の Seam コンポーネントの jndiPattern という名前のプロパティを設定します。

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

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

例 1.5.

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
    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">

    <!-- Seam -->

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

    <!-- 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.6. JSF 設定 : faces-config.xml

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

例 1.6.

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

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

</faces-config>

faces-config.xml ファイルは、Seam を JSF に統合します。 JSF 管理 Bean 宣言が不要なことに留意してください。 JSF 管理 Bean は、Seam コンポーネントです。 普通の JSF アプリケーションと比較すると、 Seam アプリケーションは、faces-config.xml をほとんど使用しません。

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

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

1.2.1.7. EJB 配備記述子 : ejb-jar.xml

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

<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.8. 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.9. ビュー : register.jspregistered.jsp

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

例 1.7.

<%@ 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>Register New User</title>
 </head>
 <body>
  <f:view>
   <h:form>
     <table border="0">
       <s:validateAll>
         <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>
       </s:validateAll>
     </table>
     <h:messages/>
     <h:commandButton type="submit" value="Register" action="#{register.register}"/>
   </h:form>
  </f:view>
 </body>
</html>

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

例 1.8.

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

これは、標準 JSF コンポーネントを使用した何の変哲もない旧式の JSP ページです。 Seam 独特のものはありません。

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>
        <java>jboss-seam.jar</java>
    </module>
    <module>
        <java>el-api.jar</java>
    </module>
    <module>
        <java>el-ri.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 を見つけ、 それを返します。 JSF は、register() アクションリスナメソッドを呼び出します。

この呼び出しを続行する前に、 Seam はメソッドコールをインターセプトし、 Seam セッションコンテキストから User エンティティをインジェクトします。

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

そのユーザ名のユーザが存在しない場合は、 "/registered.jsp" 結果 (outcome) により registered.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.10.

@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

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

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

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

例 1.11.

@Stateful
@Scope(SESSION)
@Name("messageManager")
public class MessageManagerBean implements Serializable, MessageManager
{

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

}
(1)

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

(2)

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

(3)

@Outアノテーションは、次に選択された値を直接ページに公開します。 従って、クリック可能一覧の行が選択されるたびに、 Message は、ステートフル Bean の属性にインジェクションされ、 続いて message という名前のイベントコンテキスト変数にアウトジェクションされます。

(4)

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

(5)

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

(6)

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

(7)

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

(8)

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

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

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

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

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

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

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

1.3.1.4. ビュー: messages.jsp

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

例 1.12.

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

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

ユーザが <h:commandButton> をクリックすると、 JSF は、delete() アクションリスナを呼び出します。 Seam はこの呼び出しをインターセプトし、 選択された行データを messageList コンポーネントの message 属性にインジェクトします。 アクションリスナが起動し、 選択 Message が一覧から削除され、 EntityManagerremove() が呼び出されます。 呼び出しの終わりに、 Seam は messageList コンテキスト変数を更新し、 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.13.

<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> ノードは 待ち状態 を表します。 ビジネスプロセスの実行が一時停止され、 1 つまたは複数のタスクが行われるのを待機します。

(3)

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

(4)

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

(5)

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

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

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

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

例 1.14.

@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.15.

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

@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 ページからユーザ入力を受け取り、 タスク内容が設定されるように、それをプロセス定義に公開します。

(2)

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

(3)

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

現実的なサンプルでは、 @StartTask@EndTask が同じメソッド上に出現することはありません。 なぜなら、 通常はそのタスクを完了するために、 アプリケーションを使用して行うべき作業があるからです。

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

例 1.17.

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

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 のインスタンスです。 以下のコードは単に、一覧中の各タスクの興味深いプロパティを表示しています。 記述内容、 優先順や、 納期の値については、 ユーザーがこれらの値を更新できるよう入力コントロールを使用します。

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

ページの 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 ナビゲーション規則がページフローを定義するのに最適な方法となります。 画面遷移に制約が多いスタイルのアプリケーションの場合、 特によりステートフルなユーザインタフェースの場合、 ナビゲーション規則ではシステムの流れを本当に理解するのは困難になります。 フローを理解するには、 ビューページ、 アクション、 ナビゲーション規則からフローに関する情報をかき集める必要があります。

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

1.5.1. コードの理解

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

例 1.18.

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

<page> 要素は、待ち状態を定義しています。 ここでは、システムは特定の 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 式を評価することによって次に実行されるノードを決定します。

JBossIDE ページフローエディタでのページフローは以下のようになります。

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

これはアプリケーションの主要なページ numberGuess.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>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.20.

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

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

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

1.6.2. 予約サンプルの概要

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

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

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

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

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

1.6.3. Seam 対話の理解

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

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

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

さて、HttpSession に保管された状態を考察してみましょう。 非常に注意深くプログラミングを行うことにより、 セッションデータのサイズを制御できる場合があるかもしれませんが、 想像するよりずっと困難です。 なぜならば、 WEB ブラウザは非線形な画面操作を臨機応変に許可しているためです。 しかし、システムの開発途中で、 ユーザは複数の並列対話を持つことを許されると述べられたシステム要件を突然見つけたとします (私も同様の状況に遭遇しました)。 別々の並列対話に関連するセッションステートを分離するメカニズムを開発すること、 そhしてブラウザウィンドウやタブを閉じることでユーザーが対話のいずれかを中止する場合に対話ステートが必ず破棄されるようフェールセーフを組み込むことは、 気が小さくてはできないでしょう。 (私は 2 度ほど実装したことがあります。 1 つはクライアントアプリケーション、1 つはSeam でした。 だけど、私は病的なことで有名です。)

さらによい方法があります。

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

通常、対話コンテキスト中で保持するコンポーネントはステートフルセッション Bean です。 (対話コンテキスト中でエンティティ Bean や JavaBean を保持することもできます。) Java コミュニティには、ステートフルセッション Bean が スケーラビリティを無効にしてしまうというデマが古くからあります。 1988年に WebFoobar 1.0 がリリースされた頃は真実だったかもしれませんが、 今日ではもはや真実ではありません。 JBoss 4.0 のようなアプリケーションサーバーはステートフルセッション Bean ステートの複製に対して非常に優れたメカニズムを持っています。 (例えば、 JBoss EJB3 コンテナは微細な部分まで複製を行い、 実際に変化した bean 属性値のみの複製を行います。) なぜステートフル Bean が非効率的かに関する従来の技術的な議論はすべて HttpSession にも等しく当てはまります。 従って、 パフォーマンスを改善するために、 ビジネス層のステートフルセッション Bean から WEB セッションに移行する実践については信じられないほど誤った方向に誘導されているので注意してください。 ステートフルセッション Bean を使う、 ステートフル bean を誤って使う、 ステートフル bean を不正なことに使うなどして拡張性のないアプリケーションを記述することはたしかに可能です。 しかし、 絶対に使うべきではないという意味ではありません。 とにかく、 Seam は安全な使用モデルを目的として案内しています。 ようこそ、 2005 年へ。

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

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

手書きの JavaScript を使用することなくリッチクライアントの動作を実装するために、 Ajax4JSF を使用していることも示しています。

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

例 1.22.

@Stateful                                                                                (1)
@Name("hotelSearch")
@Scope(ScopeType.SESSION)
@Restrict("#{identity.loggedIn}")                                                        (2)
public class HotelSearchingAction implements HotelSearching
{
   
   @PersistenceContext
   private EntityManager em;
   
   private String searchString;
   private int pageSize = 10;
   private int page;
   
   @DataModel
   private List<Hotel> hotels;                                                           (3)
   
   public String find()
   {
      page = 0;
      queryHotels();   
      return "main";
   }

   public String nextPage()
   {
      page++;
      queryHotels();
      return "main";
   }
      
   private void queryHotels()
   {
      String searchPattern = searchString==null ? "%" : '%' + searchString.toLowerCase().replace('*', '%') + '%';
      hotels = em.createQuery("select h from Hotel h where lower(h.name) like :search or lower(h.city) like :search or lower(h.zip) like :search or lower(h.address) like :search")
            .setParameter("search", searchPattern)
            .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;
   }

   public String getSearchString()
   {
      return searchString;
   }

   public void setSearchString(String searchString)
   {
      this.searchString = searchString;
   }
   
   @Destroy @Remove
   public void destroy() {}                                                              (4)

}
(1)

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

(2)

@Restrict アノテーションはコンポーネントへのセキュリティ制限に適用します。 ログインユーザだけがコンポーネントにアクセスを許されるように制限します。 セキュリティの章では、Seam におけるセキュリティがさらに詳細に説明されています。

(3)

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

(4)

EJB 標準 @Remove アノテーションは、 アノテーション付きのメソッドが呼ばれた後に、 ステートフルセッション Bean が削除され、そして、その状態が破棄されることを定義しています。 Seam では、 すべての ステートフルセッション Bean は、 @Destroy @Remove 付きメソッドが定義される必要があります。 これは、Seam がセッションコンテキストを破棄するときに呼ばれる EJB remove メソッドです。 実際、 @Destroy アノテーションは、 Seam コンテキストが終了するときに発生するさまざまな種類のクリーンアップに利用可能であり、 とても実用的です。 @Destroy @Remove メソッドがないと、 状態がリークするのでパフォーマンス関連の問題に悩まされます。

このアプリケーションの主なページは、Facelets ページです。 ホテルを検索に関連する抜粋を見てみましょう。

例 1.23.

<div class="section">
<h:form>
  
  <span class="errors">
    <h:messages globalOnly="true"/>
  </span>
    
  <h1>Search Hotels</h1>
  <fieldset> 
     <h:inputText value="#{hotelSearch.searchString}" style="width: 165px;">
        <a:support event="onkeyup" actionListener="#{hotelSearch.find}"                  (1)
                   reRender="searchResults" />
     </h:inputText>
     &#160;
     <a:commandButton value="Find Hotels" action="#{hotelSearch.find}" 
                      styleClass="button" reRender="searchResults"/>
     &#160;
     <a:status>                                                                          (2)
        <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">                                                       (3)
  <div class="section">
  <h:outputText value="No Hotels Found" 
                rendered="#{hotels != null and hotels.rowCount==0}"/>
  <h:dataTable 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 value="View Hotel" action="#{hotelBooking.selectHotel(hot)}"/>             (4)
    </h:column>
  </h:dataTable>
  <s:link value="More results" action="#{hotelSearch.nextPage}" 
          rendered="#{hotelSearch.nextPageAvailable}"/>
  </div>
</a:outputPanel>
(1)

Ajax4JSF <a:support> タグは、 onkeyup のような JavaScript イベントが発生したとき、 JSF アクションイベントリスナが非同期の XMLHttpRequest から呼ばれることを可能にしています。 さらに良いことには、 再レンダリング属性は、JSF ページの一部分だけのレンダリングを可能とし、 非同期のレスポンスを受信したときに、部分的なページ更新の実行を可能にしています。

(2)

Ajax4JSF <a:status> タグは、 非同期のリクエストが返されるのを待つ間に、 ちょっとしたアニメーションイメージを表示させます。

(3)

Ajax4JSF <a:outputPanel> タグは、 非同期リクエストによって再レンダリング可能なページの領域を定義します。

(4)

Seam <s:link> タグは、 JSF アクションリスナを普通の (非 JavaScript) HTML リンクに付けることができます。 標準 <h:commandLink> と比べてこれが有利なのは、 "新しいウィンドウで開く" や "新しいタブで開く"といった操作を損なわないことです。 パラメータ #{hotelBooking.selectHotel(hot)} のメソッドバインディングを利用していることにも留意してください。 これは、標準統一された EL式 では不可能ですが、 Seam は、 すべてのメソッドバインディング表現でパラメータ使用できるよう EL式 を拡張しています。

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

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

例 1.24.

@Stateful
@Name("hotelBooking")
@Restrict("#{identity.loggedIn}")
public class HotelBookingAction implements HotelBooking
{
   
   @PersistenceContext(type=EXTENDED)                                                    (1)
   private EntityManager em;
   
   @In                                                                                   (2)
   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;
   
   @Begin                                                                                (3)
   public String selectHotel(Hotel selectedHotel)
   {
      hotel = em.merge(selectedHotel);
      return "hotel";
   }
   
   public String 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() );
      
      return "book";
   }

   public String setBookingDetails()
   {
      if (booking==null || hotel==null) return "main";
      if ( !booking.getCheckinDate().before( booking.getCheckoutDate() ) )
      {
         facesMessages.add("Check out date must be later than check in date");
         return null;
      }
      else
      {
         return "confirm";
      }
   }

   @End                                                                                  (4)
   public String confirm()
   {
      if (booking==null || hotel==null) return "main";
      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.raiseEvent("bookingConfirmed");
      return "confirmed";
   }
   
   @End
   public String cancel()
   {
      return "main";
   }
   
   @Destroy @Remove                                                                      (5)
   public void destroy() {}

}
(1)

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

(2)

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

(3)

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

(4)

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

(5)

Seam は、対話コンテキストを破棄するとき、 この EJB remove メソッドは、呼び出されます。 このメソッドを定義することを忘れないでください。

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

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

1.6.4. The Seam UI 管理ライブラリ

予約アプリケーションの WAR ファイルの中身をチェックすれば、 WEB-INF/lib ディレクトリ中に seam-ui.jar が見つかります。 このパッケージは、Seam と統合する多くの JSF カスタムコントロールを含んでいます。 この予約アプリケーションは、 検索画面からホテルページへの画面遷移制御に <s:link> を使用しています。

<s:link value="View Hotel" action="#{hotelBooking.selectHotel}"/>

ここで、<s:link> を使用することで、 ブラウザの "新しいウィンドウを開く" 機能を阻害することなく、 アクションリスナに HTML リンクを付けることが可能です。 標準 JSF <h:commandLink> は、"新しいウィンドウを開く" と連動しません。 <s:link> は、 対話伝播ルールを含むその他多くの便利な機能を提供します。

予約アプリケーションは、特に、/book.xhtml においていくつかの他の Seam コントロールと Ajax4JSF コントロールを使用します。 ここでは、それらコントロールの詳細には触れませんが、 このコードを理解したいならば、 JSF フォームバリデーションのための Seam の機能を対象とする章を参照してください。

1.6.5. Seam デバッグページ

WAR は、seam-debug.jar も含みます。 この JAR が Facelet とともに WEB-INF/lib にデプロイされ、 そして、web.xml あるいは、seam.properties 中に以下のように Seam プロパティを設定した場合、

<context-param>
    <param-name>org.jboss.seam.core.init.debug</param-name>
    <param-value>true</param-value>
</context-param>

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

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

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

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

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

TODO

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

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

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

TODO

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

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

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

TODO

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

1.10. RESTful Seam アプリケーション: Blog サンプル

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

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

1.10.1. "PULL" 型 MVC の使用

index.xhtml facelets ページの一部は、 最新の Blog 登録のリストを表示しています。

例 1.25.

<h:dataTable value="#{blog.recentBlogEntries}" var="blogEntry" rows="3">
   <h:column>
      <div class="blogEntry">
         <h3>#{blogEntry.title}</h3>
         <div>
            <h:outputText escape="false" 
                  value="#{blogEntry.excerpt==null ? blogEntry.body : blogEntry.excerpt}"/>
         </div>
         <p>
            <h:outputLink value="entry.seam" rendered="#{blogEntry.excerpt!=null}">
               <f:param name="blogEntryId" value="#{blogEntry.id}"/>
               Read more...
            </h:outputLink>
         </p>
         <p>
            [Posted on 
            <h:outputText value="#{blogEntry.date}">
               <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
            </h:outputText>]
            &#160;
            <h:outputLink value="entry.seam">[Link]
               <f:param name="blogEntryId" value="#{blogEntry.id}"/>
            </h:outputLink>
         </p>
      </div>
   </h:column>
</h:dataTable>

ブックマークからこのページに移動する場合、 <h:dataTable> により使用されるデータは実際にはどのように初期化されるのでしょうか。 ここで何が起きているかというと、 BlogBlog という名前の Seam コンポーネントによって、 必要なときに遅延して取得されて — 引き出されて — いるわけです。 これは、 Struts のような従来の WEB アクションに基づくフレームワークでは一般的なフロー制御と逆になっています。

例 1.26.

@Name("blog")
@Scope(ScopeType.STATELESS)
public class BlogService 
{
   
   @In                                                                                   (1)
   private EntityManager entityManager;
  
   @Unwrap                                                                               (2)
   public Blog getBlog()
   {
      return (Blog) entityManager.createQuery("from Blog b left join fetch b.blogEntries")
            .setHint("org.hibernate.cacheable", true)
            .getSingleResult();
   }

}
(1)

このコンポーネントは、Seam 管理永続コンテキスト を使用します。 これまで見てきた他のサンプルとは異なり、 この永続コンテキストは、Seam によって管理されます。 この永続コンテキストは、WEB リクエスト全体で有効で、 取得していないビュー関連にアクセスするときに発生するあらゆる例外の回避を可能にします。

(2)

@Unwrap アノテーションは、 Seam がクライアントに対して実際の BlogService コンポーネントではなくメソッドの返り値 — Blog — を渡すよう指示します。 これが Seam 管理コンポーネントパターン です。

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

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

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

例 1.27.

<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 にリダイレクトします。 別の方法として、以下のようなナビゲーションルールを定義することも可能です。

例 1.28.

<navigation-rule>
   <navigation-case>
      <from-outcome>searchResults</from-outcome>
      <to-view-id>/search.xhtml</to-view-id>
      <redirect/>
   </navigation-case>
</navigation-rule>

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

例 1.29.

<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 は提供します。 WEB-INF/pages.xml で定義された Seam page parameter を使用します。

例 1.30.

<pages>
   <page view-id="/search.xhtml">
      <param name="searchPattern" value="#{searchService.searchPattern}"/>
   </page>
   ...
</pages>

これは、ページにリダイレクトするときに、Seam に searchPattern の名前のリクエストパラメータとして #{searchService.searchPattern} の値を含み、ページをレンダリングする前に、パラメータの値をモデルに再適用するよう指示します。

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

例 1.31.

<h:dataTable value="#{searchResults}" var="blogEntry">
   <h:column>
      <div>
         <h:outputLink value="entry.seam">
            <f:param name="blogEntryId" value="#{blogEntry.id}"/>
            #{blogEntry.title}
         </h:outputLink>
         posted on 
         <h:outputText value="#{blogEntry.date}">
            <f:convertDateTime timeZone="#{blog.timeZone}" locale="#{blog.locale}" type="both"/>
         </h:outputText>
      </div>
   </h:column>
</h:dataTable>

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

例 1.32.

@Name("searchService")
public class SearchService 
{
   
   @In
   private EntityManager entityManager;
   
   private String searchPattern;
   
   @Factory("searchResults")
   public List<BlogEntry> getSearchResults()
   {
      if (searchPattern==null)
      {
         return null;
      }
      else
      {
         return entityManager.createQuery("select be from BlogEntry be where lower(be.title) like :searchPattern or lower(be.body) like :searchPattern order by be.date desc")
               .setParameter( "searchPattern", getSqlSearchPattern() )
               .setMaxResults(100)
               .getResultList();
      }
   }

   private String getSqlSearchPattern()
   {
      return searchPattern==null ? "" : '%' + searchPattern.toLowerCase().replace('*', '%').replace('?', '_') + '%';
   }

   public String getSearchPattern()
   {
      return searchPattern;
   }

   public void setSearchPattern(String searchPattern)
   {
      this.searchPattern = searchPattern;
   }

}

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

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

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

例 1.33.

@Name("entryAction")
@Scope(STATELESS)
public class EntryAction
{
   @In(create=true) 
   private Blog blog;
   
   @Out
   private BlogEntry blogEntry;
   
   public void loadBlogEntry(String id) throws EntryNotFoundException
   {
      blogEntry = blog.getBlogEntry(id);
      if (blogEntry==null) throw new EntryNotFoundException(id);
   }
   
}

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

例 1.34.

<pages>
   ...

   <page view-id="/entry.xhtml" action="#{entryAction.loadBlogEntry(blogEntry.id)}">
      <param name="blogEntryId" value="#{blogEntry.id}"/>
   </page>

   <page view-id="/post.xhtml" action="#{loginAction.challenge}"/>

   <page view-id="*" action="#{blog.hitCount.hit}"/>

</pages>

上記の例は、 その他の機能に対してページアクションを使っています — ログインチャレンジやページビューカウンタなど。 また、 ページアクションメソッドのバインディングにパラメータを使用しているのにも注目してください。 これは JSF EL の標準機能ではありませんが、 Seam ではページアクションだけでなく JSF メソッドのバインディングでも使用できるようになっています。

entry.xhtml ページがリクエストされると、 Seam は最初にページパラメータ blogEntryId をモデルにバインドし、 次に必要なデータ — blogEntry — を取得するページアクションを実行してから、 それを Seam イベントコンテキストに配置します。 最後に、以下がレンダリングされます。

例 1.35.

<div class="blogEntry">
   <h3>#{blogEntry.title}</h3>
   <div>
      <h:outputText escape="false" 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 であって欲しいので、 例外クラスのアノテーションを付けます。

例 1.36.

@ApplicationException(rollback=true)
@HttpError(errorCode=HttpServletResponse.SC_NOT_FOUND)
public class EntryNotFoundException extends Exception
{
   EntryNotFoundException(String id)
   {
      super("entry not found: " + id);
   }
}

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

例 1.37.

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

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

1.11. JSF 1.2 RI を使用した JBoss Seam サンプルの実行

JBoss AS 4.0 は JSF 1.1 実装 Apache MyFaces を同梱しています。 長い間待ちましたが、 いまだ MyFaces からの JSF 1.2 実装はありません。 これ以外の理由も含めて、 JBoss AS 4.2 はデフォルトで JSF 1.2 リファレンス実装を組み込むことになります。 4.2 のリリース後すぐに、 Seam サンプルを JSF 1.2 に移行する予定です。

待つことができない人のために、Seam は、すでに JSF 1.2 互換であり、 JBoss 4.0.5 上で JSF 1.2 を実行する Seam サンプルを手に入れることは簡単です。 あの予約サンプルで始めましょう。

  • jsf-api.jarjsf-impl.jarel-api.jarel-ri.jarserver/default/deploy/tomcat/jbossweb-tomcat55.sar/jsf-libsにコピーします。

  • myfaces-api.jarmyfaces-impl.jarserver/default/deploy/tomcat/jbossweb-tomcat55.sar/jsf-libs から削除します。

  • server/default/deploy/tomcat/jbossweb-tomcat55.sar/conf/web.xml を編集して、 myfaces-impl.jarjsf-impl.jar に置き換えます。

  • examples/booking/resources/WEB-INF/web.xml を編集して、 MyFaces listener を削除し RI listener のコメントを外します。

  • examples/booking/resources/WEB-INF/faces-config.xml を編集して、 JSF 1.2 XML スキーマ宣言を使用する SeamELResolver をインストールする行のコメントを外します。

  • examples/booking/resources/META-INF/application.xml を編集して、 Java モジュールとして el-api.jarel-impl.jar を宣言している行を削除します。

JBoss を再起動し、 examples/booking ディレクトリで ant と入力します。