S2JSFの採用はひとまず保留

んー。
S2JSFは社内のWeb系開発者数人と相談するということでひとまず保留。
しばらくはMyFaces1.0.9を相手にフレームワーク化するにあたっての技術的な大枠を固めることにする。
後でS2JSFを使うことになった場合に不要になる箇所が出てくるかもしれないが、それはまぁ、気にしないということで。

コネクション/トランザクションの管理

まず、コネクション/トランザクションの管理についてはSeasar2を使うという時点であまりやることがないのでスルー。便利だ。

ただ、今後、複数のデータソースをひとつのDAOで使い分けるしくみをサポートすることになるかもしれない。
これは、データセンターか何かにシステムを一個どーんと置いておき、複数の会社からアクセスするというような構成の場合に必要になる。
データソースで会社を分けずにデータ自体に識別子を付けてあげれば良いじゃんという気がしなくもないが、いろんな事情で別にしたい場合があるようだ。
DIコンテナ(というかそれに付属のAOP機能)でコネクションの管理をする場合、DAOごとに別のデータソースを設定することができても、単一のDAOで動的にデータソースをハンドリングすることができない(会社数が少し多いので会社ごとにDAO自体を切り替えるというやり方は難しい)。
静的なファイルで依存関係を設定しているので、当たり前といえば当たり前の制約だし、そもそも、自前で簡単に実装できるので誰も必要性を感じていないのかもしれない。
でもね、せっかくDIコンテナにコネクション/トランザクションの管理を任せているのだから、自前のコネクション実装は避けたいなぁと思ってみたり。


……ん?
diconファイル内にあるXADataSourceのプロパティの部分にOGNL式を書けば可変にできるかも?
OGNL式で特定のメソッドを呼んで、そのメソッド内でSessionからログイン時に保持した会社情報を取得するとか。
んー、そんなんできちんと動作するのか?ちょっと怖い。


冒頭で「スルー」とか言っておきながらまったくスルーしてないな。

例外のハンドリング

次に、フレームワークとしてはクライアントモジュールで発生した例外を一括してハンドリングするしくみを検討する。
脳内で候補に挙がったいくつかの方法とそれに対する検討内容を以下に記す。

AOPでtry-catchする

まず、「AOPでtry-catchする」という方法を考える。
AOP使うとカッコよさそうだよねぇ」などと安易に考えていたが、「ハンドリングを設定するのは特定の箇所のみ」且つ「でも、その特定の箇所が結構多い」ということでAOPを適用する場所の設定が結構面倒くさそうである。
AOPにしちゃうと「エラー」→「エラーページの出力」というフローもやり難いし。
なのでAOPは却下。
そもそも、「一括して例外のハンドリングしたい」というのは横断的関心とは違うか。

MyFacesServletのサブクラスを作ってservice()の中でtry-catchする

次に、「MyFacesServletのサブクラスを作ってservice()の中でtry-catchする」という方法を考える。
要は、MyFacesServletのサブクラスをFront Controllerに見立ててイベント汎用な処理を突っ込むということである。
この方法は、非JSFのエラーページが相手の場合にはおそらくそれなりにうまく動くし、フローとしても妥当なものなのだが、いかんせんJSFの範囲外なのである。
「入出力文字のエンコード」や「ブラウザキャッシュの無効化」といったPresentation層での設定はServletやFilterに入れるのが妥当であるが、今回のようにフローの根っこになる処理をServletやFilterに入れてしまうと、Servlet以外の環境にアプリケーションを移行する際に困ってしまう。
これと同様の理由で「Filterを作ってtry-catchする」という方法も却下である。

デフォルトActionListenerでtry-catchする

次に、「デフォルトActionListenerでtry-catchする」という方法を考える。
今度はちゃんとJSFの範囲内である。
しかも、faces-config.xmlのaction-listenerに設定してあげるだけで本来のデフォルトActionListenerと差し替えることができる。
しかし、デフォルトActionListenerは、actionの処理をラップしているが、actionListenerの処理はラップしていない。
一般のEvent/Listenerパターンのように、Eventが発生したらListenerコレクションをイテレートしてEventを通知していくため、デフォルトActionListenerの中で例外をハンドリングしても、コレクション内の別要素であるactionListenerの例外はハンドリングできないのである。
今回はクライアントモジュールで発生した例外を一括してハンドリングすることが目的なので、この方法は却下ということになる。

LifeCycle#execute()でtry-catchする

「じゃあ、JSFの範囲内でクライアントモジュールの例外を一括してハンドリングできるところってどこよ」って言ったときに出てくるのがLifeCycleである。
まぁ、LifeCycleはFacesServletから呼ばれているので、本来ならServletでのハンドリングを検討した次の段階でここに辿り着くべきである。
で、MyFacesのFacesServlet#service()の中では、LifeCycle#execute()とLifeCycle#render()が仲良くならんで記述されている。
今回ハンドリングしたいのはLifeCycle#execute()の中で発生した例外なので、LifeCycleのサブクラスを作ってexecute()の中にハンドリング処理を入れるということでようやく一件落着となる。
具体的には、例外が発生したらcatchして、ViewHandler#createView()でUIViewRootを作成し、FacesContext#renderResponse()とFacesContext#setViewRoot()を呼ぶ、という処理になる。
強制ログアウトさせたいレベルの例外であればFacesContext#getExternalContext().getSession(false)でHttpSessionを取得し、HttpSession#invalidate()でセッションを無効化すればよい(Servletに依存するのが気持ち悪いが、ExternalContext#getSession()の戻り値がObjectであるため仕方がない。抽象的なSessionを用意してくれればいいのに)。

LifeCycle#render()で例外が発生した場合は、web.xmlのerror-pageで単純なエラーページを表示する。
ただ、この場合は強制ログアウトの処理ができない。
どうしても強制ログアウトの処理をしたい場合、単純じゃないエラーページを呼べば良いのだが、レンダリングで失敗してるのに単純じゃないエラーページを表示できるのかどうかが心配なので、ひとまず必要になるまでは単純なエラーページで代用する。

アクセスの制限

アクセスの制限の処理をどこに入れるかについては、例外のハンドリングと同じことがいえる。
Web上にはNavigationHandlerに入れるという方法が示されていたが、このやり方だと「Invoke Applicationg」フェーズを通らないリクエスト(「Restore View」フェーズしか通らない場合など)であっさりとページが表示されてしまう。
これを避けるため、NavigationHandlerではなくLifeCycle#execute()の冒頭でアクセス制限のチェックをし、アクセスが許可されていない場合は適当なページを設定して終了するという形で実装する。

あと、忘れちゃいけないのが「JSPファイル直指定」への対応。
Servlet+JSPでガシガシコードを書いていたときにはWEB-INFの下にJSPファイルを置くことで対応していたが、JSFだとなんだかうまくいかない。
今回は「*.faces」でJSF Servletを呼んでいるので、下記のような設定をweb.xmlに入れることで対応する(JSF Servletマッピングした拡張子と同じファイル拡張子にしておいた方が楽か?)。

<security-constraint>
	<web-resource-collection>
		<web-resource-name>Restrict direct access to JSP pages</web-resource-name>
		<url-pattern>*.jsp</url-pattern>
	</web-resource-collection>
	<auth-constraint>
	</auth-constraint>
</security-constraint>