Eclipse+JSF+JPAで作るアプリ(11)―ManagedBeanと画面

前回は、xhtmlで書かれたテンプレートとそのパーツ、テンプレートクライアントについて記述しました。

今回は、マネージドビーンについて記述します。

  • JSFでの画面要素は、Managed Beanと呼ばれるPOJOにマップされます。
  • また、画面上のアクション(コマンドボタンの実行やリンク)も、Managed BeanのStringを返すメソッドにマップされます。
  • このメソッドの戻り値である文字列を、画面遷移先として定義された文字列として返すことによりたページに遷移します。


Managed Beanは以前、バッキングビーン(Backing Bean)とも呼ばれています。
Managed Beanのクラス名ですが、Xxx, XxxManage, XxxBean, XxxManagedBean, XxxPage, XxxViewといくつも流派があるようです。
本ブログでは、PrimefacesのShowcaseに従ってXxxViewで統一します。

パッケージ構成

sample.yourlibraryに、viewパッケージを追加します。
ログイン画面用に、LoginView、ユーザ編集画面用に、EditUserView、ログインユーザをセッションスコープで管理するために、SessionInfoを作成します。
ViewUtilは、メッセージ(FacesMessage)と、上記SessionInfoを取得するためのAPIを提供しています。
f:id:tshix:20150726041201p:plain

LoginViewと、login.xhtml

ログイン画面は、アカウント、パスワードの2つのフィールドと、「ログイン」ボタンのみです。
これらは、LoginViewクラスの2つのフィールドaccount, passwordと、1つのメソッドloginにマッピングされます。

LoginView.java

package sample.yourlibrary.view;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

import lombok.Getter;
import lombok.Setter;
import sample.yourlibrary.entity.User;
import sample.yourlibrary.logic.UserManager;

@ManagedBean(name="loginView")
@ViewScoped
public class LoginView {
	@Getter @Setter
	private String account;
	@Getter @Setter
	private String password;

	public String login()
	{
		User loginUser = UserManager.login(account, password);
		if( loginUser == null )
		{
			ViewUtil.AddErrorMessage("ログイン失敗", "アカウントまたはパスワードが一致しません。");
			return "failure";
		}
		SessionInfo sessionInfo = ViewUtil.getSessionInfo();
		sessionInfo.setLoginUser(loginUser);
		return "success";
	}
}

(コードを短くするため、Lombok.jarを導入しています。@Getter, @Setterでアクセッサメソッドが自動で生成されます。
@DataだとtoStringの循環参照などやり過ぎになるため、現在のところ、Getter、Setterのみ使用しています。
Lombokについては、
LombokとLombok-pg: Javaコードを減量する魔法のスパイス - I am programmer and proud
【Java】Lombokで冗長コードを削減しよう | キャスレーコンサルティング 技術ブログ
などを参照ください)


前回は、標準のhttp://java.sun.com/jsf/htmlのタグ(h:commandButtonなど)を利用していましたが今回からはPrimefacesのタグに一新します。
ほとんどのJSFタグはオーバーライドされていますが、所々違うため注意が必要です。

  • <h:outputText> → <p:outputLabel> 通常テキスト(ラベル)
  • <h:inputText> → <p:inputText> エディットボックス
  • <h:inputSecret> → <p:password> パスワード
  • <h:commandButton> → <p:commandButton> エディットボックス
  • <h:messages> → <p:messages> エディットボックス
<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"
      xmlns:p="http://primefaces.org/ui"
      >
<h:form id="loginForm">
	<p:growl showDetail="true" autoUpdate="true"/>
	<p:graphicImage url="./resource/image/top.jpg"/>
	<p:panelGrid columns="2">
		<p:outputLabel value="アカウント:" />
		<p:inputText value="#{loginView.account}" />

		<p:outputLabel value="パスワード:" />
		<p:password value="#{loginView.password}" />
	</p:panelGrid>
	<p:commandButton value="ログイン" action="#{loginView.login}" />
	<p:messages showDetail="true" style="color:red" />
</h:form>
</html>
"action"アトリビュートについて

Stringを返すManagedBeanのメソッドと対応させます。
戻す文字列は、LoginView.loginを参照すると分かるようにfailureとsuccessを返しています。
これは、faces-config.xmlのnavigation-ruleタグで以下のように定義しています。

 <navigation-rule>
	<from-view-id>/index.xhtml</from-view-id>
	<navigation-case>
		<from-outcome>failure</from-outcome>
		<to-view-id>/index.xhtml?faces-redirect=true</to-view-id>
	</navigation-case>
	<navigation-case>
		<from-outcome>success</from-outcome>
		<to-view-id>/editUser.xhtml?faces-redirect=true</to-view-id>
	</navigation-case>
</navigation-rule>

この戻り値の文字列は、直接ページ名を指定することもできます。

growlについて
	<p:growl showDetail="true" autoUpdate="true"/>

とありますが、これは、FacesMessageをポップアップで表示するものです。
次の画像は右上のように表示されます。 
f:id:tshix:20150726082040p:plain
自動でフェードアウトしますが、sticky="true"にすると、×ボタンを押すまで残ります。

EditUserViewと、userlist.xhtml

ユーザ編集画面は、ユーザ登録のためのフィールドと追加ボタンのフォーム、ユーザー一覧のフォームです。
この2つのフォームは、Strutsと異なり、1つのView、EditUserViewにマップされます。

  • @ManagedBean(name="editUserView")は、ApplicationServerのコンテナにこの名前で管理対象ビーンとして生成させる指定をしています。
  • @ViewScope 同一ページ内での遷移でフォームの情報をキープするスコープを指定しています。
  • @PostConstructアノテーションメソッドは、ビューが初期化された際に呼び出されるメソッドです。@PreDestoryというアノテーションもあります。そちらは破棄する直前に呼ばれるメソッドです。void型である必要があります。
package sample.yourlibrary.view;

import java.util.List;

import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

import lombok.Getter;
import lombok.Setter;
import sample.yourlibrary.entity.User;
import sample.yourlibrary.logic.UserManager;

@ManagedBean(name="editUserView")
@ViewScoped
public class EditUserView {
	@Getter @Setter
	private String account;
	@Getter @Setter
	private String name;
	@Getter @Setter
	private String password;
	@Getter @Setter
	private String email;
	@Getter @Setter
	private boolean isAdmin;

	@Getter @Setter
	private List<User> users;

	@PostConstruct
	public void init()
	{
		users = UserManager.findAll();
	}
	
	public String addUser()
	{
		if( account == null || name == null )
			return null;
		User user = UserManager.createUser(account, name);
		user.setPassword(password);
		user.setEmail(email);
		user.setAdmin(isAdmin);
		user = UserManager.updateUser(user);
		users = UserManager.findAll();
		return "success";
	}
}


userlist.xhtmlです。
登録フォームは、login.xhtmlとほとんど同じですが、一覧フォームはPrimefacesのp:datatableタグを大活用しています。

<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"
      xmlns:p="http://primefaces.org/ui"
      >
<h2>ユーザー編集</h2>
<h:form id="addUserForm">
	<p:growl showDetail="true" autoUpdate="true"/>

	<p:panelGrid columns="2" >
		<p:outputLabel value="アカウント:" />
		<p:inputText id="account" value="#{editUserView.account}" 
			required="true" requiredMessage="アカウント名は必須です。"/>
		
		<p:outputLabel value="名前:" />
		<p:inputText id="name" value="#{editUserView.name}" 
			required="true" requiredMessage="名前は必須です。"/>
		
		<p:outputLabel value="パスワード:" />
		<p:password value="#{editUserView.password}" />
	
		<p:outputLabel value="E-mail:" />
		<p:inputText id="email" value="#{editUserView.email}" />
	</p:panelGrid>
	<p:messages />
	<p:commandButton value="ユーザの追加" action="#{editUserView.addUser}" 
		update=":userListForm" />
</h:form>
<br />
<br />
<h2>ユーザー一覧</h2>
<h:form id="userListForm">
	<p:dataTable id="userList" var="user" value="#{editUserView.users}"
		paginator="true" paginatorPosition="top"
		rows="10" rowsPerPageTemplate="5,10,15,30,50"
		sortMode="multiple"
		>
		<p:column sortBy="#{user.account}" filterBy="#{user.account}" filterMatchMode="contains">
			<f:facet name="header">
				<p:outputLabel value="アカウント" />
			</f:facet>
			<p:outputLabel value="#{user.account}" />
		</p:column>

		<p:column sortBy="#{user.name}" filterBy="#{user.name}" filterMatchMode="contains">
			<f:facet name="header">
				<p:outputLabel value="名前" />
			</f:facet>
			<p:outputLabel value="#{user.name}" />
		</p:column>

		<p:column sortBy="#{user.email}" filterBy="#{user.email}" filterMatchMode="contains">
			<f:facet name="header">
				<p:outputLabel value="e-mail" />
			</f:facet>
			<p:outputLabel value="#{user.email}" />
		</p:column>
	</p:dataTable>
</h:form>
</html>

p:datatableタグでできること

画像を見ていただいてから、p:datatableを説明します。前回のh:datatableよりはるかにリッチになっていることがわかるはずです。
f:id:tshix:20150726084512p:plain

<p:dataTable id="userList" var="user" value="#{editUserView.users}"
	paginator="true" paginatorPosition="top"
	rows="10" rowsPerPageTemplate="5,10,15,30,50"
	sortMode="multiple"
	>
	<p:column sortBy="#{user.account}" filterBy="#{user.account}" filterMatchMode="contains">

これらの機能は、xhtml上でタグのアトリビュートを変更するだけで設定可能になっています。

  • ページナビゲータがついています。paginator=trueです。paginatorPosition=topで上側だけにナビゲータを付けています。rows=10で、一度に表示する行を10にしています。rowsPerPageTemplateでコンボボックスに表示する行数を5,10,15,30,50としています。
  • ソートができます。p:columnタグのsortByでソートに使う属性を指定しています。p:datatableタグのsortMode="multiple"で複数カラムのソートを行っています。
  • フィルタができます。p:columnタグのfilterByでフィルタに使う属性を、filterMatchModeでフィルタの条件を指定しています。

p:datatableはできることが沢山あるため、別途掘り下げていく予定です。

Primefaces showcaseにも、以下のように大量の例があります。
f:id:tshix:20150726085350p:plain

SessionInfo

ログインユーザの情報を保持するためのセッションスコープのビーンです。
ログアウトを行っています。ログアウト方法ですが、セッションを破棄し、リクエストからlogoutを呼び出しています。
たかがレルムされどレルム GlassFish で始める詳細 JDBC レルム | 寺田 佳央 - Yoshio Teradaの中ほどのスライド85pを参考にしています。

package sample.yourlibrary.view;

import java.io.Serializable;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;

import lombok.Getter;
import lombok.Setter;
import sample.yourlibrary.entity.User;

@ManagedBean(name="sessionInfo")
@SessionScoped
public class SessionInfo implements Serializable {
	private static final long serialVersionUID = 9186759612086888662L;

	@Getter @Setter
	private User loginUser;

	public String logout()
	{
		ExternalContext externalContext = FacesContext.getCurrentInstance().getExternalContext();
		externalContext.invalidateSession();
		HttpServletRequest request = (HttpServletRequest)externalContext.getRequest();
		try {
			request.logout();
		} catch (ServletException e) {
			e.printStackTrace();
		}
		return "/index.xhtml?faces-redirect=true";
	}	
}

ViewUtil

FacesMessageと上記SessionInfoを取得するためのAPI、ログアウトのAPIです。
ページ間をまたがる情報はgetSessionInfoで渡す予定です。(カートに追加されている映画などの情報。)
この作法がJSFの設計にマッチしているか不明です。
Flash+@PostConstruct,@PreDestoryを使った方法でも可能と思われます。(いざStrutsからJSF 2へ! 移行における最大のポイントは?──最新Java EE開発“虎の穴” 第2回 岩崎浩文氏 - page2 - builder by ZDNet Japanで紹介されています)
@ManagedPropertyを各Viewに設定するよりは、ユーティリティで取得するほうが便利だからそうしています。

package sample.yourlibrary.view;

import javax.el.ELContext;
import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.context.FacesContext;

public class ViewUtil {

	public static void AddMessage(String summary, String detail) {
		AddMessageInner(FacesMessage.SEVERITY_INFO, summary, detail);
	}

	public static void AddWarningMessage(String summary, String detail) {
		AddMessageInner(FacesMessage.SEVERITY_WARN, summary, detail);
	}

	public static void AddErrorMessage(String summary, String detail) {
		AddMessageInner(FacesMessage.SEVERITY_ERROR, summary, detail);
	}

	public static void AddFatalMessage(String summary, String detail) {
		AddMessageInner(FacesMessage.SEVERITY_FATAL, summary, detail);
	}

	private static void AddMessageInner(Severity severity, String summary, String detail) {
		FacesMessage message = new FacesMessage(severity, summary, detail);
		FacesContext.getCurrentInstance().addMessage(null, message);
	}

	public static SessionInfo getSessionInfo() {
		ELContext elContext = FacesContext.getCurrentInstance().getELContext();
		SessionInfo sessionInfo = (SessionInfo) FacesContext.getCurrentInstance()
				.getApplication().getELResolver()
				.getValue(elContext, null, "sessionInfo");
		return sessionInfo;
	}
}

以上が、画面の詳細です。

次回は、Primefacesを使ったテーマの変更から入ります。
このブログは機能より、見た目や実装したいギミックを重視しています。