環境
- jsf 2.2
- primefaces 6.1
背景
- ドメイン機能をcustom componentで実現したいと思ったが、custom componentのベストプラクティスみたいなのをぐぐってもうまく見つけられなかった。
- custom tagを使った古い実装例が多く引っ掛かってくるのも残念な感じだった。(jsfってやっぱり人気ないのかな)
- 『パーフェクト Java EE』にも少ししか記載がなく、今回の事例についてはあまり参考になかった。
ゴール
jsfのcustom componentで拡張コンポーネントを作る場合にどのように実装するのがいいのか、jsfとprimefacesのソースを参考に考えてみる。
確認したこと
jsfとprimefacesのinputTextのコンポーネントとレンダラーの仕組みについて
参考にしたソースコード
primefaces
https://www.primefaces.org/downloads/
primefaces-6.1.jar
primefaces-6.1-sources.jar
jsfのinputText仕組み
コンストラクタでrendererTypeを指定して、それに対応したrendererが呼ばれる。
public class HtmlInputText extends javax.faces.component.UIInput implements ClientBehaviorHolder {
public HtmlInputText() {
super();
setRendererType("javax.faces.Text");
}
}
TODO:ここのマッピング定義がうまく見つけられなかった。要確認。
public class TextRenderer extends HtmlBasicInputRenderer {
@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {...}
}
primefacesのinputText仕組み
jsfと同じ。
public class InputText extends HtmlInputText implements org.primefaces.component.api.Widget {
public static final String DEFAULT_RENDERER = "org.primefaces.component.InputTextRenderer";
public InputText() {
setRendererType(DEFAULT_RENDERER);
}
}
<renderer>
<component-family>org.primefaces.component</component-family>
<renderer-type>org.primefaces.component.InputTextRenderer</renderer-type>
<renderer-class>org.primefaces.component.inputtext.InputTextRenderer</renderer-class>
</renderer>
public class InputTextRenderer extends InputRenderer {
@Override
public void decode(FacesContext context, UIComponent component) {...}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {...}
}
コンポーネントとレンダラーの処理フロー
UIComponentのencodeAllで「encodeBegin/encodeEnd」を呼び出し、
「encodeBegin/encodeEnd」がそれぞれRendererの「encodeBegin/encodeEnd」を呼び出している。
さらにRendererの「encodeEnd」で「getEndTextToRender」を呼び出している。
public void encodeAll(FacesContext context) throws IOException {
...
encodeBegin(context);
if (getRendersChildren()) {
encodeChildren(context);
} else if (this.getChildCount() > 0) {
for (UIComponent kid : getChildren()) {
kid.encodeAll(context);
}
}
encodeEnd(context);
}
public void encodeBegin(FacesContext context) throws IOException {
...
String rendererType = getRendererType();
if (rendererType != null) {
Renderer renderer = this.getRenderer(context);
if (renderer != null) {
renderer.encodeBegin(context, this);
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Can't get Renderer for type " + rendererType);
}
}
}
}
public void encodeEnd(FacesContext context) throws IOException {
...
String rendererType = getRendererType();
if (rendererType != null) {
Renderer renderer = this.getRenderer(context);
if (renderer != null) {
renderer.encodeEnd(context, this);
}
}
popComponentFromEL(context);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
...
String currentValue = getCurrentValue(context, component);
getEndTextToRender(context, component, currentValue);
}
custom componentの良い拡張方法
属性追加はコンポーネントに、表示制御はレンダラーにというのが良さそう。
主なレンダラーの拡張するメソッドは以下あたり。
-
レンダリングの初期値などを変更
-
encodeBegin
-
レンダリングの処理を変更
-
encodeEnd
-
getEndTextToRender
-
レンダリングの子要素を変更
-
encodeChildren
-
リクエスト値の変換処理などを変更
-
decode
例1 属性追加+レンダリング変更
import javax.faces.component.FacesComponent;
import javax.faces.component.html.HtmlInputText;
@FacesComponent(value="org.test.component.TestInputText", tagName="inputText" createTag=true, namespace="http://test.org/component")
public class TestInputText extends HtmlInputText {
public static final String DEFAULT_RENDERER = "org.test.component.TestInputTextRenderer";
public static final String COMPONENT_FAMILY = "org.test.component";
// 属性追加
public enum PropertyKeys {
param1;
String toString;
PropertyKeys(String toString) {
this.toString = toString;
}
PropertyKeys() {}
public String toString() {
return ((this.toString != null) ? this.toString : super.toString());
}
}
public TestInput() {
super();
setRendererType(DEFAULT_RENDERER);
}
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
public String getParam1() {
return (String) getStateHelper().eval(PropertyKeys.param1, null);
}
public void setParam1(String param1) {
getStateHelper().put(PropertyKeys.param1, param1);
}
}
import com.sun.faces.renderkit.html_basic.TextRenderer;
import javax.faces.context.FacesContext;
public class TestInputTextRenderer extends TextRenderer {
@Override
public void encodeBegin(FacesContext context, UIComponent component) {
// レンダリングの変更点
TestInputText input = (TestInputText)component;
if ("xxx".equals(input.getParam1())) {
input.setMaxlength(10);
}
}
}
<renderer>
<component-family>org.test.component</component-family>
<renderer-type>org.test.component.TestInputTextRenderer</renderer-type>
<renderer-class>org.test.component.TestInputTextRenderer</renderer-class>
</renderer>
例2 属性追加はせず、共通的にレンダリング変更
import com.sun.faces.renderkit.html_basic.TextRenderer;
import javax.faces.context.FacesContext;
public class TestInputTextRenderer extends TextRenderer {
@Override
public void encodeBegin(FacesContext context, UIComponent component) {
// レンダリングの変更点
component.setMaxlength(10);
}
}
<renderer>
<!-- すべての<h:inputText>に適用される -->
<component-family>javax.faces.Input</component-family>
<renderer-type>javax.faces.Text</renderer-type>
<renderer-class>org.test.component.TestInputTextRenderer</renderer-class>
</renderer>
<h:inputText>のコンポーネントクラスであるHtmlTextInputのrendererTypeが「javax.faces.Text」のため、すべての<h:inputText>に適用される。
コンポーネントは作る必要はない。
メモ
- rendererではPropertyKeysは使えないので、文字列でパラメータを指定するしかない。primefacesもjsfもそのような実装になっていたので仕方ないと思う。
- 「1ドメイン=1コンポーネント」もしくは「多ドメイン=1コンポーネント」でドメイン機能を実装するといい気がする
- 「composit component」と「custom component」の使い分けは、既存のcomponentで実現できるるものは前者、できないものは後者な感じ?ただ「composit component」って見辛いし、if文とか書くときついから、「custom component」のほうが個人的には好きだったりする。
参考情報