めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

JSF2.0のLifeCycleその2

JSF2.0のSystem Eventsを使うとLifeCycleがリアルに確認できていいですね。カスタムコンポーネントと組み合わせて、いろいろ覗いてみました。

使用するサンプル

「Core JavaServer Faces(Third Edition)」にあったUISpinnerをカスタムコンポーネントとして使います。画面キャプチャを貼るのが面倒なので、Lynxの画面で説明します。

[Down][Up]ボタンで数値を増減して、[Submit]すると結果が表示されます。

   What is your number?
   3___________________ Down Up
   Submit
   Your number: 3
   Return

ソース

・カスタムタグの指定
web.xml

<?xml version="1.0"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
・・・
	<context-param>
		<param-name>javax.faces.FACELETS_LIBRARIES</param-name>
		<param-value>/WEB-INF/sandbox.taglib.xml</param-value>
	</context-param>
</web-app>

sandbox.taglib.xml

<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib version="2.0"
   xmlns="http://java.sun.com/xml/ns/javaee"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
   http://java.sun.com/xml/ns/javaee/web-facelettaglibary_2_0.xsd">
   <namespace>http://sandbox.com</namespace>
   <tag>
      <tag-name>spinner</tag-name>
      <component>
         <component-type>sandbox.Spinner</component-type>         
      </component>      
   </tag>
</facelet-taglib>

・Facelets
index.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" xmlns:sandbox="http://sandbox.com">

<h:head>
	<title>HOGE</title>
</h:head>
<h:body>
	What is your number?<br/>
	<h:form>
		<sandbox:spinner value="#{myBean.value}" id="mySpinner" />
		<br/>
		<h:commandButton value="Submit" action="output" />
	</h:form>
	<h:messages />
</h:body>
</html>

output.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"
	xmlns:sandbox="http://sandbox.com">

<h:head>
	<title>HOGE</title>
</h:head>
<h:body>
	<h:form>
	Your number: <h:outputText value="#{myBean.value}" />
		<br />
		<h:commandButton value="Return" action="index" />
	</h:form>
</h:body>
</html>

・カスタムコンポーネント
sandbox/UISpinner.java

package sandbox;

import java.io.IOException;
import java.util.Map;

import javax.faces.component.FacesComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.IntegerConverter;

@FacesComponent("sandbox.Spinner")
public class UISpinner extends UIInput {
	private static final String MORE = ".more";
	private static final String LESS = ".less";
	
	public UISpinner() {
		System.out.println("Constructing UISpinner" + this);
		setConverter(new IntegerConverter());
		setRendererType(null);
	}
	
	@Override
	public void encodeBegin(FacesContext context) throws IOException {
		System.out.println("Encoding UISpinner " + this);
		ResponseWriter writer = context.getResponseWriter();
		String clientId = getClientId(context);
		
		encodeInputField(writer, clientId);
		encodeDecrementButton(writer, clientId);
		encodeIncrementButton(writer, clientId);
	}
	
	@Override
	public void decode(FacesContext context) {
		System.out.println("Decoding UISpinner" + this);
		Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap();
		String clientId = getClientId(context);

		int increment;
		if (requestMap.containsKey(clientId + MORE)) increment = 1;
		else if (requestMap.containsKey(clientId + LESS)) increment = -1;
		else increment = 0;
		
		try {
			int submittedValue = Integer.valueOf((String) requestMap.get(clientId));
			int newValue = submittedValue + increment;
			setSubmittedValue("" + newValue);
		} catch (NumberFormatException ex) {
			setSubmittedValue((String) requestMap.get(clientId));
		}
	}
	
	// private
	private void encodeInputField(ResponseWriter writer, String clientId) throws IOException {
		writer.startElement("input", this);
		writer.writeAttribute("name", clientId, null);
		Object v = getValue();
		if (v != null) writer.writeAttribute("value", v, "value");
		writer.endElement("input");
	}
	
	private void encodeIncrementButton(ResponseWriter writer, String clientId) throws IOException {
		writer.startElement("input", this);
		writer.writeAttribute("type", "submit", null);
		writer.writeAttribute("name", clientId + MORE, null);
		writer.writeAttribute("value", "Up", "value");
		writer.endElement("input");
	}
	
	private void encodeDecrementButton(ResponseWriter writer, String clientId) throws IOException {
		writer.startElement("input", this);
		writer.writeAttribute("type", "submit", null);
		writer.writeAttribute("name", clientId + LESS, null);
		writer.writeAttribute("value", "Down", "value");
		writer.endElement("input");
	}
}

コンストラクタ、encodeBegin、decodeのタイミングでログを吐くようにしてあります。Backing Beanのソースは省略。

・Event Listenerの仕込み
faces-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
	version="2.0">
	<lifecycle>
		<phase-listener>lib.PhaseTracker</phase-listener>
	</lifecycle>
	<application>
		<system-event-listener>
			<system-event-listener-class>lib.ViewEventListener</system-event-listener-class>
			<system-event-class>javax.faces.event.PostAddToViewEvent</system-event-class>
		</system-event-listener>
		<system-event-listener>
			<system-event-listener-class>lib.ViewEventListener</system-event-listener-class>
			<system-event-class>javax.faces.event.PreRemoveFromViewEvent</system-event-class>
		</system-event-listener>
	</application>
</faces-config>

PhaseEventに加えて、PostAddToViewEvent、PreRemoveFromViewEventのリスナーを追加しています。リスナーの実装は次の通り。

lib/PhaseTracker.java

package lib;

import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;

public class PhaseTracker implements PhaseListener {
	private static final long serialVersionUID = -2917841296997807166L;

	@Override
	public PhaseId getPhaseId() {
		return PhaseId.ANY_PHASE;
	}

	@Override
	public void beforePhase(PhaseEvent event) {
		System.out.println( "--> Before: " + event.getPhaseId().toString());
	}

	@Override
	public void afterPhase(PhaseEvent event) {
		System.out.println("<-- After: " + event.getPhaseId().toString());
	}
}

ViewEventListener.java

package lib;

import javax.faces.event.AbortProcessingException;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;

public class ViewEventListener implements SystemEventListener {

	@Override
	public boolean isListenerForSource(Object obj) {
		if (obj instanceof sandbox.UISpinner)
			return true;
		return false;
	}

	@Override
	public void processEvent(SystemEvent event) throws AbortProcessingException {
		String sourceClass = event.getSource().getClass().getCanonicalName();
		String eventName = event.toString();
		System.out.println(sourceClass + ": " + eventName + " is Called");
	}
}

全てのPhaseのBefore/AfterとUISpinnerのAdd/Remove Viewについてログを吐きます。

実行結果

・初回画面表示

10:37:00,902 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: RESTORE_VIEW 1
10:37:00,939 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: RESTORE_VIEW 1
10:37:00,940 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: RENDER_RESPONSE 6
10:37:00,989 INFO  [stdout] (http--127.0.0.1-8080-1) Constructing UISpinnersandbox.UISpinner@68b57582
10:37:01,032 INFO  [stdout] (http--127.0.0.1-8080-1) sandbox.UISpinner: javax.faces.event.PostAddToViewEvent[source=sandbox.UISpinner@68b57582] is Called
10:37:01,046 INFO  [stdout] (http--127.0.0.1-8080-1) Encoding UISpinner sandbox.UISpinner@68b57582
10:37:01,066 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: RENDER_RESPONSE 6

RENDER_RESPONSEフェーズで、UISpinnerオブジェクトが生成されて、対応するHTMLが吐き出されています。

・[Up]ボタン実行

:38:29,891 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: RESTORE_VIEW 1
10:38:29,893 INFO  [stdout] (http--127.0.0.1-8080-1) Constructing UISpinnersandbox.UISpinner@2b3a9ca5
10:38:29,894 INFO  [stdout] (http--127.0.0.1-8080-1) sandbox.UISpinner: javax.faces.event.PostAddToViewEvent[source=sandbox.UISpinner@2b3a9ca5] is Called
10:38:29,895 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: RESTORE_VIEW 1
10:38:29,895 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: APPLY_REQUEST_VALUES 2
10:38:29,896 INFO  [stdout] (http--127.0.0.1-8080-1) Decoding UISpinnersandbox.UISpinner@2b3a9ca5
10:38:29,896 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: APPLY_REQUEST_VALUES 2
10:38:29,896 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: PROCESS_VALIDATIONS 3
10:38:29,915 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: PROCESS_VALIDATIONS 3
10:38:29,915 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: UPDATE_MODEL_VALUES 4
10:38:29,916 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: UPDATE_MODEL_VALUES 4
10:38:29,916 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: INVOKE_APPLICATION 5
10:38:29,916 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: INVOKE_APPLICATION 5
10:38:29,917 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: RENDER_RESPONSE 6
10:38:29,919 INFO  [stdout] (http--127.0.0.1-8080-1) Encoding UISpinner sandbox.UISpinner@2b3a9ca5
10:38:29,919 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: RENDER_RESPONSE 6

私には予想外だったのですが、RESTORE_VIEWフェーズで、UISpinnerオブジェクトが再度生成されています。Viewのツリー構造はどこかに保存されているはずですが、付随するコンポーネントのオブジェクトは毎回生成しているようです。RENDER_RESPONSEでは、先のフェーズで生成済みのオブジェクトを使って、HTMLを吐きます。

・[Submit]実行

10:40:16,960 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: RESTORE_VIEW 1
10:40:16,962 INFO  [stdout] (http--127.0.0.1-8080-1) Constructing UISpinnersandbox.UISpinner@5721d4eb
10:40:16,962 INFO  [stdout] (http--127.0.0.1-8080-1) sandbox.UISpinner: javax.faces.event.PostAddToViewEvent[source=sandbox.UISpinner@5721d4eb] is Called
10:40:16,963 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: RESTORE_VIEW 1
10:40:16,964 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: APPLY_REQUEST_VALUES 2
10:40:16,964 INFO  [stdout] (http--127.0.0.1-8080-1) Decoding UISpinnersandbox.UISpinner@5721d4eb
10:40:16,964 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: APPLY_REQUEST_VALUES 2
10:40:16,965 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: PROCESS_VALIDATIONS 3
10:40:16,966 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: PROCESS_VALIDATIONS 3
10:40:16,966 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: UPDATE_MODEL_VALUES 4
10:40:16,967 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: UPDATE_MODEL_VALUES 4
10:40:16,967 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: INVOKE_APPLICATION 5
10:40:16,968 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: INVOKE_APPLICATION 5
10:40:16,969 INFO  [stdout] (http--127.0.0.1-8080-1) --> Before: RENDER_RESPONSE 6
10:40:16,981 INFO  [stdout] (http--127.0.0.1-8080-1) <-- After: RENDER_RESPONSE 6

やはり、RESTORE_VIEWフェーズで、UISpinnerオブジェクトが再度生成されています。今回は、新しいページに遷移するので、RENDER_RESPONSEフェーズでは、もはやUISpinnerは使われません。