About this Tutorial
This tutorial will show you how to create master/detail screens with Spring MVC. 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-spring archetype and have already completed the Persistence and Services tutorials. If you're using the appfuse-modular-spring 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 Spring MVC, you're likely to be confused and nothing will work in this tutorial. |
Table of Contents
- Introduction to Spring MVC
- Create a PersonControllerTest
- Create a PersonController that will fetch people
- Create persons.jsp that shows search results
- Create a PersonFormControllerTest and PersonFormController
- Create personform.jsp to edit a person
- Configure Validation
- Create a Canoo WebTest to test browser-like actions
- Add link to menu
| Source Code The code for this tutorial is located in the "tutorial-spring" 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-spring |
Introduction to Spring MVC
Spring MVC offers two main controllers: a Controller interface and a SimpleFormController class. The Controller is best suited for displaying read-only data (such as list screens), while the SimpleFormController handles forms (such as edit, save, delete). The Controller interface is quite simple, containing a single handleRequest(request, response) method.
package org.springframework.web.servlet.mvc; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.ModelAndView; public interface Controller { /** * Process the request and return a ModelAndView object which the * DispatcherServlet will render. A null return is not an error: * It indicates that this object completed request processing * itself, thus there is no ModelAndView to render. */ ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception; }
The handleRequest() method returns a ModelAndView class. This class holds both the model and the view, which are both very distinct. The model is the information that you intend to display, and the view is the logical name where you want to display it. The model can be a single object with a name, or it can be a java.util.Map containing several objects. The view can be a View object (which is an interface for the different view types), or it can be a String name that is determined by a ViewResolver (pre-configured for JSPs in AppFuse).
The SimpleFormController is a concrete class with several methods that are invoked while processing a data-entry form. The reason one is an interface and the other is a super-class is primarily for flexibility. All Controllers in Spring use the Controller interface, whereas the SimpleFormController is an implementation with default settings for many of its methods. AppFuse leverages this class in its BaseFormController class. This class contains a number of convenience methods that are useful when developing applications with Spring MVC.
| Learning Spring MVC If you want a more in-depth learning experience, I suggest you read Spring Live or Expert Spring MVC and Web Flow. Other Spring books can be found in the Spring Reference Guide. |
Create a PersonControllerTest
Testing is an important part of any application, and testing a Spring MVC web application is pretty easy. Not only are Spring's controllers lightweight, they're also easy to test using Spring's Mock library. This library has mock implements for much of the Servlet API and makes it quite simple to test Spring Controllers.
Create a PersonControllerTest.java class in src/test/java/**/webapp/controller:
package org.appfuse.tutorial.webapp.controller; import org.appfuse.webapp.controller.BaseControllerTestCase; import org.springframework.ui.ModelMap; import org.springframework.web.servlet.ModelAndView; import java.util.List; public class PersonControllerTest extends BaseControllerTestCase { private PersonController c; public void setPersonController(PersonController c) { this.c = c; } public void testHandleRequest() throws Exception { ModelAndView mav = c.handleRequest(null, null); ModelMap m = mav.getModelMap(); assertNotNull(m.get("personList")); assertTrue(((List) m.get("personList")).size() > 0); } }
This class will not compile until you create the PersonController class.
Create a PersonController that will fetch people
Create a PersonController.java class (that extends Spring's Controller interface) in src/main/java/**/webapp/controller:
package org.appfuse.tutorial.webapp.controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.appfuse.service.GenericManager; import org.appfuse.tutorial.model.Person; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.Controller; public class PersonController implements Controller { private final Log log = LogFactory.getLog(PersonController.class); private GenericManager<Person, Long> personManager = null; public void setPersonManager(GenericManager<Person, Long> personManager) { this.personManager = personManager; } public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { log.debug("entering 'handleRequest' method..."); return new ModelAndView().addObject(personManager.getAll()); } }
AppFuse leverages Spring MVC 2.0's convention over configuration features. This means the following:
- This controller will be available at /persons.html thanks to the ControllerClassNameHandlerMapping.
- The default view page will be located at src/main/webapp/WEB-INF/pages/persons.jsp because of the RequestToViewNameTranslator (check out the DefaultRequestToViewNameTranslator).
- The list of people (the model) will be available from "personList" thanks to ModelMap.
Open src/main/webapp/WEB-INF/dispatcher-servlet.xml and add a bean definition for personController:
<bean id="personController" class="org.appfuse.tutorial.webapp.controller.PersonController"> <property name="personManager" ref="personManager"/> </bean>
Run the PersonControllerTest using your IDE or mvn test -Dtest=PersonControllerTest.
Create persons.jsp to show search results
Create a src/main/webapp/WEB-INF/pages/persons.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'/>"/> <meta name="menu" content="PersonMenu"/> </head> <c:set var="buttons"> <input type="button" style="margin-right: 5px" onclick="location.href='<c:url value="/personform.html"/>'" value="<fmt:message key="button.add"/>"/> <input type="button" onclick="location.href='<c:url value="/mainMenu.html"/>'" value="<fmt:message key="button.done"/>"/> </c:set> <c:out value="${buttons}" escapeXml="false"/> <display:table name="personList" cellspacing="0" cellpadding="0" requestURI="" id="personList" pagesize="25" class="table personList" export="true"> <display:column property="id" escapeXml="true" sortable="true" url="/personform.html" paramId="id" paramProperty="id" titleKey="person.id"/> <display:column property="firstName" escapeXml="true" sortable="true" titleKey="person.firstName"/> <display:column property="lastName" escapeXml="true" 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>
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
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 }
|
Create a PersonFormControllerTest and PersonFormController
To start creating the detail screen, create a PersonFormControllerTest.java class in src/test/java/**/webapp/controller:
package org.appfuse.tutorial.webapp.controller; import org.appfuse.webapp.controller.BaseControllerTestCase; import org.appfuse.tutorial.model.Person; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.web.servlet.ModelAndView; public class PersonFormControllerTest extends BaseControllerTestCase { private PersonFormController c; public void setPersonFormController(PersonFormController c) { this.c = c; } public void testEdit() throws Exception { log.debug("testing edit..."); MockHttpServletRequest request = newGet("/personform.html"); request.addParameter("id", "1"); ModelAndView mv = c.handleRequest(request, new MockHttpServletResponse()); Person person = (Person) mv.getModel().get(c.getCommandName()); assertNotNull(person); } public void testSave() throws Exception { MockHttpServletRequest request = newGet("/personform.html"); request.addParameter("id", "1"); ModelAndView mv = c.handleRequest(request, new MockHttpServletResponse()); Person person = (Person) mv.getModel().get(c.getCommandName()); assertNotNull(person); request = newPost("/personform.html"); super.objectToRequestParameters(person, request); request.addParameter("lastName", "Updated Last Name"); mv = c.handleRequest(request, new MockHttpServletResponse()); Errors errors = (Errors) mv.getModel().get(BindException.MODEL_KEY_PREFIX + "person"); assertNull(errors); assertNotNull(request.getSession().getAttribute("successMessages")); } public void testRemove() throws Exception { MockHttpServletRequest request = newPost("/personform.html"); request.addParameter("delete", ""); request.addParameter("id", "2"); c.handleRequest(request, new MockHttpServletResponse()); assertNotNull(request.getSession().getAttribute("successMessages")); } }
Nothing will compile at this point; you need to create the PersonFormController that you're referring to in this test.
In src/main/java/**/webapp/controller, create a PersonFormController.java class that extends AppFuse's BaseFormController. Populate it with the following code:
package org.appfuse.tutorial.webapp.controller; import org.apache.commons.lang.StringUtils; import org.appfuse.service.GenericManager; import org.appfuse.tutorial.model.Person; import org.appfuse.webapp.controller.BaseFormController; import org.springframework.validation.BindException; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; public class PersonFormController extends BaseFormController { private GenericManager<Person, Long> personManager = null; public void setPersonManager(GenericManager<Person, Long> personManager) { this.personManager = personManager; } public PersonFormController() { setCommandClass(Person.class); setCommandName("person"); } protected Object formBackingObject(HttpServletRequest request) throws Exception { String id = request.getParameter("id"); if (!StringUtils.isBlank(id)) { return personManager.get(new Long(id)); } return new Person(); } public ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception { log.debug("entering 'onSubmit' method..."); Person person = (Person) command; boolean isNew = (person.getId() == null); String success = getSuccessView(); Locale locale = request.getLocale(); if (request.getParameter("delete") != null) { personManager.remove(person.getId()); saveMessage(request, getText("person.deleted", locale)); } else { personManager.save(person); String key = (isNew) ? "person.added" : "person.updated"; saveMessage(request, getText(key, locale)); if (!isNew) { success = "redirect:personform.html?id=" + person.getId(); } } return new ModelAndView(success); } }
In the class above, there are a few methods you might not be familiar with. The formBackingObject() method is used to supply the object this Controller operates on. The onSubmit() method is called on POST requests and handles delete/add/update of a person.
You might notice a number of keys in this file - "person.deleted", "person.added" and "person.updated". These are all keys that need to be in your i18n bundle (ApplicationResources.properties). You should've added these at the beginning of this tutorial. If you want to customize these messages, to add the a person's name or something, simply add a {0} placeholder in the key's message and then use the getText(key, stringtoreplace, locale) method.
Now you need to tell Spring MVC that this bean exists. Add a bean definition for personFormController to src/main/webapp/WEB-INF/dispatcher-servlet.xml:
<bean id="personFormController" class="org.appfuse.tutorial.webapp.controller.PersonFormController"> <!--property name="validator" ref="beanValidator"/--> <property name="successView" value="redirect:persons.html"/> <property name="personManager" ref="personManager"/> </bean>
| The "validator" property is commented out in the above XML block because we haven't defined any validation rules for the Person object. We'll uncomment this value when we add validation. |
If you look at your PersonFormControllerTest, 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>
DbUnit loads this file before you run any tests, so these records will be available to your PersonFormControllerTest class. Save all your files and run the tests in PersonFormControllerTest using the command mvn test -Dtest=PersonFormControllerTest.
BUILD SUCCESSFUL
Total time: 20 seconds
Create personForm.jsp to edit a person
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> <form:form commandName="person" method="post" action="personform.html" id="personForm"> <form:errors path="*" cssClass="error" element="div"/> <form:hidden path="id"/> <ul> <li> <appfuse:label styleClass="desc" key="person.firstName"/> <form:errors path="firstName" cssClass="fieldError"/> <form:input path="firstName" id="firstName" cssClass="text medium"/> </li> <li> <appfuse:label styleClass="desc" key="person.lastName"/> <form:errors path="lastName" cssClass="fieldError"/> <form:input path="lastName" id="lastName" cssClass="text medium"/> </li> <li class="buttonBar bottom"> <input type="submit" class="button" name="save" value="<fmt:message key="button.save"/>"/> <c:if test="${not empty person.id}"> <input type="submit" class="button" name="delete" onclick="return confirmDelete('person')" value="<fmt:message key="button.delete"/>" /> </c:if> <input type="submit" class="button" name="cancel" value="<fmt:message key="button.cancel"/>"/> </li> </ul> </form:form> <script type="text/javascript"> Form.focusFirstElement($('personForm')); </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 error messages: <%-- Error Messages --%>
<c:if test="${not empty errors}">
<div class="error" id="errorMessages">
<c:forEach var="error" items="${errors}">
<img src="<c:url value="/images/iconWarning.gif"/>"
alt="<fmt:message key="icon.warning"/>" class="icon" />
<c:out value="${error}"/><br />
</c:forEach>
</div>
<c:remove var="errors"/>
</c:if>
<%-- Success Messages --%>
<c:if test="${not empty successMessages}">
<div class="message" id="successMessages">
<c:forEach var="msg" items="${successMessages}">
<img src="<c:url value="/images/iconInformation.gif"/>"
alt="<fmt:message key="icon.information"/>" class="icon" />
<c:out value="${msg}"/><br />
</c:forEach>
</div>
<c:remove var="successMessages" scope="session"/>
</c:if>
|
Configure Validation
Spring MVC supports a number of different options for configuring validation. AppFuse 2.x currently uses Spring Modules' Commons Validator support. However, you can change this to use another validation framework if you like. The instructions below show you how to configure Commons Validator with Spring MVC.
Open src/main/webapp/WEB-INF/validation.xml and add rules for the person object, so both the first and last names are required:
<form name="person"> <field property="firstName" depends="required"> <arg0 key="person.firstName"/> </field> <field property="lastName" depends="required"> <arg0 key="person.lastName"/> </field> </form>
You'll also need to uncomment the "validator" property you commented out earlier. Make sure the "personFormController" <bean> in src/main/webapp/WEB-INF/dispatcher-servlet.xml has the following XML:
<bean id="personFormController" class="org.appfuse.tutorial.webapp.controller.PersonFormController"> <property name="validator" ref="beanValidator"/> <property name="successView" value="redirect:persons.html"/> <property name="personManager" ref="personManager"/> </bean>
After making these changes and saving all your files, the first and last name fields should be required. To test, go to http://localhost:8080/personform.html and try to add a new person with no first or last name. You should see the following validation errors:

To enable client-side validation, you need to make the following changes to personform.jsp:
- Add an onsubmit() handler to the form.
- Add bCancel=true to the onclick() handlers of the delete and cancel buttons (to cancel validation when they're clicked).
- Add JSP Tags after the form to render the validation JavaScript functions.
Below is the revised contents of the <form:form> tag with these changes. Replace the <form:form> in your personform.jsp with these changes.
<form:form commandName="person" method="post" action="personform.html" id="personForm" onsubmit="return validatePerson(this)"> <form:errors path="*" cssClass="error" element="div"/> <form:hidden path="id"/> <ul> <li> <appfuse:label styleClass="desc" key="person.firstName"/> <form:errors path="firstName" cssClass="fieldError"/> <form:input path="firstName" id="firstName" cssClass="text medium"/> </li> <li> <appfuse:label styleClass="desc" key="person.lastName"/> <form:errors path="lastName" cssClass="fieldError"/> <form:input path="lastName" id="lastName" cssClass="text medium"/> </li> <li class="buttonBar bottom"> <input type="submit" class="button" name="save" value="<fmt:message key="button.save"/>"/> <c:if test="${not empty person.id}"> <input type="submit" class="button" name="delete" onclick="bCancel=true;return confirmDelete('person')" value="<fmt:message key="button.delete"/>" /> </c:if> <input type="submit" class="button" name="cancel" value="<fmt:message key="button.cancel"/>" onclick="bCancel=true"/> </li> </ul> </form:form> <v:javascript formName="person" cdata="false" dynamicJavascript="true" staticJavascript="false"/> <script type="text/javascript" src="<c:url value="/scripts/validator.jsp"/>"></script>
After saving all your files and running mvn jetty:run-war, client-side validation should kick in when you try to save this form. To test, go to http://localhost:8080/personform.html and try to add a new person with no first or last name. You should get the following JavaScript alert:

Create a Canoo WebTest to test browser-like actions
The next (optional) step in this tutorial is to create a Canoo WebTest to test your UI. 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 person.
- Add - http://localhost:8080/personform.html.
- Edit - Go to http://localhost:8080/persons.html and click on an existing record.
- Delete - Use the edit link above and click on the Delete button.
- Save - Use the edit link above and click the Save button.
| 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="SearchPersons,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="SearchPersons" description="Tests search for and displaying all persons"> <webtest name="searchPersons"> &config; <steps> &login; <invoke description="click View Persons 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="View Persons List" url="/persons.html"/> <clicklink label="1" description="Click edit link"/> <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 person"> <webtest name="savePerson"> &config; <steps> &login; <invoke description="click Edit Person link" url="/personform.html?id=1"/> <verifytitle description="we should see the personDetail title" text=".*${personDetail.title}.*" regex="true"/> <!-- update some of the required fields --> <setinputfield description="set firstName" name="firstName" value="firstName"/> <setinputfield description="set lastName" name="lastName" value="lastName"/> <clickbutton label="${button.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="/personform.html"/> <verifytitle description="we should see the personDetail title" text=".*${personDetail.title}.*" regex="true"/> <!-- enter required fields --> <setinputfield description="set firstName" name="firstName" value="Canoo"/> <setinputfield description="set lastName" name="lastName" value="Test"/> <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="/personform.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>
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)"/>
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>
Where menu.viewPeople is an entry in src/main/resources/ApplicationResources.properties.
menu.viewPeople=View People
| 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" width="120" roles="ROLE_ADMIN,ROLE_USER"/>
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="WEB-INF/classes/cssHorizontalMenu.vm" permissions="rolesAdapter"> <ul id="primary-nav" class="menuList"> <li class="pad"> </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>
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 Spring MVC - 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 23 seconds
Comments (1)
May 04, 2008
Andreas Kollegger says:
Currently (AppFuse 2.0.1) experiencing a problem running multiple tests with hsq...Currently (AppFuse 2.0.1) experiencing a problem running multiple tests with hsqldb, or launching the app with Jetty, because the database lock doesn't get released. The error will be a localized (for your project) variation of this:
For now, always specify a test instead of trying to run all of them. And, skip tests when running the application by using:
mvn jetty:run-war -Dmaven.test.skip=true