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

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

ConversationScopedのタイムアウトからの画面遷移

他のExceptionHandlerを実装している中で 以前の記事

vermeer.hatenablog.jp

で扱っていなかったタイムアウト周りの制御を やってみました。
ベースとするプロジェクトは、以前のサンプルのものではなく 以下のものに実装を加えて行いました。

vermeer.hatenablog.jp

やりたいこと

  • ConversationScopedがタイムアウトしたら「会話スコープの開始位置」へ遷移
  • もし 遷移先の画面描画でエラーがあったら、その時はどうしようもないのでエラー画面に遷移

会話スコープが終わった状態で操作をしたら NonexistentConversationException がスローされます。 この際、他の例外と同じ扱いをしてエラー画面へ強制遷移させるというやり方もあると思います。
セッションタイムアウトでも無い「一定操作をしていなかったから その操作を無効にします」というだけで エラー画面から、ときには再ログインを誘導するというのは ちょっと違うのではないかな?という理由です。

実装とポイント

ExceptionHandlerWrapper

JSFでの例外処理は ExceptionHandlerWrapperを拡張したクラスで制御します

public class CustomExceptionHandler extends ExceptionHandlerWrapper {

    private final ExceptionHandler wrapped;
    private final ThrowableHandlerFactory throwableHandlerFactory;
    private final ErrorPageNavigator errorPageNavigator;

    CustomExceptionHandler(ExceptionHandler exception, ThrowableHandlerFactory throwableHandlerFactory, ErrorPageNavigator errorPageNavigator) {
        this.wrapped = exception;
        this.throwableHandlerFactory = throwableHandlerFactory;
        this.errorPageNavigator = errorPageNavigator;
    }

    @Override
    public ExceptionHandler getWrapped() {
        return this.wrapped;
    }

    @Override
    public void handle() {

        final Iterator<ExceptionQueuedEvent> it = getUnhandledExceptionQueuedEvents().iterator();

        while (it.hasNext()) {

            ExceptionQueuedEventContext eventContext = (ExceptionQueuedEventContext) it.next().getSource();
            Throwable throwable = getRootCause(eventContext.getException()).getCause();

            ThrowableHandler throwableHandler = this.throwableHandlerFactory.createThrowableHandler(throwable, eventContext);

            try {
                throwableHandler.execute();

            } catch (Exception ex) {
                this.errorPageNavigator.navigate(ex);

            } finally {
                // 未ハンドリングキューから削除する
                it.remove();
            }

            getWrapped().handle();
        }

    }
}

コードは省略しますが

vermeer.hatenablog.jp

で紹介したように、DIもできます。

例外を制御

もし会話スコープの開始が任意のタイミングだったら結構面倒なことになったと思うのですが、方式として会話スコープの開始を常に index.xhtmlになるようにしているので、やることは単純です。
NonexistentConversationExceptionを受けたら index.xhtmlに遷移させるだけです。

画面遷移には ExternalContextを使用します。
NavigationHandlerを使って画面遷移をしたのですが、フェーズのタイミングが違うのか 上手く行きませんでした。*1

なお、エラー画面への遷移は、個別の例外処理で行うのではなくて ThrowableHandler の具象クラスにおいて 実行時例外としてThrowさせて共通化を図っています。

あとメッセージ出力をさせるために、状態を持ちまわします。
FacesMessageへ出力する実装もしたのですが、それだとメッセージが出力されなかったための処置です。*2

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

    private final ConversationLifecycleManager conversationLifecycleManager;
    private final FacesContext facesContext;
    private final NonexistentConversationExceptionMessage nonexistentConversationExceptionMessage;

    public NonexistentConversationExceptionHandler(ConversationLifecycleManager conversationLifecycleManager, NonexistentConversationExceptionMessage nonexistentConversationExceptionMessage, FacesContext facesContext) {
        this.conversationLifecycleManager = conversationLifecycleManager;
        this.facesContext = facesContext;
        this.nonexistentConversationExceptionMessage = nonexistentConversationExceptionMessage;
    }

    /**
     * {@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.");
            }
            nonexistentConversationExceptionMessage.setException();
            externalContext.redirect(forwardPage);
        } catch (IOException ex) {
            throw new ThrowableHandlerException(ex);
        }
    }

}


会話開始画面へ遷移して会話を再開するときにメッセージを出力します。

/**
 * 会話スコープのライフサイクルを操作するクラスです.
 *
 * @author Yamashita,Takahiro
 */
@Named
@ApplicationScoped
public class ConversationLifecycleManager {

   (略)

    /**
     * 会話スコープの開始します。
     * <P>
     * 会話スコープが未開始にもかかわらず、indexページ以外を遷移先として指定していた場合は、強制的にindexページへ遷移させます.
     *
     * @return 会話スコープ開始済みの場合は指定のページ、未開始の場合はindexページ
     */
    public String startAndForwardIndexPage() {

        // NonexistentConversationException があった場合にメッセージを出力します.
        if (nonexistentConversationExceptionMessage.state()
            == NonexistentConversationExceptionMessage.State.HAS_EXCEPTION) {
            String message = this.messageConverter.toMessage(nonexistentConversationExceptionMessage.message());
            this.messageWriter.appendErrorMessage(message);
        }

        String currentViewId = context.currentViewId();
        if (this.conversation.isTransient() == false) {
            return currentViewId;
        }

        this.startConversation();

        if (currentViewId.equals("index.xhtml") == false) {
            return (String) context.responseViewId("index.xhtml");
        }
        return (String) context.responseViewId(currentViewId);
    }

   (略)

エラー画面への遷移をするクラス

これは、このクラスが読みだされる場合、必ずしも会話スコープを使っているとは限らないので、 念のため会話スコープを終了させつつ、エラー画面へ遷移させます。

@ApplicationScoped
public class ErrorPageNavigator {

    private ConversationLifecycleManager conversationLifecycleManager;

    public ErrorPageNavigator() {
    }

    @Inject
    public ErrorPageNavigator(ConversationLifecycleManager conversationLifecycleManager) {
        this.conversationLifecycleManager = conversationLifecycleManager;
    }

    /**
     * エラー画面へ遷移させます.
     */
    public void navigate(Exception ex) {
        System.err.println(ex.getMessage());
        System.err.println(Arrays.toString(ex.getStackTrace()));
        conversationLifecycleManager.endConversation();

        FacesContext facesContext = FacesContext.getCurrentInstance();
        NavigationHandler navigationHandler = facesContext.getApplication().getNavigationHandler();
        String forwardPage = "/error.xhtml?faces-redirect=true";
        navigationHandler.handleNavigation(facesContext, null, forwardPage);
        facesContext.renderResponse();
    }
}

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

参考情報

stackoverflow.com

jjug-jsf/MyExceptionHandler.java at master · MasatoshiTada/jjug-jsf · GitHub

JSF2.0のエラーハンドリング - 見習いプログラミング日記

さいごに

NavigationHandlerで画面遷移が思ったように出来なかったのは想定外でしたが、こういうのも色々と学びにつながって良いですね。
ちなみに、まだ BusyConversationExceptionについては対応していません。
どっかでやんないとなぁ。

追記(2018/12/6)

このやり方はスレッドアンセーフな気がします。
コードの記述を省略しているNonexistentConversationExceptionMessageはスコープをSessionScopedにしており、複数の会話スコープが並列に存在すると適切に処理できない可能性が大きい気がします。
別対応に関連して、方式自体を見直しているため検証はしませんが、参考にされる方は ご自身でテストをするなどしてください。 補足として、メッセージを出力後 ステータスは初期化しているので「操作でエラーが出て、画面に表示されるまでのステータス」となっているので 概ね問題は発生しないとは思いますが、複数タブを開いて それぞれで操作をしていると 想定していない挙動になる可能性は あるにはあると思います。


*1:勝手にこういうことなのかな?という想像はありますが、それを調べるのは、、まぁ良いかなと。。

*2:NavigationHandlerで画面遷移が出来なかった理由と同じような理由だと想像しています。