第7章 ページフローとビジネスプロセス

JBoss jBPM は、Java SE や EE 環境のためのビジネスプロセス管理エンジンです。 jBPM は、ビジネスプロセスや、ユーザインタラクションを、 待ち状態、 デシジョン、タスク、WEBページなどを、 ノードの図式として表現を可能にします。 図式は、簡単でとても読みやすい jPDL と呼ばれる XML 表現を使用して定義されており、 Eclipse プラグインを利用して、編集、グラフィックによる視覚化が可能です。 jPDL は拡張可能な言語であり、 WEB アプリケーションのページフローを定義することから、典型的なワークフローの管理、SOA 環境におけるサービスのオーケストレーションまで適応します。

Seam アプリケーションは jBPM を2 つの異なる問題に使用します。

これら 2 つのものを混乱しないでください。 それらはかなり違うレベルあるいは粒度で動作します。 ページフロー対話、 そして、タスク すべてはシングルユーザとの 1 つのインタラクションを参照します。 ビジネスプロセスはいくつものタスクをまたぎます。 さらに、jBPM の 2 つのアプリケーションは直交しています (互いに独立していること) 。 それらを一緒に使うことも、独立して使うことも、使わないこともできます。

Seam を使うために、jPDL を知る必要はありません。 JSF あるいは、Seam ナビゲーション規則を使って、ページフローを定義することに満足な場合、 あるいは、アプリケーションがプロセス駆動というよりデータ駆動の場合、 おそらくjBPM は不要でしょう。 しかし、明確な図式表現でユーザインタラクションを考えることが、 より堅牢なアプリケーションの構築に役立つことは理解できます。

7.1. Seam でのページフロー

Seam には、ページフローを定義する 2 つの方法があります。

  • JSFあるいはSeam ナビゲーション規則の利用 - ステートレスなナビゲーションモデル

  • jPDL の利用 - ステートフルなナビゲーションモデル

簡単なアプリケーションでは、ステートレスなナビゲーションモデルで十分です。 とても複雑なアプリケーションは、場所に応じて両方を使用します。 それぞれのモデルは、それぞれの強みも弱みもあります。

7.1.1. 2 つのナビゲーションモデル

ステートレスなモデルは、 一組の名前の付いた論理的なイベントの結果 (outcome) から 直接、結果として生じるビューのマッピングを定義します。 ナビゲーション規則は、どのページがイベントのソースであったかということ以外、 アプリケーションによって保持されたどのような状態も全く気にしません。 これは、アクションリスナメソッドがページフローを決めなければならないことがあることを意味しています。 なぜなら、それらだけがアプリケーションの現在の状態にアクセスできるからです。

これは JSF ナビゲーション規則を使用したページフローの例です。

<navigation-rule>
    <from-view-id>/numberGuess.jsp</from-view-id>
        
    <navigation-case>
        <from-outcome>guess</from-outcome>
        <to-view-id>/numberGuess.jsp</to-view-id>
        <redirect/>
    </navigation-case>

    <navigation-case>
        <from-outcome>win</from-outcome>
        <to-view-id>/win.jsp</to-view-id>
        <redirect/>
    </navigation-case>
        
    <navigation-case>
        <from-outcome>lose</from-outcome>
        <to-view-id>/lose.jsp</to-view-id>
        <redirect/>
    </navigation-case>

</navigation-rule>

これは Seam ナビゲーション規則を使用したページフローの例です。

<page view-id="/numberGuess.jsp">
        
    <navigation>
        <rule if-outcome="guess">
            <redirect view-id="/numberGuess.jsp"/>
        </rule>
        <rule if-outcome="win">
            <redirect view-id="/win.jsp"/>
        </rule>
        <rule if-outcome="lose">
            <redirect view-id="/lose.jsp"/>
        </rule>
    </navigation-case>

</navigation-rule>

ナビゲーション規則が冗長過ぎると考えるならば、 アクションリスナーメソッドから直接、ビューIDを返すことが可能です。

public String guess() {
    if (guess==randomNumber) return "/win.jsp";
    if (++guessCount==maxGuesses) return "/lose.jsp";
    return null;
}

これは、リダイレクトの結果であることに留意ください。 リダイレクト中に使用するパラメータを指定することも可能です。

public String search() {
    return "/searchResults.jsp?searchPattern=#{searchAction.searchPattern}";
}

ステートフルなモデルは、 名前の付いた論理的なアプリケーションの状態間で起こる遷移の組み合わせを定義します。 このモデルでは、jPDL ページフロー定義中に、どのようなユーザインタラクションのフロー表現も可能であり、 インタラクションのフローを全く知らないアクションリスナーメソッドを書くことも可能です。

これは jPDL を使用したページフロー定義の例です。

<pageflow-definition name="numberGuess">
    
   <start-page name="displayGuess" view-id="/numberGuess.jsp">
      <redirect/>
      <transition name="guess" to="evaluateGuess">
      	<action expression="#{numberGuess.guess}" />
      </transition>
   </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="win" view-id="/win.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
   <page name="lose" view-id="/lose.jsp">
      <redirect/>
      <end-conversation />
   </page>
   
</pageflow-definition>

ここで、すぐに気づく 2 つのことがあります。

  • JSF/Seam ナビゲーション規則は、より 簡単です。 (しかし、これは、根底となる Java コードがより複雑化であるという事実をあいまいにしています。)

  • jPDL は、JSP や Java コードを見る必要がなく、 即座に、ユーザインタラクションの理解ができます。

それに加えて、ステートフルモデルは、もっと 制約的 です。 各論理的な状態 (ページフローの各ステップ) に対して他の状態に遷移可能な制約された組み合わせがあります。 ステートレスモデルは、アドホックな モデルです。 それは、アプリケーションではなく、 比較的制約のない、ユーザが次に行きたいところを決めるフリーフォームナビゲーションに適しています。

ステートフル / ステートレスナビゲーションの判断は、 典型的なモーダル / モーダレスインタラクションの考え方ととてもよく似ています。 さて、アプリケーションをモーダルな振る舞いから回避することは、 対話を持つ 1 つの主な理由ですが、 Seam アプリケーションは、 通常、単純な意味でのモーダルではありません。 しかし、Seam アプリケーションは、 特定な対話レベルで、モーダル可能であり、しばしばそうです。 モーダルな振る舞いは、 可能な限り回避したものとして知られています。 ユーザがしたいことの順番を予測することは、とても困難です。 しかし、ステートフルモデルの存在意義があるのは疑う余地はありません。

2 つのモデルの最大の違いは、 戻るボタンの振る舞いです。

7.1.2. Seam と 戻るボタン

JSF あるいは Seam ナビゲーション規則が使用されている場合、 Seam は、ユーザに戻る、進む、更新ボタンの自由なナビゲーションを可能にします。 これが発生したとき、 内部的な対話状態の一貫性を保持することは、 アプリケーションの責任です。 Struts や WebWork のような対話モデルをサポートしない WEB アプリケーションフレームワーク、 そして、EJB ステートレスセッションBean や Spring framework のようなステートレスコンポーネントモデルの組み合わせの経験は、 多くの開発者にこれをすることは、ほとんど不可能であることを教えていました。 しかし、Seam のコンテキストでの経験から、 ステートフルセッション Bean に裏付けられた明確な対話モデルがあるところでは、 それは実際とても簡単です。 通常、それは、アクションリスナーメソッドの最初に、 no-conversation-view-id アノテーションと null チェックの使用を組合わせる程度に簡単です。 私たちは、フリーフォームナビゲーションのサポートは、 ほぼいつも要求されるものと考えています。

この場合、no-conversation-view-idの宣言は pages.xmlで行います。 対話中のレンダリングされたページからのリクエストの場合、 異なるページにリダイレクトして、その対話は存在していないことを Seamに伝えることになります。

<page view-id="/checkout.xhtml" 
        no-conversation-view-id="/main.xhtml"/>

一方、ステートフルモデルでは、 戻るボタンを押すことは、前の状態への未定義な遷移として中断されます。 なぜなら、ステートフルモデルは、 現在の状態からの遷移の組み合わせを強制します。 ステートフルモデルでは、戻るボタンは、デフォルトで無効です。 Seamは透過的に戻るボタンの使用を検知し、 前の "古い" ページからのアクションが実行されるのをブロックし、 そして、単純に、"現在の" ページをリダイレクトします。 (そして、faces メッセージを表示します。) これを特徴と考えるか、あるいは、ステートフルモデルの制約と考えるかは、 アプリケーション開発者としての視点次第です。 ユーザとしては、この特徴はイライラさせられるかもしれません。 特定のページからの back="enabled" 設定により、 戻るボタンナビゲーションを可能とすることもできます。

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

これは、checkout 状態 から以前のどの状態 にでも戻るボタンでの遷移が可能です。

もちろん、ページフローのレンダリングされたページからのリクエストの場合も、 異なるページにリダイレクトして、そのページフローでの対話は存在していないことを 定義しなければなりません。この場合、no-conversation-view-id の宣言は、ページフロー定義で行います:

<page name="checkout" 
        view-id="/checkout.xhtml" 
        back="enabled" 
        no-conversation-view-id="/main.xhtml">
    <redirect/>
    <transition to="checkout"/>
    <transition name="complete" to="complete"/>
</page>

実際、どちらのナビゲーションモデルも、それにふさわしい場所があります。 いつ、どちらのモデルがふさわしいかは、すぐに理解できるようになります。

7.2. jPDL ページフローの使用

7.2.1. ページフローの設定

Seam の jBPM 関連のコンポーネントをインストールし、 ページフロー定義の場所を指示する必要があります。 この components.xml に Seam 設定を指定することができます。

<core:jbpm>
    <core:pageflow-definitions>
        <value>pageflow.jpdl.xml</value>
    </core:pageflow-definitions>
</core:jbpm>

最初の行は jBPM を設定します、2 番目は jPDL ベースのページフロー定義を指定しています。

7.2.2. ページフローの開始

@Begin@BeginTask あるいは、 @StartTask アノテーションを使用して、 プロセス定義の名前を指定することによって、 jPDL ベースのページフローを開始します:

@Begin(pageflow="numberguess")
public void begin() { ... }

もしくは、pages.xmlを使用してページフローを開始できます。

<page>
        <begin-conversation pageflow="numberguess"/>
    </page>

RENDER_RESPONSE フェーズの間にページフローを開始する場合、 — 例えば @Factory または @Create メソッドの期間 — 私達は既にレンダリングされているページにいると考えます。 そして、 上記のサンプルのように、 ページフローの最初のノードとして <start-page> ノードを使用します。

しかし、ページフローがアクションリスナ呼び出しの結果として開始される場合、 アクションリスナの結果 (outcome) は、レンダリングされる最初のページを決定します。 この場合、ページフローの最初のノードとして <start-state> を使用し、 それぞれの可能な結果 (outcome) のために遷移を宣言します。

<pageflow-definition name="viewEditDocument">

    <start-state name="start">
        <transition name="documentFound" to="displayDocument"/>
        <transition name="documentNotFound" to="notFound"/>
    </start-state>
    
    <page name="displayDocument" view-id="/document.jsp">
        <transition name="edit" to="editDocument"/>
        <transition name="done" to="main"/>
    </page>
    
    ...
    
    <page name="notFound" view-id="/404.jsp">
        <end-conversation/>
    </page>
    
</pageflow-definition>

7.2.3. ページノードと遷移

<page> ノードは、システムがユーザ入力を待っている状態を表します。

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

view-id は JSF ビューIDです。 <redirect/> 要素は、 JSF ナビゲーション規則の <redirect/> と同じ作用、 つまり、ブラウザの更新ボタンの問題を解決するために、 post-then-redirect を行います。 (Seam は、 ブラウザのリダイレクトを超えて対話コンテキストを伝播します。 従って、Seamでは、Ruby on Rails スタイルの "flash" の概念は不要です。)

遷移名は、numberGuess.jsp において、 ボタン あるいは、リンクをクリックすることによって起動された JSF 結果 (outcome) の名前です。

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

遷移が、このボタンをクリックすることによって起動されるときに、 numberGuess コンポーネントの guess () メソッドと呼び出すことによって、 jBPM は、遷移のアクションを起動します。 jPDL においてアクションを指定するために使わるシンタックスは、 JSF EL 式とよく似ていること、 そして、遷移のアクションハンドラは、 ちょうど現在の Seam コンテキストにおける Seam コンポーネントのメソッドであることに注意してください。 従って、JSF イベントのために既に持っているものと、ちょうど同じ jBPM イベントのモデルを持ちます。 (一貫した原則 (The One Kind of Stuff principle))

nullでのoutcome の場合 (例えば、action が定義されていないコマンドボタン)、 もし、名前のない遷移があるならば、Seam は遷移するためのシグナルを送ります。 あるいは、もし、すべての遷移が名前を持つならば、単純にページを再表示します。 従って、サンプルページフローを少し単純化でき、このボタンは

<h:commandButton type="submit" value="Guess"/>

以下の名前のない遷移でのアクションを実行します。

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

ボタンにアクションメソッドを呼ばせることも可能です。 この場合、アクション結果 (outcome) が遷移を決定します。

<h:commandButton type="submit" value="Guess" action="#{numberGuess.guess}"/>
<page name="displayGuess" view-id="/numberGuess.jsp">
    <transition name="correctGuess" to="win"/>
    <transition name="incorrectGuess" to="evaluateGuess"/>
</page>

しかし、これは質の悪いスタイルだと考えます。 なぜなら、フロー制御の責任をページフロー定義の外側の他のコンポーネントに移動しているからです。 ページフローに関連することをそれ自身に集中することは、より良いことです。

7.2.4. フローの制御

通常、ページフローを定義するとき、jPDL より強力な機能はいりませんが、 <decision> ノードが必要です。

<decision name="evaluateGuess" expression="#{numberGuess.correctGuess}">
    <transition name="true" to="win"/>
    <transition name="false" to="evaluateRemainingGuesses"/>
</decision>

デシジョンは Seam コンテキスト中では JSF EL 式によって評価されます。

7.2.5. フローの終了

<end-conversation>、または、@End を使用して対話を終了します。 (実際、可読性のために、両方 の使用を勧めます。)

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

オプションとして、transition 名を指定して、タスクを終了することができます。 この場合、Seam はビジネスプロセスにおいて現在のタスク終了の信号を送るでしょう。

<page name="win" view-id="/win.jsp">
    <redirect/>
    <end-task transition="success"/>
</page>

7.3. Seam でのビジネスプロセス管理

ビジネスプロセスは、だれ (who) がタスクを実行することができるか、 いつ (when) タスクを実行すべきかという明確なルールに従って、 ユーザ、あるいは、ソフトウェアのシステムによって実行されなければならない明確なタスクの集合です。 Seam jBPMインテクグレーションは、ユーザにタスクリストを表示し、それらのタスクを管理することを容易にします。 Seam はまた BUSINESS_PROCESS コンテキスト中のビジネスプロセスに関連する、 状態をアプリケーションに保管させ、 jBPM 変数経由でその状態を永続化させます。

<page> の代わりに、<task-node> ノードを持つ以外、 簡単なビジネスプロセス定義はページフロー定義とほぼ同じであるように見えます。 (一貫した原則 (The One Kind of Stuff principle)) 長期間のビジネスプロセスにおいて、 待ち状態は、システムが、ユーザがログインし、タスクを実行するのを待っているところです。

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

同じプロジェクトの中に、jPDL ビジネスプロセス定義と、 jPDL ページフロー定義を持つことは可能です。 そうであれば、2 つの関係は ビジネスプロセス中の <task>は ページフロー <process-definition>全体と一致します。

7.4. jPDL ビジネスプロセス定義の使用

7.4.1. プロセス定義の設定

jBPM を設定し、そのjBPMにビジネスプロセス定義の場所を指示する必要があります。

<core:jbpm>
    <core:process-definitions>
        <value>todo.jpdl.xml</value>
    </core:process-definitions>
</core:jbpm>

7.4.2. アクターIDの初期化

いつでも現在ログインしているユーザを知っている必要があります。 jBPM は、actor idgroup actor idによって、ユーザを識別します。 actor と呼ばれる組み込み Seam コンポーネントを使用することにより、 現在の actor id を指定します。

@In Actor actor;

public String login() {
    ...
    actor.setId( user.getUserName() );
    actor.getGroupActorIds().addAll( user.getGroupNames() );
    ...
}

7.4.3. ビジネスプロセスの初期化

ビジネスプロセスインスタンスを初期化するためには、 @CreateProcess アノテーションを使用します。

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

また、 pages.xmlを使用してビジネスプロセスの初期化も行えます:

<page>
    <create-process definition="todo" />
</page>

7.4.4. タスク割り当て

プロセスが開始したときタスクインスタンスが生成されます。 これらにはユーザまたはユーザグループを割り当てなければなりません。 actor ids は、ハードコーディングすることも、Seam コンポーネントに委譲することもできます。

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

この場合、 単純に現在のユーザにタスクを割り当てます。 タスクをプールに割り当てることもできます。

<task name="todo" description="#{todoList.description}">
    <assignment pooled-actors="employees"/>
</task>

7.4.5. タスクリスト

いくつかの組み込み Seam コンポーネントによりタスクリストの表示が容易になっています。 pooledTaskInstanceList は、 ユーザが自分自身に割り当てることができるプールされたタスクのリストです。

<h:dataTable value="#{pooledTaskInstanceList}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{pooledTask.assignToCurrentActor}" value="Assign" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

<s:link> の代わりに、 普通の JSF <h:commandLink> を使用することもできます。

<h:commandLink action="#{pooledTask.assignToCurrentActor}"> 
    <f:param name="taskId" value="#{task.id}"/>
</h:commandLink>

pooledTask コンポーネントは、 単純にタスクを現在のユーザに割り当てる組み込みコンポーネントです。

taskInstanceListForType コンポーネントは、 現在のユーザに割り当てられた特定タイプのタスクを含んでいます。

<h:dataTable value="#{taskInstanceListForType['todo']}" var="task">
    <h:column>
        <f:facet name="header">Description</f:facet>
        <h:outputText value="#{task.description}"/>
    </h:column>
    <h:column>
        <s:link action="#{todoList.start}" value="Start Work" taskInstance="#{task}"/>
    </h:column>            	
</h:dataTable>

7.4.6. タスクの実行

タスクの作業を開始させるために、リスナメソッドに、 @StartTask あるいは @BeginTaskを使用します。

@StartTask
public String start() { ... }

また、 タスクの実行を pages.xml を使用して始めることもできます:

<page>
    <start-task />
</page>

これらのアノテーションは、 ビジネスプロセス全体に関して意味を持つ、 特殊な種類の対話を開始します。 この対話による処理はビジネスプロセスコンテキストの中で保持する状態にアクセスできます。

@EndTask を使用して対話を終了する場合、 Seam はタスクの完了サインを送信します。

@EndTask(transition="completed")
public String completed() { ... }

また、 pages.xmlも使用できます:

<page>
    <end-task transition="completed" />
</page>

(もしくは、 上記のように、<end-conversation> も使用可能です。)

この時点で、 jBPM はビジネスプロセス定義を引継ぎ、実行を続行します。 (より複雑なプロセスにおいては、 プロセス実行が再開する前に完了する必要があるタスクがあるかもしれません。)

複雑なビジネスプロセスの管理を実現する各種の高度な機能の全体的な概要については jBPM ドキュメントを参照してください。