CreateActions_zh |
|
Your trail: |
This is version 4.
It is not the current version, and thus it cannot be edited.
[Back to current version]
[Restore this version]
Part III: 创建Actions和JSPs - 在AppFuse架构下创建Struts 的Actions和JSPs。
- 本教程依赖于Part II: 创建管理器Manager。
关于本教程
本教程将向你展示如何创建一个Struts Action,一个JUnit Test(使用 StrutsTestCase),和一个form的JSP,我们创建的这个Action将与教程创建管理器Manager创建的PersonManager交互。
通常情况下,AppFuse使用Struts作为它的web框架,作为1.6+,你可以使用Spring或者WebWork作为web框架,在1.7, 增加了对JSF和Tapestry的支持。
如果希望安装struts以外的web框架,只需转到extras目录下你所期望安装的框架目录下,在相应目录下的README.txt文件会有进一步的说明,针对其他几个框架的教程列在下面。
让我们从创建一个新的Struts Action和JSP作为开始。
- 我在真实世界中实际操作的方式用蓝色斜体表示。
目录
- 为创建generate PersonForm对Person添加XDoclet Tags
- 使用XDoclet创建骨架JSPs
- 创建测试PersonAction的PersonActionTest
- 创建PersonAction
- 运行PersonActionTest
- 清理JSP来使它更好看
- 创建Canoo WebTests来测试模拟浏览器的actions
我们首先在web层为Struts创建PersonForm对象,为此,我们需要对Person.java添加标签来产生我们的Struts ActionForm,在Person.java的JavaDoc添加@struts.form标签(如果你需要一个实例,可以参考User.java):
* @struts.form include-all="true" extends="BaseForm"
|
- 我们扩展org.appfuse.webapp.form.BaseForm,因为它的方法toString()中会调用log.debug(formName)来打印易读的Form信息。
- 如果你没有把"org.appfuse"包换成你的"com.company"或者没有把你的模型类放到默认的包,你必须确认在@struts.form中的标签对org.appfuse.webapp.form.BaseForm使用完全的引用。
使用XDoclet创建骨架JSPs
在这一步,你将会创建来显示Person对象信息的JSP页面,它会包括Strut的JSP标签用来表现Person.java每一个属性,AppGen工具建立在一个StrutsGen工具上,用来做这件事,这个StrutsGen工具起初由Erik Hatcher编写,它只是一些XDoclet模版和一些附加类,所有的这些文件在extras/appgen目录里。
以下是生成JSP和包含标签及表单元素属性文件的简单步骤
- 在命令行环境下,转到"extras/appgen"目录
- 执行ant -Dobject.name=Person -Dappgen.type=pojo会在extras/appgen/build/gen产生一些文件,事实上,它会产生本教程所需的所有文件,但是我们只获取我们所需要的那些。
- web/WEB-INF/classes/Person.properties (form元素的标签)
- web/personForm.jsp (察看单个文件的JSP文件)
- web/personList.jsp (察看People列表的JSP文件)
- 把Person.properties的内容拷贝到web/WEB-INF/classes/ApplicationResources_en.properties,这是所有form中需要的标题关键字,以下是你将要添加到ApplicationResources_en.properties的内容的例子:
# -- person form --
personForm.id=Id
personForm.firstName=First Name
personForm.lastName=Last Name
person.added=Information for <strong>{0}</strong> has been added successfully.
person.updated=Information for <strong>{0}</strong> has been updated successfully.
person.deleted=Information for <strong>{0}</strong> has been deleted successfully.
# -- person list page --
personList.title=Person List
personList.heading=Persons
# -- person detail page --
personDetail.title=Person Detail
personDetail.heading=Person Information
- 拷贝personForm.jsp到web/pages/personForm.jsp,拷贝personList.jsp到web/pages/personList.jsp,注意,所有新建文件的第一个字母是小写的。
- "pages"目录中的文件在部署环境下会放到"WEB-INF/pages"目录下,容器会对所有WEB-INF目录下的文件提供安全保护,这种保护针对客户端的请求,而通过Struts ActionServlet的转发可以访问,把这些JSPs文件放到WEB-INF目录下确保它们只会被Actions访问,而不会被客户或者彼此直接访问,这允许安全保护转移到Action,这样可以保证保护更有效,脱离表示层这个基础。
AppFuse的web应用安全机制确认所有的*.html形式的访问是被保护的(除了/signup.html和/passwordHint.html),这保证了客户必须通过Action来访问JSP(或者至少pages中的一些)。
注意:如果你想为某一页定制CSS,你必须在文件的开头添加<body id="pageName"/>,这会被SiteMesh看到并且应用到最终的页面,然后你可以一页一页的定制你的CSS,就像如下的方式:
body#pageName element.class { background-color: blue }
- 在ApplicationResources_en.properties中添加JSP文件出现的标题和题头,在生成的JSPs里有两处标题(浏览器顶端显示)的关键字,这些字段提供了personDetail.title和personDetail.heading关键字名。
如上,我们在文件里添加"personForm.*"关键字,我们为什么使用personDetail而不是personForm作为标题呢?最好的一个原因是我们需要区分form的标签和页面上的文本,另一个原因是*Form.*的形式给你数据库里所有的字段更好的展现形式。
最近我有一个客户期望数据库里的所有字段是可查询的,这样作就会比较容易,我只需要查看ApplicationResources.properties中的保存的所有关键字来寻找"Form.",然后记录下来,在用户界面下,用户只需要输入想查找的项目和字段。我对能够在项目里区分对待Form和Detail感到高兴!
创建测试PersonAction的PersonActionTest
为了给PersonAction创建StrutsTestCase Test,首先要在test/web/**/action目录下创建PersonActionTest.java文件:
package org.appfuse.webapp.action;
import org.appfuse.Constants;
import org.appfuse.webapp.form.PersonForm;
public class PersonActionTest extends BaseStrutsTestCase {
public PersonActionTest(String name) {
super(name);
}
public void testEdit() throws Exception {
setRequestPathInfo("/editPerson");
addRequestParameter("method", "Edit");
addRequestParameter("id", "1");
actionPerform();
verifyForward("edit");
assertTrue(request.getAttribute(Constants.PERSON_KEY) != null);
verifyNoActionErrors();
}
public void testSave() throws Exception {
setRequestPathInfo("/editPerson");
addRequestParameter("method", "Edit");
addRequestParameter("id", "1");
actionPerform();
PersonForm personForm =
(PersonForm) request.getAttribute(Constants.PERSON_KEY);
assertTrue(personForm != null);
setRequestPathInfo("/savePerson");
addRequestParameter("method", "Save");
// update the form from the edit and add it back to the request
personForm.setLastName("Feltz");
request.setAttribute(Constants.PERSON_KEY, personForm);
actionPerform();
verifyForward("edit");
verifyNoActionErrors();
}
public void testRemove() throws Exception {
setRequestPathInfo("/editPerson");
addRequestParameter("method", "Delete");
addRequestParameter("id", "2");
actionPerform();
verifyForward("mainMenu");
verifyNoActionErrors();
}
}
|
你需要为src/dao/**/Constants.java添加PERSON_KEY变量,这个"personForm"与struts-config.xml中的名称相对应。
/**
* The request scope attribute that holds the person form.
*/
public static final String PERSON_KEY = "personForm";
|
如果你尝试运行测试,你会得到几个NoSuchMethodErrors,所以,让我们定义PersonAction的edit、save和delete方法。
创建PersonAction
在src/web/**/action,创建PersonAction.java,有下面的内容:
package org.appfuse.webapp.action;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;
import org.appfuse.model.Person;
import org.appfuse.service.PersonManager;
import org.appfuse.webapp.form.PersonForm;
/**
* @struts.action name="personForm" path="/editPerson" scope="request"
* validate="false" parameter="method" input="mainMenu"
*/
public final class PersonAction extends BaseAction {
public ActionForward cancel(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
return mapping.findForward("mainMenu");
}
public ActionForward delete(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("Entering 'delete' method");
}
ActionMessages messages = new ActionMessages();
PersonForm personForm = (PersonForm) form;
// Exceptions are caught by ActionExceptionHandler
PersonManager mgr = (PersonManager) getBean("personManager");
mgr.removePerson(personForm.getId());
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.deleted",
personForm.getFirstName() + ' ' +
personForm.getLastName()));
// save messages in session, so they'll survive the redirect
saveMessages(request.getSession(), messages);
return mapping.findForward("mainMenu");
}
public ActionForward edit(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("Entering 'edit' method");
}
PersonForm personForm = (PersonForm) form;
// if an id is passed in, look up the user - otherwise
// don't do anything - user is doing an add
if (personForm.getId() != null) {
PersonManager mgr = (PersonManager) getBean("personManager");
Person person = mgr.getPerson(personForm.getId());
personForm = (PersonForm) convert(person);
updateFormBean(mapping, request, personForm);
}
return mapping.findForward("edit");
}
public ActionForward save(ActionMapping mapping, ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
if (log.isDebugEnabled()) {
log.debug("Entering 'save' method");
}
// Extract attributes and parameters we will need
ActionMessages messages = new ActionMessages();
PersonForm personForm = (PersonForm) form;
boolean isNew = ("".equals(personForm.getId()));
if (log.isDebugEnabled()) {
log.debug("saving person: " + personForm);
}
PersonManager mgr = (PersonManager) getBean("personManager");
Person person = (Person) convert(personForm);
mgr.savePerson(person);
// add success messages
if (isNew) {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.added",
personForm.getFirstName() + " " +
personForm.getLastName()));
// save messages in session to survive a redirect
saveMessages(request.getSession(), messages);
return mapping.findForward("mainMenu");
} else {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.updated",
personForm.getFirstName() + " " +
personForm.getLastName()));
saveMessages(request, messages);
return mapping.findForward("edit");
}
}
}
|
你会注意到以上的代码有许多对convert的调用来转化PersonForm到Person,这个转化convert方法在BaseAction.java(调用ConvertUtil.convert())中,并且使用BeanUtils.copyProperties 来实现POJOs → ActionForms和ActionForms → POJOs的转化。
- 如果你运行Eclipse,你需要对project运行"refresh"来显示PersonForm,它在build/web/gen目录中,这个目录必须是project的源目录之一,这是Eclipse看到和导入PersonForm的唯一方式,因为它是通过XDoclet产生并且不在通常的源目录中,你可以在build/web/gen/org/appfuse/webapp/form/PersonForm.java找到它。
- 在BaseAction里你可以定义自己的转换器(例如 DateConverter),因此BeanUtils.copyProperties能够知道Strings → Objects的转化。如果你的POJOs存在Lists(例如 父子关系),你需要使用convertLists(java.lang.Object)方法手工完成这个转化。
现在你需要添加edit forward和savePerson action-mapping,这两个早就在PersonActionTest中提到,为此,可以在PersonAction.java添加一些额外的XDoclet标签,在类的声明前面加入这些注释,你一定已经有了editPerson action-mapping的XDoclet标签,我在这里展示你会在类中看到的所有的XDoclet标签。
/**
* @struts.action name="personForm" path="/editPerson" scope="request"
* validate="false" parameter="method" input="mainMenu"
*
* @struts.action name="personForm" path="/savePerson" scope="request"
* validate="true" parameter="method" input="edit"
*
* @struts.action-forward name="edit" path="/WEB-INF/pages/personForm.jsp"
*/
public final class PersonAction extends BaseAction {
|
action-mappings中editPerson和savePerson主要区别是savePerson的XDoclet标签的validation标记是true(见validation="true"),注意"input"属性必须指向到一个forward,而不是个路径(例如/editPerson.html),如果你希望edit同save一样是是用用save的路径也是可以的,只要确保validate="false",然后在你的"save"方法中,你可以执行form.validate()来处理错误。
你一定注意到了这里是用PersonManager的方式与PersonManagerTest中的一样,PersonAction和PersonManagerTest都是PersonManagerImpl的客户,这很有意义。
本教程几乎所有的事情都搞定了,让我们来运行这些测试!
运行PersonActionTest
如果你查看PersonActionTest,你会发现所有的测试依赖于数据库中一条id=1的记录(testRemove依赖于id=2),所以要添加那些样本数据(metadata/sql/sample-data.xml),我会添加到底部,顺序并不重要(目前),因为它们互相并不存在依赖关系。
<table name='person'>
<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会在执行测试前加载这些数据,所以这些记录PersonActionTest可以得到。
如果现在你执行ant test-web -Dtestcase=PersonAction - 所有事情会按计划执行,在作尝试前请确定Tomcat已经运行。
BUILD SUCCESSFUL
Total time: 1 minute 21 seconds
清理JSP来使它更好看
让我们开始清理personForm.jsp,修改<html:form>的action为"savePerson",这保证验证起作用,同时修改focus属性,从focus=""改为focus="firstName",这样在页面打开后光标会落到firstName字段(使用JavaScript)。
现在你如果执行ant db-load deploy,启动Tomcat,然后在你的浏览器输入http://localhost:8080/appfuse/editPerson.html?id=1,你会看到如下信息:
注意:如果你修改了web中的任何文件,请使用deploy-web,否则,使用deploy来编译和部署。
最后,为了使页面更加的用户友好,你也许希望为用户在form上面添加一段信息,只需在personForm.jsp页的顶端添加(使用 <fmt:message>)。
[可选的] 创建Canoo WebTests来测试模拟浏览器的actions
本教程最后的步骤(可选的)是创建Canoo WebTest来测试JSPs。
- 我说是可选的,是因为你可以通过浏览器运行同样的测试。
你可以使用如下的URLs来测试不同actions的adding、editing和saving。
Canoo测试非常的灵活,只需要配置XML文件,为了测试add、edit、save和delete,打开test/web/web-tests.xml添加如下的XML,你一定注意到这些片断的target名称是PersonTests,会运行所有相关的测试。
- 我使用CamelCase的target命名方式(vs. 传统的小写,下划线分割的方式)是因为当拼写-Dtestcase=Name时,我会经常习惯于使用CamelCase方式来命名我的JUnit Tests。
<!-- 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">
<webtest name="editPerson">
&config;
<steps>
&login;
<invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
<verifytitle description="we should see the personDetail title"
text=".*${personDetail.title}.*" regex="true"/>
</steps>
</webtest>
</target>
<!-- Edit a person and then save -->
<target name="SavePerson"
description="Tests editing and saving a user">
<webtest name="savePerson">
&config;
<steps>
&login;
<invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
<verifytitle description="we should see the personDetail title"
text=".*${personDetail.title}.*" regex="true"/>
<setinputfield description="set lastName" name="lastName" value="Canoo"/>
<clickbutton label="Save" description="Click Save"/>
<verifytitle description="Page re-appears if save successful"
text=".*${personDetail.title}.*" regex="true"/>
</steps>
</webtest>
</target>
<!-- Add a new Person -->
<target name="AddPerson"
description="Adds a new Person">
<webtest name="addPerson">
&config;
<steps>
&login;
<invoke description="click Add Button" url="/editPerson.html"/>
<verifytitle description="we should see the personDetail title"
text=".*${personDetail.title}.*" regex="true"/>
<setinputfield description="set firstName" name="firstName" value="Abbie"/>
<setinputfield description="set lastName" name="lastName" value="Raible"/>
<clickbutton label="${button.save}" description="Click button 'Save'"/>
<verifytitle description="Main Menu appears if save successful"
text=".*${mainMenu.title}.*" regex="true"/>
<verifytext description="verify success message"
text="Information for <strong>Abbie Raible</strong> has been added successfully."/>
</steps>
</webtest>
</target>
<!-- Delete existing person -->
<target name="DeletePerson"
description="Deletes existing Person">
<webtest name="deletePerson">
&config;
<steps>
&login;
<invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
<clickbutton label="${button.delete}" description="Click button 'Delete'"/>
<verifytitle description="display Main Menu" text=".*${mainMenu.title}.*" regex="true"/>
<verifytext description="verify success message"
text="Information for <strong>Matt Canoo</strong> has been deleted successfully."/>
</steps>
</webtest>
</target>
|
完成添加后,你一定可以在Tomcat运行时运行ant test-canoo -Dtestcase=PersonTests,或者使用ant test-jsp -Dtestcase=PersonTests让Ant为你启动/关闭Tomcat,为了在所有的Canoo测试中包括PersonTests,把它作为"run-all-tests"的依赖target。
你必须注意到Canoo没有客户端的日志,如果你期望看到一些信息,你可以在每一个target的</canoo>和</target>之间添加如下代码。
<loadfile property="web-tests.result"
srcFile="${test.dir}/data/web-tests-result.xml"/>
<echo>${web-tests.result}</echo>
BUILD SUCCESSFUL
Total time: 11 seconds
Next Up: Part IV: 添加验证和列表页面 - 给personForm的firstName和lastName字段添加必添的验证逻辑和显示person所有记录的页面。
Attachments:
|