JSFでカスタムコンポーネントを作るときの覚書きです。参考にした『JavaServer Faces完全ガイド』は、ツリーコンポーネントをカスタムコンポーネントとして作る手順が載っていました。

ここでは、カスタムコンポーネントを作るときに必要となるパーツと、注意点などをまとめておこうと思います。カスタムコンポーネントは大きく、入力用のコンポーネントと出力用のコンポーネントの2つに分けれます。入力用のコンポーネントはリクエスト間で値の保存・読み込みやデコードレンダリングなどを行わなければなりません。出力用のコンポーネントの場合は、値を保持し、エンコードレンダリングを行うことで機能を表現します。

ここでは、出力用のコンポーネントの話をしています。説明が断片的なので、上記の本を読んだほうが手っ取り早いかも・・・ ^^;

[参考]

作るもの

  コンポーネント レンダラー タグハンドラ
継承 UIComponentBase Renderer UIComponentTag
UIComponentBodyTag
famiry名 任意
(getFamiryメソッドで返すようにする)
任意
(コンポーネントとあわせる必要がある。faces-context.xmlに定義する)
不要
タイプ 完全クラス名。定数で定義するとよい。 完全クラス名。定数で定義するとよい。 getComponentTypeで使用するコンポーネントタイプを返す。
getRendererTypeで使用するレンダラータイプを返す。
実装するメソッド コンポーネントで使用するプロパティのget/set等 encode系メソッド setPropertiesメソッド、タグで受け取る属性のget/set等
その他 faces-context.xmlにcomponentの定義を記述 faces-context.xmlにrendererの定義を記述 TLDファイルを作成する

コンポーネントクラス

コンポーネントクラスはjavax.faces.component.UIComponentBaseを継承して作ります。似たようなコンポーネントを継承して作ってもいいでしょう。コンポーネントが特殊で、レンダラーの責務を別クラスにする必要がない場合は、コンポーネントの encode系メソッドを実装します。

コンポーネントは、famiryを定義します。同一のfamiry名を持つコンポーネントとレンダラがセットで使われます。コンポーネントのfamiryは getFamiryメソッドで返すようにします。

ValueBindingで値を受け取らない場合は、setValueBinding(String, ValueBinding)をオーバーライドして、特定の値しかValueBindingで受け取らないことを明示するとよい。

setValueBindingのオーバーライド例

// @Overwride
public void setValueBinding(final String name, ValueBinding bind) {
  if (name.equals("value")) {
    throw new IllegalArgumentException("valueは値結合でなければなりません。");
  }        
  super.setValueBinding(name, bind);
}

ValueBindingで受け取る値を返すgetメソッドは、getValueBindingメソッドを使って値結合を解決しなければならない。毎回、値結合を解決するとパフォーマンスが悪くなるので、キャッシュするようにする。

ValueBindingされている値のgetメソッド例

public Object getValue() {
  if (this.value != null) {
    return this.value;
  } else {
    ValueBinding bind = getValueBinding("value");
    if (bind != null) {
      this.value = bind.getValue(getFacesContext()); 
      return this.value;
    } else {
      return null;
    }
  }
}

レンダラー

javax.faces.render.Rendererクラスを継承して作ります。主にオーバーライドして使うのは下のメソッド。

  • encodeBegin(FacesContext, UIComponent)
  • encodeChildren(FacesContext, UIComponent)
  • encodeEnd(FacesContext, UIComponent)
  • getRendersChildren()

レンダラーは、基本的には自分が処理できるコンポーネントだけをレンダリングします。ただし、<h:dataTable>などは、子に現れる<h:column>などのコンポーネントも処理します。子のコンポーネントの処理も自分で行う場合は、getRendersChildren()で true を返す必要があります。

子のコンポーネントを処理する場合に、例えば次のようにします。

<h:dataTable value="#{xxx}" var="child"> 
  <h:column> 
    <h:outputText value="#{child.name}"/> 
  </h:column> 
</h:dataTable>

child の部分に格納される値をセットしておかなければならない場合があります。これは、リクエストにコンポーネントのvar変数が表す名前(ここではchild)をキーとして、値をセットしておくことで対応できます。

子コンポーネントに値を受け渡す方法

Map requestMap = facesContext.getExternalContext().getRequestMap(); 
requestMap.put(component.getVar(), component.getVarValue());

エンコードは、encodeBegin、getRendersChildrenがtrueを返すときencodeChildren、encodeEndの順で処理されます。

タグハンドラー

タグにbodyが必要ない場合はjavax.faces.webapp.UIComponentTagクラスを継承します。例えば<h:outputText /> のようなタグの場合です。

タグにbodyが必要な場合は javax.faces.webapp.UIComponentBodyTagクラスを継承します。このクラスの方が多少パフォーマンスが悪いようです。bodyが不要の場合はUIComponentTagを使うようにします。

実装するメソッドは次の2つです。

  • getComponentType
  • getRendererType

前者はコンポーネントのタイプ(完全クラス名にしておくことをおすすめ)を返すようにします。後者はレンダラーのタイプ(完全クラス名にしておくことをおすすめ)を返すようにします。

さらに、タグハンドラーには、タグの属性の値を保持するプロパティを実装します。

タグハンドラの例1

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;
 
public class MyComponentTag extends UIComponentTag {
    
  private String value;
  private String var;
  private String onmouseover;
 
  protected void setProperties(UIComponent component) {
    super.setProperties(component);
        
    FacesContext context = getFacesContext();
    MyComponent mycomp = (MyComponent) component;
    if (this.value != null) {
      if (UIComponentTag.isValueReference(this.value)) {
        ValueBinding bind =
          context.getApplication().createValueBinding(this.value);
        component.setValueBinding("value", bind);
      } else {
        mycomp.setValue(this.value);
      }
    }
    if (this.var != null) {
      mycomp.setVar(this.var);
    }
    if (this.onmouseover != null) {
      if (UIComponentTag.isValueReference(this.onmouseover)) {
        ValueBinding bind =
          context.getApplication().createValueBinding(this.onmouseover);
        component.setValueBinding("onmouseover", bind);
      } else {
        mycomp.setOnmouseover(this.onmouseover);
      }
    }
  }
 
  public String getComponentType() {
    return MyComponent.COMPONENT_TYPE;
  }
 
  public String getRendererType() {
    return MyComponentRenderer.RENDERER_TYPE;
  }
 
  public String getOnmouseover() {
    return onmouseover;
  }
 
  public void setOnmouseover(String onmouseover) {
    this.onmouseover = onmouseover;
  }
 
  public String getValue() {
    return value;
  }
 
  public void setValue(String value) {
    this.value = value;
  }
 
  public String getVar() {
    return var;
  }
 
  public void setVar(String var) {
    this.var = var;
  }
}

注目するメソッドはsetProperties(UIComponent)です。このメソッドでは、タグの属性で指定された値をコンポーネントにセットする役割があります。 ValueBinding される可能性のある属性は、18行目のように UIComponentTag.isValueReference を使ってValueBinding形式かどうかを判定します。ValueBindingの場合は、FacesContextからValueBindingインスタンスを作成し、UIComponentのsetValueBindingを使ってセットします。

ただの値の場合には、コンポーネントのsetメソッドを使って直接セットします。

faces-context.xml と タグディスクリプションファイル(TLD)の作成

faces-context.xml に、カスタムコンポーネントとレンダラーの設定を書きます。TLDファイルには、タグハンドラの設定を書きます。

参考

  • 一番詳しい解説書
  • リファレンスとしては秀逸
  • 概念を学ぶにはいい本