システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

ConversationのBeginをviewAction以外で実現する

vermeer.hatenablog.jp

vermeer.hatenablog.jp

課題
viewActionはルートとなるViewに記述しないといけない
に対する対処です。

方式として、Interceptorが使えないというのは 検討済みでした。

会話が未開始の画面から遷移したときには、良い感じにBeginしてくれます。 問題は会話中の画面から別のフォルダに遷移したときです。
Interceptor内でEndしてStartすると、Conversatinが同一IDになってしまいます。
おそらくですが、ConversationIdはクライアントとの往復をもって実現しているため、同一フェーズでOff・Onしても「終わったこと」になっていなくて、新しいConversationIdを採番しないためだと考えられます。 ということで、望むタイミングでConversationをBeginするためには、xhtmlの初期表示タイミングでクライアントから明示的に実行するしかないと思います。

これに加えて、viewActionも使えないということになると、
あとは f:event type="preRenderView"を使うことになります。

欠点は、ActionListenerであるため遷移先を指定できません。

つまり 会話開始前に index.xhtml以外のページへアクセスをしようした際の会話開始ページ(index.xhtml)への強制遷移が実現できなくなります。

そこで「会話開始前に開始ページ以外へのアクセスする行為を 例外ルート、つまり実行時例外として扱う」という方式で実装しました。

ただし、今回は、一旦 会話スコープの開始と強制遷移までとして、その他の実行時例外や メッセージ出力については 別検討にします。

やりたいこと

  • f:event で 画面開設時に会話開始処理を行う。
  • 開始前に index.xhtml以外にアクセスしたら実行時例外をスローして、例外ハンドリングにて index.xhtmlへ遷移させる

実装

web.xml

他のConversationに関する例外を扱うにあたって、方式を再見直ししなくて良いように 前提として ServletFilterで Conversationの実行時例外の順序を先に指定しておきます。

    <!-- ConversationExceptionFilter -->

    <filter>
        <filter-name>ConversationExceptionFilter</filter-name>
        <filter-class>ee.filter.conversationexception.ConversationExceptionFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>ConversationExceptionFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter-mapping>
        <filter-name>CDI Conversation Filter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- ConversationExceptionFilter -->

※実行時例外を扱うフィルタークラスには、Conversationに関連する例外である BusyConversationExceptionNonexistentConversationExceptionをキャッチするようにしていますが、今回は両例外に関する処置はしません。

/**
 * Conversationに関する例外を扱うフィルターです.
 *
 * @author Yamashita,Takahiro
 */
public class ConversationExceptionFilter implements Filter {

    /**
     * {@inheritDoc }
     */
    @Override
    public void init(FilterConfig filterConfig) {
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
        } catch (BusyConversationException | NonexistentConversationException ex) {
            System.err.println(Arrays.toString(ex.getStackTrace()));
        }
    }

    /**
     * {@inheritDoc }
     */
    @Override
    public void destroy() {
    }

}

会話の開始制御

共通テンプレートとなる xhtmlに 画面描画時に会話スコープを開始するリスナーを記述します。 以前は、ここをf:viewActionで実装していましたが、それでは駄目なので f:event で対応します。 ポイントは typeの指定値です。

<f:event type="postAddToView" listener="#{conversationLifecycleManager.startConversation()}" />

一般的に f:event を使う場合は preRenderVIewとペアにしますが、これだと ページ情報を構築しようとするため 遷移前の情報を使った実装箇所があると エラーになります。
postAddToViewを指定すれば、コンポーネント構築前なので、そうしたエラーを回避できます。

会話を開始するメソッドでは、開始ページ以外から会話を開始しようとすると実行時例外をスローします。

    public void startConversation() {
        if (this.conversation.isTransient() == false) {
            return;
        }

        if (context.currentViewId().endsWith("/index.xhtml") == false) {
            throw new ConversationException();
        }

        this.conversation.begin();
        this.conversation.setTimeout(300000);

    }

あとは、例外ハンドラーにて、強制的に 同一パスの index.xhtmlへ遷移させます。

/**
 * {@link spec.scope.conversation.ConversationException}の捕捉後の処理を行う機能を提供します.
 * <p>
 * 会話スコープが既に終わっている場合の実行時例外なので、会話のスタートに位置する{@code index.xhtml}へ遷移させます。
 * 同時に会話スコープも終了させてから、再開させます。
 *
 * @author Yamashita,Takahiro
 */
public class ConversationExceptionHandler implements ThrowableHandler {

    private final ConversationLifecycleManager conversationLifecycleManager;
    private final FacesContext facesContext;

    public ConversationExceptionHandler(ConversationLifecycleManager conversationLifecycleManager, FacesContext facesContext) {
        this.conversationLifecycleManager = conversationLifecycleManager;
        this.facesContext = facesContext;
    }

    /**
     * {@inheritDoc }
     * <p>
     * {@code NavigationHandler} では正しく画面遷移が実現しなかったので、{@code ExternalContext} で遷移させます.
     */
    @Override
    public void execute() {
        conversationLifecycleManager.endConversation();

        String contextPath = facesContext.getExternalContext().getRequestContextPath();
        String currentPage = facesContext.getViewRoot().getViewId();
        String indexRootPath = currentPage.substring(0, currentPage.lastIndexOf("/") + 1);
        String indexPage = indexRootPath + "index.xhtml";
        String forwardPage = contextPath + indexPage;
        ExternalContext externalContext = facesContext.getExternalContext();
        try {
            ServletContext servletContext = (ServletContext) facesContext.getExternalContext().getContext();
            if (servletContext.getRealPath(indexPage) == null) {
                throw new ThrowableHandlerException("Target context file could not find.");
            }
            externalContext.redirect(forwardPage);
        } catch (IOException ex) {
            throw new ThrowableHandlerException(ex);
        }
    }

}

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

会話順序を守っていないシーケンスは ユースケースとしても「例外」とするし、 当初案よりも例外を使った制御の方が 個人的には分かりやすいように思います。