Webコンポーネントで検索条件を組み立てる

管理ツールなどの検索フォームで、使ってみて後から検索条件を増やしたくなることって結構ある。


その度にテンプレートファイル、フォーム、アクション、ロジッククラス、DAOクラスのメソッド、DAOクラスのアノテーションや外だしSQLファイルなどを変更しないといけないことがある。検索条件を一つ増やすだけなのに何カ所も修正しないと行けないのはDRYじゃないし、作業的にも効率悪いと思う。


そこで、Webコンポーネントを使ってHTMLテンプレートを修正するだけで検索条件を追加できるようにしてみた。


今回はコンポーネント側をJSF+Faceletsで、DBへの問い合わせ部分にCayenneを使った。Cayenneは初めて使ったけどEOFっぽくて使いやすかった。

内容

  1. テンプレートファイルおよび出力例
  2. 感想・まとめ
  3. 環境
  4. DBとモデルファイルの準備
  5. ページクラスとテンプレートファイルを準備
  6. ニックネームで検索する機能を普通に実装
  7. Expressionをコンポーネントで生成するように変更
  8. コンポーネントtagをテンプレート部品(前回使用したテクニック)に変更
  9. Like+テキスト入力以外の部品の作成
    • プルダウンで選択した数値とのイコール検索
    • テキストで入力した数値でイコール検索
    • 二つの日付をプルダウンで入力し、between検索
    • 使用例
  10. 複数項目でのAND検索
  11. おまけ1. 数値専用でなくconverterを指定できるようにする
  12. おまけ2. プルダウンで無選択を許可する
  13. おまけ3. 入力しない場合は検索条件に含めない
  14. おまけ4. ui:includeが長いのでtaglib的に記述したい
  15. おまけ5. 日本語が化ける/数値文字参照にならないようにする

テンプレートファイルおよび出力例

イメージがつかみにくいと思うので、サンプルとして会員情報を検索するページのテンプレートファイルとレンダリング結果を例示する。


テンプレートファイルは次のようになる。上段で検索フォーム、下段で検索結果の表示をおこなっている。

<?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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:q="http://terazzo.dyndns.org/jsf/qualifier" xmlns:tmpl="http://terazzo.dyndns.org/jsf/tmpl">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
<q:and value="${memberPage.qualifier}" var="${qualifiers}">
    ID:<tmpl:equalText
        qualifier="${qualifiers.id}" attribute="id"
        parameters="${memberPage.queryParameters}"
        converter="javax.faces.Integer" /><br/>
    ニックネーム:<tmpl:likeText
        qualifier="${qualifiers.nickname}" attribute="nickname"
        parameters="${memberPage.queryParameters}" /><br/>
    入会日:<tmpl:betweenDate
        qualifier="${qualifiers.registerDate}" attribute="registerDate"
        parameters="${memberPage.queryParameters}"/><br/>
    血液型:<tmpl:equalSelector
        qualifier="${qualifiers.bloodType}" attribute="bloodType"
        parameters="${memberPage.queryParameters}"
        list="${memberPage.bloodTypes}" labelKey="label" valueKey="value"
        converter="javax.faces.Integer" /><br/>
</q:and>
     <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>

<c:if test="${!empty memberPage.list}">
<table>
    <tr><th>会員ID</th><th>ニックネーム</th><th>生年月日</th><th>入会日時</th> </tr>
    <c:forEach items="${memberPage.list}" var="member"  varStatus="status">
    <tr>
        <td>
           <h:outputText value="${member.id}"/>
        </td>
        <td>
            <h:outputText value="${member.nickname}"/>
        </td>
        <td>
            <h:outputText value="${member.birthday}" > 
               <f:convertDateTime pattern="yyyy年MM月dd日 HH時mm分" timeZone="JST"/>
            </h:outputText>
        </td>
        <td>
            <h:outputText value="${member.registerDate}" > 
               <f:convertDateTime pattern="yyyy年MM月dd日 HH時mm分" timeZone="JST"/>
            </h:outputText>
        </td>
    </tr>
    </c:forEach>
</table>
</c:if>

</body>
</html>


レンダリング結果のHTML

感想・まとめ

HTMLテンプレート上に問い合わせ条件を書くっていうと、JSP上にSQLJDBC呼び出しをハードコーディングする暗黒時代の再来だと思う人もいるかもしれない。でも自分はそうではないと言いたい。という話を別エントリにした。


HTMLテンプレート上だけで検索条件の組み立てを出来るメリットはいろいろある。

  • テンプレートファイルを差し替えるだけで検索条件を変更できる(基本)
  • 外部のデータを元に検索フォームを生成できる(ui:includeのsrc属性をELにすれば動的な値も取れますよ。繰り返しも出来るし。)
  • HTMLテンプレートを処理すれば、検索条件を取り出せる

2番目と3番目は若干矛盾する気もするけど……。上の二つは以前実際に仕事で必要だった。(その時はCayenneじゃなくて自作のO/R Mapperだったけど。) 「管理ツールを作る管理ツール」を作るには必須の機能だと思う。

環境

  • PostgreSQL 8.3.6 (多分RDBMSならなんでもいい)
  • Apache Tomcat 6.0.18
  • J2SE 5.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 (facelets-1.1.14)
    • cayenne.jar (Cayenne 2.0.4)
    • postgresql-8.3-604.jdbc3.jar

それ以外に、日本語が数値文字参照になるのを防ぐ為の対処(「おまけ5. 日本語が化ける/数値文字参照にならないようにする」参照)をおこなっている。

DBとモデルファイルの準備

まず会員情報テーブルと中のデータを用意する。
データベースを作成

postgres$ createdb sample
postgres$ psql sample

psqlで以下を入力してテーブル作成。ID、ニックネーム、血液型、生年月日、入会日付、退会日付を持つ会員テーブル

create table member (
id serial primary key,
nickname varchar(64) not null,
blood_type integer null,
birthday timestamp null,
register_date timestamp not null,
resign_date timestamp
);

psqlで以下を入力してダミーでデータを入れておく。長嶋は一人。

insert into member (nickname, blood_type, birthday, register_date)
values ('青木', 3, '1960-1-31', '2009-4-2 11:22');
insert into member (nickname, blood_type, birthday, register_date)
values ('長嶋', 1, '1966-2-25', '2009-4-3 22:11');
insert into member (nickname, blood_type, birthday, register_date)
values ('山本', 2, '1971-3-20', '2009-4-4 10:31');
insert into member (nickname, blood_type, birthday, register_date)
values ('二ノ宮', 4, '1977-4-20', '2009-4-5 2:01');
insert into member (nickname, blood_type, birthday, register_date)
values ('鈴木', 4, '1981-5-15', '2009-4-6 17:39');
insert into member (nickname, blood_type, birthday, register_date)
values ('マイケル', 1, '1989-6-10', '2009-4-7 14:25');


Cayenneモデラーで、以下の手順でモデルファイルとクラスを作成する。

  1. CayenneModelerを起動する。
  2. PostgreSQLJDBCドライバを追加する
    • メニュー「Tools」>「Preference」でプリファレンスパネルを開き、左側のカラムからClassPathを選択し、「Add Jar/Zip」ボタンを押してpostgresql-8.3-604.jdbc3.jarを追加
  3. 新規モデルファイルを作成
    • メニュー「File」>「New Project」
    • DataDomainNameに「SampleDomain」と入力
  4. データノードを作成
    • メニュー「Project」>「Create DataNode」(またはウィンドウ上部の円筒アイコンをクリック)
    • JDBC Configurationを設定する
  5. データマップを作成
    • メニュー「Project」>「Create DataMap」(またはウィンドウ上部の球x3アイコンをクリック)
    • DataMap Nameに「Membership」と入力
    • Java Packageに「sample.model.membership」と入力
  6. データベースのスキーマ情報を読み込んでデータマップを設定
    • メニュー「Tools」>「Reengineer Database Schema」
    • 接続情報が設定されていることを確認して「Continue」ボタンをクリック
    • Select Schema:で「public」を選んで「Continue」ボタンをクリック
    • ウィンドウ左側の「MemberShip」の下にMemberとmemberが出来ていることを確認
  7. モデルファイルをセーブ
    • メニュー「File」>「Save」で、プロジェクト下のディレクトリを選んで「Select」ボタンをクリック
    • ※「cayenne.xml」、「Membership.map.xml」、「SampleSiteNode.driver.xml」の3ファイルが生成される
  8. クラスファイルをセーブ
    • ウィンドウ左側の「MemberShip」を選択してメニュー「Tool」>「Genrate Classes」
    • Output Directoryにプロジェクト下のソースディレクトリを選んで「Generate」ボタンをクリック

Cayenne wayではないけど、今回は以下の設定でidも検索で取得できるようにする

  1. エンティティにidアトリビュートを追加
    • ウィンドウ左側の「MemberShip」/「Member」を選択、タブで「Attribute」を選択
    • 上部左端のアイコンをクリックし、属性を追加
      • ObjAttributeに「id」、JavaTypeに「java.lang.Integer」を入力、DbAttributeで「id」を選択
  2. モデルファイル、クラスファイルをセーブ

出来上がったモデルファイルのイメージ


モデルファイルはクラスパスの通っているところに配置する必要がある。「cayenne-model」などの名前でクラスフォルダを作成しておいて、その中にセーブすれば良いと思う。

ページクラスとテンプレートファイルを準備

以下、基本的な画面から順番に作っていく。


最初にページクラスの空実装とテンプレートファイルを作成する。

まず、ページクラスを作成

package sample.pages.membership;

import java.util.List;

import sample.model.membership.Member;

public class MemberPage {
    /** 検索結果である会員情報の一覧をMemberのListで保持 */
    private List<Member> list = null;
    
    public MemberPage() {
    }

    public String searchAction() {
        // ここに検索処理を書くよ            
        return null;
    }
    /** 検索結果を戻す */
    public List<Member> getList() {
        return list;
    }
}

テンプレートファイルを作成(/membership/list.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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
    <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>

<c:if test="${!empty memberPage.list}">
<table>
    <tr>
        <th>会員ID</th>
        <th>ニックネーム</th>
        <th>生年月日</th>
        <th>入会日時</th>
    </tr>
    <c:forEach items="${memberPage.list}" var="member"  varStatus="status">
    <tr>
        <td>
           <h:outputText value="${member.id}"/>
        </td>
        <td>
            <h:outputText value="${member.nickname}"/>
        </td>
        <td>
            <h:outputText value="${member.birthday}" > 
               <f:convertDateTime pattern="yyyy年MM月dd日 HH時mm分" timeZone="JST"/>
            </h:outputText>
        </td>
        <td>
            <h:outputText value="${member.registerDate}" > 
               <f:convertDateTime pattern="yyyy年MM月dd日 HH時mm分" timeZone="JST"/>
            </h:outputText>
        </td>
    </tr>
    </c:forEach>
</table>
</c:if>
</body>
</html>

faces-configに登録

    <managed-bean>
          <managed-bean-name>memberPage</managed-bean-name>
          <managed-bean-class>sample.pages.membership.MemberPage</managed-bean-class>
          <managed-bean-scope>session</managed-bean-scope>
    </managed-bean>

    <navigation-rule>
        <from-view-id>/membership/list.xhtml</from-view-id>
        <navigation-case>
            <from-outcome>success</from-outcome>
            <to-view-id>/membership/list.xhtml</to-view-id>
        </navigation-case>
    </navigation-rule>

Searchボタンを押すとmemberPage.searchAction()が呼ばれ、memberPage.listが空でなければID、ニックネーム、生年月日、登録日付を表示するというもの。
この段階では、ページ上でボタンを押すとsearchAction()が呼ばれるが、検索処理が実装されていないため、何も表示されない。

ニックネームで検索する機能を普通に実装

Cayenneの練習がてら普通にニックネーム検索機能を付ける。
クラスファイルを修正し、検索機能を実装。

package sample.pages.membership;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cayenne.access.DataContext;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.ExpressionParameter;
import org.apache.cayenne.query.SelectQuery;

import sample.model.membership.Member;

public class MemberPage {
    private List<Member> list = null;
    /** dataContextをPageインスタンスに保持。(普通はSessionらしい) */
    private DataContext dataContext = DataContext.createDataContext();
    /** 検索条件生成時のパラメータを保持するMap */
    Map<String, Object> queryParameters = new HashMap<String, Object>();
    
    public MemberPage() {
    }

    public String searchAction() {
        // Memberエンティティ検索用のSelectQueryを生成
        SelectQuery proto = new SelectQuery(Member.class);
        // nickname属性をplaceholderを使って検索(「where nickname like ?」と同じ)
        Expression qualifier =
            ExpressionFactory.likeExp("nickname", new ExpressionParameter("nickname"));
        proto.setQualifier(qualifier);
        
        // 入力値をパラメータとして設定
        SelectQuery query = proto.queryWithParameters(queryParameters);
        // 検索し、結果をlistに格納
        list = dataContext.performQuery(query);
        
        return null;
    }
    public List<Member> getList() {
        return list;
    }
    /** 検索パラメータをページで入力できるようにgetterを追加 */
    public Map<String, Object> getQueryParameters() {
        return queryParameters;
    }
}

おおざっぱに言えば、ここではExpression=where句の部分、SelectQuery=SQL全体と考えればよい。
LIKE検索用のExpressionをExpressionFactory.likeExp()で生成し、queryParametersを渡して検索を実行している。queryParametersにはフォームで入力された検索語が、キー:nickname、値:入力値の形で入っている想定。

※ ExpressionParameterの"nickname"は属性名ではなくて、queryParametersのキーを指定しているので注意。


併せてテンプレートファイルも修正

<?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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
    <!-- memberPageのqueryParametersというMapにキーnicknameで値をセット -->
    ニックネーム: <h:inputText value="${memberPage.queryParameters.nickname}" /><br/>
    <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>

<!-- 以下は同じ -->
...

実行し、list.xhtmlを表示すると、検索フォームが一つ表示され、文字列を入力するとニックネームで検索される。

  • 例: 「青木」と入力 -> 青木が1件表示される
  • 例: 「%木」と入力 -> 「青木」と「鈴木」が2件表示される

%を入力するUIはどうかと思うけど今回はあくまでサンプルとして。

Expressionをコンポーネントで生成するように変更

JSFコンポーネントはモデル値として文字列や数値ではなく、それ以外のJavaオブジェクトを生成するようにも作れる。
であればCayenneのExpressionを生成するUIComponentも作れるはず。


早速コンポーネントクラスを作成。

package sample.faces.component.qualifier;

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

import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.ExpressionParameter;


public class LikeExpressionInput extends UIInput implements NamingContainer {
    public static final String ATTRIBUTE_ATTRIBUTE_NAME = "attribute";

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

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

    public void updateModel(FacesContext context) {
        /* Expressionを生成してセット*/
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(ATTRIBUTE_ATTRIBUTE_NAME);
        String attributeName = (String) var.getValue(elContext);
        String bindingKey = attributeName; // パラメータキー=属性名とする

        Expression qualifier =
            ExpressionFactory.likeExp(attributeName, new ExpressionParameter(bindingKey));

        setValue(qualifier);

        super.updateModel(context);        
    }
}

attribute="nickname"と指定してやれば、"where nickname like ?"にあたるようなExpressionを生成してvalueにセットしてくれる。


faces-config-common.xmlに追加。TemplateRenderには前回の「何もしないRenderer」を指定。

...
    <component>
            <component-type>sample.faces.LikeExpressionInput</component-type>
            <component-class>sample.faces.component.qualifier.LikeExpressionInput</component-class>
    </component>
    <render-kit>
        <renderer>
            <component-family>sample.faces.ExpressionInput</component-family>
            <renderer-type>sample.faces.TemplateRenderer</renderer-type>
            <renderer-class>sample.faces.component.TemplateRenderer</renderer-class>
        </renderer>
    </render-kit>
...

faceletsのtaglibファイルにも登録(/WEB-INF/tags/qualifier.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/qualifier</namespace>
    <tag>
        <tag-name>likeText</tag-name>
          <component>
            <component-type>sample.faces.LikeExpressionInput</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に登録。複数の設定ファイルを指定する場合、faces-configは「,」区切り、taglibは「;」区切りなので注意

...
    <context-param>
        <param-name>javax.faces.CONFIG_FILES</param-name>
        <param-value>/WEB-INF/faces-config-common.xml,/WEB-INF/faces-config.xml</param-value>
    </context-param>
    <context-param>
        <param-name>facelets.LIBRARIES</param-name>
        <param-value>
        /WEB-INF/tags/sample.taglib.xml;/WEB-INF/tags/qualifier.taglib.xml
        </param-value>
    </context-param>
...


これでコンポーネント側は準備できたのでテンプレートファイルとMemberPageクラスを変更する。
まずMemberPageクラス

package sample.pages.membership;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.cayenne.access.DataContext;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.ExpressionParameter;
import org.apache.cayenne.query.SelectQuery;

import sample.model.BloodType;
import sample.model.membership.Member;

public class MemberPage {
    private List<Member> list = null;
    private DataContext dataContext = DataContext.createDataContext();
    /** Expressionをコンポーネント側で生成する */
    Expression qualifier;
    
    Map<String, Object> queryParameters = new HashMap<String, Object>();
    
    public MemberPage() {
    }

    public String searchAction() {
        SelectQuery proto = new SelectQuery(Member.class);
        proto.setQualifier(qualifier);

        SelectQuery query = proto.queryWithParameters(queryParameters);
        list = dataContext.performQuery(query);
        
        return null;
    }

    public List<Member> getList() {
        return list;
    }

    public Map<String, Object> getQueryParameters() {
        return queryParameters;
    }

    public Expression getQualifier() {
        return qualifier;
    }
    /** コンポーネントからセットされる */
    public void setQualifier(Expression qualifier) {
        this.qualifier = qualifier;
    }
}

アクション内ではもはやExpressionは生成せず、与えられたExpressionとパラメータでSelectQueryを生成して実行するだけ。


呼び出すテンプレートファイル側

<?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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core"
      xmlns:q="http://terazzo.dyndns.org/jsf/qualifier"
>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
    ニックネーム:
    <q:likeText value="${memberPage.qualifier}" attribute="nickname">
        <h:inputText value="${memberPage.queryParameters.nickname}" />
    </q:likeText>

     <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>

<!-- 以下は同じ -->
...

Expressionの生成場所を移動しただけなので、実行結果は変更前と変わらない。

コンポーネントtagをテンプレート部品(前回使用したテクニック)に変更

上のlikeTextコンポーネントは、パラメータキーに属性名を使っているので、例えば同一の属性に対して複数の検索条件がある場合に上手くない。そこで他の値をキーに使う必要があるが、それにはコンポーネントのフォーム内で一意になるclientIdを使うのが良そうだ。


FaceletsのTaglibを使って取り出せるようにする。まず、「パラメータキーを生成するインタフェース」を定義

package sample.faces.component.qualifier;

import javax.faces.context.FacesContext;

public interface SingleBindingKeyGenerator {
    /** バインディング値を格納するキーを生成する */
    public String generateBindingKey(FacesContext facesContext);
}

これをコンポーネント側で実装し、ついでにExpressionParameter生成部も修正

package sample.faces.component.qualifier;

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

import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.ExpressionParameter;


public class LikeExpressionInput extends UIInput implements NamingContainer, SingleBindingKeyGenerator {
    public static final String ATTRIBUTE_ATTRIBUTE_NAME = "attribute";

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

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

    public void updateModel(FacesContext context) {
        /* Expressionを生成してセット*/
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(ATTRIBUTE_ATTRIBUTE_NAME);
        String attributeName = (String) var.getValue(elContext);
        String bindingKey = generateBindingKey(context); // generateBindingKey()の結果を利用

        Expression qualifier =
            ExpressionFactory.likeExp(attributeName,  new ExpressionParameter(bindingKey));

        setValue(qualifier);

        super.updateModel(context);        
    }
    /** SingleBindingKeyGeneratorを実装。この実装ではclientIdを使用する */
    public String generateBindingKey(FacesContext facesContext) {
        return getClientId(facesContext);
    }
}

キーをテンプレート上で利用できるようにするため、FaceletsのComponentHandlerを作成。

package sample.faces.component.qualifier;

import java.io.IOException;

import javax.el.ELException;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import com.sun.facelets.FaceletContext;
import com.sun.facelets.tag.TagAttribute;
import com.sun.facelets.tag.jsf.ComponentConfig;
import com.sun.facelets.tag.jsf.ComponentHandler;

public class ExpressionComponentHandler extends ComponentHandler {

    private static final String  PARAMETER_KEY_ATTRIBUTE_NAME = "parameterKey";

    /** パラメータキーを設定する先を指定する為のTagAttribute */
    private TagAttribute parameterKeyAttribute;

    public ExpressionComponentHandler(ComponentConfig config) {
        super(config);
        this.parameterKeyAttribute = getRequiredAttribute(PARAMETER_KEY_ATTRIBUTE_NAME);
    }

    @Override
    protected void applyNextHandler(FaceletContext context, UIComponent component)
            throws IOException, FacesException, ELException {
        if (!(component instanceof SingleBindingKeyGenerator)) {
            throw new IllegalStateException("No SingleBindingKeyGenerator component specified:" +
                    component.getClass());
        }
        FacesContext facesContext = context.getFacesContext();
        // パラメータキーを生成する
        String bindingKey = ((SingleBindingKeyGenerator) component).generateBindingKey(facesContext);
        
        // パラメータキーを現在のコンテキストに設定する
        String parameterKey = parameterKeyAttribute.getValue();
        context.setAttribute(parameterKey, bindingKey);

        super.applyNextHandler(context, component);
    }
}

parameterKeyを指定すると、生成した値がその名前で利用できるようになる。


コンポーネントを利用する為のテンプレート部品を作っておく。こうする事で、q:likeText&h:inputTextのセットを毎回書く必要がなくなる。(詳しくは前回参照)
/components/likeText.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:q="http://terazzo.dyndns.org/jsf/qualifier">
<ui:component>
    <q:likeText value="${qualifier}" attribute="${attribute}" parameterKey="parameterKey">
        <h:inputText value="${parameters[parameterKey]}" />
    </q:likeText>
</ui:component>
</html>

q:likeTextが生成したキー(${parameterKey}で取り出せる)を使って、h:inputTextのvalueの格納先をMap形式で指定している。


ページ側を、直接q:likeTextを使う方法から、上のテンプレート部品をincludeする方法に修正
/membership/list.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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
    ニックネーム:<ui:include src="/components/likeText.xhtml">
        <ui:param name="qualifier" value="${memberPage.qualifier}" />
        <ui:param name="attribute" value="nickname" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>

     <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>


<!-- 以下は同じ -->
...

パラメータキーの決定方法を変更しただけなので、実行結果は変更前と変わらない。

Like+テキスト入力以外の部品の作成

上の例では、テキスト属性に対するLIKE検索しか出来ない。以下、LIKE以外の条件指定もできるように、いろいろ作成してみる。

プルダウンで選択した数値とのイコール検索

likeExp()ではなくmatchExp()を使うコンポーネントを用意

package sample.faces.component.qualifier;

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

import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.ExpressionParameter;


public class EqualExpressionInput extends UIInput implements NamingContainer, SingleBindingKeyGenerator {
    public static final String ATTRIBUTE_ATTRIBUTE_NAME = "attribute";

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

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

    public void updateModel(FacesContext context) {
        /* Expressionを生成してセット*/
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(ATTRIBUTE_ATTRIBUTE_NAME);
        String attributeName = (String) var.getValue(elContext);
        String bindingKey = generateBindingKey(context);

        Expression qualifier =
            ExpressionFactory.matchExp(attributeName,  new ExpressionParameter(bindingKey));

        setValue(qualifier);

        super.updateModel(context);        
    }
    /** bindingKeyにはclientIdを使用 */
    public String generateBindingKey(FacesContext facesContext) {
        return getClientId(facesContext);
    }
}

テンプレート部品。前回作成したlistSelector.xhtmlを使用して作成する。
/components/equalSelector.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:q="http://terazzo.dyndns.org/jsf/qualifier">
<ui:component>
    <q:equalSelector value="${qualifier}" attribute="${attribute}" parameterKey="parameterKey">
        <ui:include src="/components/listSelector.xhtml">
            <ui:param name="value" value="${parameters[parameterKey]}" />
            <ui:param name="list" value="${list}" />
            <ui:param name="labelKey" value="${labelKey}" />
            <ui:param name="valueKey" value="${valueKey}" />
            <ui:param name="converter" value="javax.faces.Integer" />
        </ui:include>
    </q:equalSelector>
</ui:component>
</html>

faceletsのtaglibファイルに登録(/WEB-INF/tags/qualifier.taglib.xml)

...
    <tag>
        <tag-name>equalText</tag-name>
          <component>
            <component-type>sample.faces.EqualExpressionInput</component-type>
              <renderer-type>sample.faces.TemplateRenderer</renderer-type>
             <handler-class>sample.faces.component.qualifier.ExpressionComponentHandler</handler-class>
        </component>
    </tag>
...

使用例は後ほど。

テキストで入力した数値でイコール検索

コンポーネントクラスはEqualExpressionInputを流用。


テンプレート部品を作成
/components/equalText.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:q="http://terazzo.dyndns.org/jsf/qualifier">
<ui:component>
    <q:equalText value="${qualifier}" attribute="${attribute}" parameterKey="parameterKey">
        <h:inputText value="${parameters[parameterKey]}" converter="javax.faces.Integer"/>
    </q:equalText>
</ui:component>
</html>

faceletsのtaglibファイルに登録(/WEB-INF/tags/qualifier.taglib.xml)

...
    <tag>
        <tag-name>equalSelector</tag-name>
          <component>
            <component-type>sample.faces.EqualExpressionInput</component-type>
              <renderer-type>sample.faces.TemplateRenderer</renderer-type>
             <handler-class>sample.faces.component.qualifier.ExpressionComponentHandler</handler-class>
        </component>
    </tag>
...

使用例は後ほど。

二つの日付をプルダウンで入力し、between検索

今までのものと違い、パラメータキーが複数必要なので、若干クラスを作り足す必要がある。

パラメータキー(From/To)を作成するインタフェース

package sample.faces.component.qualifier;

import javax.faces.context.FacesContext;

public interface BetweenBindingKeyGenerator {
    /** From用のバインディング値を格納するキーを生成する */
    public String generateFromBindingKey(FacesContext facesContext);
    /** To用のバインディング値を格納するキーを生成する */
    public String generateToBindingKey(FacesContext facesContext);
}

Between用のキーを取り出す為のComponentHandler

package sample.faces.component.qualifier;

import java.io.IOException;

import javax.el.ELException;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;

import com.sun.facelets.FaceletContext;
import com.sun.facelets.tag.TagAttribute;
import com.sun.facelets.tag.jsf.ComponentConfig;
import com.sun.facelets.tag.jsf.ComponentHandler;

public class BetweenExpressionComponentHandler extends ComponentHandler {

    private static final String  PARAMETER_FROM_KEY_ATTRIBUTE_NAME = "parameterFromKey";
    private static final String  PARAMETER_TO_KEY_ATTRIBUTE_NAME = "parameterToKey";

    /** パラメータキー(From)を設定する先を指定する為のTagAttribute */
    private TagAttribute parameterFromKeyAttribute;
    /** パラメータキー(To)を設定する先を指定する為のTagAttribute */
    private TagAttribute parameterToKeyAttribute;

    /**
     * @param config
     */
    public BetweenExpressionComponentHandler(ComponentConfig config) {
        super(config);
        this.parameterFromKeyAttribute = getRequiredAttribute(PARAMETER_FROM_KEY_ATTRIBUTE_NAME);
        this.parameterToKeyAttribute = getRequiredAttribute(PARAMETER_TO_KEY_ATTRIBUTE_NAME);
    }

    @Override
    protected void applyNextHandler(FaceletContext context, UIComponent component)
            throws IOException, FacesException, ELException {
        if (!(component instanceof BetweenBindingKeyGenerator)) {
            throw new IllegalStateException("No BetweenBindingKeyGenerator component specified:" +
                    component.getClass());
        }
        // パラメータキーを生成する
        FacesContext facesContext = context.getFacesContext();
        String fromBindingKey =
            ((BetweenBindingKeyGenerator) component).generateFromBindingKey(facesContext);
        String toBindingKey =
            ((BetweenBindingKeyGenerator) component).generateToBindingKey(facesContext);
        
        // パラメータキーを現在のコンテキストに設定する
        String parameterFromKey = parameterFromKeyAttribute.getValue();
        context.setAttribute(parameterFromKey, fromBindingKey);
        String parameterToKey = parameterToKeyAttribute.getValue();
        context.setAttribute(parameterToKey, toBindingKey);

        super.applyNextHandler(context, component);
    }
}

Between用のExpressionを生成するコンポーネント。betweenExp()を使用する。

package sample.faces.component.qualifier;

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

import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.exp.ExpressionParameter;


public class BetweenExpressionInput extends UIInput implements NamingContainer, BetweenBindingKeyGenerator {
    public static final String ATTRIBUTE_ATTRIBUTE_NAME = "attribute";

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

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

    public void updateModel(FacesContext context) {
        /* Expressionを生成してセット*/
        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(ATTRIBUTE_ATTRIBUTE_NAME);
        String attributeName = (String) var.getValue(elContext);
        String fromBindingKey = generateFromBindingKey(context);
        String toBindingKey = generateToBindingKey(context);

        Expression qualifier =
            ExpressionFactory.betweenExp(attributeName,
                new ExpressionParameter(fromBindingKey),
                new ExpressionParameter(toBindingKey));

        setValue(qualifier);

        super.updateModel(context);        
    }
    public String generateFromBindingKey(FacesContext facesContext) {
        return getClientId(facesContext) + ":from";
    }
    public String generateToBindingKey(FacesContext facesContext) {
        return getClientId(facesContext) + ":to";
    }
}

テンプレート部品。前回作成したcomponents/date.xhtmlを使用して作成する。
/components/betweenDate.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:q="http://terazzo.dyndns.org/jsf/qualifier">
<ui:component>
    <q:betweenDate value="${qualifier}" attribute="${attribute}"
            parameterFromKey="parameterFromKey" parameterToKey="parameterToKey">
        <ui:include src="/components/date.xhtml">
            <ui:param name="value" value="${parameters[parameterFromKey]}" />
        </ui:include>
        ~
        <ui:include src="/components/date.xhtml">
            <ui:param name="value" value="${parameters[parameterToKey]}" />
        </ui:include>
    </q:betweenDate>
</ui:component>
</html>
使用例

それぞれページテンプレートに組み込むと、単一の項目で検索するフォームを作成できる。まだ同時には使用できない。

...
<h1>会員情報検索</h1>
<h:form>
<!-- 
    ニックネーム:<ui:include src="/components/likeText.xhtml">
        <ui:param name="qualifier" value="${memberPage.qualifier}" />
        <ui:param name="attribute" value="nickname" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>

    ID:<ui:include src="/components/equalText.xhtml">
        <ui:param name="qualifier" value="${memberPage.qualifier}" />
        <ui:param name="attribute" value="id" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>

    血液型:<ui:include src="/components/equalSelector.xhtml">
        <ui:param name="qualifier" value="${qualifiers.bloodType}" />
        <ui:param name="attribute" value="bloodType" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
        <ui:param name="list" value="${memberPage.bloodTypes}" />
        <ui:param name="labelKey" value="label" />
        <ui:param name="valueKey" value="value" />
     </ui:include><br/>

 -->
    入会日:<ui:include src="/components/betweenDate.xhtml">
        <ui:param name="qualifier" value="${memberPage.qualifier}" />
        <ui:param name="attribute" value="registerDate" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>
     <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>
...

複数項目でのAND検索

Cayenneには、複数個のExpressionからAND条件を作り出す方法が用意されている。そこで複数のExpression生成コンポーネントを組み合わせて、それらのAND条件のExpressionを生成するようなコンポーネントを作成する。


コンポーネントの生成したExpressionを取得する方法として、前回の「日付コンポーネントの実装」で使用した、コンポーネントを組み合わせてコンポーネントを作るテクニックを使用する。


方針として、PageContextにぶら下げるオブジェクト(前回の例で言えばDateHolderにあたる部分)をMapにし、子コンポーネントはそれぞれ自分のキーを指定してMapにExpressionをセットするようにする(つまり、ぶら下げるオブジェクトはMapになる。)

package sample.faces.component.qualifier;

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

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

import org.apache.cayenne.exp.Expression;


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

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

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

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

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

        super.processUpdates(context);
        
        clearDateHolder(context);
    }

    public void updateModel(FacesContext context) {
        /* 子供等がupdateModel()でupdateした値を使ってAND条件を生成しする */

        ELContext elContext = context.getELContext();
        ValueExpression var = getValueExpression(VAR_ATTRIBUTE_NAME);
        Map<String, Expression>qualifierMap = (Map<String, Expression>) var.getValue(elContext);

        Expression qualifier = null;
        if (qualifierMap != null && !qualifierMap.isEmpty()) {
            Collection<Expression> qualifiers = qualifierMap.values();
            for (Expression aQualifier: qualifiers) {
                if (qualifier == null) {
                    qualifier = aQualifier;
                } else {
                    qualifier = qualifier.andExp(aQualifier);
                }
            }
        }

        /* 生成したAND条件を自分の値としてセット */
        setValue(qualifier);

        super.updateModel(context);     
    }

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


これを使って、ID、ニックネーム、入会日付、血液型のAND検索をおこなうフォームを用意する

<?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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:q="http://terazzo.dyndns.org/jsf/qualifier">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
<q:and value="${memberPage.qualifier}" var="${qualifiers}">

    ID:<ui:include src="/components/equalText.xhtml">
        <ui:param name="qualifier" value="${qualifiers.id}" />
        <ui:param name="attribute" value="id" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>
    ニックネーム:<ui:include src="/components/likeText.xhtml">
        <ui:param name="qualifier" value="${qualifiers.nickname}" />
        <ui:param name="attribute" value="nickname" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>
    入会日:<ui:include src="/components/betweenDate.xhtml">
        <ui:param name="qualifier" value="${qualifiers.registerDate}" />
        <ui:param name="attribute" value="registerDate" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
    </ui:include><br/>
    血液型:<ui:include src="/components/equalSelector.xhtml">
        <ui:param name="qualifier" value="${qualifiers.bloodType}" />
        <ui:param name="attribute" value="bloodType" />
        <ui:param name="parameters" value="${memberPage.queryParameters}" />
        <ui:param name="list" value="${memberPage.bloodTypes}" />
        <ui:param name="labelKey" value="label" />
        <ui:param name="valueKey" value="value" />
     </ui:include><br/>

</q:and>
     <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>

....

q:andで囲まれた部分の検索条件のAND条件で検索がおこなわれる。


実際に、入力して検索した際のログを見てみる。(見やすいように改行しています)
入力内容がID=「2」、ニックネーム=「青木」、入会日=「2009年01月01日〜2009年04月27日」血液型=「O型」の場合

INFO  QueryLogger: --- will run 1 query.
INFO  QueryLogger: --- transaction started.
INFO  QueryLogger: SELECT t0.birthday, t0.blood_type, t0.id, t0.nickname, t0.register_date, t0.resign_date
 FROM public.member t0 WHERE (t0.nickname LIKE ?) AND (t0.register_date BETWEEN ? AND ?)
 AND (t0.blood_type = ?) AND (t0.id = ?) 
 [bind: '青木', '2009-01-01 00:00:00.0', '2009-04-27 00:00:00.0', 3, 2]
INFO  QueryLogger: === returned 1 row. - took 3 ms.
INFO  QueryLogger: +++ transaction committed.

ちゃんとAND検索になっていることが確認できる。

おまけ1. 数値専用でなくconverterを指定できるようにする

equalTextとequalSelectorはconverter="javax.faces.Integer"を決めうちしてるので他の値に使用できない。そこで外部からconverterを設定できるようにしたいが、h:inputTextなどにconverter="${converter}"のようにEL式を書くと、converterId文字列ではなくConverterオブジェクトを要求するようになる。


そこで、Faceletsの機能でEL関数を作成して文字列変数からコンバータを生成できるようにする。

まず関数にしたい処理をstaticメソッドで定義したクラスファイルを作成。

package sample.faces.component;

import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public class SampleFunction {
    public static Converter converter(FacesContext context, String converterId) {
        if (converterId == null || converterId.equals("")) {
            return null;
        }
        return context.getApplication().createConverter(converterId);
    }
}

この例ではconverter()関数はFacesContextとconverterId文字列の2引数を取りConverterを戻す。


次に関数を登録する為のTagLibraryを作成

package sample.faces.component;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import com.sun.facelets.tag.AbstractTagLibrary;
import com.sun.facelets.tag.TagLibrary;


public class SampleLibrary extends AbstractTagLibrary implements TagLibrary {
    public static final String NAMESPACE = "http://terazzo.dyndns.org/jsf/function";

    public SampleLibrary() {
        super(NAMESPACE);

        addFunctions();
    }
    private void addFunctions() {
        try {
            Method[] methods = SampleFunction.class.getMethods();
            for (int i = 0; i < methods.length; i++) {
                if (Modifier.isStatic(methods[i].getModifiers())) {
                    addFunction(methods[i].getName(), methods[i]);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}


最後にこのTagLibraryを読み込むように設定ファイルを追加する。
WEB-INF/tags/function.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>
    <library-class>sample.faces.component.SampleLibrary</library-class>
</facelet-taglib>

web.xml:

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


これで、Faceletsテンプレートファイル内のELでconverter()関数が使用できるようになる。
/components/equalText.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:q="http://terazzo.dyndns.org/jsf/qualifier" xmlns:fn="http://terazzo.dyndns.org/jsf/function">
<ui:component>
    <q:equalText value="${qualifier}" attribute="${attribute}" parameterKey="parameterKey">
        <h:inputText value="${parameters[parameterKey]}"
              converter="${fn:converter(facesContext, converter)}"/>
    </q:equalText>
</ui:component>
</html>

${converter}には"javax.faces.Integer"などのコンバータID文字列を指定する。

おまけ2. プルダウンで無選択を許可する

${noSelectionString}が設定されている場合だけ、空のf:selectItemを表示するようにする

/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:c="http://java.sun.com/jstl/core"
      xmlns:sample="http://terazzo.dyndns.org/jsf/sample" xmlns:fn="http://terazzo.dyndns.org/jsf/function">
<ui:component>
    <sample:listSelector list="${list}"
            valueKey="${valueKey}" labelKey="${labelKey}"
            selectItemsVar="selectItems">
        <h:selectOneMenu value="${value}" converter="${fn:converter(facesContext, converter)}">
           <c:if test="${!empty noSelectionString}">
               <f:selectItem itemLabel="${noSelectionString}" itemValue=""/>
           </c:if>
           <f:selectItems value="#{selectItems}"/>
        </h:selectOneMenu>
    </sample:listSelector>
</ui:component>
</html>

呼ぶ側:

        <ui:include src="/components/listSelector.xhtml">
            <ui:param name="value" value="${parameters[parameterKey]}" />
            <ui:param name="list" value="${list}" />
            <ui:param name="labelKey" value="${labelKey}" />
            <ui:param name="valueKey" value="${valueKey}" />
            <ui:param name="noSelectionString" value="選択してください" />
            <ui:param name="converter" value="javax.faces.Integer" />
        </ui:include>

おまけ3. 入力しない場合は検索条件に含めない

Cayenneには元々そういう機能はあるのだけど、(HashMapなどのように)Mapに値=nullが入っていると検索条件を生成してしまう。そこでPaga側でパラメータからnull値をすっ飛ばす処理を追加

....
        SelectQuery query = proto.queryWithParameters(
                removeNullValues(queryParameters)); // 未入力のエントリを除去したMapを生成
....
    private <K, V> Map<K, V> removeNullValues(Map<K, V> map) {
        HashMap<K, V> results = new HashMap<K, V>();
        for (Map.Entry<K, V> entry: map.entrySet()) {
            V value = entry.getValue();
            if (value == null || (value instanceof String && value.equals(""))) {
                continue;
            }
            results.put(entry.getKey(), value);
        }
        return results;
    }


また、数値入力フォームで空入力をnullとして扱いたい場合、Tomcatのバージョンによっては起動パラメータに次のように追加しないと行けない場合がある

-Dorg.apache.el.parser.COERCE_TO_ZERO=false

おまけ4. ui:includeが長いのでtaglib的に記述したい

前回と同じく、Faceletsのtaglib化機能を使う

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>
...(前回のものに追加)...
    <tag>
        <tag-name>betweenDate</tag-name>
        <source>../../components/betweenDate.xhtml</source>
    </tag>
    <tag>
        <tag-name>equalSelector</tag-name>
        <source>../../components/equalSelector.xhtml</source>
    </tag>
    <tag>
        <tag-name>equalText</tag-name>
        <source>../../components/equalText.xhtml</source>
    </tag>
    <tag>
        <tag-name>likeText</tag-name>
        <source>../../components/likeText.xhtml</source>
    </tag>
</facelet-taglib>

WEB-INF/web.xml:

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


使う側:

<?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:f="http://java.sun.com/jsf/core"
      xmlns:c="http://java.sun.com/jstl/core" xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:q="http://terazzo.dyndns.org/jsf/qualifier"
      xmlns:tmpl="http://terazzo.dyndns.org/jsf/tmpl">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>

<h1>会員情報検索</h1>
<h:form>
<q:and value="${memberPage.qualifier}" var="${qualifiers}">
    ID:<tmpl:equalText qualifier="${qualifiers.id}" attribute="id"
        parameters="${memberPage.queryParameters}"
        converter="javax.faces.Integer" /><br/>
    ニックネーム:<tmpl:likeText qualifier="${qualifiers.nickname}" attribute="nickname"
                   parameters="${memberPage.queryParameters}" /><br/>
    入会日:<tmpl:betweenDate qualifier="${qualifiers.registerDate}" attribute="registerDate"
              parameters="${memberPage.queryParameters}"/><br/>
    血液型:<tmpl:equalSelector qualifier="${qualifiers.bloodType}" attribute="bloodType"
              parameters="${memberPage.queryParameters}"
              list="${memberPage.bloodTypes}" labelKey="label" valueKey="value"
              converter="javax.faces.Integer" /><br/>
</q:and>
     <h:commandButton action="${memberPage.searchAction}" value="Search" />
</h:form>
...

おまけ5. 日本語が化ける/数値文字参照にならないようにする

MyFacesに付き物の問題。みんなどうしてるのか知りたい。汚いけど今回は次の方法で回避した。

  • Seasar2からorg.seasar.extension.filter.EncodingFilter、S2JSFからorg.seasar.jsf.html.HtmlResponseWriterを借りてくる
  • FaceletViewHandlerを継承したクラスでcreateResponseWriter()をオーバーライドし、上のHtmlResponseWriterを使用するように修正
  • 文字コード指定用のFilterをweb.xmlで指定、ViewHandlerをfaces-config.xmlで指定
package sample.faces;

import java.io.IOException;

import javax.faces.FacesException;
import javax.faces.application.ViewHandler;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.servlet.ServletResponse;

import org.seasar.jsf.html.HtmlResponseWriter;

import com.sun.facelets.FaceletViewHandler;

public class MyFaceletViewHandler extends FaceletViewHandler {

    public MyFaceletViewHandler(ViewHandler parent) {
        super(parent);
    }
    @Override
    protected ResponseWriter createResponseWriter(FacesContext context)
            throws IOException, FacesException {
        super.createResponseWriter(context);
        ExternalContext extContext = context.getExternalContext();
        ServletResponse response = (ServletResponse) extContext.getResponse();
        String contentType = null;
        String encoding = response.getCharacterEncoding();

        ResponseWriter writer = createResponseWriter(contentType, encoding);
        writer = writer.cloneWithWriter(response.getWriter());
        return writer;

    }
    private ResponseWriter createResponseWriter(String characterEncoding, String contentType) {
        HtmlResponseWriter responseWriter = new HtmlResponseWriter();

        responseWriter.setContentType(contentType);
        responseWriter.setCharacterEncoding(characterEncoding);

        return responseWriter;

    }
}

WEB-INF/faces-config-common.xml

...
    <application>
        <!-- tell JSF to use Facelets -->
        <view-handler>sample.faces.MyFaceletViewHandler</view-handler>
    </application>
...

WEB-INF/web.xml

...
    <filter>
        <filter-name>encodingfilter</filter-name>
        <filter-class>org.seasar.extension.filter.EncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingfilter</filter-name>
        <url-pattern>*.xhtml</url-pattern>
    </filter-mapping>
...