appfuse文档(五)--创建Action和JSP

类别:Java 点击:0 评论:0 推荐:

创建ActionJSP

本章将向你展示怎样创建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 LavandowskaBen 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 来创建JSPskeleton(骨架)

这一步,我们将生成skeleton或者用于显示PersonForm信息的JSP. 我说生成的是skeleton是因为它只是<form>自己。它将包含表的数据和对应PersonForm.java中每个属性的Struts标签<html:text>。我们使用的工具只是一个类(FormTagsHandler.java) 和2个XDoclet模板(FormKeys.xdtStrutsForm_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.jspweb/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文件的 titleheadingKey

在生成的JSP中,有2个Key,用于title (浏览器窗口的顶部)和header (页头). 我们现在需要添加这2个key (personDetail.titlepersonDetail.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 {

EditPersonsavePerson 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>就可以了。 [可选] 创建测试ActionCanoo 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 &lt;strong&gt;Abbie Raible&lt;/strong&gt; 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 &lt;strong&gt;Matt Canoo&lt;/strong&gt; 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是必填字段,并且添加一个屏幕列表数据库中所有人的记录.

本文地址:http://com.8s8s.com/it/it11213.htm