LoginSignup
2
3

More than 5 years have passed since last update.

jsf custom componentを使ったコンポーネント拡張

Last updated at Posted at 2017-12-26

環境

  • jsf 2.2
  • primefaces 6.1

背景

  • ドメイン機能をcustom componentで実現したいと思ったが、custom componentのベストプラクティスみたいなのをぐぐってもうまく見つけられなかった。
  • custom tagを使った古い実装例が多く引っ掛かってくるのも残念な感じだった。(jsfってやっぱり人気ないのかな)
  • 『パーフェクト Java EE』にも少ししか記載がなく、今回の事例についてはあまり参考になかった。

ゴール

jsfのcustom componentで拡張コンポーネントを作る場合にどのように実装するのがいいのか、jsfとprimefacesのソースを参考に考えてみる。

確認したこと

jsfとprimefacesのinputTextのコンポーネントとレンダラーの仕組みについて

参考にしたソースコード

jsf
http://grepcode.com/file/repo1.maven.org/maven2/javax.faces/javax.faces-api/2.2/javax/faces/component/html/HtmlInputText.java?av=f

primefaces
https://www.primefaces.org/downloads/
primefaces-6.1.jar
primefaces-6.1-sources.jar

jsfのinputText仕組み

コンストラクタでrendererTypeを指定して、それに対応したrendererが呼ばれる。

HtmlInputText
public class HtmlInputText extends javax.faces.component.UIInput implements ClientBehaviorHolder {
    public HtmlInputText() {
       super();
       setRendererType("javax.faces.Text");
    }
}
faces-config.xml
TODO:ここのマッピング定義がうまく見つけられなかった。要確認。
TextRenderer
public class TextRenderer extends HtmlBasicInputRenderer {
    @Override
    public void encodeBegin(FacesContext context, UIComponent component) throws IOException {...}
}

primefacesのinputText仕組み

jsfと同じ。

InputText
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);
    }
}
faces-config.xml
<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>
InputTextRenderer
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」を呼び出している。

UIComponent
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);
}
UIComponentBase
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);
}

HtmlBasicRenderer
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 属性追加+レンダリング変更

TestInputText
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);
    }
}
TestInputTextRenderer
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);
        }
    }
}
faces-config.xml
<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 属性追加はせず、共通的にレンダリング変更

TestInputTextRenderer
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);
    }
}
faces-config.xml
<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」のほうが個人的には好きだったりする。

参考情報

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3