conversationはJBoss Seamのconversationから着想を得た機能で、sessionよりも小さい単位で一連の処理を扱うためのものです。
Ymirのconversationには以下の特徴があります。
- conversationは識別子として名前を持ちます。
- conversationにはオブジェクトをバインドすることができます。バインドされたオブジェクトは、conversationが存在する間有効です。このスコープをconversationスコープと呼びます。
- conversationには複数のPageクラスを関連付けることができます。関連付けられたPageクラス毎にphaseという文字列を割り当てることができます。
- conversationに関連付けられたPageクラスには、どのphaseから遷移可能かという制約を付与することができます。許可されたphase以外から遷移した場合は不正遷移を表す例外(IllegalTransitionException)がスローされます。また、phaseに関する制約があったとしても、今のphaseから同じphaseに遷移することは許されています(例:自画面に帰ってくるような遷移)。
- conversationは開始指示によって開始されます。conversationに参加していない状態で開始指示なしにconversationに参加しようとした場合や、あるconversationから開始指示なしに別のconversationに遷移した場合は、IllegalTransitionExceptionがスローされます。
- conversationは終了指示によって終了されます。終了するとconversationは削除され、conversationにバインドされたオブジェクトも自動的にアンバインドされます。ただし、終了せずに別のconversationを開始することも許されています。この場合でも自動的にそれまでのconversationは削除されます。
- conversationは基本的に同一sessionにつき同時に1つしか存在できませんが、あるconversationから別のconversation(これをsub conversationと言います)を開始して終了後に元のconversationに戻ってくるようにすることは可能です。
- conversationに参加している状態で、conversationに関連付けられていないPageに遷移することは可能です。この場合、その遷移自体行なわれないのと同じことになります。従って、元のconversationに遷移を戻すこともできます。
以下、コメントの入力→確認→登録完了という一連の処理についてconversationを定義する例を通してconversation機能の使い方を説明します。
conversationにPageを関連付ける
conversationにPageを関連付けるには、次の例のようにPageクラスに@Conversationアノテーションを付与します。
なお、投稿完了画面を表示するためのCompletedPageクラスには@Conversationアノテーションを付与していませんが、これはこのページへはConfirmPageにてconversationを終了してから遷移するような作りにしているためです。
@Conversation(name = "comment", phase = "input")
public class InputPage extends InputPageBase {
...
@Begin
public void _get() {
...
}
...
}
...
@Conversation(name = "comment", phase = "confirm", followAfter = "input")
public class ConfirmPage extends ConfirmPageBase {
...
@End
public String _post_ok() {
// コメントの投稿処理
....
return "redirect:comment_completed.html";
}
...
}
...
public class CompletedPage extends CompletedPageBase {
...
}
nameプロパティにはconversationの名前を指定します。上の例ではコメント投稿処理なので「comment」という名前にしています。
phaseプロパティにはこのPageがconversation中のどういうフェーズを表すのかを文字列で指定します。phaseプロパティは省略可能ですが、極力指定しておくことをお勧めします。
conversation中でページ遷移を決まった順序で行なわせたい場合は、followAfterプロパティで直前のphaseを1つまたは複数指定します。followAfterプロパティが指定されると、指定されたphaseからの遷移しか許可しないようになります。上の例では、confirmフェーズはinputフェーズからしか遷移できないように指定しています。
conversationの開始と終了
conversationは開始指示がないと開始できないため、開始指示を追加します。開始指示は@Beginアノテーションで行ないます。
public class InputPage extends InputPageBase {
...
@Begin
public void _get() {
...
}
...
}
@Beginアノテーションはアクションメソッドに付与します。_getメソッドに@Beginアノテーションを付与していますので、普通にInputPageのURLに遷移することでcomment conversationが開始されます。
conversationを終了するには終了指示として@Endアノテーションを付与します。
public class ConfirmPage extends ConfirmPageBase {
...
@End
public String _post_ok() {
// コメントの投稿処理
....
return "redirect:comment_completed.html";
}
...
}
この例では、投稿確認ページで「OK」ボタンが押された場合に_post_okアクションが呼び出され、コメントの投稿処理が終わった後にconversationが終了するようにしています。
ここで注意してほしいのは、conversationの終了処理は@Endアノテーションが付与されているアクションの実行が完了した時点で行なわれるということです。このようにしているのは、@Endアノテーションが付与されているアクションの処理中にもconversationスコープのオブジェクトを利用できるようにするためです。なお、アクションの実行中に実行時例外等がスローされて処理が中断された場合でもconversationは終了します。
このため、アクションの中で何らかの判定を行なってその結果でconversationを終了させるかどうかを決定したい場合は工夫が必要です。
一番簡単なのは、アクションの中でconversationを終了させるかどうか判定し、終了させる場合は終了用のアクションにリダイレクトするようにしておき、そのアクションに@Endアノテーションを付与する方法です。
このような画面構成が取れない場合は、アクションの呼び出し前にYmirのconstraintの仕組みで判定を行なう方法を使うことができます。具体的には、「conversationの終了条件を満たしていること」という制約を表す専用のConstraintクラスとアノテーションを作成し、そのConstraintクラスの中で判定を行なうようにします。または、Pageクラスにページ固有のバリデーションとしてconversationの終了条件を満たしていることを確認するためのメソッドを作成し、該当アクション呼び出し時にだけこのバリデーションが呼ばれるようにしておくという方法もあります。
この他にも、Seasar2のインターセプタとして判定処理を実装し、該当アクションにそのインターセプタを掛けることでも条件分岐を実現することができます。
conversationスコープ
conversationスコープにオブジェクトをバインドしたい場合や、conversationスコープからオブジェクトを取り出したい場合は@Inアノテーションと@Outアノテーションを使用します。
@In(ConversationScope.class)
public void setComment(CommentDto comment) {
comment_ = comment;
}
...
@Out(ConversationScope.class)
public CommentDto getComment() {
return comment_;
}
@Inアノテーションや@Outアノテーションの詳細については「オブジェクトスコープ」を参照して下さい。
sub conversation
例えばコメント投稿の画面遷移の途中で、認証が済んでいなければユーザ認証画面を経由させてから投稿画面に戻す、というような遷移を実現したいとします。このように、あるconversaion中から一時的に別のconversationに遷移させ、そのconversaionが終了してから元のconversationに復帰させたい場合はsub conversationを使います。
sub conversationに遷移させるには@BeginSubConversationアノテーションを使用します。
@Conversation(name = "comment", phase = "start")
public class StartPage extends StartPageBase {
...
@BeginSubConversation(reenter = "redirect:comment_input.html")
public String _post_logininput() {
return "redirect:login_input.html";
}
...
}
上の例では、コメント投稿開始ページ(StartPage)にて「ログイン」ボタンが押され、_post_logininputアクションが呼び出されて処理が完了したタイミングでログイン処理用のconverstionを開始するようになります。なお、sub conversationの開始処理もconversationの終了処理と同様、アクションの処理が完了した時点で行なわれますが、アクションの処理中に例外がスローされるとsub conversationは開始されません。
@BeginSubConversationのreenterプロパティには、sub conversationが終了した時に元々のconversationに帰ってくるための遷移先をYmirのアクションの文字列の返り値と同じ形式で指定して下さい。上の例では、ログイン処理が終わった後にコメントの入力画面(comment_input.html)に遷移するように指定しています。
sub conversationを終了するには特別な処理は必要ありません。普通のconversationを終了するのと同じように@Endアノテーションで終了させるだけです。
@Conversation(name = "login", phase = "input")
public class InputPage extends InputPageBase {
...
@End
public String _post_login() {
// ログインのために必要な処理
...
return "redirect:login_completed.html";
}
...
}
上の例では、ログインフォーム画面(InputPage)にて_post_loginアクションが呼び出された場合にlogin conversationが終了します。終了後の遷移先は通常はアクションの返り値に従ってlogin_completed.htmlになりますが、現在のconversationがsub conversationである場合はアクションの返り値は無視され、代わりにsub conversation開始時に@BeginSubConversationアノテーションで指定されたreenterプロパティの値が使用されます。従って、上の例ではcomment_input.htmlに遷移することになります。
なお終了後の遷移先を差し替える都合上、sub conversationを終了させる可能性のある@Endアノテーションは返り値がStringまたはObject型であるアクションメソッドに付与する必要があります。返り値がStringでもObjectでもないアクションメソッドに@Endを付与してそのアクションメソッド経由でsub conversationを終了させようとした場合はRuntimeExceptionがスローされます。