Clickのページ
ページは、Webアプリケーションの要です。ClickのページはHTMLリクエストの処理とHTMLレスポンスの出力を担当します。ここでは、そのページに関して、以下の話題について述べていきます。
- Classes - Pageおよびその関連クラス
- Execution - ページが実行される処理の流れ
- Request Param Auto Binding - リクエストパラメーターの自動バインディング
- Security - セキュリティ
- Navigation - ページ間の遷移
- Page Templating - 共通部分のテンプレート化
- Direct Rendering - ページの直接描画
- Stateful - ステートフルなページ(1.4-RC3以降)
- Error Handling - エラー処理
- Page Not Found - ページが見つからない時
- Message Properties - ページのメッセージプロパティ
Clickでは、ひとつの論理的なページは1つのJavaクラスと1つのVelocityテンプレートから構成されており、click.xml内のpage要素で定義されています。
<page path="search.htm" classname="com.mycorp.page.Search"/>
ここでのpath属性は、ページのVelocityテンプレートの位置を示しており、classname属性はページのJavaクラスを指定しています。
もし、例えばFreemarkerのような代わりのテンプレートエンジンを使用する場合、上記と同じに設定します。
その代わりVelocityではなくJSPを使う場合は以下のようになります。
<page path="search.jsp" classname="com.mycorp.page.Search"/>
Pageおよびその関連クラス
Clickで作成されたページは全てPageクラスのサブクラスです。Pageクラスとその関連するクラス群との関係は図1のようになります。
図1:Pageクラスの関連図 - created with Enterprise Architect courtesy Sparx Systems
PageクラスはVelocityテンプレートによって出力される全てのオブジェクトを保持する1つのページモデルをmodelプロパティとして提供します。このページモデルは、ユーザーインターフェースとなるControlオブジェクトも含んでもよいです。
Pageはリクエストに伴う全てのjavax.servletオブジェクトを参照する1つのContextオブジェクトを持っています。Clickでプログラムを記述する際には、Contextオブジェクトを通してHttpServletRequestの属性やパラメータ、そしてHttpSessionオブジェクトにアクセスすることができます。
Execution
Pageクラスは、空のハンドラメソッドをいくつか提供します。サブクラスではこれをオーバーライドすることによって、機能を提供することになります。
ClickServletはPageをインスタンス化するときに引数無しのコンストラクタを使うことになっているので、Pageのサブクラスを作成する際には不適切なコンストラクタを追加しないようにする必要があります。GETリクエストの実行シーケンスは以下のようになります。
図2:GETリクエストのシーケンス図 - created with Enterprise Architect courtesy Sparx Systems
このGET実行シーケンス順にまずPageインスタンスが生成され、Pageの各属性がセットされます(context,format,headers,path)。次に、リクエストのパラメータの値が該当するPageのpublicフィールドに結びつけられます。
次にonSecurityCheck()ハンドラーが呼ばれます。このメソッドは、ユーザーがページへのアクセスが認証されていることを確実にするためのもので、必要ならばそれ以上の処理を行わないように打ち切ることも可能です。
その後、onInit()メソッドが呼ばれます。ここにコンストラクタ呼呼出し後の初期化コードを記述します。onInit()はForm、FieldやTableのようなコントロールを生成する理想的な場所です。上記のダイアグラムではページのonInit()呼出し後にページ内の各コントロールのonInit()が呼び出されています。
その次は、ページ内のControlの処理です。CleckServletはControlのリストを取得し、それぞれにonProcess()を実行します。もしonProcess()でfalseを返すControlがあれば、以後の各Controlへの処理とPageのonGet()メソッドを打ち切ります。
ここまでうまく処理が進んでようやくonGet()メソッドが呼ばれます。
次にページテンプレートを処理し、表示するHTMLを生成します。ClickServletは、ページからモデル(Map)を取得し、以下のオブジェクトをmodelに追加します。
- ページのパブリックなフィールド(フィールド名使用)
- context - サーブレットのコンテキストパス。例) /mycorp
- cssImports - ページヘッダに含まれるCSSインポートとStyleブロック。PageImportsに詳しい説明があります。
- format - オブジェクトを表示するためのFormatオブジェクト
- imports - ページヘッダに含まれるCSSとJavaScriptインポート。PageImportsオブジェクトに詳しい説明があります。
- jsImports - JavaScriptインポートとページフッターにインクルードされるスクリプトブロック。PageImportsオブジェクトに詳しい説明があります。
- messages - getMessage()メソッド用のMessagesMap
- path - ページテンプレートのpath
- request - HttpServletRequestオブジェクト
- response - HttpServletResponseオブジェクト
- session - ユーザーのHttpSessionに対応するSessionMap
そしてテンプレートにページモデルを加え、結果をHttpServletResponseに出力します。モデルがテンプレートに加えられる時に、モデル内のControlはtoString()メソッドで出力されるでしょう。
最後のステップとして、コントロール群のonDestroy()メソッドが呼びだされ、最後にページのonDestroy()が呼び出されます。このメソッドは、ガーベジコレクトされる前に、ページに関連するリソースを消去します。onDestroy()はここまでの処理で例外が発生した場合にも呼び出されることが保証されています。
POSTリクエストの場合もGETとほぼ同じです。違いはonGet()のかわりにonPost()が実行されるだけです。こちらのPOSTリクエストのシーケンスダイアグラムも参照してください。
別の視点としてPageの実行の流れを、アクティビティ図で記述すると、以下になります。
図3:ページ処理のアクティビディ図 - created with Enterprise Architect courtesy Sparx Systems
リクエストパラメータの自動バインディング
Clickは自動的にリクエストのパラメータの値を、同名のPageのパブリックなフィールドに自動的にバインドします。そしてバインドされる際に、値は適切な型に変換されます。
このことを理解するには、例を見てもらうのが一番でしょう。
以下に対するGETリクエストを受けたとします。
http://localhost:8080/mycorp/customer-details.htm?customerId=7203
このリクエストはCustomerDetailsページによって自動的に処理されます。
このCustomerDetailsページが作成された後に、リクエストパラメータ"customerId"の値"7023"は自動的にIntegerに変換されパブリックフィールドのcustomerIdに代入されます。
もうひとつのClickの特徴は、パブリックフィールドが出力される前に自動でページのモデルに追加されることです。これらの値は、ページテンプレートとして利用可能になるということです。この例ではcustomerIdフィールドはページのmodelに追加され、ページテンプレートに利用できるのです。
customer-details.htm ページテンプレートは以下のとおりで
このリクエストを処理した後、ページは以下のように出力されます。
Customer ID: 7203
自動バインディングの設定
自動バインディングは、リクエストの文字列パラメータをJavaクラスに変換します。Integer, Double, Boolean, Byte, Character, Short, Long, Float, BigInteger, BigDecimal, String そして 種々の Dateに。
既定では、この変換はClickServletのgetTypeConverter()メソッドで取得されるRequestTypeConverterクラスによって実行されます。
もし上記以外の型変換が必要になる場合は、自前の型変換クラスを作成し、その自作の型変換を使うようにしたClickServletのサブクラスを使うことになるでしょう。
例えば、リクエストのパラメータで顧客IDが指定された場合に、データベースからCustomerオブジェクトを自動的にロードしたいとします。
この自作型変換は以下のリクエストに対し、"7203"の顧客IDを持つcustomerオブジェクトをロードします。
http://localhost:8080/mycorp/customer-details.htm?customer=7203
このリクエストはcustomerIdの値が"7203"のオブジェクトをデータベースからロードします。ClickServletはこのこのオブジェクトを対応するフィールドに代入します。
この型変換を有効にするには、ClickServletのサブクラスを作成しgetTypeConverter()メソッドをオーバーライドする必要があります。
セキュリティ
Clickのページは、プログラム可能なセキュリティモデルを実装するためにオーバーライドするonSecurityCheckイベントハンドラを提供しています。
一般的にはこの機能を使う必要はなく、可能ならばJEEセキュリティモデルを使用すべきです。ベストプラクティスのセキュリティの項目を参照してください。
アプリケーション認証
個々のアプリケーションでは、それ専用のセキュリティモデルを実装するためにonSecurityCheck()メソッドを利用することができます。以下の例では、基底クラスとなるSecureページクラスを提供しています。そこからの派生したページクラスでは、ユーザーがログインしていることを保証しています。この例では、ユーザーが認証に成功するとログインページがセッションを生成しています。Secureページは、ユーザーがセッション中かを確認し、そうでなければログインページへリクエストをリダイレクトします。
コンテナ認証
サーブレットコンテナは、ロールベースのアクセス制御(認証)を実施する機能を提供します。以下の例にあるAdminPageから派生したページでは、"admin"ロールを持つユーザーのみがページにアクセス可能であり、そうでないユーザーはログインページにリダイレクトされます。
ログアウト
ログアウトを実施するには、アプリケーションベースセキュリティであれコンテナベースセキュリティであれ、単にセッションを無効化するだけです。
ページ間の遷移
ページ間の遷移は、ページテンプレートのパスを指定することによりフォワードやリダイレクトで実施します。
フォワード
別のページにservletのRequestDispatcherを使ってフォワードするには、ページのforwardプロパティをセットします。例えばindex.htmへのフォワードは、以下のようになります。
これで、index.htmに相当する新しいPageクラスのインスタンスが呼び出されます。ただ、リクエストが別のページにフォワードされた時に、飛んだ先のページでは各コントロールが処理されないことに注意してください。これは最初のページからのPOSTリクエストを処理しようとする飛んだ先のページのフォームのように、混乱や間違いを防ぎます。
フォワード パラメータ渡し
別のページにフォワードした時、リクエストのパラメータは維持されています。これは、状態情報を受け渡す手軽な手段となります。例えばリクエストパラメータとしてフォワード先のページのテンプレートで表示されるcustomerオブジェクトを追加されます。
ページテンプレートview-customer.htmにフォワードされます。
リクエストのAttributeは自動的にVelocityのContext objectに追加され、ページテンプレートで利用可能となります。
ページ指定フォワード
ページ指定フォワードはページ間で情報を受け渡すもうひとつの手段です。ContextのcreatePage()メソッドを使ってフォワードしたいを生成し、プロパティを直接指定します。最後に生成したページをフォワード先として設定します。例として
createPage()メソッドを使用してページを生成するとき、ページパスの先頭に"/"をつけるようにしてください。
ページがユニークパスの場合に限り、フォワード先のページをそのクラスで指定することができます。(ただし、同じクラスに複数のパスをマップすることは可能ですが珍しいです。その場合、Context.createPageの呼び出し時に例外をスローします。なぜならClickはどちらのパスを使用するのかを決定できないからです。)
このテクニックを使ったコードが以下にあります:
このフォワード手法は、コンパイル時のチェックによる安全を提供しコードの中でページのパスを指定する煩わしさを緩和する、ベストプラクティスです。
ページの依存性注入をClickで可能にするため、つねにこのContextのcreatePage()メソッドを使うようにしてください。
テンプレートパス
新しいページにフォワードする別の手段としては、そのページテンプレートのパスを指定する方法があります。このアプローチで出力されるページテンプレートは生成されるページオブジェクトを持たずに、必要なものは全部保持しなければなりません。先ほどのonViewClick()をこの手法をで記述すると以下のようになります。
Customerオブジェクトがページモデルのテンプレートにどのように渡されるかに注意してください。このようにaddModelでデータを渡す方法は、setForwardなどでフォワードする場合は利用できません。これは、フォワード先のページが生成される前に元のページオブジェクトが破棄されてモデルの値が失われてしまうからです。
リダイレクト
リダイレクトは、ページ間の移動によく使われるもうひとつの手法です。詳細に関しては HttpServletResponse.sendRedirect(location) を参照してください。
リダイレクトが効果を発揮するのは、ユーザーがブラウザで実際にみているページに相当する、実際のURLが表示されるということです。これは、ユーザーがページをブックマークするとき、重要なことです。逆にリダイレクトの問題点は、別のページへのリクエストをユーザーのブラウザから行うため通信が発生することです。これは、処理の時間がかかるというだけでなく、元のページやリクエストの情報が失われるということも意味します。
例として、logout.htmへのリダイレクトは以下のようになります。
リダイレクトしたいパスが"/"から始まっている場合は、WEBアプリケーションのコンテキストパスを前につけたところへリダイレクトされます。つまり、"mycorp"に配置されたアプリケーションで setRedirect("/customer/details.htm") を呼べば、リクエストは"/mycorp/customer/details.htm"にリダイレクトされます。
また、リダイレクトしたいページのクラスからパスを取得することも可能です。
この手法を使う場合、リダイレクト先のページクラスのパスが一意でなければなならいことに注意してください。てっとりばやくリダイレクトするには、以下の例のように、リダイレクト先のページクラスをそのまま指定するのがシンプルです。
リダイレクト パラメータ渡し
リダイレクトされるページへの情報は、URLリクエストパラメータを使うことで受け渡すことが可能です。ClickServletは、HttpServletResponse.encodeRedirectURL(url) を使うことでURLエンコードします。
以下の例で、ユーザーは支払い確認のために「OK」ボタンを押すとします。onOkClcik()のハンドラ内で支払い処理が行われ、支払いのトランザクションIDを取得し、URLエンコードされたトランザクションIDとともにtrans-complete.htmページへリダイレクトされます。
trans-complete.htmのページクラスは、リクエストのパラメータtransIdからトランザクションIDを取得することができます。
Post Redirect
上記パラメータ渡しのサンプルは、ポストリダイレクトの例でもあります。ポストリダイレクトの技法は、ユーザーがリフレッシュボタンを押すことによるフォームの二重送信を防ぐのに非常に有効です。
共通部分のテンプレート化
Clickは、(StrutsでいうところのTilesのような)ページテンプレートを提供します。これにより、Webアプリケーションに共通化されたLook&Feelを提供し、メンテナンスに必要なHTMLの記述量を大幅に減らします。
ページのテンプレートを実装するには、実際のページが派生することになる、基底クラスを定義します。この、テンプレートとなる基底クラスは、PageクラスのgetTemplate()メソッドをオーバーライドし、出力するボーダーテンプレートのパスを返します。
BorderedPageのテンプレートであるborder.htmは以下のようになります。
これをテンプレートとしたページでは、Velocityの#parseディレクティブを使ってその内容をページのpathに渡すことで挿入します。$pathは、ClickServletにより、VelocityContextに自動的に追加されます。
テンプレートを利用したHomeページは以下になります。
<page path="home.htm" classname="Home"/>
Homeページの内容であるhome.htmは以下のようになります。
<b>Welcome</b> to Home page your starting point for the application.
home.htmへのリクエストがなされたとき、Velocityはhorder.htmとhome.htmをマージして返します。
これは以下のように出力されます。
border.htmテンプレートの中で$titleとして参照されているモデルのtitleの値をHomeクラスの中でどう定義しているかに注意してください。それぞれのテンプレートを使用したページはそれぞれのtitleを定義することができます。
JSPを利用したテンプレートの利用も同様にサポートされています。Click Examplesを参照してください。
ダイレクトレンダリング
ページはservlet responseに直接描画し、ページテンプレートの描画を迂回するためにダイレクトレンダリングをサポートしています。これはHTML以外のコンテンツ(PDFやExcelドキュメントなど)をresponseに描画したい場合に有効なシナリオです。これを行うためには:
- servlet responseオブジェクトを取得する
- responseのcontent typeを指定する
- responseのoutput streamを取得する
- output streamに出力する
- output streamをクローズする
- ClickServletに描画が完了したことを通知するため、page pathにnullを設定する
ダイレクトレンダリングの例は以下になります。
ステートフルなページ
(訳注)ステートフルなページは、Click 1.4-RC3からの機能になります。
Clickは、ユーザーのリクエスト間で状態が保存されるステートフルなページをサポートしています。ステートフルなページは、以下のような多くの場面で有用です。
- 検索ページと編集ページとの間での相互操作。次のようなケース:フィルターが適用されているステートフルな検索ページから、そのひとつの対象を編集するページへと遷移します。そこで対象を編集・更新すると、元の検索ページへとリダイレクトされ、フィルターされている状態のまま表示されます。
- 多くのフォームやテーブルがあり、その操作に多くの状態を扱うことが必要な場合。
ページをステートフルにするには、単にpageのstatefulプロパティをtrueにセットし、Serializableインターフェースを実装してserialVersionUIDをセットします。例として
ステートフルなページのインスタンスは、ユーザーのHttpSessionにページのクラス名をキーとして保存されます。上記の例では、ページはcom.mycorm.page.SearchPageというクラス名でユーザーセッションに格納されます。
ページの生成
ステートフルなページはセッションの最初で一度のみ生成され、そのセッション中はセッションの中から取り出されます。対してページのイベントハンドラは、onInit()メソッドも含め、リクエストごとに実行されます。
一般的にステートフルページを使う時には、全てのコントロールを生成するコードをページのコンストラクタに入れて一番最初だけ実行されるようにし、リクエスト毎に呼ばれるonInit()の中には書かないようにします。
動的なコントロール生成を行っている場合にはonInit()に置くことになるとは思いますが、ページのコントロールやモデルが準備できていないことに注意する必要があります。
ページの実行
デフォルトのClickでは、個別のリクエストやスレッドごとに新しいページのインスタンスを生成するので、その実行モデルはスレッドセーフです。ステートフルなページでは、複数のリクエスト・スレッドでひとつのページインスタンスを使いまわします。ページ実行のスレッドセーフを確保するため、ページのインスタンスは同期を取って、ある時点では、ひとつのリクエストのスレッドしかそのページインスタンスを実行できないようにしてください。
ページの破棄
通常のページインスタンスは実行後参照されなくなり、JVMによってガベージコレクションされます。しかしステートフルなページは、HttpSessionの中に格納され続けるので、メモリーや実行性能上の問題を引き起こすような、大量なオブジェクトをページインスタンスに載せないよう注意する必要があります。
ページの実行が完了したとき、ページ上の全てのコントロールのonDestroy()が実行され、そしてページ自体のonDestroy()が実行されます。大きなオブジェクトを参照から切り離して保存させないようにするに、これらのメソッドを利用するとよいでしょう。例えば、テーブルコントロールはデフォルトの動作としてそのonDestroy()で表示しているrowListの参照を切り離しています。
エラー処理
Pageオブジェクトを処理中ないしテンプレートを出力している最中に例外が発生すると、そのエラーは登録されているハンドラに処理が委譲されます。Clickにおける既定のエラーハンドラはErrorPageであり、自動的に以下のように設定されています。
<page path="click/error.htm" classname="net.sf.click.util.ErrorPage"/>
これとは別のエラーハンドラを登録するには、ErrorPageのサブクラスを作成し、"click/error.htm"のパスを使ってページを定義します。
<page path="click/error.htm" classname="com.mycorp.page.ErrorPage"/>
ClickServletが起動するときに、clickディレクトリにあるerror.htmが存在するか確認します。もし見つけられなかった場合、ClickServletは自動的にそれを配置します。必要に応じてclick/error.htmを調整することができ、そうして作成されたerror.htmはClickServletにより上書きされることはありません。
既定のエラー表示用テンプレートは、developmentモードないしdebugモードで起動中ならば、豊富なデバッグ情報を表示します。例えば
- NullPointerException - in a page method
- ParseErrorException - in a page template
productionモードではシンプルなエラーメッセージが表示されるだけです。アプリケーションのモード設定の詳細に関してはClickの設定を参照してください。
また、Click Examplesの中にも、エラー処理についてのデモがありますので、あわせて参照してください。
ページが見つからない時
ClickServletがclick.xmlの設定内容からリクエストのあったページを見つけられなかった場合、登録されているnot-found.htmを使用します。
この見つけられなかったときに表示されるページは、以下のように自動的に設定されています。
<page path="click/not-found.htm" classname="org.apache.click.Page"/>
この既定の設定をオーバーライドして自作のクラスを指定することができますが、パスに関しては変更できません。
ClickServletが起動するときに、clickディレクトリにあるnot-found.htmが存在するか確認します。もし見つけられなかった場合、ClickServletは自動的にそれを配置します。
このclick/not-found.htmは必要に応じて調整可能です。このページテンプレートは、通常のClickオブジェクトとしてアクセスします。
必要に応じてclick/error.htmを調整することができ、そうして作成されたerror.htmはClickServletにより上書きされることはありません。
ページのメッセージプロパティ
Pageクラスは、そのページにローカライズされた文字列のMessagesMapであるmessagesプロパティを持ちます。これらの文字列はそのキーを使って出力される際にVelocityContextで利用可能です。なので、titleがあるなら、ページテンプレート上で以下のように利用できます。
このmessages mapはページクラスのプロパティの紐付けによりロードされます。例えば、com.mycorp.page.CustomerListというページのクラスがあるとして、ローカライズされた文字列を含むプロパティファイルは以下になります。
/com/mycorp/page/CustomerList.properties
また、アプリケーション全体のページのmessagesは以下のファイルで定義可能です。
/click-page.properties
このファイルで定義された文字列はこのアプリケーション上の全てのページで利用できます。ただ個別のクラスで定義すると、このアプリケーション全体の定義を上書きすることに注意してください。
このページのmessagesも、コントロールのmessagesに上書きされます。詳細に関しては「コントロールのメッセージプロパティの項目」を参照してください。