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)

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

}

1画面目 (index.xhtml)

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

2画面目 (confirm.xhtml)

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

3画面目 (completed.xhtml)

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

実行結果

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