JSFのConversation Scopedを改めて試してみた

Java EE 7+JSF 2.2に行きたい所は山々なのだが、現在EE 5やら6から逃れられない全国のエンプラ開発現場の方々お疲れ様です。

さてJSF 2.2からはCDIにFlow Scopedという便利なものが付き、開始・終了が自動になったのであるが、現在EE 6の場合、そんなもの使えないのである。しょうがないので、どうしても類似のことがやりたければ、JSF 2.1でも動作するCDI+Conversation Scopedで誤魔化すしか無いのである。

Conversation Scopedは会話(Conversation)の開始・終了を明示的に行わないといけないので、結構めんどくさいということと、どうやれば綺麗に開始・終了できるのかイマイチ成功例がわからなかったのであるが、やっぱり避けられなくなったので改めて何パターンか試してみた。これが一番綺麗かな? というのが見つかったので、メモ代わりに紹介しておく。これが使いこなせれば、継承とアノテーションを駆使してCDI上でViewScopedと似たことがJSF 2.1でも実現できるかも知れない。いやーさすがにこれは無駄な努力かなぁ・・・

※ちなみに本例ではFaceletをJSF 2.2仕様で書いているが、2.1仕様であればjsfc=”h:inputText”とかで書き換える必要がある。

※JSFの基本的な作り方については、先週発表のスライドにまとめてあるので、そっちば参照してはいよ。
http://www.slideshare.net/iwasakihirofumi/java-ee-7-jsf-22

とりあえず、ポイントは一体どこで会話をスタートさせるのか、だ。JSFの場合、Phase Listenerで制御が一応できるので、Pre Render Viewで引っかけて強引に呼ぶ例をWebで見つけた。他にもGetterメソッドを作ってFaceletの先頭で表示させるついでに#{bean.property}で強引に呼び出す方法もあるが、こっちの方がスマートかも知れないし、タイミングは早いから安全ではある。f:eventを書くのめんどくさいけど。

注意点としては、開始と終了の書き方はほとんど固定で、下記のように書くしかなさそう、という点だ。また、間違って2回conversation.start()が呼ばれると、前に格納されていた値は全消去されてしまうので、あくまで最初に1回だけ呼んでやる必要がある。ここを間違えると「?」な挙動になるので注意するべしである。

遷移画面(3画面)

index.xhtml –> confirm.xhtml –> completed.xhtml

Backing Bean (CDI)

[java]
package com.mushagaeshi.jsftest;

import java.io.Serializable;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

@Named
@ConversationScoped
public class ConversationBean implements Serializable {

private static final long serialVersionUID = 5792114893614101597L;

// 会話制御のためのオブジェクトを挿入する
@Inject
private Conversation conversation;

private String id;

private String password;

// 会話の開始メソッド。1頁目の先頭でf:eventにて強制的に呼び出す。
public void initConversation() {
// ポストバックでなくて、且つ、会話が開始されてなかったら開始する。
if (!FacesContext.getCurrentInstance().isPostback()
&& this.conversation.isTransient()) {
this.conversation.begin();
}
}

/**
* Creates a new instance of ConversationBean
*/
public ConversationBean() {
}

// 1画面目でボタンが押された際のメソッド。書かなくても行けるけど念のため。
public String clickNext() {
return "confirm?faces-redirect=true";
}

// 2画面目でボタンが押された際のメソッド。書かなくても行けるけど念のため。
public String clickConfirm() {
return "completed?faces-redirect=true";
}

// 3画面目でボタンが押された際のメソッド。会話を完了させて遷移する。これで状態が消える。
public String clickCompleted() {
// 会話が開始されてたら終了する。
if (!this.conversation.isTransient()) {
this.conversation.end();
}
return "index?faces-redirect=true";
}

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

}
[/java]

1画面目 (index.xhtml)

[html]
<?xml version=’1.0′ encoding=’UTF-8′ ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<!– 強制的に会話開始をここで呼び出す –>
<f:event type="preRenderView" listener="#{conversationBean.initConversation()}"/>
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
Hello from Facelets
<form jsf:id="form">
<input type="text" jsf:value="#{conversationBean.id}"/>
<input type="password" jsf:value="#{conversationBean.password}"/>
<input type="submit" jsf:action="#{conversationBean.clickNext()}" valuie="submit"/>
</form>
</h:body>
</html>
[/html]

2画面目 (confirm.xhtml)

[html]
<?xml version=’1.0′ encoding=’UTF-8′ ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
<h1>Confirm</h1>
<p>#{conversationBean.id}</p>
<p>#{conversationBean.password}</p>
<form jsf:id="form">
<input type="submit" jsf:action="#{conversationBean.clickConfirm()}" value="confirm"/>
</form>
</h:body>
</html>
[/html]

3画面目 (completed.xhtml)

[html]
<?xml version=’1.0′ encoding=’UTF-8′ ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:jsf="http://xmlns.jcp.org/jsf"
xmlns:h="http://xmlns.jcp.org/jsf/html">
<h:head>
<title>Facelet Title</title>
</h:head>
<h:body>
<h1>Completed</h1>
<p>Thank you, #{conversationBean.id}!</p>
<form jsf:id="form">
<input type="submit" jsf:action="#{conversationBean.clickCompleted()}" value="completed"/>
</form>
</h:body>
</html>
[/html]

実行結果

1画面目。入力してみる。
Screen Shot 2013-11-18 at 10.54.04 PM

2画面目。確認画面。ちゃんと入力された値が表示されている。
Screen Shot 2013-11-18 at 10.54.15 PM

3画面目。完了画面。まだ入力された値は生き残ってる。下のボタンを押すと会話が完了し値が消え、1画面目に戻る。
Screen Shot 2013-11-18 at 10.54.26 PM

戻った1画面目。Conversationが再度startされているので、値は消えて再度状態が開始されている(つまり空っぽ)。
Screen Shot 2013-11-18 at 10.54.36 PM

こんな感じですね。これであればBacking BeanJava EE 6でも動くし、EE 7が出てもそのまま持って行ける。ただ、繰り返しになるがHTML friendly style tagsだけは違うので、やっぱりEE 7への移行の際には画面に影響があるのである。返す返すも無念である。

Leave a Reply