appFuse2.x第六篇--Using Struts 2

This tutorial will show you how to create master/detail screens with Struts 2. The list (master) screen will have the ability to sort columns, as well as page 25 records at a time. The form (detail) screen will use an elegant CSS form layout (courtesy of Wufoo). You will also configure client and server-side validation to improve your users' experience.

This tutorial assumes you've created a project with the appfuse-basic-struts archetype and have already completed the Persistence and Services tutorials. If you're using the appfuse-modular-struts archetype, please morph your mind into using the web module as the root directory. If you created your project with a different web framework than Struts, you're likely to be confused and nothing will work in this tutorial.

Table of Contents

  1. Introduction to Struts 2
  2. Create a PersonActionTest
  3. Create a PersonAction that will fetch people
  4. Create personList.jsp to show search results
  5. Modify PersonActionTest and PersonAction for edit(), save() and delete() methods
  6. Create personForm.jsp to edit a person
  7. Configure Validation
  8. Create a Canoo WebTest to test browser-like actions
  9. Add link to menu
Source Code

The code for this tutorial is located in the "tutorial-struts2" module of the appfuse-demos project on Google Code. Use the following command to check it out from Subversion:

svn checkout http://appfuse-demos.googlecode.com/svn/trunk/tutorial-struts2

Introduction to Struts 2

Struts 2 (formerly WebWork) is a web framework designed with simplicity in mind. It's built on top of XWork, which is a generic command framework. XWork also has an IoC container, but it isn't as full-featured as Spring and won't be covered in this section. Struts 2 controllers are called Actions, mainly because they must implement the Action interface. The ActionSupport class implements this interface, and it is most common parent class for Struts 2 actions.

The figure below shows how Struts 2 fits into a web application's architecture.

 

 

Struts 2 actions typically contain methods for accessing model properties and methods for returning strings. These strings are matched with "result" names in a struts.xml configuration file. Actions typically have a single execute() method, but you can easily add multiple methods and control execution using URLs and button names.

Struts 2 uses interceptors to intercept the request and response process. This is much like Servlet Filters, except you can talk directly to the action. Struts 2 uses interceptors in the framework itself. A number of them initialize the Action, prepare it for population, set parameters on it and handle any conversion errors.

Create a PersonActionTest

Testing is an important part of any application, and testing a Struts application is easier than most. The generic command pattern provided by XWork doesn't depend on the Servlet API at all. This makes it easy to use JUnit to test your Actions.

Create a PersonActionTest.java class in src/test/java/**/webapp/action.

<script class="javascript" src="/download/resources/confluence.ext.code:code/shCore.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushCSharp.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushPhp.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushJScript.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushVb.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushSql.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushXml.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushShell.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushDelphi.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushPython.js" type="text/javascript"></script><script class="javascript" src="/download/resources/confluence.ext.code:code/shBrushJava.js" type="text/javascript"></script>
package org.appfuse.tutorial.webapp.action;  
 
import com.opensymphony.xwork2.ActionSupport;  
import org.appfuse.service.GenericManager;  
import org.appfuse.tutorial.model.Person;  
import org.appfuse.webapp.action.BaseActionTestCase;  
 
public class PersonActionTest extends BaseActionTestCase {  
    private PersonAction action;
    @Override  
    protected void onSetUpBeforeTransaction() throws Exception {  
        super.onSetUpBeforeTransaction();  
        action = new PersonAction();  
        GenericManager personManager = (GenericManager) applicationContext.getBean("personManager");  
        action.setPersonManager(personManager);  
 
        // add a test person to the database  
        Person person = new Person();  
        person.setFirstName("Jack");  
        person.setLastName("Raible");  
        personManager.save(person);  
    }  
 
    public void testSearch() throws Exception {  
        assertEquals(action.list(), ActionSupport.SUCCESS);  
        assertTrue(action.getPersons().size() >= 1);  
    }  
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

This class won't compile yet; you must first create the PersonAction class.

Create a PersonAction that will fetch people

Create a PersonAction.java class (that extends AppFuse's BaseAction) in src/main/java/**/webapp/action:

package org.appfuse.tutorial.webapp.action;  
 
import org.appfuse.webapp.action.BaseAction;  
import org.appfuse.tutorial.model.Person;  
import org.appfuse.service.GenericManager;  
 
import java.util.List;  
 
public class PersonAction extends BaseAction {  
    private GenericManager<Person, Long> personManager;  
    private List persons;  
 
    public void setPersonManager(GenericManager<Person, Long> personManager) {  
        this.personManager = personManager;  
    }  
 
    public List getPersons() {  
        return persons;  
    }  
 
    public String list() {  
        persons = personManager.getAll();  
        return SUCCESS;  
    }  
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Struts 2 actions are typically both the controller and the model. In this example, the list() method acts as the controller, and the getPersons() method retrieves data from the model. This simplification of the MVC paradigm makes this web framework very easy to program with.

Run the PersonActionTest using your IDE or mvn test -Dtest=PersonActionTest.

Zero Configuration

Struts' Zero Configuration feature is turned on by default. If you want to configure your Actions as Spring beans, you can do that by using class="beanId" in your Action definition, and then defining the bean in applicationContext.xml. Otherwise, they will automatically be wired up by name with Spring dependencies. All you need to do is add a setter to your Action to get a Spring bean injected into it.

Create personList.jsp to show search results

Create a src/main/webapp/WEB-INF/pages/personList.jsp page to display the list of people:

<%@ include file="/common/taglibs.jsp"%>  
 
<head>  
    <title><fmt:message key="personList.title"/></title>  
    <meta name="heading" content="<fmt:message key='personList.heading'/>"/>  
</head>  
 
<c:set var="buttons">  
    <input type="button" style="margin-right: 5px" 
        οnclick="location.href='<c:url value="/editPerson.html"/>'" 
        value="<fmt:message key="button.add"/>"/>  
      
    <input type="button" οnclick="location.href='<c:url value="/mainMenu.html"/>'" 
        value="<fmt:message key="button.done"/>"/>  
</c:set>  
 
<c:out value="${buttons}" escapeXml="false" />  
 
<s:set name="persons" value="persons" scope="request"/>  
<display:table name="persons" class="table" requestURI="" id="personList" export="true" pagesize="25">  
    <display:column property="id" sortable="true" href="editPerson.html"   
        paramId="id" paramProperty="id" titleKey="person.id"/>  
    <display:column property="firstName" sortable="true" titleKey="person.firstName"/>  
    <display:column property="lastName" sortable="true" titleKey="person.lastName"/>  
 
    <display:setProperty name="paging.banner.item_name" value="person"/>  
    <display:setProperty name="paging.banner.items_name" value="people"/>  
 
    <display:setProperty name="export.excel.filename" value="Person List.xls"/>  
    <display:setProperty name="export.csv.filename" value="Person List.csv"/>  
    <display:setProperty name="export.pdf.filename" value="Person List.pdf"/>  
</display:table>  
 
<c:out value="${buttons}" escapeXml="false" />  
 
<script type="text/javascript">  
    highlightTableRows("personList");  
</script> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

The most important line in this file is just above the <display:table> tag. This is the <s:set> tag. This tag calls PersonAction.getPersons() and sets the resulting List into the request scope, where the <display:table> tag can grab it. This is necessary because the Display Tag doesn't have any knowledge of the ValueStack used by Struts 2.

Open the struts.xml file in the src/main/resources directory. Define an <action> (at the bottom of this file) and set its class attribute to match the fully-qualified class name of the PersonAction class.

<action name="persons" class="org.appfuse.tutorial.webapp.action.PersonAction" method="list">   
    <result>/WEB-INF/pages/personList.jsp</result>   
</action> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

The default result type is "dispatcher" and its name is "success". This configured result type simply forwards you to the personList.jsp file when "success" is returned from PersonAction.list(). Other result types include redirect and chain. Redirect performs a client-side redirect and chain forwards you to another action. For a full list of result types, see Struts 2's Result Types documentation.

The "method" attribute of this action has a list attribute, which calls the list() method when the "persons.html" URL is invoked. If you exclude the method attribute, it calls the execute() method.

Open src/main/resources/ApplicationResources.properties and add i18n keys/values for the various "person" properties:

# -- person form --  
person.id=Id  
person.firstName=First Name  
person.lastName=Last Name  
 
person.added=Person has been added successfully.  
person.updated=Person has been updated successfully.  
person.deleted=Person 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 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Run mvn jetty:run-war and open http://localhost:8080/persons.html in your browser. Login with admin/admin and you should see a screen similar to the figure below.

 

 

Security settings for AppFuse specify that all *.html url-patterns should be protected (except for /signup.html and /passwordHint.html). This guarantees that clients must go through an Action to get to a JSP (or at least the ones in WEB-INF/pages).

CSS Customization
If you want to customize the CSS for a particular page, you can add <body id="pageName"/> to the top of the file. This will be slurped up by SiteMesh and put into the final page. You can then customize your CSS on a page-by-page basis using something like the following:
body#pageName element.class { background-color: blue } 

Modify PersonActionTest and PersonAction for edit(), save() and delete() method

To create the detail screen, add edit(), save(), and delete() methods to the PersonAction class. Before doing this, create tests for these methods.

Open src/test/java/**/webapp/action/PersonActionTest.java and add test methods for edit, save, and delete operations:

public void testEdit() throws Exception {  
    log.debug("testing edit...");  
    action.setId(1L);  
    assertNull(action.getPerson());  
    assertEquals("success", action.edit());  
    assertNotNull(action.getPerson());  
    assertFalse(action.hasActionErrors());  
}  
 
public void testSave() throws Exception {  
    MockHttpServletRequest request = new MockHttpServletRequest();  
    ServletActionContext.setRequest(request);  
    action.setId(1L);  
    assertEquals("success", action.edit());  
    assertNotNull(action.getPerson());  
      
    // update last name and save  
    action.getPerson().setLastName("Updated Last Name");  
    assertEquals("input", action.save());  
    assertEquals("Updated Last Name", action.getPerson().getLastName());  
    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(2L);  
    action.setPerson(person);  
    assertEquals("success", action.delete());  
    assertNotNull(request.getSession().getAttribute("messages"));  
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

This class will not compile yet because you need to update your src/main/java/**/action/PersonAction.java class. The cancel and delete properties capture the click of the Cancel and Delete buttons. The execute() method routes the different actions on the form to the appropriate method.

private Person person;  
private Long id;  
 
public void setId(Long id) {  
    this.id = id;  
}  
 
public Person getPerson() {  
    return person;  
}  
 
public void setPerson(Person person) {  
    this.person = person;  
}  
 
public String delete() {  
    personManager.remove(person.getId());  
    saveMessage(getText("person.deleted"));  
 
    return SUCCESS;  
}  
 
public String edit() {  
    if (id != null) {  
        person = personManager.get(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);  
 
    person = personManager.save(person);  
 
    String key = (isNew) ? "person.added" : "person.updated";  
    saveMessage(getText(key));  
 
    if (!isNew) {  
        return INPUT;  
    } else {  
        return SUCCESS;  
    }  
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

If you look at your PersonActionTest, all the tests depend on having a record with id=1 in the database (and testRemove depends on id=2), so let's add those records to our sample data file (src/test/resources/sample-data.xml). Adding it at the bottom should work - order is not important since it (currently) does not relate to any other tables. If you already have this table, make sure the 2nd record exists so testRemove() doesn't fail.

<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>Bob</value>  
    <value>Johnson</value>  
  </row>  
</table> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

DbUnit loads this file before you run any tests, so this record will be available to your Action test.

Save all your files and run the tests in PersonActionTest using the command mvn test -Dtest=PersonActionTest.

BUILD SUCCESSFUL
Total time: 31 seconds

Create personForm.jsp to edit a person's information

Create a src/main/webapp/WEB-INF/pages/personForm.jsp page to display the form:

<%@ include file="/common/taglibs.jsp"%>  
 
<head>  
    <title><fmt:message key="personDetail.title"/></title>  
    <meta name="heading" content="<fmt:message key='personDetail.heading'/>"/>  
</head>  
 
<s:form id="personForm" action="savePerson" method="post" validate="true">  
<s:hidden name="person.id" value="%{person.id}"/>  
 
    <s:textfield key="person.firstName" required="true" cssClass="text medium"/>  
    <s:textfield key="person.lastName" required="true" cssClass="text medium"/>  
 
    <li class="buttonBar bottom">           
        <s:submit cssClass="button" method="save" key="button.save" theme="simple"/>  
        <c:if test="${not empty person.id}">   
            <s:submit cssClass="button" method="delete" key="button.delete" οnclick="return confirmDelete('person')" theme="simple"/>  
        </c:if>  
        <s:submit cssClass="button" method="cancel" key="button.cancel" theme="simple"/>  
    </li>  
</s:form>  
 
<script type="text/javascript">  
    Form.focusFirstElement($("personForm"));  
</script> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Struts reduces the amount of HTML you have to write for a form. The <s:form> tag writes the <form> and structure tags for you. The <s:textfield> tag writes the whole row, including the <ul> and <li> tags to hold the input field's label.

Next, update the src/main/resources/struts.xml file to include the "editPerson" and "savePerson" actions.

<action name="editPerson" class="org.appfuse.tutorial.webapp.action.PersonAction" method="edit">   
    <result>/WEB-INF/pages/personForm.jsp</result>  
    <result name="error">/WEB-INF/pages/personList.jsp</result>  
</action>  
 
<action name="savePerson" class="org.appfuse.tutorial.webapp.action.PersonAction" method="save">  
    <result name="input">/WEB-INF/pages/personForm.jsp</result>  
    <result name="cancel" type="redirect">persons.html</result>  
    <result name="delete" type="redirect">persons.html</result>  
    <result name="success" type="redirect">persons.html</result>  
</action> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Run mvn jetty:run-war, open your browser to http://localhost:8080/persons.html, and click on the Add button.

 

 

Fill in the first name and last name fields and click the Save button. This should route you to the list screen, where a success message flashes and the new person displays in the list.

Displaying success messages
The src/main/webapp/common/messages.jsp file in AppFuse renders the success message in this screen. This file is included in decorators/default.jsp. It also handles displaying validation errors:
<s:if test="hasActionErrors()">
    <div class="error" id="errorMessages">
      <s:iterator value="actionErrors">
        <img src="<c:url value="/images/iconWarning.gif"/>"
            alt="<fmt:message key="icon.warning"/>" class="icon" />
        <s:property escape="false"/><br />
      </s:iterator>
   </div>
</s:if>

<%-- FieldError Messages - usually set by validation rules --%>
<s:if test="hasFieldErrors()">
    <div class="error" id="errorMessages">
      <s:iterator value="fieldErrors">
          <s:iterator value="value">
            <img src="<c:url value="/images/iconWarning.gif"/>"
                alt="<fmt:message key="icon.warning"/>" class="icon" />
             <s:property escape="false"/><br />
          </s:iterator>
      </s:iterator>
   </div>
</s:if>

Configure Validation

Struts 2 allows two types of validation: per-action and model-based. Since you likely want the same rules applied for the person object across different actions, this tutorial will use model-based.

Create a new file named Person-validation.xml in the src/main/resources/**/model directory (you'll need to create this directory). It should contain the following XML:

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" 
    "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">  
<validators>  
    <field name="person.firstName">  
        <field-validator type="requiredstring">  
            <message key="errors.required"/>  
        </field-validator>  
    </field>  
    <field name="person.lastName">  
        <field-validator type="requiredstring">  
            <message key="errors.required"/>  
        </field-validator>  
    </field>  
</validators> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

The "errors.message" key in ApplicationResources*.properties (listed below) will use the field's "name" attribute to do internationalization. You can also give the <message> element a body if you don't need i18n.

errors.required=${getText(fieldName)} is a required field. 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Now you need to configure PersonAction to know about visitor validation. To do this, create a PersonAction-validation.xml file in src/main/resources/**/webapp/action (you'll need to create this directory). Fill it with the following XML:

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" 
    "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">  
<validators>  
    <field name="person">  
        <field-validator type="visitor">  
            <param name="appendPrefix">false</param>  
            <message/>  
        </field-validator>  
    </field>  
</validators> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>
Unfortunately, Struts doesn't have a transparent mechanism for reading from the Person-validation.xml file and marking fields as required on the UI. AppFuse's Spring MVC implementation use a LabelTag that makes this possible, but it also both use Commons Validator. It is my hope to someday provide this same functionality for Struts. In the meantime, the JSP tags "required" attribute has nothing to with the validation rules you specify. Rather, they simply add an asterisk to the label with no further functionality.
Client-side validation
Client-side validation of model-based validation rules doesn't work with the Struts setup that AppFuse uses. Furthermore, I believe that Struts's client-side validation needs some additional features, namely: allow cancelling and showing all errors in one dialog. Because of this, only server-side validation works in Struts+AppFuse. If you'd like, you can read more about my frustrations with client-side validation.

As a workaround, you can use per-action validation. Just copy the Person-validation.xml file to the "webapp.action" package and rename it to PersonAction-validation.xml.

Struts' validation interceptor is enabled by default, so you don't need to configure anything for validation to work.

After saving all your files and running mvn jetty:run-war, validation should kick in when you try to save this form. To test, go to http://localhost:8080/editPerson.html?id=1. If you erase the values in the firstName and lastName fields and click the Save button, you should see the following:

 

 

AppFuse is configured so that methods cancel, execute, delete, edit, list, and start are not validated. This allows you to go back from a form with errors on (like above) by pressing the Cancel button.

Create a Canoo WebTest to test browser-like actions

The next (optional) step in this tutorial is to create a Canoo WebTest to test the JSPs. This step is optional, because you can run the same tests manually through your browser. Regardless, it's a good idea to automate as much of your testing as possible.

You can use the following URLs to test the different actions for adding, editing and saving a user.

WebTest Recorder
There is a WebTest Recorder Firefox plugin that allows you to record your tests, rather than manually writing them.

Canoo tests are pretty slick in that they're simply configured in an XML file. To add tests for add, edit, save and delete, open src/test/resources/web-tests.xml and add the following XML. You'll notice that this fragment has a target named ''PersonTests'' that runs all the related tests.

<!-- runs person-related tests -->  
<target name="PersonTests"   
    depends="SearchPeople,EditPerson,SavePerson,AddPerson,DeletePerson" 
    description="Call and executes all person test cases (targets)">  
    <echo>Successfully ran all Person UI tests!</echo>  
</target>  
 
<!-- Verify the people list screen displays without errors -->  
<target name="SearchPeople" description="Tests search for and displaying all people">  
    <webtest name="searchPeople">  
        &config;  
        <steps>  
            &login;  
            <invoke description="click View People link" url="/persons.html"/>  
            <verifytitle description="we should see the personList title"   
                text=".*${personList.title}.*" regex="true"/>  
        </steps>  
    </webtest>  
</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="person.lastName" value="Canoo"/>  
            <clickbutton label="${button.save}" description="Click Save"/>  
            <verifytitle description="Page re-appears if save successful" 
                text=".*${personDetail.title}.*" regex="true"/>  
            <verifytext description="verify success message" text="${person.updated}"/>  
        </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="person.firstName" value="Abbie"/>  
            <setinputfield description="set lastName" name="person.lastName" value="Raible"/>  
            <clickbutton label="${button.save}" description="Click button 'Save'"/>  
            <verifytitle description="Person List appears if save successful" 
                text=".*${personList.title}.*" regex="true"/>  
            <verifytext description="verify success message" text="${person.added}"/>  
        </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"/>  
            <prepareDialogResponse description="Confirm delete" dialogType="confirm" response="true"/>  
            <clickbutton label="${button.delete}" description="Click button 'Delete'"/>  
            <verifyNoDialogResponses/>  
            <verifytitle description="display Person List" text=".*${personList.title}.*" regex="true"/>  
            <verifytext description="verify success message" text="${person.deleted}"/>  
        </steps>  
    </webtest>  
</target> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

After adding this, you should be able to run mvn integration-test -Dtest=PersonTests and have these tests execute. If this command results in "BUILD SUCCESSFUL" - nice work!

To include the PersonTests when all Canoo tests are run, add it as a dependency to the "run-all-tests" target in src/test/resources/web-test.xml.

<target name="run-all-tests"   
    depends="Login,Logout,PasswordHint,Signup,UserTests,FlushCache,FileUpload,PersonTests" 
    description="Call and executes all test cases (targets)"/> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Add link to menu

The last step is to make the list, add, edit and delete functions visible to the user. The simplest way is to add a new link to the list of links in src/main/webapp/WEB-INF/pages/mainMenu.jsp. Since this file doesn't exist in your project, you can copy it from target/projectname-version/WEB-INF/pages/mainMenu.jsp to your project with the following command:

cp target/projectname-version/WEB-INF/pages/mainMenu.jsp src/main/webapp/WEB-INF/pages

Then add the following link:

<li>  
    <a href="<c:url value="/persons.html"/>"><fmt:message key="menu.viewPeople"/></a>  
</li> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Where menu.viewPeople is an entry in src/main/resources/ApplicationResources.properties.

menu.viewPeople=View People 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>
Modifying AppFuse core files
You can run also run war:inplace to get the mainMenu.jsp file in your project. You'll want to check your project into source control before you do this so you can delete files you don't modify.

The other (more likely) alternative is that you'll want to add it to the menu. To do this, add the following to src/main/webapp/WEB-INF/menu-config.xml:

<Menu name="PeopleMenu" title="menu.viewPeople" page="/persons.html"/> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Make sure the above XML is inside the <Menus> tag, but not within another <Menu>. Then create src/main/webapp/common/menu.jsp and add the following code to it:

<%@ include file="/common/taglibs.jsp"%>  
 
<menu:useMenuDisplayer name="Velocity" config="cssHorizontalMenu.vm" permissions="rolesAdapter">  
<ul id="primary-nav" class="menuList">  
    <li class="pad">&nbsp;</li>  
    <c:if test="${empty pageContext.request.remoteUser}">  
    <li><a href="<c:url value="/login.jsp"/>" class="current">  
        <fmt:message key="login.title"/></a></li>  
    </c:if>  
    <menu:displayMenu name="MainMenu"/>  
    <menu:displayMenu name="UserMenu"/>  
    <menu:displayMenu name="PeopleMenu"/>  
    <menu:displayMenu name="AdminMenu"/>  
    <menu:displayMenu name="Logout"/>  
</ul>  
</menu:useMenuDisplayer> 
<script class="javascript" type="text/javascript"> if(!window.newcodemacro_initialised) { window.newcodemacro_initialised = true; window.oldonloadmethod = window.onload; window.onload = function(){ dp.SyntaxHighlighter.HighlightAll('newcodemacro'); if(window.oldonloadmethod) { window.oldonloadmethod(); } } } </script>

Now if you run mvn jetty:run-war and go to http://localhost:8080/mainMenu.html, you should see something like the screenshot below.

 

 

Notice that there is a new link in your main screen (from mainMenu.jsp) and on the top in your menu bar (from menu.jsp).

That's it!
You've completed the full lifecycle of developing a set of master-detail pages with AppFuse and Struts 2 - Congratulations! Now the real test is if you can run all the tests in your app without failure. To test, run mvn integration-test. This will run all the unit and integration tests within your project.

Happy Day!

BUILD SUCCESSFUL
Total time: 1 minute 30 seconds
   
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值