思いつくままにやってみる
Packageは、study.xxx.extention.struts.processorにしよう。
まずは、テストクラスの作成だけどどうやって作ればいいんだろう。
S2Strutsのテストクラスを参考にしよう。
ActionExecuteProcessorImplTestでテストしてるみたい。
わかりやすい。
自分でもできそうだ。
XxxActionExecuteProcessorImplTestを作成
public class XxxActionExecuteProcessorImplTest extends S2TestCase { private ActionExecuteProcessor actionExecuteProcessor; protected void setUp() throws Exception { super.setUp(); include("s2struts.dicon"); include("XxxActionExecuteProcessorImplTest.dicon"); } protected void tearDown() throws Exception { super.tearDown(); } public void testActionMessage() throws Exception { MessageActionImpl action = new MessageActionImpl(); Object form = null; ActionMapping mapping = new MockActionMapping(); mapping.setType(MessageActionImpl.class.getName()); ActionForward actionForward = actionExecuteProcessor.processActionExecute( getRequest(), getResponse(), action, form, mapping); ActionMessages errors = (ActionMessages) getRequest().getAttribute(Globals.ERROR_KEY); assertNotNull(errors); assertFalse(errors.isEmpty()); ActionMessage error = (ActionMessage) errors.get().next(); assertEquals("errors.test", error.getKey()); } }
public class MessageActionImpl { private static final String errors_EXPORT = "errors"; private Map errors = new HashMap(); public String test() { errors.put("errors.test", new Object[] {}); return "success"; } public Map getErrors() { return errors; } }
こんな感じかな。
コンパイルエラー。MockActionMappingクラスがないって。
s2xxx-studyTestプロジェクトに
s2-struts-unit-EA1.jar
を追加しよう。
まだ、コンパイルエラーがでてる。
HttpServletResponseを解決できません、だって。
servlet.jarがないからかな。やっぱり。
servlet-api.jar
をs2xxx-studyTestプロジェクトに追加。
コンパイルエラーがなくなったから、実行。
障害トレースをみるとメソッドが見つかってないみたい。
ちゃんとMessageActionインターフェースをつくって、MessageActionImplでimplementしてtestActionMessageメソッドを修正して再度実行。
この方法でしかS2StrutsでのAction製造方法を知らない・・・勉強不足だ。
期待した失敗になった。かなり前進した。
XxxActionExecuteProcessorImplの作成。
申し訳ないけど、ActionExecuteProcessorImplの内容をそのままコピーして、BindingUtilをセッター・インジェクションするように変更。
BindingUtilクラス名を変えないと。XxxActionPropertyBinderImplにしよう。ActionPropertyBinderインターフェースもついでに作ろう。
処理内容は、BindingUtilをコピーしてそれぞれのメソッドのstaticをはずして。
あと、s2struts.diconを修正して実行。
さっきと同じ失敗だけど、本当に自分が作ったほうを実行しているのかなー。
instanceofでactionExecuteProcessorがXxxActionPropertyBinderImplなのかを確認するテストを追加。
public void testInstance() { assertTrue(actionExecuteProcessor instanceof XxxActionExecuteProcessorImpl); }
このテストはうまくいってる。
きちんと自分が作ったActionExecuteProcessorを利用してる。
次はやっとtestActionMessageをグリーンバーにする作業。
まずは、StrutsのAction#addErrors()をXxxActionPropertyBinderクラスにコピー。
XxxActionPropertyBinder#exportProperties()をどう変更するか考慮中。
こんな感じにしてみた。
public void exportProperties(Object action, S2Container container, BeanDesc beanDesc) { for (int i = 0; i < beanDesc.getPropertyDescSize(); ++i) { PropertyDesc propertyDesc = beanDesc.getPropertyDesc(i); if (propertyDesc.hasReadMethod()) { Object var = propertyDesc.getValue(action); if (var != null) { String fieldName = propertyDesc.getPropertyName() + EXPORT_SUFFIX; if (isSessionProperty(beanDesc, fieldName)) { exportSessionProperty(container, propertyDesc, var); } else if (isErrorsProperty(beanDesc, fieldName)) { exportErrorsProperty(container, propertyDesc, var); } else { container.getRequest().setAttribute( propertyDesc.getPropertyName(), var); } } } } } private boolean isSessionProperty(BeanDesc beanDesc, String fieldName) { if (beanDesc.hasField(fieldName)) { Field field = beanDesc.getField(fieldName); String value = (String) FieldUtil.get(field, null); return SESSION.equalsIgnoreCase(value); } return false; } private void exportSessionProperty(S2Container container, PropertyDesc propertyDesc, Object var) { container.getSession().setAttribute( propertyDesc.getPropertyName(), var); } private boolean isErrorsProperty(BeanDesc beanDesc, String fieldName) { if (beanDesc.hasField(fieldName)) { Field field = beanDesc.getField(fieldName); String value = (String) FieldUtil.get(field, null); return ERRORS.equalsIgnoreCase(value); } return false; } private void exportErrorsProperty(S2Container container, PropertyDesc propertyDesc, Object var) { if (!(var instanceof Map)) { throw new RuntimeException("errors is not instanceof Map."); } Map mapErrors = (Map) var; ActionMessages errors = new ActionMessages(); for (Iterator it = mapErrors.keySet().iterator(); it.hasNext();) { String key = (String) it.next(); ActionMessage error = new ActionMessage(key, mapErrors.get(key)); errors.add(ActionMessages.GLOBAL_MESSAGE, error); } addErrors(container, errors); } private void addErrors(S2Container container, ActionMessages errors) { if (errors == null) { return; } ActionMessages requestErrors = (ActionMessages) container.getRequest() .getAttribute(Globals.ERROR_KEY); if (requestErrors == null) { requestErrors = new ActionMessages(); } requestErrors.add(errors); if (requestErrors.isEmpty()) { container.getRequest().removeAttribute(Globals.ERROR_KEY); return; } container.getRequest().setAttribute(Globals.ERROR_KEY, requestErrors); }
実行。
前回のエラーとかわってない。
どうしよう。
うーん。こういうときはSystem.out.printlnデバックだ。
どのメソッドまで呼ばれているかSystem.out.printlnを埋め込んで確認。
isErrorsPropertyでfalseになるなぜ?
MessageActionImplをみると
private static final String errors_EXPORT = "errors";
だからだ。たぶん。publicに修正して再実行。
そしてグリーンバー
これって、今までの失敗をすべて無駄にする間違いだなー。
テストクラスのバグを見つけるにはどうするべきか考えないといけない。うーん。自分では無理。
この問題は重大だけどほっとこう。
これでやっと、LogonActionImplの修正に入れる。
public class LogonActionImpl implements LogonAction { private PartyLogic partyLogic; private PartyDto logonForm; public static final String errors_EXPORT = "errors"; private Map errors = new HashMap(); public LogonActionImpl(PartyLogic partyLogic) { this.partyLogic = partyLogic; } public String logon() { if (!partyLogic.existParty(logonForm)) { errors.put("errors.logon", new Object[] {}); return "error"; } return "success"; } public PartyDto getLogonForm() { return logonForm; } public void setLogonForm(PartyDto logonForm) { this.logonForm = logonForm; } public Map getErrors() { return errors; } }
こんな感じに修正して、struts-config.xmlのerrorの場合のpathを/pages/logon.jspに変更。
そしてTomcat起動。
LOGON画面を表示して、パスワードをわざと間違えてLOGON。
エラー。
[ESSR0071]SQLで例外が発生しました。理由はjava.sql.SQLException: socket creation error
また、HSQLDBを起動してなかった。
HSQLDBを起動して再度挑戦。
うまくできた。
せっかくLOGONしたんだから、該当するPartyオブジェクトをsessionに格納しよう。
LogonActionImplをまたちょっと修正。
public static final String logonUser_EXPORT = "session"; private PartyDto logonUser; public String logon() { if (!partyLogic.existParty(logonForm)) { errors.put("errors.logon", new Object[] {}); return "error"; } logonUser = partyLogic.getParty(logonForm.getAccount()); return "success"; } public PartyDto getLogonUser() { return logonUser; }
そしてPartyLogic#getParty()を作成。
sessionに格納されていることを確認するためにメニュー欄に名前を表示するように変更。
うまくいった。
メニュー欄にLOGONしたAccountの名前が表示されてる。
これでLogonActionは完成だ。
次はLogoffActionを作るんだけど、これはsessionをクリアするだけの簡単なAction。
すぐにできるはず。
まず、LogoffActionインターフェイスを作ってLogoffActionImplで実装する。
sessionのクリア方法は・・・
たぶんnullにすればいいんだろう。
public class LogoffActionImpl implements LogoffAction { public static final String logonUser_EXPORT = "session"; public String logoff() { return "success"; } public PartyDto getLogonUser() { return null; } }
こんな感じかな。
party.diconに追加して、struts-config.xmlの修正。
あとメニュー欄のmenu.jspにLOGOFFのリンクを作れば完成。
おっ、きちんと動いてる。
でも、本当にあれでsessionクリアできてるのかなー。
XxxActionExecuteProcessorImplTestにテストを追加して確認しよう。
public void testSessionClear() throws Exception { SessionClearAction action = new SessionClearActionImpl(); Object form = null; ActionMapping mapping = new MockActionMapping(); mapping.setType(SessionClearAction.class.getName()); getRequest().getSession().setAttribute("data", "sessiondata"); ActionForward actionForward = actionExecuteProcessor .processActionExecute(getRequest(), getResponse(), action, form, mapping); assertEquals(null, getRequest().getSession().getAttribute("data")); }
実行。失敗。session情報がそのままになっている。
XxxActionPropertyBinderImpl#exportPropertiesでnullのときは何もしないようになってる。
よく考えるとnullを設定してたら、removeAttribute()を呼ぶってのは、変なのかもしれないなー。
でも、今いい方法を思いつかないし。しっくりくるかどうかわからないけどやってみよう。それから悩もう。
ということで、XxxActionPropertyBinderImpl#exportPropertiesを修正。
あわせてexportSessionPropertyとexportErrorsPropertyも修正。
テスト実行。グリーンバー。とりあえずはこんなもんか。
これで、LogonAction/LogoffAction完成。
S2DaoもS2StrutsもSiteMeshもStrutsも順調に動作してそう。いい感じだ。
次の課題は、Strutsを使ってたらいつも発生するやつ。
1.Actionクラスが増えすぎるからDispatchActionでまとめたい。
LogonActionとLogoffActionは、AuthActionとして1つにまとめ、AuthAction#logon()とAuthAction#logoff()にしたい。
2.普通のDispatchActionは日本語では使いづらい。
普通、
<input type="submit" name="method" value="execute">
とはせず
<input type="submit" name="method" value="実行">
ってしてしまうから、JavaScriptを使う必要が出てくる。
だけど、たかが画面遷移程度がJavaScriptに依存するのもなんかやだ。
3.DispatchActionにすると1つのAction-mappingでvalidateするかどうかを決めるのはうまくできなくなる。
AuthAction#logon()はvalidateするけど、AuthAction#logoff()はvalidateしないって感じにしたい。
「1.」は、S2Strutsが提供してくれているから大丈夫。
「2.」は、以前の開発でやってた方法で
<input type="submit" name="/site/party:list" value="実行">
みたいなnameにして、valueから呼び出すメソッドを特定するんじゃなくてnameから特定するようにする。
「3.」はどうしよう。
以前の開発では、Action-mappingによるvalidateは常にfalseにしてメソッド内でValidateForm#validate()を呼び出してた。
でも今回はS2Dao、S2Strutsを真似てアノテーションでやろうかなー。
Actionインタフェースに
public static final String method名_VALIDATE = "/pages/error.jsp";
があったら、validateするってことにしよう。Interceptorを作る勉強もかねて。
「1.」「2.」は同時にしよう。
ActionExecuteProcessorImplをコピーして作ったXxxActionExecuteProcessorImplの処理内容を確認。
XxxActionExecuteProcessorImpl#execute()ですべて行ってる。
思いつきだけど。execute()の内容を別クラスにするのがいいかな。
ActionExecutorインターフェースを作って、s2struts.diconファイルでXxxActionExecuteProcessorImplと関連付けるようにする。
ActionExecutorの種類はSingleMethodActionExecutorとかDispatchActionExecutorかな。
「2.」の処理を行うActionExecutorは、NameDispatchActionExecutorとしよう。
よし、テストメソッドを作ろう。
XxxActionExecuteProcessorImplTest#testCalledSingleMethodAction()を作成。
public void testCalledSingleMethodAction() throws Exception { SingleMethodActionImpl action = new SingleMethodActionImpl(); Object form = null; ActionMapping mapping = new MockActionMapping(); mapping.setType(SingleMethodAction.class.getName()); ActionForward actionForward = actionExecuteProcessor .processActionExecute(getRequest(), getResponse(), action, form, mapping); assertEquals("called", action.getTestMessage()); }
こんな感じであわせて、SingleMethodActionインタフェースと実装をつくってDICONファイルに設定。
で、テスト実行。
グリーンバー。
ActionExecutorインターフェースとその実装の作成に入ろう。
ActionExecutorインターフェースの引数はXxxActionExecuteProcessorImpl#execute()と同じで問題ないだろう。たぶん。
うーん。SingleMethodActionExecutorを作っているときに問題発覚。
イメージとしては、XxxActionExecuteProcessorImpl#execute()を
private String execute(HttpServletRequest request, Class actionInterface, Object action, ActionMapping mapping) { for (Iterator it = actionExecutors.iterator(); it.hasNext();) { ActionExecutor executor = (ActionExecutor) it.next(); if (executor.hasCalledAction(request, actionInterface, action, mapping) { return executor.execute(request, actionInterface, action, mapping); } } return null; }
って感じにしようと思ってたけど、ActionExecutor#hasCalledAction()の処理内容とActionExecutor#execute()の処理内容が似てきてしまう。
いちいちActionExecutor#hasCalledAction()で確認するのは、良くないのかも。
ActionExecutor#execute()の返却値で、該当するActionを実行できたかを判断するようにすべきかな。
こんな感じで。
private String execute(HttpServletRequest request, Class actionInterface, Object action, ActionMapping mapping) { String forward = null; for (Iterator it = actionExecutors.iterator(); it.hasNext();) { ActionExecutor executor = (ActionExecutor) it.next(); forward = executor.execute(request, actionInterface, action, mapping); if (!ActionExecutor.NOT_CALLED_ACTION.equals(forward)) { return forward; } } return null; }
うん。そうしよう。
ActionExecutorが実際に呼び出すメソッドがあるかを判断するためにはリフレクションで実行するしかない場合もあるかもしれないし。
そうしよう。そうしよう。ここも違和感があるけど、感覚優先でいこう。
SingleMethodActionExecutorはこれで。
public class SingleMethodActionExecutor implements ActionExecutor { public String execute(HttpServletRequest request, Class actionInterface, Object action, ActionMapping mapping) { Method[] methods = actionInterface.getMethods(); if (methods.length == 1) { return (String) MethodUtil.invoke(methods[0], action, null); } return ActionExecutor.NOT_CALLED_ACTION; } }
次は、XxxActionExecuteProcessorImplを修正しよう。
XxxActionExecuteProcessorImpl#execute()はできたから、複数のActionExecutorをインジェクションする方法を調べよう。
メソッド・インジェクションを使えばできそう。初めてつかうことになる。
s2struts.diconを修正しよう。
<component name="singleMethodActionExecutor" class="study.xxx.extention.struts.processor.SingleMethodActionExecutor"/> <component name="executeProcessor" class="study.xxx.extention.struts.processor.XxxActionExecuteProcessorImpl"> <initMethod name="addActionExecutor"><arg>singleMethodActionExecutor</arg></initMethod> </component>
これでテストを実行。
グリーンバー。
本当にうまくいってるのか?
s2struts.diconのinitMethodをコメントアウトしてから実行してみよう。
失敗2件。やっぱりうまくいってたみたい。
s2struts.diconを元に戻してっと。
次は、DispatchActionExecutorを作ろう。
まずはテストメソッドから。
そういえば、HttpServletRequestにどうやってparameterを設定すればいいんだろう。
とりあえず、S2TestCase#getRequest()を見てみよう。
おぉ、MockHttpServletRequestを返すことになっていて、MockHttpServletRequest#setParameter()が提供されてる。
たすかるなー。
public void testCalledDispatchAction() throws Exception { DispatchActionImpl action = new DispatchActionImpl(); Object form = null; ActionMapping mapping = new MockActionMapping(); mapping.setType(DispatchAction.class.getName()); mapping.setParameter("method"); getRequest().setParameter("method", "test2"); ActionForward actionForward = actionExecuteProcessor .processActionExecute(getRequest(), getResponse(), action, form, mapping); assertEquals("called", action.getTestMessage()); }
テストメソッド完成。テスト実行。
エラー。
[ESSR0046]コンポーネント(interface study.xxx.extention.struts.processor.DispatchAction)が見つかりません
DICONファイルに記述を忘れてた。
記述して、再度実行。
期待通り失敗。
呼び出せてないみたい。
DispatchActionExecutorを作ろう。
public class DispatchActionExecutor implements ActionExecutor { public String execute(HttpServletRequest request, Class actionInterface, Object action, ActionMapping mapping) { String param = mapping.getParameter(); if (param == null) { return ActionExecutor.NOT_CALLED_ACTION; } String methodName = request.getParameter(param); if (methodName == null) { return ActionExecutor.NOT_CALLED_ACTION; } try { Method method = ClassUtil.getMethod(action.getClass(), methodName, null); return (String) MethodUtil.invoke(method, action, null); } catch (RuntimeException e) { return ActionExecutor.NOT_CALLED_ACTION; } } }
そしてs2struts.diconを修正して、テスト実行。
グリーンバー。調子いいなー。
次は本題のNameDispatchActionExecutorを作ろう
まずはつくるのがなれてきたテストメソッド。
public void testCalledNameDispatchAction() throws Exception { DispatchActionImpl action = new DispatchActionImpl(); Object form = null; ActionMapping mapping = new MockActionMapping(); mapping.setType(DispatchAction.class.getName()); mapping.setPath("/do/path"); getRequest().setParameter("/do/path:test2", ""); ActionForward actionForward = actionExecuteProcessor .processActionExecute(getRequest(), getResponse(), action, form, mapping); assertEquals("called", action.getTestMessage()); }
ActionクラスはDipatchActionで問題なしと。呼び出しの規則が変わるだけだから。
DICONクラスの修正は不要。テスト実行。
期待通り失敗。
NameDispatchActionExecutorを作るんだけど、これは以前のソースをコピーして修正。
public class NameDispatchActionExecutor implements ActionExecutor { public String execute(HttpServletRequest request, Class actionInterface, Object action, ActionMapping mapping) { String methodName = NameDispatchUtil.getPathMethodName(request, mapping); if (methodName == null) { return ActionExecutor.NOT_CALLED_ACTION; } Method method; try { method = ClassUtil.getMethod(action.getClass(), methodName, null); } catch (RuntimeException e) { return ActionExecutor.NOT_CALLED_ACTION; } NameDispatchUtil .addCalledPathMethodName(request, mapping, methodName); return (String) MethodUtil.invoke(method, action, null); } }
NameDispatchUtilっていうヘルパーを利用してる。
このクラスは手をつけずにコピーしよう。
public class NameDispatchUtil { private static final String EXEC_PARAM = "study.xxx.extention.struts.util.EXEC_PARAM"; private static final String SEP_KEY = ":"; public static String getPathMethodName(HttpServletRequest request, ActionMapping mapping) { String command = getPathCommand(request, mapping); if (command == null) { return null; } return toPathMethodName(mapping, command); } public static void addCalledPathMethodName(HttpServletRequest request, ActionMapping mapping, String methodName) { List calledMethods = getCalledMethods(request); calledMethods.add(request.getRequestURI() + SEP_KEY + mapping.getPath() + SEP_KEY + methodName); } private static String getPathCommand(HttpServletRequest request, ActionMapping mapping) { List calledMethods = getCalledMethods(request); String path = mapping.getPath() + SEP_KEY; for (Enumeration params = request.getParameterNames(); params .hasMoreElements();) { String command = (String) params.nextElement(); if (command.startsWith(path)) { String value = request.getRequestURI() + SEP_KEY + command; // 別ActionにForwardしたときにすでに呼び出したcommandを // 再度実行しないようにするために、 // まだ呼び出されていないcommandだということを確認する。 if (!calledMethods.contains(value)) { return command; } } } return null; } private static String toPathMethodName(ActionMapping mapping, String command) { String path = mapping.getPath() + SEP_KEY; return command.replaceFirst(path, ""); } private static List getCalledMethods(HttpServletRequest request) { List result = (List) request.getAttribute(EXEC_PARAM); if (result == null) { result = new ArrayList(); request.setAttribute(EXEC_PARAM, result); } return result; } }
EXEC_PARAMの内容は変えたけど、それ以外はそのまま。
思った以上に長いソースだ。ふーん。3行ほどコメントがあるけど本当にそうなのかを確認するテストを思いつかない。ほっとこう。
バグがあったら、そのときに見直そう。今は気にしないようにしよう。
s2struts.diconを修正して、テスト実行。
グリーンバー。
これでやっと、LogonActionとLogoffActionを統合できる。
統合したAuthActionの作成。
public class AuthActionImpl implements AuthAction { private PartyLogic partyLogic; private PartyDto logonForm; public static final String logonUser_EXPORT = "session"; private PartyDto logonUser; public static final String errors_EXPORT = "errors"; private Map errors = new HashMap(); public AuthActionImpl(PartyLogic partyLogic) { this.partyLogic = partyLogic; } public String logon() { if (!partyLogic.existParty(logonForm)) { errors.put("errors.logon", new Object[] {}); return "logonError"; } logonUser = partyLogic.getParty(logonForm.getAccount()); return "logon"; } public String logoff() { logonUser = null; return "logoff"; } // ----------------------------------------------------------------------- public PartyDto getLogonForm() { return logonForm; } public void setLogonForm(PartyDto logonForm) { this.logonForm = logonForm; } public PartyDto getLogonUser() { return logonUser; } public Map getErrors() { return errors; } }
統合によって、返却する文字列を修正。
party.diconへの追加。これは問題なし。
最後にstruts-config.xmlの修正。
ただ、validateの問題は解決してないから、Action-mappingのvalidateをfalseにしておこう。
<action path="/auth" type="study.xxx.web.AuthAction" name="logonForm" scope="request" unknown="false" validate="false" > <forward name="logon" path="/do/site/top" redirect="true" /> <forward name="logonError" path="/pages/logon.jsp" redirect="false" /> <forward name="logoff" path="/pages/logon.jsp" redirect="true" /> </action>
あとlogoffのglobal-forwardも修正しないと
<forward name="logoff" path="/do/auth?/auth:logoff"/>
これで、Tomcat起動して確認するのみ。
あれ・・・。うまくいかない。コンソールにログすら出力されてない?
なぜ?
JSPの修正をわすれてた。logon.jspのhtml:submitのpropertyを修正しないと。
うん。LOGONできた。JSPの修正を忘れないようにしないと。
LOGOFFもできた。だけどLOGOFF後のURLが
http://localhost:8080/s2xxx-study/pages/logon.jsp
となっているからAction-mappingを少し修正。
Tomcatを再起動して確認。
きちんと
http://localhost:8080/s2xxx-study/do/logon
ってなってる。
もう過去の遺産となったLogonActionとLogoffActionを削除しよう。お疲れ様。
次はやっと、Actionのメソッド単位でのvalidateの実装。
今日もがんばりすぎた。