S2Strutsで元の画面に帰りましょう処理

前から余裕が出来たら書こう書こうと思ってたネタを。

前にSeasarコンで、id:kanagさんの隣にたまたまなったので、ちょこっと話した事なんですが。以下、POJO Action かつ無設定Struts前提で。

validatorでひっかかるとタグか、StrutsActionアノテーションのinput属性(まあstruts-config.xmlの、ですが)で指定したページへ戻る訳です。

でも、validatorでひっかかった分だけでなくて、ロジックの中でハンドリングするエラーも同じように元の画面に戻してやりたい。ログイン失敗とか。S2Struts-exampleで云うと、AppRuntimeExceptionを継承した例外をキャッチした場合。

今やってるの、ログイン専用画面のほかにトップページのサイドバーにもログインフォームがあるんですよ。validatorでひっかかったのはトップページに戻るんだけど、ログイン失敗だと、色々と工夫しないと元の画面に戻れない。

Actionクラスをログイン専用画面とトップ画面のそれぞれ別に作る、とか。

で、要するに、input属性かタグの値が判ればいいわけですよ。そっから値をとってそっちへforwardしてやればよろしい。

タグの場合は簡単。普通にリクエストパラメータを取得してやれば宜敷い。Base64でデコードしてやればよいのじゃ。

String param = request.getParameter(Constants.PAGE_NAME_ELEMENT_VALUE);
String inputUrl = new String(Base64Util.decode(param)); 

RequestDispatcher dispatcher = request.getRequestDispatcher(inputUrl);

try {
    dispatcher.forward(request,response);
} catch (ServletException e) {
    //あれやこれや
} catch (IOException e) {
    //あれやこれや
}

できたぞー。

これでまあいいかって思ったんですけど、一部入力確認画面とかでinput属性を指定してる所がありまして、やっぱりそっちも対応しないといけないでしょう、と。

どうやってinput属性って取るんだろうと思って、色々とソース追っかけたんですけど、ActionMappingクラスってのにgetInputメソッドなんてのがあるじゃないですか。これだー。

どうやってとろう・・・。こちとらPOJO Actionですから、うーむ。

で、S2RequestProcessorにThrowsInterceptorかまして、そいつでAppRuntimeExceptionをハンドリングしてやればいいじゃんって事に。S2RequestProcessorのprocessとかprocessActionFormとか、引数にActionMappingクラスがあるメソッドをpoint cutにすれば、MethodInvocationから引数取れますから、一件落着。

出来たー。と、ここでサンプルソースを出さないのには訳がある。

デバグしながらなんとなくrequestの中身を見てたら、居ましたよ、ActionMappingが。

ActionMapping am = (ActionMapping)request.getAttribute("org.apache.struts.action.mapping.instance");
String inputUrl = am.getInput();

とれたー!

てな訳で、先のThrowsInterceptorはActionクラスにかます事になりました。requestやresponseの取り方は、http://d.hatena.ne.jp/rokugen/20060610#1149866602のコメント欄の見ましょう。

ActionMapping am = (ActionMapping)request.getAttribute("org.apache.struts.action.mapping.instance");

String inputUrl = null;

if(am != null){
    inputUrl = am.getInput(); 
}

if(inputUrl == null || "".equals(inputUrl)){
    String param = request.getParameter(Constants.PAGE_NAME_ELEMENT_VALUE);
    inputUrl = new String(Base64Util.decode(param));
}

RequestDispatcher dispatcher = request.getRequestDispatcher(inputUrl);

try {
    dispatcher.forward(request,response);
} catch (ServletException e) {
//あれや
} catch (IOException e) {
//これや
}

取り敢えず全部のせで。本当のソースはちゃんとメソッドやクラスを分割してもうちょっと綺麗ですよ。

結局この処理、AppRuntimeExceptionが投げられた時以外にも任意のタイミングで呼び出したくなったので(キャンセルとか)、interceptorからは外に出したんですけどね(だからtry〜catchが書いてある)。

で、こうしてみたらですね、これはなかなかよかった。今まではActionクラスで、Service層でAppRuntimeExceptionが投げられた時のページ遷移を設定しなきゃいけないので、Service層の実装を覗きみないといけなかったんですが、interceptorにしたらService層でどんなAppRuntimeExceptionが投げられるのか、はたまた投げられないのか、意識しなくていいんですよ。なんにもcatchしないの。

public class LoginActionImpl implements LoginAction {
    private LoginService loginService;  
    private LoginDto loginDto;
    
    public String login() {
        loginService.login(loginDto);
        return SUCCESS;
    }
}

これでおしまい。ユーザーが存在しません、とかパスワードが間違ってます、とか、そういうのはService層でよろしくやってくれます。

エラー文言は、S2Struts-exampleにあった

errors.detail={0}

に、AppRuntimeException#getMessage()の内容を突っ込んじゃえばいいし。

但し、規約として、必ずタグかinput属性は書いておきましょう、ってのを守ってもらわないと白紙の画面が。

えー長くなりましたが、こんなんで上手くいきました。