JavaEE/JSF/Flow

Last-modified: 2013-09-16 (月) 00:31:38

Faces Flowについての解説

Faces Flow

画面と画面遷移をセットにして部品化する仕組み。

フローを定義する。

フローは、おおまかに言えば、画面と画面遷移のセット。フローは有向グラフ(ノードとエッジの集合)として表現される。

Faces Flowにおけるノードは以下の5種類がある。(JavadocのFlowHandler)

View
通常の、FaceletやJSPで作成したビュー。
Return
フローの呼び出し元に戻る出力を表す。
Faces Flow Call
フローから別のフローの呼び出しを表す。
Switch
EL式のリストで、最初にtrueを返した式に応じて次のノードが決定される。
Method Call
任意のアプリケーション路実行の呼び出し。呼び出し後、その出力によってナビゲーションルールに従った遷移をさせることが可能。

フローの入口となるノードを一つ定める必要がある。これを開始ノードと呼ぶ。必ずしもViewノードでなくてもよい。

フローはネストすることが可能。フローから別のフローを呼び出したら、呼び出されたフローがReturnノードに到達したときに元のフローに返ってくる。

フローは次のどちらの方法でも定義できる。

  • XMLで定義する
  • FlowBilderクラスを使って、プログラムで定義する。

フローをXMLで定義

ファイル名とファイルの配置場所については後述。

記述例(JavaEE 7 Tutorial より引用)

<faces-config version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd">
   <!-- ★フロー1個を定義。idで名前を定義 -->
   <flow-definition id="joinFlow">
       <!-- ★ Return ノードの定義 必須。 -->
       <flow-return id="returnFromJoinFlow">
           <from-outcome>#{joinFlowBean.returnValue}</from-outcome>
       </flow-return>
       <!-- ★ このフローの入力パラメータの定義
               受け取った値は、EL式でバインドされたプロパティに代入される。 -->
       <inbound-parameter>
           <name>param1FromCheckoutFlow</name>
           <value>#{flowScope.param1Value}</value>
       </inbound-parameter>
       <inbound-parameter>
           <name>param2FromCheckoutFlow</name>
           <value>#{flowScope.param2Value}</value>
       </inbound-parameter>
       <!-- ★ Faces Flow Callノードの定義。他のフローを呼び出す場合に使う。-->
       <flow-call id="callcheckoutFlow">
           <flow-reference>
               <!-- どのフローを呼び出すか -->
               <flow-id>checkoutFlow</flow-id>
           </flow-reference>
           <!-- 呼び出し時の引数。名前と値をそれぞれ定義。
                名前は、呼び出し先のinbound-parameterで定義した名前と一致させる。-->
           <outbound-parameter>
               <name>param1FromJoinFlow</name>
               <!-- 値は、この例では固定的な文字列になってしまっているが、
                    通常はEL式を記述して、Beanのプロパティの値を、引数の値として使う。-->
               <value>param1 joinFlow value</value>
           </outbound-parameter>
           <outbound-parameter>
               <name>param2FromJoinFlow</name>
               <value>param2 joinFlow value</value>
           </outbound-parameter>
       </flow-call>
   </flow-definition>
</faces-config>

このほかにも、

  • faces-config.xml同様、navigation-ruleが定義できる。
  • フローに入る時と出るときの処理をinitializer, finalizerとして定義できるようだ。
  • 開始ノードは start-nodeで指定できる。
    • 省略時はフローの名前と同じ名前のビュー。この例では、joinFlow.xhtml。(JSF2.2仕様 11.4.3.3 Packaging Flows in Directories)

記述に使うタグは、JSF Schemaの当該部分の説明を参照。

この定義ファイルを作成し、中身を完全に空にした場合は、デフォルトの定義が適用される(JSF2.2仕様 11.4.3.3 Packaging Flows in Directories)。ただ、現実には実用にならないと思われる。デフォルトの定義では、flow-returnのfrom-outcomeが固定的に/<flowName>-return になる。これは、/<flowName>-return.xhtmlというビューを用意しなければならないし、フロー完了時にその画面に遷移するという動作になる。

フローには、「フローを定義したドキュメントのID(definingDocumentId)」という属性がある。フローはアプリケーションから切り離してライブラリ的に作成できるようで、フロー名の衝突を避けるために名前空間的に利用されると思われる。フローを呼び出すときにフローの名前を指定するが、厳密には、フローのIDとdefiningDocumentIdのペアを指定することになっている。ただし、デフォルトでは「フローを定義したドキュメントのID」は""(空文字列)であり、フロー呼び出し時もdefiningDocumentIdを省略できる。<faces-config>配下に<name>を記述した場合は、それが「フローを定義したドキュメントのID」として扱われる。(JSF2.2仕様 11.4.3.1 Defining Flows)

フローをプログラムで定義

Java EE 7 Tutorialを参照。

あまりメリットは無いように思われる。XMLの記述のほうがわかりやすい。また、動的にフローを構築する必要はあまり無いのではないか。

フロー(の.xhtmlと.xml)を置く場所

XMLファイルは以下のどちらかのパス・ファイル名として作成する。

  • /<flowName>/<flowName>-flow.xml
  • /WEB-INF/<flowName>/<flowName>-flow.xml

  • /foo/foo-flow.xml
  • /WEB-INF/foo/foo-flow.xml

ここで、<flowName>部分は1階層。つまり、/foo/bar/foobar-flow.xml というのはNG。(JSF2.2仕様 11.4.3.3 Packaging Flows in Directories)

ビュー(XHTML)は、上記のディレクトリに格納しておくと、自動的にViewノードと認識されるため、<viewName>-flow.xmlへの記述が省略できる。上記のディレクトリ以外に格納することも可(らしい)。

フローを単独のJARファイルとしてパッケージ化することもできる。(JSF2.2仕様 11.4.3.2 Packaging Faces Flows in JAR Files)

Flowスコープ

フローが終わるまでインスタンスが維持される。

正確な定義がどこにあるか不明。

FlowScoped:view-sourceからたどれる説明をみても、漠然としている。

import javax.faces.flow.FlowScoped;
import javax.inject.Named;
@Named
@FlowScoped(value="joinFlow")
public class JoinController implements Serializable {

@FlowScopedにはvalue="..."でフローのidの指定が必須。(名前が必須なのは、フローの呼び出しがネストしても、どのフローに対応付くかわかるようにするためか?)

フローの呼び出し

まだどのフローにも入っていない場合

ビューに遷移するのにビューIDを指定していたのと同じように、フローのIDを指定する。フローIDを指定して遷移すると、フローの開始ノードに遷移する。

<h:link outcome="joinFlow" value="joinFlowに入る"/>

呼び出し先のフローに入力パラメータが定義されていても、渡す手段がないと思われる。

いずれかのフロー内の場合

まず、.xmlなどでFaces Flow Callノード(<flow-call>)を定義し、id="..." で名前を付け、呼び出し先のフロー、渡す引数を定義する。 (※)

そして、ビュー遷移時にビューIDを指定するのと同様に、flow-callのidを指定する。

(前掲の記述と別の例となってしまうが、記述例を挙げる)

呼び出し元フロー内のある画面のXHTML

<h:commandButton value="編集" action="#{memberController.doEnterEditor(item)}"/>

memberController(Java)

   public String doEnterEditor(Member member) {
       this.member = member;
       return "callMemberEditor";
   }

呼び出し元フローのxxx-flow.xml

       <flow-call id="callMemberEditor">
           <flow-reference>
               <flow-id>member-editor</flow-id>
           </flow-reference>
           <outbound-parameter>
               <name>member</name>
               <value>#{memberController.member}</value>
           </outbound-parameter>
       </flow-call>

呼び出し先フローのxxx-flow.xml

   <flow-definition id="member-editor">
       <inbound-parameter>
           <name>member</name>
           <value>#{memberEditorController.member}</value>
       </inbound-parameter>

※ これは、呼び出し先のフローが同じでも、渡す引数が異なるのであれば、異なるFaces Flow Callノードを定義しなければならない、ということを表している。また、引数を受け渡す手段として、Javaプログラムからは引数の値を一旦どこかのプロパティに置き、XMLでそのプロパティの場所を指し示す、というまわりくどい手段をとっている(とらざるを得ない)ことも表している。JSFはビュー間の値の受け渡しが困難な仕組みであることがよく表れている。

フローからの戻り

遷移先のビューIDを指定するのと同様に、Returnノードのid (<flow-return>のid属性で定義した名前)を指定する。

戻るとどのビューに遷移するのか、については、

  • flow-returnのfrom-outcomeで指定した値を、outcome値として扱い、
  • 戻り先のnavigation-ruleに基づいて遷移先を決定する、
    のではないかと思われる。JSF2.2仕様 7.4.2 Default NavigationHandler Algorithm に説明があるのではないかと思うが、複雑で読み切れない。

フローからフローを呼び出す場合には、呼び出し元のフローのXMLにnavigation-ruleを書いておくことで、戻ってきたあとに呼び出し元フローのどの画面に遷移するかをコントロールできる。

記述例
/member/member-flow.xml

<navigation-rule>
    <from-view-id>/member-editor/*</from-view-id> <!-- callMemberEditor, member-editor ではヒットしない。 -->
    <navigation-case>
        <from-outcome>return</from-outcome>
        <to-view-id>/member/member.xhtml</to-view-id> <!-- member.xhtml というように相対パスで書くと、/member.xhtmlと解釈されてエラーになる -->
    </navigation-case>
</navigation-rule>
<flow-call id="callMemberEditor">
    <flow-reference>
        <flow-id>member-editor</flow-id>
    </flow-reference>
    <outbound-parameter>
        <name>member</name>
        <value>#{memberController.member}</value>
    </outbound-parameter>
</flow-call>

この例では、呼び出し先のフロー member-editorは、/member-editor/member-editor-flow.xmlに定義されている。member-editor-flow.xmlではflow-returnを次のように定義している。

<flow-return id="return">
    <from-outcome>return</from-outcome>
</flow-return>

member-flow.xmlには、navigation-ruleを定義し、outcomeとして"return"を受け取った場合の遷移先を定義する。不可解なことに、to-view-idは相対パス(member.xhtml)では正常に遷移できない(GlassFish 4.0で確認)。また、from-view-idは、呼び出し先フロー内のビューIDを指定する必要があるようだ。フローIDやflow-callのidでは意図通りに動作しない。

フロー呼び出し時には値を引き渡せたが、フローからの戻りでは引き渡せない。

留意点、不可解な点

(GlassFish 4.0での動作に基づく)

flow-returnのfrom-outcome

  • *-flow.xmlのflow-returnのfrom-outcomeでViewIDを指定するのが前提となっているように思われる。Java EE 7 Tutorialの例はそうなっている。ViewIDを指定するには、戻り先のビュー構成について知っている必要があり、再利用性が低い。
  • from-outcomeでoutcome値を返し、呼び出し元の*-flow.xmlでnavigation-ruleを書くことで一見動作するように見えるが、h:link outcome="xxx"で xxxにflow-returnのidを指定した場合に、そのlinkで遷移した(フローから戻った)先の画面で異常が出ることがある。バグっぽい動作。本来navigation-ruleどおりに動作すべきなのか、navigation-ruleが無視されるべきなのかわからない。

フロー内からフロー外の画面への遷移

  • h:linkのoutcomeに、フロー外部のViewIDを指定することはできない。outcomeにはflow-returnのidを指定し、そのfrom-outcomeにViewIDを指定すれば、フロー外の画面へ遷移できる。
  • しかし、フロー外部の画面に脱出したいケースでは記述が増えて手間である。 また、画面共通にナビゲーションバーを付けようとした場合には、その全リンク先についてflow-returnの定義が必要になる。

フローからの戻り

  • flowから出るときに、値を呼び出し元に返す機構はない。呼び出し時に器を渡して、それに入れさせればよいのか?

Java EE 7 Tutorialの例にならうのであれば、

  • flow呼び出し時には、戻り先のViewIDを渡す。呼び出されたflowは、flow-returnのfrom-outcomeに# {....}を使ってそのViewIDをoutcome値とする。この方法により、前述のとおり、戻り先のビューのIDを事前に知っておく必要はなくなる。しかし、flowをスタックしている恩恵にあずかれない・・・。
  • flow外からflowの呼び出し時には引数を渡せないので、・・・どうするんだろう。@PostConstructとRequestスコープのBeanを併用して渡す?トップレベルからフローに組み込む?

その他

  • Flowを使うと、URLには特殊なパラメータがいくつか追加される。自動的にClientWindowの機能がONになるためと思われる(コンテキストパラメタ javax.faces.CLIENT_WINDOW_MODE に "url"が指定されたかのように動作する)。(JSF2.2仕様 11.4.3 Faces Flows)

JavaServer Faces Specification Version 2.2

JSF仕様の該当部分を翻訳してみたが、記述例がないので、これだけでは理解できない。(泣)

7.5 FlowHandler

どんなJSFアプリも有効グラフとしてモデル化できる。その有効グラフでは、ノードがビューで、線はビュー間の遷移を表す。
Faces Flowは、関連するビューと線を一緒にカプセル化する機能を提供することによって、
この有効グラフにいくつかの別の種類のノードを導入する。
アプリケーションは、機能のモジュールの組み合わせとして作成されるが、
各モジュールは well-definedな入口/出口条件を持ち、
各モジュール内のノード間で状態を共有することができる。

この機能は
Oracle Fusion Middleware の ADF Task Flowの設計に、
また、Spring Web FlowとApache MyFaces CODIに、非常に強く影響を受けている。

この機能の正規の仕様は、Javadocのjavax.faces.flow.FlowHandler、および、Section 7.4
“NavigationHandler”の関連する要求事項を参照されたし。

この節では、使い方の例、機能の手引きを説明することにより、本仕様の他の部分のうち、本機能と関わりのある部分が理解できるようにする。

7.5.1 Non-normative example

機能の概要を説明するための単純な例を説明する。
本機能のすべての特徴を利用しているわけではない。
この例は、2つのflowを持ち、それぞれ互いを引数付きで呼び出す。
flowの外のビューはどちらのflowにもリンクできる。これらのflowの名前をそれぞれflow-a, flow-bとする。

(図)

この図は以下のルールにしたがって書かれている。

  • ビューノードは矩形
  • faces flow return ノードは円
  • faces flow call ノードは角の欠けた矩形
  • @FlowScoped Beanは角丸矩形
  • 開始ノードは"start"と印付け
  • 入力/出力引数は名前の一覧で(?) (are listed by name)
  • 矢印は、ノード間の有効な遷移

これらのflowは、構成要素の名前を除いて同一であり、それぞれ以下のプロパティを持つ。

  • 3つのビューノード。そのうちひとつは、明示的に開始ノードとされたもの。
  • 1つのfaces flow returnノード。それぞれ、出力"return1"を返す。
  • 1つのflow call ノード。それは他のフローを呼び出す。2つの出力引数付きで。その引数の名前は、他方のflowと対応(一致? match up with)する。
  • 2つの入力引数。名前は、他方のflowと対応(一致?)する。

上記の説明に出てきた各種のノードの種類については、javax.faces.flow.FlowHandlerのJavadocに定義されている。

上記のflowを含んだbasic_faces_flow_call.warという簡単なWebアプリを考えてみる。
ファイルの配置は次のとおり。ここではMaven war packagingを使っている。

(ディレクトリツリーの図)

この例を完成させるために、
先のフローの実行を細かく見てみよう。

これらのフローを含んだアプリがデプロイされると、実行環境はflowの定義を発見し、それらを内部フローのデータ構造(internal flow data structure)に追加する。
flowのひとつは、flow-b-flow.xmlに定義されている。
これはXMLファイルで、Section 11.4 “Application Configuration Resources”で説明されているApplication Configuration Resources syntaxに合うようになっている。
もうひとつのflowは、FlowA.javaで定義されており、
@FlowDefinition アノテーションの付いたメソッドを持つクラスとなっている。
flowの発見が完了すると、アプリケーションスコープの、スレッドセーフなデータ構造がjavax.faces.flow.FlowHandlerシングルトンオブジェクトから利用可能になり、このデータ構造にflowの定義が含まれている。このデータ構造には、javax.faces.flow.Flow API経由でアクセス可能である(?)(This data structure is navigable by the runtime via the javax.faces.flow.Flow API.)。

ユーザーエージェントが http://localhost:8080/basic_faces_flow_call/faces/index.xhtml にアクセスすると、2つのボタンが配置されたページが表示される。2つのボタンのactionはそれぞれflow-aとflow-bである。どちらかのボタンをクリックすると、対応するflowに入る。ここでは、flow-a ボタンをクリックしたとしよう。@FlowScopedのFlow_a_Beanがコンテナによってインスタンス化され、ただちに、その開始ノード(flow-a.xhtml)に遷移する。ユーザーは続いてnext_a.xhtmlに進むボタンをクリックし、次いでnext_b.xhtmlへのボタンをクリックする。そのページにはボタンがあり、そのアクションはcallBとなっている。このボタンをクリックすると対応する名前のfaces flow call nodeが活性化される。そのfaces flow call nodeは指定された出力パラメータを用意し、Flow_a_Beanを非活性化させ、flow-bを呼ぶ。

flow-bに入るとき、@FlowScopedのFlow_b_Beanがコンテナによってインスタンス化され、flow-aからの出力引数がflow-bの対応する入力引数に対応付けられ(are matched up with)、開始ノード、この場合はflow-b.xhtmlに遷移する。ユーザーは続いてnext_a.xhtmlに進むボタンをクリックし、次いでnext_b.xhtmlに進むボタンをクリックする。そのページにはボタンがあり、そのアクションはtaskFlowReturn1である。このボタンをクリックすると、Flow_b_Beanは非活性化され、return1という名前のビューへの遷移が実行される。