デフォルトの JSF レンダリング機能の拡張
Mark Basler
ステータス: Early Access
課題
JSF コンポーネントを設計するとき、開発者はさまざまなオプションを検討します。その 1 つに、コンポーネントのマークアップをどのように生成するかというものがあります。ほかの既存のコンポーネントと密接に関連しない、新しい機能を持つコンポーネントを開発する場合、通常はターゲットのマークアップを生成する新しいレンダリングを開発します。その方法は簡単であり、『JavaEE5 チュートリアル』の「Creating Custom UI Components」(カスタム UI コンポーネントの作成) で詳しく説明しています。
場合によっては、作成するコンポーネントで既存のコンポーネントに機能を追加します (AJAX 機能の追加など)。開発者としては、コンポーネント全体の基本レンダリング動作を再実装したくありません。このエントリでは、既存の基本レンダリング機能を利用してコンポーネントのマークアップを生成する、別の方法を検証します。
対処法
JSF の AJAX FileUpload コンポーネントのように、作成するコンポーネントで既存のコンポーネントに機能を追加する場合は、基本レンダリング機能を利用して、ターゲットのマークアップを生成できます。FileUpload コンポーネントは、JSF の javax.faces.component.UIForm コンポーネントに AJAX 機能を追加するために設計されました。FileUpload コンポーネント用に生成されるマークアップは、一部のデフォルト属性が設定された UIForm と同じなので、レンダリングを新規にコーディングすることは意味がありません。Java 開発者のオブジェクト指向的な考え方からすれば、基本機能を拡張し、AJAX FileUpload コンポーネントに固有の機能だけを追加する方法が、理にかなっています。
新しいコンポーネントは JSP で使用するので、コンポーネントの属性を公開する別の JSP タグハンドラが必要です。このタグハンドラは、JSF の component-type 要素の renderer-type 要素への関連付けも行います。現時点では、タグハンドラの基本機能を拡張するには、ベンダー固有の方法を利用する以外にありませんが、近い将来にはこれらのファイルを自動的に作成するツールができるかもしれません。このエントリでは、カスタムの JSP タグライブラリ記述子とタグハンドラを作成する方法については説明しません。これらの作業については、『JavaEE5 チュートリアル』の「Creating the Component Tag Handler」(コンポーネントのタグハンドラの作成) で詳しく説明しています。
このあとの各節では、基本レンダリング機能を使用してコンポーネントのマークアップを生成するさまざまな方法について説明します。
カスタムレンダリングからデフォルトレンダリングを呼び出して基本機能を実行する
カスタムレンダリングを使用してコンポーネントのデフォルトレンダリングを呼び出す方法では、基本コンポーネントのマークアップを生成する方法を変更せずに、簡単に機能を追加できます。AJAX FileUpload コンポーネントのカスタムタグでは、デフォルトの JSF フォームタグの一部の属性が追加および削除されています。削除された属性は、FileUpload のカスタムレンダリングで必要な値に設定されています。追加された属性は、フォームの送信時に送信される非表示フィールドとして設定されています。HtmlForm コンポーネントに適切な値が入力されると、FacesContext を通じてデフォルトレンダリングが取得されます。デフォルトレンダリングの encodeBegin メソッドと encodeEnd メソッドが呼び出され、FileUpload に固有の値を保持するために更新された HtmlForm コンポーネントが渡されます。デフォルトレンダリングでは、カスタムタグとカスタムレンダリングを使用しない場合と同じようにコンポーネントのマークアップが生成されます。
この方法を示すコードを次に示します。わかりやくするために、コードには変更を少し加えてあります。
FileUpload カスタムタグのアーティファクト:
FileUpload タグのタグハンドラは、JSF の基本コンポーネント ("javax.faces.HtmlForm") をカスタムレンダリング ("FileUploadForm") に関連付けます。このカスタムレンダリングは、FileUpload の faces-config ファイルで component-family 要素とともに定義されています。
public class FileUploadTag extends UIComponentELTag {
...
public String getComponentType() {
return ("javax.faces.HtmlForm");
}
public String getRendererType() {
return ("FileUploadForm");
}
...
}
faces-config.xml
<render-kit>
<renderer>
<description>
Renderer for ajax fileupload component
</description>
<component-family>javax.faces.Form</component-family>
<renderer-type>FileUploadForm</renderer-type>
<renderer-class>com.sun.javaee.blueprints.components.ui.fileupload.FileUploadRenderer</renderer-class>
</renderer>
</render-kit>
FileUploadRenderer:
FileUploadRenderer.encodeBegin メソッドが、HtmlForm のデフォルト属性を設定し、"javax.faces.Form" の component-family 要素および renderer-type 要素のデフォルトレンダリングを検索します。次に、変更されたコンポーネントを使用して、基本レンダリングの encodeBegin メソッドにマークアップの生成を委託します。FileUpload コンポーネントの子のマークアップの生成は自己レンダリングに送られるため、FileUploadRenderer での処理は必要ありません。FileUploadRenderer.encodeEnd メソッドは、フォームの本体に非表示フィールドを書き出し、変更されたコンポーネントを使用して、基本レンダリングの encodeEnd メソッドにマークアップの生成を委託します。
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
if ((context == null) || (component == null)) {
throw new NullPointerException();
}
HtmlForm outComp = (HtmlForm)component;
// コンポーネントのカスタム設定
outComp.setEnctype("multipart/form-data");
....
// フォームのデフォルトレンダリングで、すべての基本フォーム属性をレンダリングする
Renderer baseRenderer=context.getRenderKit().getRenderer("javax.faces.Form", "javax.faces.Form");
baseRenderer.encodeBegin(context, outComp);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
// 特定のタグハンドラによって設定されたプロパティー−を UIOutput から取得する
String serverLocationDir=(String)component.getAttributes().get("serverLocationDir");
// サーバー上のディレクトリの場所を表す非表示フィールドを追加する
// 存在しない場合は、デフォルトで FileUploadHandler に設定される
if(serverLocationDir != null) {
writer.startElement("input", component);
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("name", id + "_" + FileUploadUtil.SERVER_LOCATION_DIR, null);
writer.writeAttribute("value", serverLocationDir, null);
writer.endElement("input");
writer.write("\n");
}
// フォームのデフォルトレンダリングで、すべての基本フォーム属性を生成する
Renderer baseRenderer=context.getRenderKit().getRenderer("javax.faces.Form", "javax.faces.Form");
baseRenderer.encodeEnd(context, component);
}
このコードからわかるように、これは、デフォルトレンダリングの機能を再実装せずに、既存コンポーネントの機能をカスタマイズできる簡単な方法です。FileUpload は AJAX コンポーネントであり、フォームは従来の方法で送信されないので、レンダリングの decode メソッドは使用しません。フォームは、Shale-Remoting を通じて、Dojo によって非同期で送信され、FileUploadHandler.handleFileUpload メソッドによって使用されます。カスタムコンポーネントで「要求値の適用」フェーズ中にフォーム送信をデコードする場合も、デフォルトレンダリングを検索し、デフォルトレンダリングの decode メソッドに呼び出しを委託する方法を使用します。
カスタムレンダリングで非表示の子コンポーネントを使用し、それにマークアップの生成を委託する
この方法でも、カスタムのタグハンドラとレンダリングを使用して JSF の基本コンポーネントをデコレートし、デフォルトレンダリングを利用してその装飾をレンダリングします。この方法は、単一の親コンポーネントを単一の子コンポーネントにマッピングするのに使用できますが、本当のメリットは、単一の親コンポーネントを複数の子コンポーネントにマッピングできることです。属性や ValueExpression を適切な子コンポーネントにマッピングしたら、各子コンポーネントの encodeBegin メソッドまたは encodeEnd メソッドを特定の順序で呼び出します。子コンポーネントは、あとで再利用のために呼び出して取得できるように、親コンポーネントのファセットマップに格納します。
この方法では、子コンポーネントの作成に必要なすべての情報を、親コンポーネントのタグ定義によって収集する必要があります。タグ属性のデータは、コンポーネントの属性マップと、UIComponent から継承された ValueExpression マップを使用して、JSF の基本コンポーネントの内部に格納します。親のマップを直接使用すると、通常は JSF のカスタムコンポーネントで定義する簡易アクセス用メソッドと簡易ミュテータメソッドの通常のパラダイムを回避できます。この方法は、コンポーネント開発者が JSF のカスタムコンポーネントに対し、追加の属性や ValueExpression の簡易メソッドを作成しない、または作成できない場合に使用できます。次に示すタグハンドラでは、この方法で、UIComponent のマップを使用して属性値と ValueExpression を格納しています (青色の部分)。
現在の FileUpload コンポーネントでは、非表示の子コンポーネントを使用するマークアップの生成方法は使用されていませんが、次のコードではその実装を示しています。
FileUploadTag のコード:
public class FileUploadTag extends javax.faces.webapp.UIComponentELTag {
private javax.el.ValueExpression serverLocationDir=null;
// FileUpload フォーム固有のプロパティーを処理する
public void setServerLocationDir(ValueExpression dir) {
serverLocationDir=dir;
}
protected void setProperties(UIComponent component) {
super.setProperties(component);
UIForm outComp outComp=(HtmlForm)component;
// serverLocationDir 属性を取得する
if (serverLocationDir != null) {
if (!serverLocationDir.isLiteralText()) {
outComp.setValueExpression("serverLocationDir", serverLocationDir);
} else {
outComp.getAttributes().put("serverLocationDir", serverLocationDir.getExpressionString());
}
}
}
}
FileUploadRenderer:
親のカスタムレンダリングの encodeBegin メソッドは、子コンポーネントが作成済みであるかどうかを確認します。子コンポーネントが存在しない場合は、適切な型の子コンポーネントを作成し、親コンポーネントのファセットマップに格納し、適切な属性と ValueExpression を使用して更新します。子コンポーネントを取得すると、子コンポーネントの encodeBegin メソッドにマークアップの生成を委託します。
親コンポーネントのカスタムレンダリングの encodeEnd メソッドが呼び出されると、親コンポーネントのファセットマップから子コンポーネントが取得され、マークアップの生成が子コンポーネントの encodeEnd メソッドに委託されます。
FileUploadRenderer のコード:
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
HtmlForm childComp=(HtmlForm)component.getFacet(CHILD_COMPONENT_ID);
if(childComp == null) {
// 新しいコンポーネントを作成し、あとで取得できるように格納する
childComp=new HtmlForm();
component.getFacets().put(CHILD_COMPONENT_ID, childComp);
// 属性をループして、主コンポーネントから子コンポーネントにコピーする
Object attr=null;
for(int ii=0; ii < formAttributes.length; ii++) {
attr=component.getAttributes().get(formAttributes[ii]);
if(attr != null) {
childComp.getAttributes().put(formAttributes[ii], attr);
}
}
// 必要に応じて、指定の ValueExpression をコピーする
childComp
.setValueExpression("serverLocationDir",
component
.getValueExpression(
"serverLocationDir"));
// FileUpload が常に正常に動作するように、カスタムレンダリングでは HTML enctype をデフォルトで設定する
childComp.getAttributes().put("enctype", "multipart/form-data");
...
}
...
// 子コンポーネントでコンポーネント自体のマークアップを生成する
childComp.encodeBegin(context);
}
public void encodeEnd(FacesContext context, UIComponent component) throws IOException
HtmlForm childComp=(HtmlForm)component.getFacet(CHILD_COMPONENT_ID);
if(childComp != null) {
// 子コンポーネントでその終わりのマークアップを生成する
childComp.encodeEnd(context);
}
}
private static String formAttributes[]={"id", "prependId","rendered","accept",
"acceptcharset","dir","enctype",lang","onclick",ondblclick","onkeydown","onkeypress",
"onkeyup","onmousedown","onmousemove","onmouseout","onmouseover","onmouseup","onreset",
"onsubmit","style","styleClass","target","title","binding"};
private static final String CHILD_COMPONENT_ID="bpui_fileupload_childcomponent";
このコードからわかるように、この方法は複雑ですが、とても強力です。JSF コンポーネントの任意の数の機能をほかのコンポーネントが使用できるように、親コンポーネント内にラップできます。この FileUpload のサンプルは AJAX コンポーネントであり、フォームは従来の方法で送信されないので、レンダリングの decode メソッドは使用しません。フォームは、Shale-Remoting を通じて、Dojo によって非同期で送信され、FileUploadHandler.handleFileUpload メソッドによって使用されます。カスタムコンポーネントで「要求値の適用」フェーズ中にフォーム送信をデコードする場合も、親コンポーネントのファセットマップから子コンポーネントを取得し、子コンポーネントの decode メソッドに呼び出しを委託する方法を使用します。
一般に、子コンポーネントの decode メソッドから親コンポーネントにモデルデータが適切に転送されれば、この非表示の子コンポーネントを使用する方法に関係なく、JSF の残りの機能は同じです。親コンポーネントが子コンポーネントで複雑な動作を実装しようとすると (コンバータ、バリデータ、イベントなど) 警告が表示される可能性がありますが、これについては次のエントリで取り上げます。
© Sun Microsystems 2006. Java BluePrints Solutions Catalog の内容はすべて著作権保護されており、サン・マイクロシステムズ社の書面による許可なしに他の著作物に発表することを禁止します。