本章では、 そろそろ Seam の対話モデルについて詳細に理解していくことにします。
事のはじまりは、 3 つの思いつきが融合した結果、 Seam 「対話」の概念となったことです。
ワークスペース という目的、 これは 2002 年にビクトリア州政府 (オーストラリア) のプロジェクトで思いつきました。 このプロジェクトで、私は Struts の上にワークスペース管理を実装せざるを得なくなりました。2 度と繰り返したいとは思わないような経験でした。
楽観的セマンティクスで動作するアプリケーショントランザクションという思いつきに加え、 ステートレスなアーキテクチャをベースとする既存のフレームワークでは拡張された永続コンテキストの効率的な管理は実現できないことを実感した事実でした。 (Hibernate チームは LazyInitializationException に対する非難は聞き飽きていましたし、 実際にはこれは Hibernate に問題があるのではなく、 むしろ Spring フレームワークや J2EE における従来の stateless session facade (anti) パターンなどステートレスアーキテクチャでサポートされる極端に限定的な永続コンテキストモデルに問題があったのです。)
ワークフロータスクという思いつき
こうした思いつきを統一しフレームワークで強力なサポートを提供することで、 以前よりすっきりしたコードでより豊かで効率的なアプリケーションをビルドできるパワフルな構成概念を得ました。
これまでに見た例は、以下の規則に従う非常に単純な対話モデルを利用します。
JSF リクエストライフサイクルである、 リクエスト値の適用、 プロセスの妥当性検証、 モデル値の更新、 アプリケーションの呼び出し、 レスポンスのレンダリングの各フェーズの期間、 常にアクティブな対話コンテキストがあります。
JSF リクエストライフサイクルであるビューの復元フェーズの終わりで、 Seam はそれまでの長期対話コンテキストの復元を試みます。 存在しなければ、 Seam は新しいテンポラリの対話コンテキストを生成します。
@Begin メソッドが出てくると、 テンポラリの対話コンテキストは長期対話に昇格します。
@End メソッドが出てくると、 どのような長期対話コンテキストでもテンポラリの対話に降格されます。
JSF リクエストライフサイクルであるレスポンスのレンダリングのフェーズの終わりで、 Seam は長期対話コンテキストの内容を記憶するか、 テンポラリ対話コンテキストの内容を破棄します。
どのような faces リクエスト (JSF ポストバック) でも対話コンテキストを伝播します。 デフォルトでは、 non-faces リクエスト (例えば、 GET リクエスト) は対話コンテキストを伝播しませんが、 これについての詳細は下記を参照してください。
JSF リクエストライフサイクルがリダイレクトによって短縮される場合、 Seam は透過的に現在の対話コンテキストを保存及び復元します。 — その対話が @End (beforeRedirect=true) で既に終了されていない限り。
Seam は透過的に JSF のポストバックとリダイレクト全体に渡り対話コンテキストを伝播します。 特に何もしなければ、 non-faces リクエスト (例えば、 GETリクエスト) は対話コンテキストを伝播せずに新しいテンポラリの対話内で処理されます。 これが通常 (必ずではないが) 望ましい動作となります。
non-faces リクエスト全体に Seam 対話を伝播させたい場合、 リクエストパラメータとして Seam 対話 ID を明示的にコード化する必要があります。
<a href="main.jsf?conversationId=#{conversation.id}">Continue</a>
JSF のようにする場合には以下のようにします。
<h:outputLink value="main.jsf"> <f:param name="conversationId" value="#{conversation.id}"/> <h:outputText value="Continue"/> </h:outputLink>
Seam タグライブラリを使用する場合、 以下は等価です。
<h:outputLink value="main.jsf"> <s:conversationId/> <h:outputText value="Continue"/> </h:outputLink>
ポストバック用の対話コンテキストの伝播を無効にしたい場合は、 同様のテクニックが使えます。
<h:commandLink action="main" value="Exit"> <f:param name="conversationPropagation" value="none"/> </h:commandLink>
Seam タグライブラリを使用する場合、 以下は等価です。
<h:commandLink action="main" value="Exit"> <s:conversationPropagation type="none"/> </h:commandLink>
対話コンテキストの伝播を無効にすることと、 対話を終了することとは全く異なることですので注意してください。
conversationPropagation リクエストパラメータ または <s:conversationPropagation> タグは、 対話の開始と終了、あるいはネストされた対話の開始にも使用することができます。
<h:commandLink action="main" value="Exit"> <s:conversationPropagation type="end"/> </h:commandLink>
<h:commandLink action="main" value="Select Child"> <s:conversationPropagation type="nested"/> </h:commandLink>
<h:commandLink action="main" value="Select Hotel"> <s:conversationPropagation type="begin"/> </h:commandLink>
<h:commandLink action="main" value="Select Hotel"> <s:conversationPropagation type="join"/> </h:commandLink>
この対話モデルは、 マルチウィンドウ操作に関して正常に動作するアプリケーションの構築を容易してくれます。 多くのアプリケーションにとって必要なものはこれだけです。 複雑なアプリケーションのなかには以下の追加要件の両方あるいはどちらかを必要とするものがあります。
対話には、 連続的に実行したり同時に実行することもある多くの小さな単位のユーザーインタラクションも含まれます。 より小さいネストされた対話には単独の対話状態セットがあり、 また外側の対話状態へのアクセスもあります。
ユーザは同じブラウザのウィンドウ内でいくつもの対話を切り換えることができます。 この機能がワークスペース管理と呼ばれるものです。
ネストされた対話は既存の対話のスコープ内で @@Begin(nested=true) とマークされたメソッドを呼び出すことによって作成されます。 ネストされた対話はそれ自身の対話コンテキストを持っていて、 また、 外側の対話のコンテキストへの読取り専用アクセスも持っています (外側の対話のコンテキスト変数を読むことはできるが、書き込みはできない) 。 次に @End が出てくると、 ネストされた対話は破棄されて外側の対話が対話スタックを「POP」することによって再開します。 対話は任意の深さにネストすることができます。
特定のユーザーアクティビティ (ワークスペース管理や戻るボタン) により、 内側の対話が終了する前に外側の対話が開始させることができます。 この場合、 同じ外側の対話に属する複数の並列ネスト対話を持つことができます。 ネストされた対話が終了する前に外側の対話が終了すると、 Seam はネストされた対話コンテキストを外側のコンテキストと共にすべて破棄します。
対話は継続可能な状態と見なすこともできます。 ネストされた対話により、 ユーザーインタラクションにおける様々なポイントにおいてアプリケーションは一貫した継続可能な状態を捕らえることができるようになります。 従って、 戻るボタンを押すことやワークスペース管理に対して正しい動作を保証します。
TODO: 戻るボタンを押した場合に、 どのようにしてネストされた対話が不正が発生しないよう防止するかを示す例。
通常、 現在ネストされている対話の親となる対話にコンポーネントが存在する場合、 このネストされている対話は同じインスタンスを使用します。ときには、 親となる対話内に存在するコンポーネントインスタンスがその子となる対話からは見えなくなるように、 ネストされるそれぞれの対話内に別々のインスタンスを持たせると便利なことがあります。 コンポーネントに @PerNestedConversation アノテーションを付けるとこれを行うことができます。
ページが non-faces リクエスト (例、 HTTP GET リクエスト) 経由でアクセスされる場合、 JSF は起動されるアクションリスナを全く定義しません。 ユーザーがそのページをブックマークする、あるいは <h:outputLink> からそのページに行き着く場合などに発生します。
ページがアクセスされたら直ちに対話を開始したい場合があります。 JSF アクションメソッドがないため、 アクションに @Begin アノテーションを付けるという普通の方法では問題を解決することができません。
このページが状態をコンテキスト変数にフェッチする必要がある場合、 さらなる問題が発生します。 すでに、2 つの問題解決方法を見てきました。 Seam コンポーネントにその状態が保持される場合、 @Create メソッドでその状態をフェッチできます。 保持されていなければ、 コンテキスト変数に対して @Factory メソッドを定義することができます。
これらのオプションがうまくいかない場合、 Seam では pages.xml ファイルに ページアクション を定義することができます。
<pages> <page view-id="/messageList.jsp" action="#{messageManager.list}"/> ... </pages>
ページがレンダリングされるようとするときは常に、 レスポンスのレンダリングフェーズの冒頭でこのアクションメソッドが呼び出されます。 ページアクションが null 以外の結果を返す場合、 Seam は適切な JSF 及び Seam ナビゲーションの規則を処理するので、 まったく異なるページがレンダリングさることになるかもしれません。
ページのレンダリング前に行いたいことが対話の開始だけなら、 ビルトインアクションメソッドを次のように使用できます。
<pages> <page view-id="/messageList.jsp" action="#{conversation.begin}"/> ... </pages>
また、 このビルトインアクションは JSF コントロールからも呼び出すことができ、 同様に #{conversation.end} を使って対話を終了することができます。
既存の対話にジョインするあるいはネストされる対話を開始する、 ページフローまたはアトミック対話を開始するためにさらに制御が必要な場合は、 <begin-conversation> エレメントを使用してください。
<pages> <page view-id="/messageList.jsp"> <begin-conversation nested="true" pageflow="AddItem"/> <page> ... </pages>
また、 <end-conversation> エレメントもあります。
<pages> <page view-id="/home.jsp"> <end-conversation/> <page> ... </pages>
1 番目の問題を解決するには、 現在 5 つのオプションから選択できます。
@Create メソッドに @Begin アノテーションを追加する
@Factory メソッドに @Begin アノテーションを追加する
Seam ページアクションメソッドに @Begin アノテーションを追加する
pages.xml で <begin-conversation> を使用する
#{conversation.begin} を Seam ページアクションメソッドとして使用する
JSF コマンドリンクは常に JavaScript でフォームサブミットを行います。 これによりウェブブラウザの「新しいウィンドウで開く」または「新しいタブで開く」機能を動作させなくしてしまいます。 プレーンの JFS でこの機能が必要な場合は、 <h:outputLink> を使用する必要があります。 ただし、 <h:outputLink> には重要な制限が 2 つあります。
JSF はアクションリスナーを <h:outputLink> につなげる方法を提供していません。
実際にフォームサブミットがないため、 JSF は選択された DataModel の列を伝播しません。
Seam は 1 番目の問題解決に対しては ページアクションという概念を提供していますが、 これは 2 番目の問題についてはまったく役に立ちません。 これについてはリクエストパラメータを渡すという RESTful 方法を使ってサーバー側にある選択オブジェクトに再問い合わせを行うことで回避できました。 Seam ブログサンプルアプリケーションなどの場合には、 実際にこれが最適の手段となります。 RESTful スタイルはサーバー側の状態を必要としないためブックマーク機能をサポートします。 ブックマークはあまり必要ないなどこれ以外の場合は、 @DataModel および @DataModelSelection を使用すると非常に便利で透過的になります。
この機能を補ってさらに対話伝播の管理をより簡略化するために、 Seam は <s:link> JSF タグを提供しています。
このリンクは JSF ビュー ID だけを指定することができます。
<s:link view=“/login.xhtml” value=“Login”/>
あるいは、 アクションメソッドを指定することができます (この場合、 アクションの結果は最終的なページを確定する)。
<s:link action=“#{login.logout}” value=“Logout”/>
JSF ビュー ID とアクションメソッドの両方を指定すると、 アクションメソッドが null 以外の結果を返さない限り「ビュー」が使用されます。
<s:link view="/loggedOut.xhtml" action=“#{login.logout}” value=“Logout”/>
リンクは <h:dataTable> 内で使用する DataModel の選択列を自動的に伝播します。
<s:link view=“/hotel.xhtml” action=“#{hotelSearch.selectHotel}” value=“#{hotel.name}”/>
既存の対話のスコープを残しておくことができます。
<s:link view=“/main.xhtml” propagation=“none”/>
対話を開始、 終了、 またはネストすることができます。
<s:link action=“#{issueEditor.viewComment}” propagation=“nest”/>
リンクが対話を開始すると、 使用されるページプローを指定することもできます。
<s:link action=“#{documentEditor.getDocument}” propagation=“begin” pageflow=“EditDocument”/>
jBPM タスクリストを使用する場合の taskInstance 属性です。
<s:link action=“#{documentApproval.approveOrReject}” taskInstance=“#{task}”/>
(上記の例は DVD ストアデモアプリケーションを参照してください。)
最後に、 ボタンとしてレンダリングされる「リンク」が必要な場合は <s:button> を使用します。
<s:button action=“#{login.logout}” value=“Logout”/>
動作に対して成功したか失敗したかをユーザーに示すメッセージを表示するのは非常に一般的です。 これには、 JSF FacesMessage を使うと便利です。 残念ながら、 成功のアクションはブラウザリダイレクトを要することが多く、 JSF はリダイレクト全体に faces のメッセージは伝播しません。 このためプレーン JSF で成功のメッセージを表示するのはかなり困難になります。
ビルトインの対話スコープ Seam コンポーネントである facesMessages がこの問題を解決してくれます。 (Seam リダイレクトフィルタをインストールしておく必要があります。)
@Name("editDocumentAction") @Stateless public class EditDocumentBean implements EditDocument { @In EntityManager em; @In Document document; @In FacesMessages facesMessages; public String update() { em.merge(document); facesMessages.add("Document updated"); } }
facesMessages に追加されるメッセージはすべてすぐ次のフェースであるレスポンスレンダリングフェーズで現在の対話に対して使用されます。 これは Seam がリダイレクト全体に一時的な対話コンテキストを維持するので長期実行の対話がない場合でも機能します。
JSF EL 式を faces メッセージサマリーに含ませることもできます。
facesMessages.add("Document #{document.title} was updated");
たとえば、 通常の方法でメッセージを表示することができます。
<h:messages globalOnly="true"/>
通常、 Seam は各セッション内の各対話に対して特に意味のない固有 ID を生成します。 対話を開始するときに、 この ID 値をカスタマイズすることができます。
この機能を使って対話 ID 生成アルゴリズムをカスタマイズすることができます。
@Begin(id="#{myConversationIdGenerator.nextId}") public void editHotel() { ... }
あるいは、 何か意味のある対話 ID を割り当てるのに使うこともできます。
@Begin(id="hotel#{hotel.id}") public String editHotel() { ... }
@Begin(id="hotel#{hotelsDataModel.rowData.id}") public String selectHotel() { ... }
@Begin(id="entry#{params['blogId']}") public String viewBlogEntry() { ... }
@BeginTask(id="task#{taskInstance.id}") public String approveDocument() { ... }
ご覧のように、 上記の例では、 特定のホテル、ブログ、あるいはタスクが選択される毎に同じ対話 id となります。 既に存在するものと同じ対話 id での対話では何が起こるのでしょうか? そうです。 Seam は既存の対話を検出し、 @Begin メソッドを再び開始することなくその対話へリダイレクトします。 この特長は、 ワークスペース管理を使用するときに生成されるワークスペースの数を制御する際に役立ちます。
ワークスペース管理は、1 つのウィンドウの中で対話を「切り換える」能力です。 Seam はワークスペース管理を Java コードのレベルで完全に透過的にします。 ワークスペース管理を可能にするために、必要なすべては以下の通りです。
それぞれのビュー ID (JSF または Seam ナビゲーションルールを使用する場合) またはページノード (jPDL ページフロー) に詳細のテキストを入力します。 この詳細テキストはワークスペース切り替えによってユーザーに表示されます。
JSF または Seam ナビゲーションルールを使用する場合、 Seam は対話の現在の view-id を復元してその対話に切り替えます。 ワークスペースの記述的なテキストは pages.xml と呼ばれるファイルで定義され、 Seam はこのファイルが WEB-INF ディレクトリ内の faces-config.xml のすぐ次に配置されていることを期待します。
<pages> <page view-id="/main.xhtml">Search hotels: #{hotelBooking.searchString}</page> <page view-id="/hotel.xhtml">View hotel: #{hotel.name}</page> <page view-id="/book.xhtml">Book hotel: #{hotel.name}</page> <page view-id="/confirm.xhtml">Confirm: #{booking.description}</page> </pages>
このファイルが期待する場所になくても Seam アプリケーションは正常に動作を続行します。 動作しない機能はワークスペースの切り替え機能のみです。
jPDL ページフロー定義を使う場合、 Seam は現在の jBPM のプロセス状態を復元することによって対話に切り替えます。 同じ view-id に現在の <page> に応じて異なる詳細を持たせることができるためこれはより柔軟なモデルになります。 詳細テキストは <page> ノードで定義されます。
<pageflow-definition name="shopping"> <start-state name="start"> <transition to="browse"/> </start-state> <page name="browse" view-id="/browse.xhtml"> <description>DVD Search: #{search.searchPattern}</description> <transition to="browse"/> <transition name="checkout" to="checkout"/> </page> <page name="checkout" view-id="/checkout.xhtml"> <description>Purchase: $#{cart.total}</description> <transition to="checkout"/> <transition name="complete" to="complete"/> </page> <page name="complete" view-id="/complete.xhtml"> <end-conversation /> </page> </pageflow-definition>
次の断片を JSP または facelets のページに含ませて、 現在の対話またはその他いずれのアプリケーションのページにも切り替えられるドロップダウンメニューを取得します。
<h:selectOneMenu value="#{switcher.conversationIdOrOutcome}"> <f:selectItem itemLabel="Find Issues" itemValue="findIssue"/> <f:selectItem itemLabel="Create Issue" itemValue="editIssue"/> <f:selectItems value="#{switcher.selectItems}"/> </h:selectOneMenu> <h:commandButton action="#{switcher.select}" value="Switch"/>
この例では、 ユーザに新しい対話を開始させる 2 つの追加アイテムに加えて、 各対話のためのアイテムを含むメニューがあります。
対話一覧は対話切り替えに非常によく似ていますが、 表形式で表示される点が異なります。
<h:dataTable value="#{conversationList}" var="entry" rendered="#{not empty conversationList}"> <h:column> <f:facet name="header">Workspace</f:facet> <h:commandLink action="#{entry.select}" value="#{entry.description}"/> <h:outputText value="[current]" rendered="#{entry.current}"/> </h:column> <h:column> <f:facet name="header">Activity</f:facet> <h:outputText value="#{entry.startDatetime}"> <f:convertDateTime type="time" pattern="hh:mm a"/> </h:outputText> <h:outputText value=" - "/> <h:outputText value="#{entry.lastDatetime}"> <f:convertDateTime type="time" pattern="hh:mm a"/> </h:outputText> </h:column> <h:column> <f:facet name="header">Action</f:facet> <h:commandButton action="#{entry.select}" value="#{msg.Switch}"/> <h:commandButton action="#{entry.destroy}" value="#{msg.Destroy}"/> </h:column> </h:dataTable>
恐らく、 多くの方が独自のアプリケーションに合うようカスタマイズを希望するだろうと思います。
対話一覧は便利ですが、 ページ上で対話一覧によって占有される領域が多いため、 すべてのページに対しては対話一覧を置きたくないでしょう。
対話一覧によりユーザーがワークスペースを破壊することができるので留意してください。
ブレッドクラムは、 ネストされた対話モデルを使うアプリケーションで役に立ちます。 ブレッドクラムは、 現在の対話スタック内の対話へのリンクの一覧になります。
<t:dataList value="#{conversationStack}" var="entry"> <h:outputText value=" | "/> <h:commandLink value="#{entry.description}" action="#{entry.select}"/> </t:dataList>
JSF は驚いたことにループに対して標準コンポーネントを全く提供していないため、 ここでは、 MyFaces <t:dataList> コンポーネントを使用していることに注意してください。
これらすべての機能が動作しているところを確認するには、 Seam 問題追跡システム (Seam Issue Tracker) デモを参照してください。
重要ではありませんが、 JSF コンポーネントへのバインディングの保持には使用できないという制限が対話型コンポーネントにはあります。 (一般的には、 アプリケーション論理からビューに対する強い依存関係を作ってしまうため、 絶対的に必要でない限り一般的にはこの JSF の機能は使用しないようにした方がよいでしょう。) postback リクエストで、 Seam 対話コンテキストが復元される前、 ビューの復元フェーズ中にコンポーネントのバインディングは更新されます。
これを回避するには、 イベントスコープコンポーネントを使ってコンポーネントバインディングを格納し、 それを必要とする対話スコープコンポーネントにインジェクトします。
@Name("grid") @Scope(ScopeType.EVENT) public class Grid { private HtmlPanelGrid htmlPanelGrid; // getters and setters ... }
@Name("gridEditor") @Scope(ScopeType.CONVERSATION) public class GridEditor { @In(required=false) private Grid grid; ... }