taglib+テンプレートベースの部品コンポーネントの作成


JSPのtaglibを書いていて思うのだけど、Javaソースコード内に直接HTMLのタグを書くのはあまり気持ちよくない。


解決策として、パラメータを加工してコンテキストに渡すだけのカスタムタグを作り、HTMLのレンダリングはタグで囲まれた内側のHTML部分でおこなうという方法をとってみた。


さらに毎回タグ内部にHTMLを書くのは無駄なので、カスタムタグ+内部のHTMLをセットでHTML部品として用意し、それをincludeするだけでtaglibのように使用できるようにした。


実例として、POJOの配列/コレクションからselectタグを作る部品と、日付入力部品の二つを作ってみた。


今回はJSF+Faceletsでやったけど、スコープを持っていて外部HTMLをincludeできるようなテンプレートエンジンなら同じようにできるはず。

感想・まとめ

InterfaceBuilder.appやらWebObjectsやらで育った身としては、コードとリソースを分けて書けるのはやはりとても気持ちがよい。


後半はコンポーネントを組み合わせてコンポーネントを作る話になっていて少し趣旨が変わってしまったかも?再利用可能なコンポーネントを組み合わせて再利用可能なコンポーネントを作っていくような部品の再利用方法は、JSFだと結構面倒くさい。


これは一つには、値のアップデート先となるオブジェクトがBacking Beanに限られていて、子コンポーネントの値を親コンポーネントが自動的に受け取るような仕組みが無いため。IDを決めうちでつけて((ValueHolder) findComponent(〔ID文字列〕)).getValue()するとか、今回のように宙ぶらりんな一時オブジェクトにアップデートさせたり、無理に実現する事は出来なくはない。


しかし、さらに入力に対するバリデーションを考えると頭が痛い。バリデーションフェーズとモデル更新フェーズが分離されているため、親のバリデーションに子の更新値を使う事が出来ないため。


やっぱり素直にWebObjectsとかTapestryとかAribaWebを使った方が良いかも?

環境

  • J2SE 5.0
  • Apache Tomcat 6.0
  • 以下のライブラリを使用(大体myfacesかfaceletsについてくる奴)
    • commons-beanutils-1.7.0.jar
    • commons-codec-1.3.jar
    • commons-collections-3.2.jar
    • commons-digester-1.8.jar
    • commons-discovery-0.4.jar
    • commons-logging-1.1.1.jar
    • myfaces-api-1.2.3.jar
    • myfaces-impl-1.2.3.jar
    • jsf-facelets.jar

使用サンプル1. POJOの配列/コレクションからselectタグを生成

イメージ湧かないと思うので実例から。


プルダウン(selectタグ)のoptionにf:selectItemsを使う時、パラメータで渡すSelectItemのリストを作る必要があるのだけど、元データがPOJOの場合、わざわざリストの種類ごとにメソッドを作ってSelectItemを生成するのは嫌だし、そもそもモデルクラスをSelectItemクラスに依存させるのが嫌だ。そこで、POJOからSelectItemを生成して渡してくれるだけのFaceletsのカスタムタグ(カスタムTagHandler)を作って解決しようと思う。


作ったカスタムタグを含む部品テンプレート(components/listSelector.xhtml):

<?xml version="1.0" encoding="UTF-8" ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:sample="http://terazzo.dyndns.org/jsf/sample">
<ui:component>
    <!-- listの中身からSelectItemのリストを生成してselectItemsVarにセットする制御専用タグ -->
    <sample:listSelector list="${list}" valueKey="${valueKey}" labelKey="${labelKey}"
            selectItemsVar="selectItems">

        <!-- 実際のHTMLのレンダリング用タグはsample:listSelectorタグの内部に書く -->
        <h:selectOneMenu value="${value}">
            <f:selectItems value="#{selectItems}"/>
        </h:selectOneMenu>

    </sample:listSelector>
</ui:component>
</html>


呼び出し側(pages/register.xhtml):

<h:form>
...
    血液型: 
    <ui:include src="/components/listSelector.xhtml">
        <ui:param name="value" value="${member.bloodType}" />
        <ui:param name="list" value="${samplePage.bloodTypes}" />
        <ui:param name="labelKey" value="label" />
    </ui:include>
...
    <br />
    <h:commandButton action="#{samplePage.update}" value="Submit" />
</h:form>

この例では、samplePageのbloodTypesには血液型を表すBloodTypeというenumの配列が入っている。BloodTypeのlabelプロパティ(「A型」など)の値を表示タイトルとしてプルダウンを表示し、ユーザが選択したBloodTypeをmemberのbloodTypeとしてセットする。


レンダリング結果(空白や改行や実体参照は読みやすく変えています):

    血液型:
    <select name="j_id2:j_id20" size="1">
    <option value="A">A型</option>
    <option value="B">B型</option>
    <option value="O">O型</option>
    <option value="AB">AB型</option></select>
....

SelectItemを生成するTagHandler(ListSelectorTagHandler)の実装

方針:

  • UIComponentは使用せずFaceletsのTagHandlerのみで実装
  • listで受け取るのは配列またはコレクション
  • listの各要素を元にSelectItemを生成する
    • 表示タイトルは、labelKeyで指定された文字列をプロパティ名としてlistの各要素から取得する
    • 選択時の値は、valueKeyで指定された文字列をプロパティ名としてlistの各要素から取得する
    • labelKey, valueKeyが無指定の場合はlistの各要素をそのまま使用する
  • 生成したSelectItemのリストはselectItemsVarで指定された文字列でコンテキストにセットする
package sample.faces.component;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.el.ELException;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.model.SelectItem;

import org.apache.commons.beanutils.PropertyUtils;

import com.sun.facelets.FaceletContext;
import com.sun.facelets.FaceletException;
import com.sun.facelets.tag.TagAttribute;
import com.sun.facelets.tag.TagConfig;
import com.sun.facelets.tag.TagHandler;

/** listの中身からSelectItemのリストを生成してselectItemsVarにセットするTagHandler  */
public class ListSelectorTagHandler extends TagHandler {

    private static final String LIST_ATTRIBUTE_NAME = "list";
    private static final String VALUE_KEY_ATTRIBUTE_NAME = "valueKey";
    private static final String LABEL_KEY_ATTRIBUTE_NAME = "labelKey";
    private static final String SELECT_ITEMS_VAR_ATTRIBUTE_NAME = "selectItemsVar";

    /** 選択肢の元になるListを渡す */
    private TagAttribute listAttribute;
    /** SelectItemのリストをセットするキーを */
    private TagAttribute selectItemsVarAttribute;
    /** 要素から値を取り出すキー */
    private TagAttribute valueKeyAttribute;
    /** 要素からラベル(表示タイトル)を取り出すキー */
    private TagAttribute labelKeyAttribute;

    /**
     * コンストラクタ。
     */
    public ListSelectorTagHandler(TagConfig config) {
        super(config);
        this.listAttribute = getRequiredAttribute(LIST_ATTRIBUTE_NAME);
        this.selectItemsVarAttribute = getRequiredAttribute(SELECT_ITEMS_VAR_ATTRIBUTE_NAME);
        this.valueKeyAttribute = getAttribute(VALUE_KEY_ATTRIBUTE_NAME);
        this.labelKeyAttribute = getAttribute(LABEL_KEY_ATTRIBUTE_NAME);
    }

    /**
     * apply(FaceletContext, UIComponent)の実装。
     */
    public void apply(FaceletContext context, UIComponent parent)
            throws IOException, FacesException, FaceletException, ELException {
        String valueKey = (String) this.valueKeyAttribute.getObject(context);
        String labelKey = (String) this.labelKeyAttribute.getObject(context);

        /* 元リストからSelectItemのListを生成 */
        Object listObject = listAttribute.getObject(context);
        Iterable dataList;
        if (listObject instanceof Iterable) {
            dataList = (Iterable)listObject;
        } else if (listObject.getClass().isArray()) {
            dataList = Arrays.asList((Object[]) listObject);
        } else {
            throw new FacesException("list is not iterable.");
        }
        List<SelectItem> selectItems =
            createSelectItems(context, dataList, valueKey, labelKey);
        
        /* SelectItemのListをセット */
        String selectItemsVar = selectItemsVarAttribute.getValue();
        context.setAttribute(selectItemsVar, selectItems);
        
        nextHandler.apply(context, parent);
    }

    /**
     * dataListの各要素からSelectItemを生成し、Listで戻す。
     * @param dataList 選択肢の元になる要素を含んだList
     * @param valueKey dataListの要素から値を取り出す為のキー文字列。
       nullならばdataListの要素を値として使用する。
     * @param labelKey dataListの要素からラベル(表示タイトル)を取り出す為の
       キー文字列。nullならばdataListの要素をラベルとして使用する。
     * @throws FacesException 値またはラベルの取り出し時に例外が
       発生した場合、FacesExceptionにラップしてthrowする。
     */
    private List<SelectItem> createSelectItems(
            FaceletContext context, Iterable dataList, String valueKey, String labelKey)
            throws FacesException {

        List<SelectItem>selectItems = new ArrayList<SelectItem>();
        if (dataList != null) {
            for (Object data : dataList) {
                try {
                    Object dataValue =
                        (valueKey != null)
                            ? PropertyUtils.getProperty(data, valueKey) : data;
                    Object dataLabel =
                        (labelKey != null)
                           ? PropertyUtils.getProperty(data, labelKey) : data;
                    selectItems.add(new SelectItem(dataValue, dataLabel.toString()));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                    throw new FacesException("Failed to create selectItems. " +
                         e.getMessage(), e);
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                    throw new FacesException("Failed to create selectItems. " +
                         e.getMessage(), e);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                    throw new FacesException("Failed to create selectItems. " +
                         e.getMessage(), e);
                }
            }
        }
        return selectItems;
    }
}


sample-taglib.xml:

<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "http://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
    <namespace>http://terazzo.dyndns.org/jsf/sample</namespace>
...
    <tag>
        <tag-name>listSelector</tag-name>
         <handler-class>sample.faces.component.ListSelectorTagHandler</handler-class>
    </tag>
</facelet-taglib>


web.xml:

...
    <context-param>
        <param-name>facelets.LIBRARIES</param-name>
        <param-value>
        /WEB-INF/tags/sample.taglib.xml
        </param-value>
    </context-param>
...

使用サンプル2. 日付入力部品

年月日がそれぞれプルダウンの日付入力部品。コンポーネント内でselectタグを書くのは嫌なので、日付と{年、月、日}の変換だけをコンポーネントでおこない、実際のプルダウン表示はカスタムタグに囲まれた部分のHTMLでおこなう。


まず年月日をプルダウンで表示する部品テンプレート(components/date.xhtml):

<?xml version="1.0" encoding="UTF-8" ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:sample="http://terazzo.dyndns.org/jsf/sample">
<ui:component>
    <!-- valueから取得した日付を元に、年月日保持用のテンポラリのホルダを
         生成してvarにセットする制御専用タグ -->
    <sample:date value="${value}" var="${dateHolder}">

        <!-- 実際のHTMLのレンダリング用タグはsample:dateタグの内部に書く -->
        <h:selectOneMenu value="${dateHolder.year}" converter="javax.faces.Integer">
            <f:selectItems value="#{dateSetting.years}"/>
        </h:selectOneMenu><h:selectOneMenu value="${dateHolder.month}" converter="javax.faces.Integer">
            <f:selectItems value="#{dateSetting.months}"/>
        </h:selectOneMenu><h:selectOneMenu value="${dateHolder.day}" converter="javax.faces.Integer">
            <f:selectItems value="#{dateSetting.days}"/>
        </h:selectOneMenu></sample:date>
</ui:component>
</html>


呼び出し側(pages/register.xhtml):

<h:form>
...
    生年月日:
    <ui:include src="/components/date.xhtml">
        <ui:param name="value" value="${member.birthday}" />
    </ui:include>
...
    <br />
    <h:commandButton action="#{samplePage.update}" value="Submit" />
</h:form>

この例では、プルダウンで選択された年月日がmemberのbirthdayにセットされる


レンダリング結果

    生年月日:<select name="j_id2:j_id5:j_id6" size="1">
    <option value="2000">2000</option>
    <option value="2001">2001</option>
...
    <option value="2019">2019</option>
    <option value="2020">2020</option></select><select name="j_id2:j_id5:j_id9" size="1">
    <option value="1">1</option>
    <option value="2">2</option>
...
    <option value="12">12</option></select><select name="j_id2:j_id5:j_id12" size="1">
    <option value="1">1</option>
    <option value="2">2</option>
...
    <option value="31">31</option></select><br />
....

日付コンポーネントの実装方針

  • コンポーネントはUIInputのサブクラスとして実装する。モデルから日付を受け取り日付をモデルにセットする。
    • valueで受け取るのはjava.util.Dateオブジェクト
    • encode時およびupdate時に、varで指定したValueExpressionに対して一時的なDateHolderオブジェクトを生成してセットする。
    • コンポーネント(「年」「月」「日」の各プルダウン)はBacking Beanではなく、上のDateHolderオブジェクトにバインドする。
    • updateModel()時に、DateHolderから入力値を集めてDateを生成し、setValue()する
  • レンダラとしては、何もせずタグ内部をレンダリングするだけのTemplateRenderクラスを作成する

いろいろなコンポーネントモデルの説明(図入り)

JSFコンポーネントモデル


WebObjectsコンポーネントモデル


今回のコンポーネントモデル

  • 1)親コンポーネントが現在のコンテキストにテンポラリの入れ物を放流する
  • 2)子コンポーネントは、上の入れ物に値をアップデートする
  • 3)親コンポーネントはテンポラリの入れ物から値を取り出して加工し、自分の値としてBacking Beanにアップデートされるようにする
  • メリット
    • managed-beanに年や月や日のアップデート先を準備しなくても良い
    • ↑のやり方だと、そもそも部品が二個あったら破綻する
  • デメリット
    • バリデーションを掛けるタイミングが難しい

日付コンポーネントの実装

package sample.faces.component;

import java.io.IOException;
import java.util.Date;

import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;

public class DateInput extends UIInput implements NamingContainer {
    public static final String VAR_ATTRIBUTE_NAME = "var";

    public static final String COMPONENT_FAMILY = "sample.faces.DateInput";
    public static final String COMPONENT_TYPE = "sample.faces.DateInput";

     public DateInput() {
        super();
    }
    public String getFamily() {
        return COMPONENT_FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        /* 子供等のencodeが呼ばれる前に値のget元を準備 */
        prepareDateHolder(context);

        super.encodeBegin(context);
    }
    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        clearDateHolder(context);
        super.encodeEnd(context);
    }
    public void processUpdates(FacesContext context) {
        /* 子供等のupdateModel()が呼ばれる前にupdate先を準備 */
        prepareDateHolder(context);

        super.processUpdates(context);
        
        clearDateHolder(context);
    }
    public void updateModel(FacesContext context) {
    
        /* 子供等がupdateModel()でupdateした値を使って日付を生成し、自分の値としてセット */
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(VAR_ATTRIBUTE_NAME);
        DateHolder dateHolder = (DateHolder) var.getValue(elContext);
        setValue(dateHolder.toDate());

        super.updateModel(context);        
    }

    /**
     * 子供のコンポーネントが値をセット/取得できるようにDateHolderをぶら下げる。
     * @param 現在のコンテキスト
     */
    private void prepareDateHolder(FacesContext context) {
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(VAR_ATTRIBUTE_NAME);
        var.setValue(elContext, new DateHolder((Date) getValue()));
    }
    /**
     * DateHolderをコンテキストから削除。
     * @param 現在のコンテキスト
     */
    private void clearDateHolder(FacesContext context) {
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(VAR_ATTRIBUTE_NAME);
        var.setValue(elContext, null);
    }
}


年、月、日の情報を一時的に保存するDateHolderクラス

package sample.faces.component;

import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 * 日付の入力値を保持する
 */
public class DateHolder implements Serializable {
    /** シリアルバージョン番号 */
    private static final long serialVersionUID = -318355117236831950L;
    private Integer year;
    private Integer month;
    private Integer day;
    
    public DateHolder() {
    
    }
    public DateHolder(Date date) {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.setTime(date);
        year = calendar.get(Calendar.YEAR);
        month = (1 - Calendar.JANUARY) + calendar.get(Calendar.MONTH);
        day = calendar.get(Calendar.DAY_OF_MONTH);
    }
    public Date toDate() {
        GregorianCalendar calendar = new GregorianCalendar();
        calendar.clear();
        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month - (1 - Calendar.JANUARY));
        calendar.set(Calendar.DAY_OF_MONTH, day);
        return calendar.getTime();
    }
    
    public Integer getYear() {
        return year;
    }
    public void setYear(Integer year) {
        this.year = year;
    }
    public Integer getMonth() {
        return month;
    }
    public void setMonth(Integer month) {
        this.month = month;
    }
    public Integer getDay() {
        return day;
    }
    public void setDay(Integer day) {
        this.day = day;
    }
}

何もしないRenderクラス

package sample.faces.component;

import javax.faces.render.Renderer;

/**
 * 単純に内部をレンダリングするだけのRenderer
 */
public class TemplateRenderer extends Renderer {
    public TemplateRenderer() {
        super();
    }
    public boolean getRendersChildren() {
        return false;
    }
}

faces-config.xml:

    <component>
            <component-type>sample.faces.DateInput</component-type>
            <component-class>sample.faces.component.DateInput</component-class>
    </component>
    <render-kit>
        <renderer>
            <component-family>sample.faces.DateInput</component-family>
            <renderer-type>sample.faces.TemplateRenderer</renderer-type>
            <renderer-class>sample.faces.component.TemplateRenderer</renderer-class>
        </renderer>
    </render-kit>

sample-taglib.xml: (tld面倒なのでComponentHandlerを指定しつつFacelets的に定義)

<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "http://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
    <namespace>http://terazzo.dyndns.org/jsf/sample</namespace>
    <tag>
        <tag-name>date</tag-name>
          <component>
            <component-type>sample.faces.DateInput</component-type>
              <renderer-type>sample.faces.TemplateRenderer</renderer-type>
             <handler-class>com.sun.facelets.tag.jsf.ComponentHandler</handler-class>
        </component>
    </tag>
...
</facelet-taglib>

web.xml:

...
    <context-param>
        <param-name>facelets.LIBRARIES</param-name>
        <param-value>
        /WEB-INF/tags/sample.taglib.xml
        </param-value>
    </context-param>
...

使用サンプルのソースコード

サンプルページ(新規会員登録的なイメージ。) 生年月日と血液型を入力する。入力画面と確認画面のみ。

package sample.faces;

import sample.model.BloodType;

public class SamplePage {
    /** 編集中のMember */
    private Member member = new Member();

    public SamplePage() {
        super();
    }
    public Member getMember() {
        return member;
    }

    /** 次へアクション。 */
    public String update() {
        return "success";
    }
    /** 戻るアクション。 */
    public String back() {
        return "back";
    }
    /** 血液型の配列を戻す。 */
    public BloodType[] getBloodTypes() {
        return BloodType.values();
    }
}


血液型enum:

package sample.model;

public enum BloodType {
    A (1, "A型"),
    B (2, "B型"),
    O (3, "O型"),
    AB (4, "AB型");
    
    /** ID値 */
    private int value;
    /** ラベル名 */
    private String label;
    
    BloodType(int value, String label) {
        this.value = value;
        this.label = label;
    }
    public int getValue() {
        return value;
    }
    public String getLabel() {
        return label;
    }
}


日付プルダウン用の設定クラス

package sample.faces.setting;

import java.util.ArrayList;
import java.util.List;

import javax.faces.model.SelectItem;

public class DateSetting {

    private int fromYear = 2004;
    private int toYear = 2010;
    private boolean initialized = false;
    
    private List<SelectItem> years;
    private List<SelectItem> months;
    private List<SelectItem> days;
    
    public DateSetting() {
        super();
    }

    public void setFromYear(int fromYear) {
        this.fromYear = fromYear;
    }

    public void setToYear(int toYear) {
        this.toYear = toYear;
    }

    private synchronized void initializeIfNeeded() {
        if (initialized) {
            return;
        }
        this.years = new ArrayList<SelectItem>();
        for (int year = fromYear; year <= toYear; year++) {
            years.add(new SelectItem(year));
        }
        
        this.months = new ArrayList<SelectItem>();
        for (int month = 1; month <= 12; month++) {
            this.months.add(new SelectItem(month));
        }
        
        this.days = new ArrayList<SelectItem>();
        for (int day = 1; day <= 31; day++) {
            this.days.add(new SelectItem(day));
        }
        initialized = true;
    }
    public List<SelectItem> getYears() {
        initializeIfNeeded();
        return years;
    }
    public List<SelectItem> getDays() {
        initializeIfNeeded();
        return days;
    }
    public List<SelectItem> getMonths() {
        initializeIfNeeded();
        return months;
    }
}

入力ページ(pages/register.xhtml):

<?xml version="1.0" encoding="UTF-8" ?>
<!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:ui="http://java.sun.com/jsf/facelets">
<body>
<h1>登録</h1>
<h:form>
    生年月日:
    <ui:include src="/components/date.xhtml">
        <ui:param name="value" value="${member.birthday}" />
    </ui:include>
    <br />
    血液型: 
    <ui:include src="/components/listSelector.xhtml">
        <ui:param name="value" value="${member.bloodType}" />
        <ui:param name="list" value="${samplePage.bloodTypes}" />
        <ui:param name="labelKey" value="label" />
    </ui:include>
    <br />
    <h:commandButton action="#{samplePage.update}" value="Submit" />
</h:form>
</body>
</html>


確認ページ(pages/confirm.xhtml):

<?xml version="1.0" encoding="UTF-8" ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html">
<body>
<h1>登録(確認)</h1>
<h:form>
    生年月日:
    <h:outputText value="${member.birthday}" > 
       <f:convertDateTime pattern="yyyy年MM月dd日" timeZone="JST"/>
    </h:outputText><br />

    血液型: 
    <h:outputText value="${member.bloodType.label}"/><br />

    <h:commandButton action="#{samplePage.back}" value="Back" />
</h:form>
</body>
</html>


faces-config.xml:

<?xml version="1.0" encoding="UTF-8"?>
<faces-config>

    <application>
        <!-- tell JSF to use Facelets -->
        <view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
    </application>

    <!-- コンポーネント定義 -->
    <component>
            <component-type>sample.faces.DateInput</component-type>
            <component-class>sample.faces.component.DateInput</component-class>
    </component>
    <render-kit>
        <renderer>
            <component-family>sample.faces.DateInput</component-family>
            <renderer-type>sample.faces.TemplateRenderer</renderer-type>
            <renderer-class>sample.faces.component.TemplateRenderer</renderer-class>
        </renderer>
    </render-kit>
    <!-- 日付プルダウン用の設定(Applicationスコープ) -->
    <managed-bean>
          <managed-bean-name>dateSetting</managed-bean-name>
          <managed-bean-class>sample.faces.setting.DateSetting</managed-bean-class>
          <managed-bean-scope>application</managed-bean-scope>
          <managed-property> 
              <property-name>fromYear</property-name> 
              <value>1920</value> 
          </managed-property>
          <managed-property> 
              <property-name>toYear</property-name> 
              <value>2020</value> 
          </managed-property> 
    </managed-bean>



    <!-- 会員情報(Sessionスコープ) -->
    <managed-bean>
          <managed-bean-name>member</managed-bean-name>
          <managed-bean-class>sample.faces.Member</managed-bean-class>
          <managed-bean-scope>session</managed-bean-scope>
    </managed-bean>
    <!-- ページオブジェクト(Sessionスコープ) -->
    <managed-bean>
          <managed-bean-name>samplePage</managed-bean-name>
          <managed-bean-class>sample.faces.SamplePage</managed-bean-class>
          <managed-bean-scope>session</managed-bean-scope>
    </managed-bean>

    <!-- 画面遷移。登録ページ⇔確認ページ -->
    <navigation-rule>
        <from-view-id>/pages/register.xhtml</from-view-id>
        <navigation-case>
            <from-outcome>success</from-outcome>
            <to-view-id>/pages/confirm.xhtml</to-view-id>
        </navigation-case>
    </navigation-rule>
    <navigation-rule>
        <from-view-id>/pages/confirm.xhtml</from-view-id>
        <navigation-case>
            <from-outcome>back</from-outcome>
            <to-view-id>/pages/register.xhtml</to-view-id>
        </navigation-case>
    </navigation-rule>
</faces-config>

おまけ その1. listSelectorのプルダウンをラジオボタンに変更

htmlを変えるだけ!

<?xml version="1.0" encoding="UTF-8" ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:sample="http://terazzo.dyndns.org/jsf/sample">
<ui:component>
    <!-- listの中身からSelectItemのリストを生成してselectItemsVarにセットする制御専用タグ -->
    <sample:listSelector list="${list}" valueKey="${valueKey}" labelKey="${labelKey}"
            selectItemsVar="selectItems">

        <!-- 実際のHTMLのレンダリング用タグはsample:listSelectorタグの内部に書く -->
        <h:selectOneRadio value="${value}">
            <f:selectItems value="#{selectItems}"/>
        </h:selectOneRadio>

    </sample:listSelector>
</ui:component>
</html>

おまけ その2. 日付入力部品をテキスト入力に変更

htmlを変えるだけ!

<?xml version="1.0" encoding="UTF-8" ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:sample="http://terazzo.dyndns.org/jsf/sample">
<ui:component>
    <!-- valueから取得した日付を元に、年月日保持用のテンポラリの
         ホルダを生成してvarにセットする制御専用タグ -->
    <sample:date value="${value}" var="${dateHolder}">

        <!-- 実際のHTMLのレンダリング用タグはsample:dateタグの内部に書く -->
        <h:inputText size="4" value="${dateHolder.year}" converter="javax.faces.Integer"/>
        <h:inputText size="2" value="${dateHolder.month}" converter="javax.faces.Integer"/>
        <h:inputText size="2" value="${dateHolder.day}" converter="javax.faces.Integer"/>

    </sample:date>
</ui:component>
</html>

おまけ その3. 日付入力部品の並びを「年月日」から「日/月/年」に変更

htmlを変えるだけ!

<?xml version="1.0" encoding="UTF-8" ?>
<!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:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:sample="http://terazzo.dyndns.org/jsf/sample">
<ui:component>
    <!-- valueから取得した日付を元に、年月日保持用のテンポラリの
         ホルダを生成してvarにセットする制御専用タグ -->
    <sample:date value="${value}" var="${dateHolder}">

        <!-- 実際のHTMLのレンダリング用タグはsample:dateタグの内部に書く -->
        <h:selectOneMenu value="${dateHolder.day}" converter="javax.faces.Integer">
            <f:selectItems value="#{dateSetting.days}"/>
        </h:selectOneMenu>
        /
        <h:selectOneMenu value="${dateHolder.month}" converter="javax.faces.Integer">
            <f:selectItems value="#{dateSetting.months}"/>
        </h:selectOneMenu>
        /
        <h:selectOneMenu value="${dateHolder.year}" converter="javax.faces.Integer">
            <f:selectItems value="#{dateSetting.years}"/>
        </h:selectOneMenu>

    </sample:date>
</ui:component>
</html>

おまけ その4. Member.bloodTypeにはenumでなくID値を保存

呼び出し側でvalueKeyを指定する

<h:form>
...
    血液型: 
    <ui:include src="/components/listSelector.xhtml">
        <ui:param name="value" value="${member.bloodType}" />
        <ui:param name="list" value="${samplePage.bloodTypes}" />
        <ui:param name="labelKey" value="label" />
        <ui:param name="valueKey" value="value" />
    </ui:include>
...
    <br />
    <h:commandButton action="#{samplePage.update}" value="Submit" />
</h:form>

おまけ その5. ui:include/ui:paramをやめてtaglib風に書きたい

Faceletsのtaglibファイルで定義

pages/register.xhtml:

<?xml version="1.0" encoding="UTF-8" ?>
<!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:ui="http://java.sun.com/jsf/facelets"
      xmlns:tmpl="http://terazzo.dyndns.org/jsf/tmpl">
<body>
<h1>登録</h1>
<h:form>
    生年月日: <tmpl:date value="${member.birthday}" /><br />
    血液型: <tmpl:listSelector value="${member.bloodType}"
               list="${samplePage.bloodTypes}" labelKey="label"/><br />
    <h:commandButton action="#{samplePage.update}" value="Submit" />
</h:form>
</body>
</html>

とてもシンプルになった。


/WEB-INF/tags/tmpl.taglib.xml:

<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
  "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
  "http://java.sun.com/dtd/facelet-taglib_1_0.dtd">
<facelet-taglib>
    <namespace>http://terazzo.dyndns.org/jsf/tmpl</namespace>
    <tag>
        <tag-name>date</tag-name>
        <source>../../components/date.xhtml</source>
    </tag>
    <tag>
        <tag-name>listSelector</tag-name>
        <source>../../components/listSelector.xhtml</source>
    </tag>
</facelet-taglib>


web.xml:

...
    <context-param>
        <param-name>facelets.LIBRARIES</param-name>
        <param-value>
        /WEB-INF/tags/sample.taglib.xml;/WEB-INF/tags/tmpl.taglib.xml
        </param-value>
    </context-param>
...