Appfuse开发实践(四)—— 创建Webwork 框架的 Actions和JSP
内容提要
这个部分将会展现给你如何创建Webwork框架的Action和JSP,同样会编写JUnit测试来测试PersonAction对象。 这个Action对象回合PersonManager交互。
接下来我们开始在AppFuse的构架下创建一个新的Acetion和JSP。如果还没有安装WebWork模块,先运行ant install-webwork任务。
内容列表
[1] 使用XDoclet创建JSP
[2] 创建PersonActionTest以便测试PersonAction
[3] 创建PersonAction对象
[4] 运行PersonActionTest
[5] 清理JSP文件并进行发布
[6] 创建Canoo WebTests模拟测试浏览器行为
一、使用XDoclet创建JSP
在这一步,我们要创建JSP文件显示Person对象的信息。这个JSP将使用Webwork的JSP Tag来把Person.java中的每个属性填充到表格的行中。我们使用的这个工具是由Erik Hatcher开发的。他由一个类(FormTagsHandler.java) 和一对XDoclet模版(FormKeys.xdt and Form_jsp.xdt组成)。这些文件都在extras/viewgen目录下。
产生一个表单元素和对应的标签属性文件只需经过下面几个基本步骤:
命令行下面切换到"extras/viewgen"目录下
执行ant -Dform.name=Person任务 在extras/viewgen/build 目录下产生下面三个文件:
Person.properties (表单元素的标签)
PersonForm.jsp (显示一个Person信息的JSP文件)
PersonList.jsp (显示People列表的JSP文件)
把Person.properties文件中的内容拷贝到web/WEB-INF/classes/ApplicationResources_en.properties文件中。下面是一个具体的Person.properties文件例子:
person.firstName=First Name
person.id=Id
person.lastName=Last Name
由于我们使用的是中文,需要把对应的信息填入web/WEB-INF/classes/ApplicationResources_cn.properties文件中,还要转化为Unicode编码。
把PersonForm.jsp文件拷贝到web/pages/personForm.jsp目录下。把PersonList.jsp拷贝到web/pages/personList.jsp目录下。注意每个文件名的名字第一个字符是小写字符。
"pages"目录下的文件在发布时将发布到"WEB-INF/pages"目录下。容器提供了WEB_INF目录下的所有文件的安全控制。这意味着来自客户端的请求,而不是由发过来的请求将不被相应。把所有的JSP文件放到WEB-INF目录下可以保证用户只能通过Actions来访问这些页面。这样就把系统安全性要求全部转移到Actions上,那里可以得到更高效的处理并且在基本的表示层外面。
AppFuse架构的web应用程序安全性保证了所有*.html文件得到了保护(除了/signup.html和/passwordHint.html文件)。这就保证了客户端必须通过Action才能访问JSP文件。
说明: 如果自定义了CSS特殊页面,那么需要在文件的最上面加上<body id="pageName"/>。这样就可以被 SiteMesh 识别并且放到最终的页面中去。你也可以使用下面的语句一个一个页面的用CSS来定义页面的格式:
在ApplicationResources_en.properties文件中加入对应JSP文件的titles和headings键值。
在自动产生的JSP文件中,有两个键表示title (浏览器的顶端)和header (页面的头上)。我们需要在ApplicationResources_en.properties文件中说明这两个键的值(personDetail.title and personDetail.heading),具体内容如下所示:
personDetail.title=Person Detail
personDetail.heading=Person Information
在这上面我们要在文件中加入"personForm.*"键,为什么我们要使用personForm和personDetail这样的表示形式?最主要的原因是在页面的标签和文本信息有个明显的区别。另外一个原因是因为*Form.*这种形式可以很好的表示数据库中的字段信息。
最近我有个客户希望所有数据库中的字段都可以查询。这个相当容易实现。我在ApplicationResources.properties 检查所有的包含"Form."的键并且把它放到下拉列表框中。在用户界面上,用户可以输入查询项并且选择想要查询的列。
二、创建PersonActionTest类测试PersonAction类
为PersonAction创建一个JUnit测试类, 首先在test/web/**/action目录下创建PersonActionTest.java文件。
import org.springframework.mock.web.MockHttpServletRequest;
import com.opensymphony.webwork.ServletActionContext;
public class PersonActionTest extends BaseActionTestCase {
private PersonAction action;
protected void setUp() throws Exception {
super.setUp();
action = (PersonAction) ctx.getBean("personAction");
}
protected void tearDown() throws Exception {
super.tearDown();
action = null;
}
public void testEdit() throws Exception {
log.debug("testing edit");
action.setId("1");
assertNull(action.getPerson());
assertEquals(action.edit(), "success");
assertNotNull(action.getPerson());
assertFalse(action.hasActionErrors());
}
public void testSave() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
ServletActionContext.setRequest(request);
action.setId("1");
assertEquals(action.edit(), "success");
assertNotNull(action.getPerson());
// update last name and save
action.getPerson().setLastName("Updated Last Name");
assertEquals(action.save(), "input");
assertEquals(action.getPerson().getLastName(), "Updated Last Name");
assertFalse(action.hasActionErrors());
assertFalse(action.hasFieldErrors());
assertNotNull(request.getSession().getAttribute("messages"));
}
public void testRemove() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
ServletActionContext.setRequest(request);
action.setDelete("");
Person person = new Person();
person.setId(new Long(2));
action.setPerson(person);
assertEquals(action.delete(), "success");
assertNotNull(request.getSession().getAttribute("messages"));
}
}
由于没有创建PersonAction类这个对象无法通过编译。
4、创建PersonAction类
在src/web/**/action目录下,创建PersonAction.java文件:
import java.util.ArrayList;
import java.util.List;
import org.myApp.model.Person;
import org.myApp.service.PersonManager;
public class PersonAction extends BaseAction {
private Person person;
private String id;
private PersonManager personManager;
public void setId(String id) {
this.id = id;
}
public void setPersonManager(PersonManager manager) {
this.personManager = manager;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public String delete() {
personManager.removePerson(String.valueOf(person.getId()));
List args = new ArrayList();
args.add(person.getFirstName() + ' ' + person.getLastName());
saveMessage(getText("person.deleted", args));
return SUCCESS;
}
public String edit() {
if (id != null) {
person = personManager.getPerson(id);
} else {
person = new Person();
}
return SUCCESS;
}
public String save() throws Exception {
if (cancel != null) {
return "cancel";
}
if (delete != null) {
return delete();
}
boolean isNew = (person.getId() == null);
List args = new ArrayList();
args.add(person.getFirstName() + ' ' + person.getLastName());
personManager.savePerson(person);
String key = (isNew) ? "person.added" : "person.updated";
saveMessage(getText(key, args));
if (!isNew) {
return INPUT;
} else {
return SUCCESS;
}
}
}
有一些Key需要加到ApplicationResources_en.properties文件中用来显示给用户看的操作成功的提示信息打开web/WEB-INF/classes目录下的ApplicationResources_en.properties并且加入如下内容:
person.added=Information for <strong>{0}</strong> has been added successfully.
person.deleted=Information for <strong>{0}</strong> has been deleted successfully.
person.updated=Information for <strong>{0}</strong> has been updated successfully.
当然你可以加入一般的added, deleted和updated提示信息,这取决于你的需要。为每个实体使用独立的信息可以在特别情况下改变它。
你可能注意到了我们以和PersonManager类类似的方式调用了TestPersonManager对象。PersonAction和PersonManagerTest都是PersonManagerImpl的客户,这是个很好的结构。
现在你需要告诉Spring和WebWork这个action的存在。首先在web/WEB-INF/action-servlet.xml文件中加入如下的PersonAction定义信息:
<property name="personManager"><ref bean="personManager"/></property>
</bean>
然后在web/WEB-INF/classes/xwork.xml中加入引用信息说明:
<result name="success">/WEB-INF/pages/personForm.jsp</result>
</action>
<action name="savePerson" class="personAction" method="save">
<!--interceptor-ref name="validationStack"/-->
<result name="cancel" type="redirect">mainMenu.html</result>
<result name="input">/WEB-INF/pages/personForm.jsp</result>
<result name="success" type="redirect">mainMenu.html</result>
</action>
在上面的书名中"validationStack" interceptor-ref注释掉了因为你没有为Person对象定义任何的validation规则. 我们会在下一章中加入校验规则并且去掉注释。
四、运行PersonActionTest
如果你看了我们的PersonActionTest, 所有的测试依赖一条id为1的纪录(testRemove依赖一套id为2的纪录),所以让我们在示例数据文件metadata/sql/sample-data.xml中加入这些纪录(原来已经添加了一条,现添加第二条)。如下所示(顺序并不重要)
<column>id</column>
<column>first_name</column>
<column>last_name</column>
<row>
<value>1</value>
<value>Matt</value>
<value>Raible</value>
</row>
<row>
<value>2</value>
<value>James</value>
<value>Davidson</value>
</row>
</table>
当我们运行任何测试时,DBUnit都会加载这个文件,因此当你运行Action test时这些纪录肯定是有效的。
确定你在project目录下并且所有文件都正确保存,启动MySql,运行ant test-web -Dtestcase=PersonAction任务
BUILD SUCCESSFUL
Total time: 21 seconds
5、清理JSP以便正常显示Person信息
现在清理自动产生的personForm.jsp文件隐藏"id"属性。把下面的代码从 web/pages/personForm.jsp文件中移除掉:
value="person.id" required="true"/>
在<table>标签后加入鲜面的代码:
如果希望提高界面的易用性,你可以把焦点设置在第一个字段的文本框上。这需要加入下面的JavaScript代码:
document.forms["person"].elements["firstName"].focus();
</script>
现在执行ant db-load deploy任务, 启动Tomcat并且在浏览器中(我用firefox)输入http://localhost:8080/myApp/editPerson.html?id=1
抛出异常:
with nested exception
com.opensymphony.xwork.config.ConfigurationException: Caught exception while loading file xwork.xml
with nested exception
org.xml.sax.SAXParseException: Element type "result" must be followed by either attribute specifications, ">" or "/>".
at com.opensymphony.xwork.config.providers.XmlConfigurationProvider.init(XmlConfigurationProvider.java:126)
at com.opensymphony.xwork.config.impl.DefaultConfiguration.reload(DefaultConfiguration.java:85)
at com.opensymphony.xwork.config.ConfigurationManager.getConfiguration(ConfigurationManager.java:56)
at com.opensymphony.xwork.DefaultActionProxyFactory.setupConfigIfActionIsCommand(DefaultActionProxyFactory.java:58)
at com.opensymphony.xwork.DefaultActionProxyFactory.createActionProxy(DefaultActionProxyFactory.java:43)
at com.opensymphony.webwork.dispatcher.ServletDispatcher.serviceAction(ServletDispatcher.java:292)
at com.opensymphony.webwork.dispatcher.ServletDispatcher.service(ServletDispatcher.java:264)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:118)
at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.displaytag.filter.ResponseOverrideFilter.doFilter(ResponseOverrideFilter.java:86)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:73)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:73)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at com.opensymphony.clickstream.ClickstreamFilter.doFilter(ClickstreamFilter.java:42)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.appfuse.webapp.filter.ActionFilter.doFilter(ActionFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.springframework.orm.hibernate.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:170)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:73)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.appfuse.webapp.filter.GZIPFilter.doFilter(GZIPFilter.java:51)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:152)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:102)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:137)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:118)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:102)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:929)
at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:160)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:799)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.processConnection(Http11Protocol.java:705)
at org.apache.tomcat.util.net.TcpWorkerThread.runIt(PoolTcpEndpoint.java:577)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683)
at java.lang.Thread.run(Thread.java:534)
with nested exception com.opensymphony.xwork.config.ConfigurationException: Caught exception while loading file xwork.xml
with nested exception
org.xml.sax.SAXParseException: Element type "result" must be followed by either attribute specifications, ">" or "/>".
com.opensymphony.xwork.config.ConfigurationException: Caught exception while loading file xwork.xml
with nested exception
org.xml.sax.SAXParseException: Element type "result" must be followed by either attribute specifications, ">" or "/>".
at com.opensymphony.xwork.config.providers.XmlConfigurationProvider.loadConfigurationFile(XmlConfigurationProvider.java:525)
at com.opensymphony.xwork.config.providers.XmlConfigurationProvider.init(XmlConfigurationProvider.java:123)
at com.opensymphony.xwork.config.impl.DefaultConfiguration.reload(DefaultConfiguration.java:85)
at com.opensymphony.xwork.config.ConfigurationManager.getConfiguration(ConfigurationManager.java:56)
at com.opensymphony.xwork.DefaultActionProxyFactory.setupConfigIfActionIsCommand(DefaultActionProxyFactory.java:58)
at com.opensymphony.xwork.DefaultActionProxyFactory.createActionProxy(DefaultActionProxyFactory.java:43)
at com.opensymphony.webwork.dispatcher.ServletDispatcher.serviceAction(ServletDispatcher.java:292)
at com.opensymphony.webwork.dispatcher.ServletDispatcher.service(ServletDispatcher.java:264)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:118)
at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.displaytag.filter.ResponseOverrideFilter.doFilter(ResponseOverrideFilter.java:86)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:73)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:73)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at com.opensymphony.clickstream.ClickstreamFilter.doFilter(ClickstreamFilter.java:42)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.appfuse.webapp.filter.ActionFilter.doFilter(ActionFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.springframework.orm.hibernate.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:170)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:73)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.appfuse.webapp.filter.GZIPFilter.doFilter(GZIPFilter.java:51)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:152)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:102)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:137)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:118)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:102)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:929)
at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:160)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:799)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.processConnection(Http11Protocol.java:705)
at org.apache.tomcat.util.net.TcpWorkerThread.runIt(PoolTcpEndpoint.java:577)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683)
at java.lang.Thread.run(Thread.java:534)
with nested exception org.xml.sax.SAXParseException: Element type "result" must be followed by either attribute specifications, ">" or "/>".
org.xml.sax.SAXParseException: Element type "result" must be followed by either attribute specifications, ">" or "/>".
at org.apache.xerces.util.ErrorHandlerWrapper.createSAXParseException(Unknown Source)
at org.apache.xerces.util.ErrorHandlerWrapper.fatalError(Unknown Source)
at org.apache.xerces.impl.XMLErrorReporter.reportError(Unknown Source)
at org.apache.xerces.impl.XMLErrorReporter.reportError(Unknown Source)
at org.apache.xerces.impl.XMLScanner.reportFatalError(Unknown Source)
at org.apache.xerces.impl.XMLNSDocumentScannerImpl.scanStartElement(Unknown Source)
at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl$FragmentContentDispatcher.dispatch(Unknown Source)
at org.apache.xerces.impl.XMLDocumentFragmentScannerImpl.scanDocument(Unknown Source)
at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source)
at org.apache.xerces.parsers.XML11Configuration.parse(Unknown Source)
at org.apache.xerces.parsers.XMLParser.parse(Unknown Source)
at org.apache.xerces.parsers.DOMParser.parse(Unknown Source)
at org.apache.xerces.jaxp.DocumentBuilderImpl.parse(Unknown Source)
at javax.xml.parsers.DocumentBuilder.parse(Unknown Source)
at com.opensymphony.xwork.config.providers.XmlConfigurationProvider.loadConfigurationFile(XmlConfigurationProvider.java:521)
at com.opensymphony.xwork.config.providers.XmlConfigurationProvider.init(XmlConfigurationProvider.java:123)
at com.opensymphony.xwork.config.impl.DefaultConfiguration.reload(DefaultConfiguration.java:85)
at com.opensymphony.xwork.config.ConfigurationManager.getConfiguration(ConfigurationManager.java:56)
at com.opensymphony.xwork.DefaultActionProxyFactory.setupConfigIfActionIsCommand(DefaultActionProxyFactory.java:58)
at com.opensymphony.xwork.DefaultActionProxyFactory.createActionProxy(DefaultActionProxyFactory.java:43)
at com.opensymphony.webwork.dispatcher.ServletDispatcher.serviceAction(ServletDispatcher.java:292)
at com.opensymphony.webwork.dispatcher.ServletDispatcher.service(ServletDispatcher.java:264)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:237)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at com.opensymphony.module.sitemesh.filter.PageFilter.parsePage(PageFilter.java:118)
at com.opensymphony.module.sitemesh.filter.PageFilter.doFilter(PageFilter.java:52)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.displaytag.filter.ResponseOverrideFilter.doFilter(ResponseOverrideFilter.java:86)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:73)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:73)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at com.opensymphony.clickstream.ClickstreamFilter.doFilter(ClickstreamFilter.java:42)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.appfuse.webapp.filter.ActionFilter.doFilter(ActionFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.springframework.orm.hibernate.support.OpenSessionInViewFilter.doFilterInternal(OpenSessionInViewFilter.java:170)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:73)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.appfuse.webapp.filter.GZIPFilter.doFilter(GZIPFilter.java:51)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:186)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:157)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:214)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardContextValve.invokeInternal(StandardContextValve.java:198)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:152)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:102)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:137)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:118)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:102)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.core.StandardValveContext.invokeNext(StandardValveContext.java:104)
at org.apache.catalina.core.StandardPipeline.invoke(StandardPipeline.java:520)
at org.apache.catalina.core.ContainerBase.invoke(ContainerBase.java:929)
at org.apache.coyote.tomcat5.CoyoteAdapter.service(CoyoteAdapter.java:160)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:799)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.processConnection(Http11Protocol.java:705)
at org.apache.tomcat.util.net.TcpWorkerThread.runIt(PoolTcpEndpoint.java:577)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:683)
at java.lang.Thread.run(Thread.java:534)
打开Dreamwaver检查xwork.xml,发现最后拷贝到xwork.xml文件中的几行字符颜色显示不正确,经过一番尝试发现去掉空格后重新输入颜色就正常了,干脆全部重新手工录入全部代码!
<result name="success">/WEB-INF/pages/personForm.jsp</result>
</action>
<action name="savePerson" class="personAction" method="save">
<!--interceptor-ref name="validationStack"/-->
<result name="cancel" type="redirect">mainMenu.html</result>
<result name="input">/WEB-INF/pages/personForm.jsp</result>
<result name="success" type="redirect">mainMenu.html</result>
</action>
删除Tomcat下webapp目录下发布好的所有文件和conf目录下的myApp.xml配置文件
现在执行ant setup任务, 启动Tomcat并且在浏览器中(我用firefox)输入http://localhost:8080/myApp/editPerson.html?id=1
登陆后,你将看到下面的界面:
最后,为了增加页面的用户友好性,你可以在表单的最上面加入标题信息,这个很容易实现只需要在personForm.jsp页面的最上方加入<fmt:message>就可以了。
六、[可选部分] 创建Canoo WebTest模拟浏览器行为测试
最后一步可以创建一个Canoo WebTest 测试Jsp页面。
之所以说这一步是可选的,因为可以通过浏览器进行测试,可以通过下面的URLs测试用户adding,editing和saving的操作。
Add - http://localhost:8080/myApp/editPerson.html
Edit - http://localhost:8080/myApp/editPerson.html?id=1 (确定在执行测试以前运行ant db-load)。
Delete - 使用上面的Edit页面点击Delete按钮。
Save - 访问edit页面然后点击Save按钮。
Canoo测试非常灵活,只需要简单在XML文件增加配置项就可以完成。为了测试add、edit、save和delete操作,打开test/web/web-tests.xml文件并且加入下面的XML代码。你可以注意到这个代码片断有一个PersonTests目标任务运行所有的相关测试。
<!-- runs person-related tests -->
<target name="PersonTests"
depends="EditPerson,SavePerson,AddPerson,DeletePerson"
description="Call and executes all person test cases (targets)">
<echo>Successfully ran all Person JSP tests!</echo>
</target>
<!-- Verify the edit person screen displays without errors -->
<target name="EditPerson"
description="Tests editing an existing Person's information">
<canoo name="editPerson">
&config;
<steps>
&login;
<invoke stepid="click Edit Person link" url="/editPerson.html?id=1"/>
<verifytitle stepid="we should see the personDetail title"
text="${webapp.prefix}${personDetail.title}"/>
</steps>
</canoo>
</target>
<!-- Edit a person and then save -->
<target name="SavePerson"
description="Tests editing and saving a user">
<canoo name="savePerson">
&config;
<steps>
&login;
<invoke stepid="click Edit Person link" url="/editPerson.html?id=1"/>
<verifytitle stepid="we should see the personDetail title"
text="${webapp.prefix}${personDetail.title}"/>
<setinputfield stepid="set lastName" name="person.lastName" value="Canoo"/>
<clickbutton label="Save" stepid="Click Save"/>
<verifytitle stepid="Page re-appears if save successful"
text="${webapp.prefix}${personDetail.title}"/>
</steps>
</canoo>
</target>
<!-- Add a new Person -->
<target name="AddPerson"
description="Adds a new Person">
<canoo name="addPerson">
&config;
<steps>
&login;
<invoke stepid="click Add Button" url="/editPerson.html"/>
<verifytitle stepid="we should see the personDetail title"
text="${webapp.prefix}${personDetail.title}"/>
<setinputfield stepid="set firstName" name="person.firstName" value="Abbie"/>
<setinputfield stepid="set lastName" name="person.lastName" value="Raible"/>
<clickbutton label="${button.save}" stepid="Click button 'Save'"/>
<verifytitle stepid="Main Menu appears if save successful"
text="${webapp.prefix}${mainMenu.title}"/>
<verifytext stepid="verify success message"
text="Information for <strong>Abbie Raible</strong> has been added successfully."/>
</steps>
</canoo>
</target>
<!-- Delete existing person -->
<target name="DeletePerson"
description="Deletes existing Person">
<canoo name="deletePerson">
&config;
<steps>
&login;
<invoke stepid="click Edit Person link" url="/editPerson.html?id=1"/>
<clickbutton label="${button.delete}" stepid="Click button 'Delete'"/>
<verifytitle stepid="display Main Menu" text="${webapp.prefix}${mainMenu.title}"/>
<verifytext stepid="verify success message"
text="Information for <strong>Matt Canoo</strong> has been deleted successfully."/>
</steps>
</canoo>
</target>
完成上面的工作后,可以在Tomcat启动的情况下运行ant test-canoo -Dtestcase=PersonTests或者在Tomcat没有启动的情况下运行ant test-jsp -Dtestcase=PersonTests。如果希望运行所有的Canoo测试(包括PersonTests)。
未通过,错误信息如下:
line:
Test step verifytitle named "we should see the login title" failed with message
"Wrong document title found! Expected "AppFuse ~ Login" but got "AppFuse ~ 登录"
"
因为我们使用环境的默认语言是中文,修改test/web/web-tests.xml文件12行:
修改为:
运行ant test-canoo -Dtestcase=PersonTests ,还是没有通过:
line:
Test step clickbutton named "Click the submit button" failed with message "Butto
n with name <<not specified>> and value <Login> not found!"
查找问题发现关于Login的Target定义在test/login.xml中,修改下面的一行:
改成:
OK,成功了!
此时Cacoo在客户端没有纪录日至。如果你想加入日志你可以在</canoo>和</target>之间在目标任务的底部加入下面的内容。
srcFile="${test.dir}/data/web-tests-result.xml"/>
<echo>${web-tests.result}</echo>
BUILD SUCCESSFUL
Total time: 10 seconds