创建Action和JSP
本章将向你展示怎样创建Action和JSP。
这部分内容依赖Part II: 创建Manager。
关于本章
本章将向你展示怎样创建一个Struts的Action, 一个JUnit Test (使用StrutsTestCase ),和一个包含form的JSP。我们创建的Action将和在上一章中创建的PersonManager进行交互。
AppFuse使用Struts 作为它默认的Web框架。在1.6中,你还可以使用Spring 或者 WebWork 。在1.7和1.8中将会把Tapestry和JSF集成进来。
我们首先创建一个Struts Action和JSP。
内容
· [1] 为Person添加XDoclet Tags来产生PersonForm
· [2] 使用XDoclet 来创建JSP的skeleton(骨架)
· [3] 创建测试PersonAction 的PersonActionTest
· [4] 创建PersonAction
· [5] 运行PersonActionTest
· [6] 清理JSP,使它合乎要求 [make it presentable]
· [7] 创建测试Action的Canoo WebTest[可以像浏览器一样测试Action]
AppGen
AppGen是1.6.1的一部分,它是基于Lance Lavandowska 和 Ben Gill 的工具产生的. 起初,我不想添加像这个B/S结构的程序产生的代码特征(表和POJO之间、DAO和Manager之间的一对一关系)。我的大部分的工程中,我使用的DAO和Manager要比POJO少得多。
默认情况下, AppGen将只产生Actions/Controllers, Action/Controller Tests, 测试数据, i18n key 和JSP.它也会为你配置好Action/Controller。它使用通用的BaseManager和BaseDAOHibernate 类(被配置为"manager"和"dao") 来减少产生的文件数。我也认识到有时候你可能需要产生所有的DAO 和Manager类及它们的测试类,所以我添加了一个可以达到这个目的的选项。
安装完框架后,如果你想使用AppGen工具,请按照下面的步骤进行:
1. 先安装框架,在model目录中创建你的POJO(ltf:该POJO的hibernate和Struts的标签都要添加,然后”ant setup”)
2. 然后配置applicationContext-hibernate.xml 配置映射文件。
<property name="mappingResources">
<list>
<value>org/appfuse/model/Person.hbm.xml</value>
<value>org/appfuse/model/Role.hbm.xml</value>
<value>org/appfuse/model/User.hbm.xml</value>
<value>org/appfuse/model/UserCookie.hbm.xml</value>
</list>
</property>
3. 进入到目录extras/appgen中,
然后运行"ant -Dmodel.name=Person -Dmodel.name.lowercase=person"。本例中,类Person应当已经存在你的"model"包里面了。这将为你产生所有你在这个手册里面创建的所有的文件。
4. 运行"ant install-detailed "来安装产生的文件(ltf:实际上就是将产生的文件复制到源代码树的相应位置)。你可以运行
"ant install-detailed -Dmodel.name=Person -Dmodel.name.lowercase=person"。
5. 修改personForm.jsp,将“id”属性变为隐藏域(hidden)。
6. 进入”cd ..\..”, 运行“ant setup”以产生新的struts-config.xml等文件。
7. 修改JSP文件,如果当前JSP中没有必须填的字段,则要将*Form页面最下部的html:javascript代码注释掉。
8. 运行“ant deploy”
注意: 如果你不想产生所有的DAO/Manager/Test, 运行"ant install " 代替"ant install-detailed "。在安装文件之前,要保证所有文件已经被创建到了目录extras/appgen/build/gen中。如果你只是想测试这个工具,你可以进入这个目录,然后运行"ant test" 。
警告: 我建议你在做这些之前备份你的工程,我已经测试了这个工具,它工作的很好,但它会更改你的源文件的目录树结构。
使用"lowercase"参数的原因是使产生的JSP的文件名开头的字母为小写的。
这个工具将自动产生CRUD代码,这样可以使你将精力集中在业务逻辑和美化界面上。
为Person添加XDoclet Tags来产生PersonForm
现在我们要生成供Web层使用的PersonForm对象。我们为Person.java需要添加Xdoclet标签来创建ActionForm。在Person.java 文件的JavaDoc中, 添加下面的@struts.form标签(可以参考 User.java):
* @struts.form include-all="true" extends="BaseForm"
我们继承org.appfuse.webapp.form.BaseForm,因为它有toString()方法,它可以使我们调用log.debug(formName)来打印一个窗体对象的友好的视图.
如果你重命名 "org.appfuse" packages为"com.company"或者其他的方式,那么你的默认包内没有model类,你要在@struts.form标签部分添加完整类名
(包括包名)[fully-qualify]来引用org.appfuse.webapp.form.BaseForm
使用XDoclet 来创建JSP的skeleton(骨架)
这一步,我们将生成skeleton或者用于显示PersonForm信息的JSP. 我说生成的是skeleton是因为它只是<form>自己。它将包含表的数据和对应PersonForm.java中每个属性的Struts标签<html:text>。我们使用的工具只是一个类(FormTagsHandler.java) 和2个XDoclet模板(FormKeys.xdt和StrutsForm_jsp.xdt),所有这些文件位于目录extras/viewgen中。
警告: "viewgen" 在AppFuse1.6.1中已经是不建议使用的工具了,在1.7中将被删除。appgen 将提供同样的功能。
下面是一个生成JSP和窗体元素的Label的属性文件(properties file)的简单步骤:
· 执行ant compile – 根据Person.java产生PersonForm.java(build/web/gen/)
· 使用命令行”cmd”, 进入到目录"extras/viewgen"
· 执行ant -Dform.name=PersonForm将在extras/viewgen/build中产生3个文件:
· PersonForm.properties (form元素的label)
· PersonForm.jsp (用来查看一个Person的JSP的骨架程序)
· PersonFormList.jsp (或是PersonList,查看People列表的JSP的骨架程序)
· 复制PersonForm.properties的内容到 web/WEB-INF/classes/ApplicationResources_en.properties.
例:以下内容要加入到ApplicationResources_en.properties文件:
# -- person form --
personForm.firstName=First Name
personForm.id=Id
personForm.lastName=Last Name
· 复制PersonForm.jsp到web/pages/personForm.jsp.
复制PersonFormList.jsp(PersonList.Jsp) 到web/pages/personList.jsp.
注意:每个新文件的名字第一个字符都是小写。
在"pages"目录下的文件在部署时将会放入"WEB-INF/pages"目录中。这样可以使用容器提供的安全机制去保护WEB-INF中的文件。这只针对客户的请求,不会阻止Struts的 ActionServlet的forward。将JSP放入WEB-INF中,以确保他们只能通过Action被访问。这样安全的问题将被转移到Action中。这样可以更有效率,同时也是它从表现层中脱离出来。
AppFuse的Web程序的安全机制指定所有*.html模式的页面都应该被保护。 (除了/signup.html和
/passwordHint.html)。这样确保客户端必须通过Action才能跳转到在pages目录下的JSP。
如果你使用Eclipse, 你可能要"refresh"工程才能看到PersonForm。它位于目录build/web/gen中。这是在Eclipse中看到、导入PersonForm的唯一途径, 因为它被 XDoclet产生,并且不在你的常规的源代码的目录树中。你可以在这里找到它:build/web/gen/org/appfuse/webapp/form/PersonForm.java.
在 BaseAction 中,你可以注册其他的Converters (如: DateConverter )所以 BeanUtils.copyProperties知道怎样去convert Strings → Objects.如果你有POJO的列表 (如:父子关系),你将需要使用convertLists(java.lang.Object)方法进行手工转换。
NOTE: 如果你想为一个特殊页指定CSS,你可以添加<body id="pageName"/> 到文件的顶部。这将被SiteMesh捕捉到并会被放如最终的页面中。这样你就可以像下面这样一页一页的定义你的CSS:
body#pageName element.class { background-color: blue }
· 在ApplicationResources_en.properties中添加JSP文件的 title和heading的Key
在生成的JSP中,有2个Key,用于title (浏览器窗口的顶部)和header (页头).
我们现在需要添加这2个key (personDetail.title和personDetail.heading) 到ApplicationResources_en.properties文件中.
打开web/WEB-INF/classes/ApplicationResources_en.properties,
然后将下面的内容添加到文件底部:
# -- person detail page --
personDetail.title=Person Detail
personDetail.heading=Person Information
在上文中我们刚刚添加"personForm.*" key到这个文件,为什么我既使用personForm又使用personDetail? 最好的解释是它很好的分离了页面上的form label和text。另一个原因是所有的*Form.*给我们一个很好的数据库所有字段的表示方法。
最近,我有一个客户,他希望数据库中的所有字段都应该是可以查询的。这很好实现。我只在ApplicationResources.properties文件中搜寻包含"Form."的Key,然后把它们放到下拉列表中。用户能够输入查询值,然后选择一个他希望搜索的列。我很高兴我将Form和Detail区分开了!
创建测试PersonAction 的PersonActionTest
为测试PersonAction, 我们创建一个StrutsTestCase,我们在test/web/**/action 目录中创建PersonActionTest.java.
实际上, 我常常复制→另存为一个已经有的ActionTest (i.e. UserActionTest). 用[P]erson覆盖 [Uu]se。
如果你的确复制了UserActionTest,确保已经改变UserFormEx为PersonForm。 UserFormEx 是一个 UserForm的扩展,它有用于 Roles的setter,返回String[]。 因为 UserForm已经产生,在User.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中配置的form名字一样.
/**
* 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 将进行如下转换:POJO → ActionForm,将ActionForm → POJO。
现在,你要添加edit forward和savePerson action-mapping, PersonActionTest中将引用它们。 为了达到这个目的, 我们要添加Xdoclet标签到 PersonAction.java的顶部. 如果你在上面的类声明中所有的都正确,你应该已经有用于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 {
EditPerson和savePerson action-mapping的主要区别是savePerson的校验已经打开(看 validation="true") 。注意:"input"必须是一个forward, 而不能是一个path (如: /editPerson.html)。如果你想让edit和save都使用save path (ltf:path?),也可以的。只要确保两者的 validate="false",再在你的"save"方法中调用form.validate() ,然后适当的处理错误。
这里有几个用来显示操作成功消息的key,你要添加到目录web/WEB-INF/classes下的ApplicationResources_en.properties文件里。打开这个文件,添加如下内容:
我通常添加到” # -- success messages –-“注释下面.
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 messages,无论你怎么工作,它都可以避免因为每改变一个实体就要将消息改变。
你可能注意到你用来调用PersonManager的代码和用在PersonManagerTest中的是一样。PersonAction和PersonManagerTest都是PersonManagerImpl的“客户端”, 这样感觉很完美。
所有一切几乎都完成了,让我们运行测试吧!
运行PersonActionTest
如果你在看PersonActionTest, 所有的测试都依赖一条记录id=1的数据库记录(testRemove依赖id=2的记录), 所以要添加示例数据文件(metadata/sql/sample-data.xml). 我已经把它们添加到下面了 – 表的顺序并不重要,因为它目前没有和任何其他的表关联。
(ltf:如果这个表和其他的表用外键/触发器关联,则必须注意表创建的顺序和数据添加的顺序)。
<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,使JSP符合规格(presentable)
现在我们开始清理personForm.jsp,我们要使"id"变成一个”hidden”. 从web/pages/personForm.jsp中删除下面的代码块:
<tr>
<th>
<appfuse:label key="personForm.id"/>
</th>
<td>
<html:text property="id" styleId="id"/>
<html:errors property="id"/>
</td>
</tr>
在 <table>标记之前添加下面的内容:
<html:hidden property="id"/>
你应该改变<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 target.
否则,使用deploy 编译并部署
最后,为了使这个页面更加友好,你可能想在form顶部向你的用户显示提示消息,这个很容易实现。
在personForm.jsp顶部使用<fmt:message>就可以了。
[可选] 创建测试Action的Canoo WebTest
这个指南最后一步使创建一个Canoo WebTest 用来测试JSP.
我之所以说这一步是可选的,是因为你可以使用浏览器达到同样目的。
你可以使用下面的URL来测试已经添加的不同的action, 编辑和保存一个用户。
· 添加 - http://localhost:8080/appfuse/editPerson.html。
· 修改 - http://localhost:8080/appfuse/editPerson.html?id=1 (确信你已经首先运行了ant db-load)。
· 删除 - http://localhost:8080/appfuse/editPerson.html?method=Delete&id=1 (or edit and click on the Delete button)。
· 保存 – 单击 edit ,然后单击Save按钮。
Canoo测试相当平滑,配置它们在XML文件中十分简单。我们现在要为add, edit, save和delete添加测试用例,打开test/web/web-tests.xml,然后添加下面的XML。你将会注意到这个片断有一个叫PersonTests的任务,它运行所有相关的测试。
I use CamelCase target names (vs. the traditional lowercase, dash-separated) because when you're typing -Dtestcase=Name, I've found that I'm used to doing CamelCase for my 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">
<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="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="firstName" value="Abbie"/>
<setinputfield stepid="set lastName" name="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,或者,如果你想start/stop Tomcat,执行ant test-jsp -Dtestcase=PersonTests。当所有Canoo tests被运行(包括PersonTests),添加它作为"run-all-tests"任务的一个必需的依赖。
你应该注意到如果使用Canoo,客户端没有日志,如果你想知道正在干什么,你可以添加下面的文字到每个任务的末端的</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
下一步: Part IV: 添加校验和菜单 – 为 personForm添加校验逻辑,使firstName和lastName是必填字段,并且添加一个屏幕列表数据库中所有人的记录.