About this Tutorial
This tutorial will show you how to create master/detail screens with Tapestry
. The current version of Tapestry used is 4.1.3, but we hope to upgrade to Tapestry 5 after its initial release. 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 a nifty 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-tapestry archetype and have already completed the Persistence and Services tutorials. If you're using the appfuse-modular-tapestry 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 Tapestry, you're likely to be confused and nothing will work in this tutorial. |
Table of Contents

- Introduction to Tapestry
- Create a PersonListTest
- Create a PersonList class that will fetch people
- Create PersonList.html to show search results
- Create a PersonFormTest and PersonForm for edit(), save() and delete() methods
- Add an edit listener to PersonList.java
- Create PersonForm.html 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-tapestry" module of the appfuse-demos project on Google Code. Use the following command to check it out from Subversion:
|
Introduction to Tapestry
Tapestry is a component-based framework for developing web applications. Unlike many other Java web frameworks, Tapestry uses a component object model similar to traditional GUI frameworks. According to Howard Lewis Ship, the founder of Tapestry:
A component is an object that fits into an overall framework; the responsibilities of the component are defined by the design and structure of the framework. A component is a component, and not simply an object, when it follows the rules of the framework. These rules can take the form of classes to inherit from, naming conventions (for classes or methods) to follow, or interfaces to implement. Components can be used within the context of the framework. The framework will act as a container for the component, controlling when the component is instantiated and initialized, and dictating when the methods of the component are invoked. – Lewis Ship, Howard. Tapestry in Action. Greenwich, CT: Manning Publications Co., 2004.
The figure below shows how Tapestry fits into a web application's architecture:
Tapestry's component model allows you to have a very high level of reuse within and between projects. You can package components in JAR files and distribute them among teams and developers.
Tapestry tries to hide the Servlet API from developers. Learning Tapestry is often characterized as an "unlearning" process. GUI programmers typically have an easier time adjusting to the way things work in Tapestry. Tapestry operates in terms of objects, methods and properties, rather than URLs and query parameters. All of the URL building, page dispatching and method invocation happens transparently.
Other benefits of Tapestry include line-precise error reporting and easy-to-use HTML templates. While other frameworks use external templating systems, Tapestry has its own templating system. Tapestry templates are often HTML files, but they can also be WML or XML. You can hook into these templates by using Tapestry-specific attributes on existing HTML elements.
About 90% of a template is regular HTML markup. This HTML has tags that work as placeholders for Tapestry components. These tags are recognized by a jwcid attribute. JWC is short for Java Web Component. Below is an example using the Insert component:
This special template language allows you to edit HTML templates using a WYSIWYG HTML editor and to view them using a browser. Graphic designers and HTML developers can easily edit dynamic pages in your web application.
Create a PersonListTest
This tutorial shows you how to create a Tapestry application using test-first development. You will use JUnit and a BasePageTestCase that instantiates page classes for you. This is necessary because Tapestry pages and components are often abstract. At runtime, Tapestry uses information from the class and its matching page or component specification to enhance the class. Enhancement is the process of creating a subclass (on-the-fly) that has implementations of abstract methods. More information about the class enhancement process is available on Tapestry's wiki
.
 | Learning Tapestry If you want a more in-depth learning experience, I suggest you read Howard Lewis Ship's Tapestry in Action . I had it close by my side and used it frequently while integrating Tapestry into AppFuse. Thanks for the help Howard! |
AppFuse does most of the hard work for you, meaning it initializes Tapestry classes using its Creator
class to instantiate abstract page classes.
Create a PersonListTest.java class in src/test/java/**/webapp/pages (you will need to create the webapp.pages package):
package org.appfuse.tutorial.webapp.pages;
import java.util.HashMap;
import java.util.Map;
import org.appfuse.webapp.pages.BasePageTestCase;
public class PersonListTest extends BasePageTestCase {
private PersonList page;
protected void onSetUpBeforeTransaction() throws Exception {
super.onSetUpBeforeTransaction();
Map<String, Object> map = new HashMap<String, Object>();
map.put("personManager", applicationContext.getBean("personManager"));
page = (PersonList) getPage(PersonList.class, map);
}
protected void onTearDownAfterTransaction() throws Exception {
super.onTearDownAfterTransaction();
page = null;
}
public void testSearch() throws Exception {
assertTrue(page.getPersons().size() >= 1);
}
}
This class will not compile until you create the PersonList class.
Create a PersonList that will fetch people
Create a PersonList.java file in src/main/java/**/webapp/pages (you will need to create the webapp.pages package):
package org.appfuse.tutorial.webapp.pages;
import org.appfuse.service.GenericManager;
import org.appfuse.tutorial.model.Person;
import org.appfuse.webapp.pages.BasePage;
import java.util.List;
public abstract class PersonList extends BasePage {
public abstract GenericManager<Person, Long> getPersonManager();
public List getPersons() {
return getPersonManager().getAll();
}
}
If you run mvn test -Dtest=PersonListTest, your test should pass.
Nice!
BUILD SUCCESSFUL
Total time: 14 seconds
Create PersonsList.html to show search results
Create a src/main/webapp/WEB-INF/tapestry/PersonList.html page to display the list of people. You will likely need to create the "tapestry" directory.
<head>
<title><span key="personList.title"/></title>
<meta name="heading" jwcid="@Any" content="message:personList.heading"/>
<meta name="menu" content="PersonMenu"/>
</head>
<span jwcid="@ShowMessage"/>
<p>
<input type="button" class="button" onclick="location.href='PersonForm.html'" jwcid="@Any" value="message:button.add"/>
<input type="button" class="button" onclick="location.href='mainMenu.html'" jwcid="@Any" value="message:button.done"/>
</p>
<table jwcid="table@contrib:Table" class="table contribTable personList" id="personList"
rowsClass="ognl:beans.rowsClass.next" row="ognl:row" source="ognl:persons"
columns="person.id:id,person.firstName:firstName,person.lastName:lastName"
arrowUpAsset="asset:upArrow" arrowDownAsset="asset:downArrow">
</table>
<input type="button" class="button" onclick="location.href='PersonForm.html'" jwcid="@Any" value="message:button.add"/>
<input type="button" class="button" onclick="location.href='mainMenu.html'" jwcid="@Any" value="message:button.done"/>
<script type="text/javascript">
highlightTableRows("personList");
</script>
In order for this page to work property, you need to create a PersonList.page file in the same directory. This file is called a "page-specification". It injects the personManager bean into PersonList.java. It also sets up the assets used by the table component. Populate it with the following code:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC
"-
"http:>
<page-specification>
<inject property="personManager" type="spring" object="personManager"/>
<property name="message" persist="flash"/>
<property name="row"/>
<bean name="rowsClass" class="org.apache.tapestry.bean.EvenOdd"/>
<asset name="upArrow" path="/images/arrow_up.png"/>
<asset name="downArrow" path="/images/arrow_down.png"/>
</page-specification>
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/PersonList.html
in your browser. Login with admin/admin and you should see a screen similar to the figure below.
 | If you get an exception from Tapestry that says: "Unable to parse OGNL expression 'persons': $BasePage_0.persons org.apache.tapestry.BindingException", it means that Tapestry is not mapping your Java file to your html file. Open src/main/webapp/WEB-INF/tapestry.application and edit org.apache.tapestry.page-class-packages to include the package where your pages' Java classes are kept. |
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 Tapestry's ApplicationServlet to get to view 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:
|
Create a PersonFormTest and PersonForm for edit(), save() and delete() methods
To start creating the detail screen, create a PersonFormTest.java class in src/test/java/**/webapp/pages:
package org.appfuse.tutorial.webapp.pages;
import org.apache.tapestry.engine.ILink;
import org.appfuse.tutorial.model.Person;
import org.appfuse.webapp.pages.MockRequestCycle;
import org.appfuse.webapp.pages.BasePageTestCase;
import org.appfuse.service.GenericManager;
import java.util.HashMap;
import java.util.Map;
public class PersonFormTest extends BasePageTestCase {
private PersonForm page;
protected void onSetUpBeforeTransaction() throws Exception {
super.onSetUpBeforeTransaction();
Map<String, Object> map = new HashMap<String, Object>();
map.put("personManager", applicationContext.getBean("personManager"));
page = (PersonForm) getPage(PersonForm.class, map);
}
protected void onTearDownAfterTransaction() throws Exception {
super.onTearDownAfterTransaction();
page = null;
}
public void testAdd() throws Exception {
Person person = new Person();
person.setFirstName("Abbie");
person.setLastName("Loo");
page.setPerson(person);
ILink link = page.save(new MockRequestCycle(this.getClass().getPackage().getName()));
assertNotNull(page.getPerson());
assertFalse(page.hasErrors());
assertEquals("PersonList" + EXTENSION, link.getURL());
}
public void testSave() {
GenericManager<Person, Long> personManager =
(GenericManager<Person, Long>) applicationContext.getBean("personManager");
Person person = personManager.get(1L);
person.setFirstName("Jack");
person.setLastName("Jack");
page.setPerson(person);
ILink link = page.save(new MockRequestCycle(this.getClass().getPackage().getName()));
assertNotNull(page.getPerson());
assertFalse(page.hasErrors());
assertNull(link);
}
public void testRemove() throws Exception {
Person person = new Person();
person.setId(2L);
page.setPerson(person);
page.delete(new MockRequestCycle(this.getClass().getPackage().getName()));
assertFalse(page.hasErrors());
}
}
Nothing will compile at this point; you need to create the PersonForm that you're referring to in this test.
In src/main/java/**/webapp/pages, create a PersonForm.java class that extends AppFuse's BasePage
. Populate it with the following code:
package org.appfuse.tutorial.webapp.pages;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.engine.ILink;
import org.apache.tapestry.event.PageBeginRenderListener;
import org.apache.tapestry.event.PageEvent;
import org.appfuse.service.GenericManager;
import org.appfuse.tutorial.model.Person;
import org.appfuse.webapp.pages.BasePage;
public abstract class PersonForm extends BasePage implements PageBeginRenderListener {
public abstract GenericManager<Person, Long> getPersonManager();
public abstract void setPerson(Person person);
public abstract Person getPerson();
public void pageBeginRender(PageEvent event) {
if (getPerson() == null) {
setPerson(new Person());
}
}
public ILink cancel(IRequestCycle cycle) {
log.debug("Entering 'cancel' method");
return getEngineService().getLink(false, "PersonList");
}
public ILink delete(IRequestCycle cycle) {
log.debug("entered 'delete' method");
getPersonManager().remove(getPerson().getId());
PersonList nextPage = (PersonList) cycle.getPage("PersonList");
nextPage.setMessage(getText("person.deleted"));
return getEngineService().getLink(false, nextPage.getPageName());
}
public ILink save(IRequestCycle cycle) {
if (getDelegate().getHasErrors()) {
return null;
}
boolean isNew = (getPerson().getId() == null);
getPersonManager().save(getPerson());
String key = (isNew) ? "person.added" : "person.updated";
if (isNew) {
PersonList nextPage = (PersonList) cycle.getPage("PersonList");
nextPage.setMessage(getText(key));
return getEngineService().getLink(false, nextPage.getPageName());
} else {
setMessage(getText(key));
return null; }
}
}
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 addMessage(key, stringtoreplace) method.
If you look at your PersonFormTest, 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 PersonFormTest class. Save all your files and run the tests in PersonFormTest using the command mvn test -Dtest=PersonFormTest.
BUILD SUCCESSFUL
Total time: 16 seconds
Add an edit listener to PersonList.java
To allow users to click on the list screen to get to the edit screen, you need to add an edit listener to PersonList.java. Open PersonListTest.java and add the following testEdit() method:
public void testEdit() throws Exception {
RequestCycle cycle = new MockRequestCycle(this.getClass().getPackage().getName());
cycle.setServiceParameters(new Object[] {1L});
page.edit(cycle);
assertFalse(page.hasErrors());
}
Add an edit() method to PersonList.java:
public void edit(IRequestCycle cycle) {
Object[] parameters = cycle.getListenerParameters();
Long id = (Long) parameters[0];
if (log.isDebugEnabled()) {
log.debug("fetching person with id: " + id);
}
Person person = getPersonManager().get(id);
PersonForm nextPage = (PersonForm) cycle.getPage("PersonForm");
nextPage.setPerson(person);
cycle.activate(nextPage);
}
Then add a <tr> element to the <table> component in PersonList.html
<table jwcid="table@contrib:Table" class="table contribTable personList" id="personList"
rowsClass="ognl:beans.rowsClass.next" row="ognl:row" source="ognl:persons"
columns="person.id:id,person.firstName:firstName,person.lastName:lastName"
arrowUpAsset="asset:upArrow" arrowDownAsset="asset:downArrow">
<tr jwcid="person_idColumnValue@Block">
<a jwcid="@DirectLink" listener="listener:edit" parameters="ognl:row.id">
<span jwcid="@Insert" value="ognl:row.id"/>
</a>
</tr>
</table>
Now you need to create the view template so you can edit a person's information.
Create PersonForm.html to edit a person
Create a src/main/webapp/WEB-INF/tapestry/PersonForm.html page to display the form:
<head>
<title><span key="personDetail.title"/></title>
<meta name="heading" jwcid="@Any" content="message:personDetail.heading"/>
</head>
<body jwcid="@Body">
<span jwcid="@ShowMessage"/>
<form jwcid="personForm">
<input type="hidden" jwcid="@Hidden" value="ognl:person.id"/>
<ul>
<li>
<label class="desc" jwcid="@FieldLabel" field="component:firstNameField"/>
<input class="text medium" jwcid="firstNameField" type="text" id="firstName"/>
</li>
<li>
<label class="desc" jwcid="@FieldLabel" field="component:lastNameField"/>
<input class="text medium" jwcid="lastNameField" type="text" id="lastName"/>
</li>
<li class="buttonBar button">
<input type="submit" class="button" jwcid="@Submit" value="message:button.save" id="save" action="listener:save"/>
<span jwcid="@If" condition="ognl:person.id != null">
<input type="submit" class="button" jwcid="@Submit" value="message:button.delete" id="delete" action="listener:delete"
onclick="return confirmDelete('Person')"/>
</span>
<input type="submit" class="button" jwcid="@Submit" value="message:button.cancel" id="cancel" action="listener:cancel"/>
</li>
</ul>
</form>
<script type="text/javascript">
Form.focusFirstElement($("personForm"));
</script>
</body>
Create a PersonForm.page page-specification file in the same directory as PersonForm.html. Populate it with the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC
"-
"http:>
<page-specification>
<inject property="engineService" object="engine-service:page"/>
<inject property="request" object="service:tapestry.globals.HttpServletRequest"/>
<inject property="response" object="service:tapestry.globals.HttpServletResponse"/>
<inject property="personManager" type="spring" object="personManager"/>
<property name="message" persist="flash"/>
<component id="personForm" type="Form"/>
<component id="firstNameField" type="TextField">
<binding name="value" value="person.firstName"/>
<binding name="displayName" value="message:person.firstName"/>
</component>
<component id="lastNameField" type="TextField">
<binding name="value" value="person.lastName"/>
<binding name="displayName" value="message:person.lastName"/>
</component>
</page-specification>
Run mvn jetty:run-war, open your browser to http://localhost:8080/PersonList.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 AppFuse renders success and error messages using two components: ShowMessage and ShowError:
|
Configure Validation
To enable server-side validation, you need to modify the PersonForm.page page specification so it has validation information. This consists of three steps:
- Define a "delegate" binding for the form component.
- Define "validator" properties on the TextFields.
- Add a ShowValidationError component to PersonForm.html.
Replace your PageForm.page with the one below - this satisfies steps 1 and 2:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC
"-
"http:>
<page-specification>
<inject property="engineService" object="engine-service:page"/>
<inject property="request" object="service:tapestry.globals.HttpServletRequest"/>
<inject property="response" object="service:tapestry.globals.HttpServletResponse"/>
<inject property="personManager" type="spring" object="personManager"/>
<property name="message" persist="flash"/>
<component id="personForm" type="Form">
<binding name="delegate" value="ognl:delegate"/>
</component>
<component id="firstNameField" type="TextField">
<binding name="value" value="person.firstName"/>
<binding name="validators" value="validators:required"/>
<binding name="displayName" value="message:person.firstName"/>
</component>
<component id="lastNameField" type="TextField">
<binding name="value" value="person.lastName"/>
<binding name="validators" value="validators:required"/>
<binding name="displayName" value="message:person.lastName"/>
</component>
</page-specification>
Add the ShowValidationError component to the top of PersonForm.html, just after the <body> tag:
<body jwcid="@Body">
<span jwcid="@ShowValidationError" delegate="ognl:delegate"/>
...
AppFuse contains a custom Label component for Tapestry. This renders will add asterisks for required fields.
 | Tapestry Validators There are a number of different validators available for Tapestry project. This example only shows a way to make Strings required. The UserForm.page file contains examples of validating e-mail addressing and validating with regular expressions. |
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/PersonForm.html
and try to add a new user 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:
- Set clientValidationEnabled to "true" on the Form component.
- Add form.onsubmit = null to the onclick() handlers of the Delete and Cancel buttons.
In PersonForm.page, turn on client-side validation by adding a new binding to the personForm component:
<component id="personForm" type="Form">
<binding name="delegate" value="ognl:delegate"/>
<binding name="clientValidationEnabled" value="true"/>
</component>
In PersonForm.html on onclick handlers to the Delete and Cancel buttons so client-side validation is disabled when they're clicked:
<li class="buttonBar button">
<input type="submit" class="button" jwcid="@Submit" value="message:button.save" id="save" action="listener:save"/>
<span jwcid="@If" condition="ognl:person.id != null">
<input type="submit" class="button" jwcid="@Submit" value="message:button.delete" id="delete" action="listener:delete"
onclick="form.onsubmit = null; return confirmDelete('Person')"/>
</span>
<input type="submit" class="button" jwcid="@Submit" value="message:button.cancel" id="cancel" action="listener:cancel"
onclick="form.onsubmit = null"/>
</li>
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 user 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 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="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="/PersonList.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 Person List" url="/PersonList.html"/>
<clicklink description="edit first record in list" label="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="View Person List" url="/PersonList.html"/>
<clicklink description="edit first record in list" label="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="firstNameField" value="firstName"/>
<setinputfield description="set lastName" name="lastNameField" 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="firstNameField" value="Canoo"/>
<setinputfield description="set lastName" name="lastNameField" 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="View Person List" url="/PersonList.html"/>
<clicklink description="delete first record in list" label="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/tapestry/mainMenu.html. Since this file doesn't exist in your project, you can copy it from target/projectname-version/WEB-INF/tapestry/mainMenu.html to your project with the following command:
Then add the following link:
<li>
<a jwcid="@PageLink" page="PersonList"><span key="menu.viewPeople"/></a>
</li>
Where menu.viewPeople is an entry in src/main/resources/ApplicationResources.properties (and src/main/webapp/WEB-INF/tapestry.properties).
 | Modifying AppFuse core files You can run also run war:inplace to get the mainMenu.html 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="/PersonList.html"/>
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"> </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.html) 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 Tapestry - 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 48 seconds