JSF2でFaceletsとSystemEventを組合せてみる

JSF2ではSystemEventなるものが存在します。これがなかなか便利でFaceletsのテンプレート組合せると、例えばログイン画面以外の画面にユーザがアクセスした場合、ログインしていなければ即座にログイン画面へ遷移させるという処理を、テンプレートで集約することも可能です。

以下のようなテンプレートを用意します。

【template.xhtml

<!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:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

    <f:view>
        <f:event listener="#{auth.isLoggedIn}" type="preRenderView" />
        <h:head>
            <title><ui:insert name="title" /></title>
        </h:head>
        <h:body>
            <ui:insert name="content" />
            <ui:debug />
        </h:body>
    </f:view>
</html>

ポイントはです。これは、Viewのレンダリングが実行される前にauthコンポーネントのisLoggedInメソッドでログイン済みかどうかの確認をpreRenderViewシステムイベントを用いてチェックしています。

preRenderViewその名の通り、Viewが表示される前に実行されるイベントです。

次にテンプレートのcontentに挿入するメイン部分の定義をしたViewを用意します。ここではtop.xhtmlとします。

【top.xhtml

<!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:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">
    
    <ui:composition template="/.../template.xhtml">
        <ui:define name="title">ようこそ</ui:define>
        
        <ui:define name="content">
            <h:outputText value="ようこそ#{auth.credentials.name}さん" />
            <br/>
            <h:outputText value="メニュー" />
            <br/>
            <h:form>
                <h:commandButton action="hoge" value="XYZ" />
                <h:commandButton action="#{auth.logout}" value="ログアウト" />
            </h:form>
        </ui:define>
    </ui:composition>
</html>

このtop.xhtmlではコンテンツ内容に「ようこそxyzさん」という情報と各種メニューを表示していますが、このページがもしログイン前にアクセスされてはユーザの名前が表示されません。また、ログインしていないのにログイン済みでなければ利用できないページに通常はアクセスさせないと思います。

そこでテンプレートでログイン済かどうかのチェックを行う定義を記述し、ログイン済みであれば利用可能なコンテンツをテンプレートに挿入して表示させるという方式をとれば、各画面でチェックを行うことを避けることが可能です。
仮にログイン済みでなければ利用できない検索画面を作成する場合は、template.xhtmlに挿入する検索画面のレイアウトのみに集中すればよいということです。

では、authクラスは次のような感じ。

【Authentication】

import javax.enterprise.context.SessionScoped;
import javax.faces.application.ConfigurableNavigationHandler;
import javax.faces.context.FacesContext;
import javax.inject.Inject;
import javax.inject.Named;

@Named("auth")
@SessionScoped
public class Authentication implements Serializable {

    @Inject
    private Credentials credentials;
    
    private boolean loggedIn = false;
    
    public String login() {
        // ログイン処理
        ...
        loggedIn = true;
        return outcome;
    }
    
    public String logout() {
        // ログアウト処理
        return outcome;
    }

    public Credentials getCredentials() {
        return credentials;
    }

    public void setCredentials(Credentials credentials) {
        this.credentials = credentials;
    }

    public void isLoggedIn() {
        if (!loggedIn) {
            ConfigurableNavigationHandler handler = (ConfigurableNavigationHandler)
                FacesContext.getCurrentInstance()
                    .getApplication().getNavigationHandler();
            handler.performNavigation("login");
        }
    }
}

isLoggedInメソッドでログイン済みでなければFacesContextをからConfigurableNavigationHandlerを取得し強制的にlogin画面に遷移させています。

ログイン画面は逆にtemplateを定義してはいけません。template.xhtmlはログイン済みでないコンテンツに対する共通のテンプレートだからです。

【login.xhtml

<!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:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:ui="http://java.sun.com/jsf/facelets">

	<h:head>
        <title>Login</title>
    </h:head>
    <h:body>
        <h:form>
            <h:panelGrid columns="2">
                <h:outputLabel value="名前" />
                <h:inputText value="#{credentials.name}" required="true"/>
                <h:outputLabel value="パスワード" />
                <h:inputSecret value="#{credentials.password}" required="true"/>
            </h:panelGrid>
            <h:commandButton action="#{auth.login}" value="ログイン" />
        </h:form>
    </h:body>
</html>

ついでにCredentialsはこんな感じです。

import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@Named
@SessionScoped
public class Credentials implements Serializable {

    private String name;
    
    private String password;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

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

テンプレートでログインチェックを行うことで、各画面での冗長な記述を削減することが可能です。

システムイベントはこれ以外にもいくつか種別があるので、いろいろ試してGoodノウハウをパターン化しておくといいかもしれません。

今回はとりあえず一番シンプルなpreRenderViewイベントをFaceletsと組合せで紹介しました。