Raible's Wiki

Raible Designs
Wiki Home
News
Recent Changes

AppFuse

Homepage
  - Korean
  - Chinese
  - Italian
  - Japanese

QuickStart Guide
  - Chinese
  - French
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish
  - Japanese

User Guide
  - Korean
  - Chinese

Tutorials
  - Chinese
  - German
  - Italian
  - Korean
  - Portuguese
  - Spanish

FAQ
  - Korean

Latest Downloads

Other Applications

Struts Resume
Security Example
Struts Menu

Set your name in
UserPreferences

Edit this page


Referenced by
Articles
Articles_cn
Articles_de
Articles_pt
Articles_zh
TapestryPages




JSPWiki v2.2.33

[RSS]


Hide Menu

ValidationAndListTapestry


Part IV: Adding Validation and List Screen - Explains validation logic that makes the firstName and lastName required fields. Also shows how to add a list screen to display all person records in the database.

This tutorial depends on Part III: Creating Tapestry Pages and HTML Templates.

About this Tutorial

This tutorial will show you how to add Validation logic to the Person object using Tapestry's Validation framework. You'll also create a list screen using the contrib:Table component to display all the people in the database.
I will tell you how I do stuff in the Real World in text like this.

Table of Contents

  • [1] View validation logic in personForm.page
  • [2] View HTML with validation added and test
  • [3] Add testGetPeople methods to DAO and Manager Tests
  • [4] Add getPeople methods to PersonDao and Manager
  • [5] Create PersonListTest
  • [6] Create PersonForm
  • [7] Create personList.html and Canoo test
  • [8] Add link to menu

View validation logic in personForm.page [#1]

Implementing validation with Tapestry is quite simple. All you need to do is configure a "validators" property on your TextField components. The validation integration for the personForm.html page is already done by AppGen in the personForm.page. If you open web/pages/personForm.page, you should see the following XML.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification PUBLIC 
    "-//Apache Software Foundation//Tapestry Specification 4.0//EN" 
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">

<page-specification class="org.appfuse.webapp.action.PersonForm">
    <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"/>

    <bean name="delegate" class="org.appfuse.webapp.action.Validator"/>
    <property name="message" persist="flash"/>

    <component id="personForm" type="Form">
        <binding name="delegate" value="ognl:beans.delegate"/>
        <binding name="clientValidationEnabled" value="true"/>
    </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>

The validation delegate (indicated with <bean name="delegate">) is an extended version of Tapestry's ValidationDelegate. This one marks fields required with an asterisk based on your .page file. It also highlights the field with appropriate messages when validation fails (if JavaScript doesn't catch it first).

There are a number of different validators you can use, this example just shows a way to validate Strings are entered. The userForm.page has examples of an EmailValidator and a PatternValidator. All input fields generated by AppGen are required by default. You can change this by modifying the extras/appgen/src/**/*_page.xdt files.

It's important to note that Tapestry does not automatically validate and return you to the form (when errors occur). You have to manually check for errors in your listener method:


    if (getDelegate().getHasErrors()) {
        return;
    }

View HTML with validation added and test [#2]

Since validation is configured in the page-specification file (a.k.a. personForm.page), whenever a field specifies a "validators" property. For those fields that don't have validation, make sure and use the @Label component for its label. Below is a non-validated field example from web/pages/userForm.html.


    <tr>
        <th>
            <label jwcid="@Label" for="phoneNumber" key="user.phoneNumber"/>
        </th>
        <td>
            <input jwcid="phoneNumberField" type="text" id="phoneNumber"/>
        </td>
    </tr>

To test the current validation configuration for the person form, run ant db-load deploy, and start Tomcat. Then go to http://localhost:8080/appfuse, login and click on the "Edit Person" link on the Main Menu.

If you erase the values in the firstName and lastName fields and click the save button, you should get the following JavaScript alert.

validation-required.png

You can see that Tapestry will only display the first validation error, rather than all of them. If you'd like to change this, I'd search the mailing list archives because I'm not aware of it being an option.

To make sure things are really working as expected, you can turn off JavaScript and ensure the server-side validation is working. This is easy in Mozilla Firefox (my favorite browser), just go to Tools → Options → Web Features and uncheck "Enable JavaScript". Now if you clear the fields and save the form, you should see the following:

validation-required-nojs.png

Add testGetPeople methods to DAO and Manager Tests [#3]

To create a List screen (also called a master screen), we need to create methods that will return all the rows from our person table. Let's start by adding tests for these methods to our PersonDaoTest and PersonManagerTest classes. I usually name this method getEntities (i.e. getUsers), but you could also use getAll or search - it's really just a matter of personal preference.

Open test/dao/**/dao/PersonDaoTest.java and add a testGetPeople method:


    public void testGetPeople() {
        person = new Person();
        List results = dao.getPeople(person);
        assertTrue(results.size() 0);
    }

The reason I'm passing in a person object to the getPeople method is to allow for filtering (based on values in person) in the future. Adding this parameter in your getPeople() method signature is optional, but the rest of this tutorial assumes you have done this.

Now open test/service/**/service/PersonManagerTest.java and add a testGetPeople method:


    public void testGetPeople() throws Exception {
        List results = new ArrayList();
        person = new Person();
        results.add(person);

        // set expected behavior on dao
        personDao.expects(once()).method("getPeople")
            .will(returnValue(results));

        List people = personManager.getPeople(null);
        assertTrue(people.size() == 1);
        personDao.verify();
    }

In order for these tests to compile, you need to add the getPeople() method to the PersonDao and PersonManager interfaces, and their implementations.

Add getPeople() method to DAO and Manager [#4]

Open src/dao/**/dao/PersonDao.java and add the getPeople() method signature:


    public List getPeople(Person person);

Now add the same method signature to src/service/**/service/PersonManager.java. Save all your files and adjust the imports in your tests. Next you need to implement the getPeople() method in your implementation classes. Open src/dao/**/dao/hibernate/PersonDaoHibernate.java and add the following method:


    public List getPeople(Person person) {
        return getHibernateTemplate().find("from Person");
    }

You'll notice here that nothing is being done with the person parameter. This is just a placeholder for now - in the future you may want to filter on it's properties using Hibernate's Query Language (HQL) or using Criteria Queries.

An example using a Criteria Query:


    Example example = Example.create(person)
                             .excludeZeroes()    // exclude zero valued properties
                             .ignoreCase();      // perform case insensitive string comparisons
    try {
        return getSession().createCriteria(Person.class)
                           .add(example)
                           .list();
    catch (Exception e) {
        throw new DataAccessException(e.getMessage());
    }
    return new ArrayList();

Now implement the getPeople() method in src/service/**/impl/PersonManagerImpl.java:


    public List getPeople(Person person) {
        return dao.getPeople(person);
    }

After saving all your changes, you should be able to run both tests by executing the following:

  • ant test-dao -Dtestcase=PersonDao
  • ant test-service -Dtestcase=PersonManager

If everything works - nice job! Now you need to add this retrieve all functionality to the web tier.

Create PersonListTest [#5]

In the last couple of tutorials, you've been working with the PersonForm to interact with your HTML form. Now you need to create a new Page class that'll simply handle getting and displaying a list of people in the database.

To begin, create test/web/**/action/PersonListTest.java and add test methods for editing and searching. The edit() method is a listener method that's invoked when you click to edit a row. This is pretty much the same as the edit() method on PersonForm. The one on PersonForm was mainly added for demonstration purposes. AppGen will won't generate an "edit" listener on the form class if you use it to generate your page classes.


package org.appfuse.webapp.action;

import org.apache.tapestry.engine.RequestCycle;
import org.appfuse.service.Manager;
import org.appfuse.model.Person;

import java.util.HashMap;
import java.util.Map;

public class PersonListTest extends BasePageTestCase {
    private PersonList page;

    protected void onSetUpBeforeTransaction() throws Exception {
        super.onSetUpBeforeTransaction();
        // these can be mocked if you want a more "pure" unit test
        Map map = new HashMap();
        map.put("personManager"(ManagerapplicationContext.getBean("personManager"));
        page = (PersonListgetPage(PersonList.class, map);
    }

    protected void onTearDownAfterTransaction() throws Exception {
        super.onTearDownAfterTransaction();
        page = null;
    }

    public void testEdit() throws Exception {
        RequestCycle cycle = new MockRequestCycle();
        cycle.setServiceParameters(new Object[] {new Long("1")});
        page.edit(cycle);
        assertFalse(page.hasErrors());
    }

    public void testSearch() throws Exception {
        assertTrue(page.getPersons().size() >= 1);
    }
}

This class will not compile until you create the PersonList class.

Create PersonList [#6]

Create src/web/**/action/PersonList.java. It should implement PageBeginRenderListener and read as follows:


package org.appfuse.webapp.action;

import java.util.List;

import org.apache.tapestry.IRequestCycle;

import org.appfuse.model.Person;
import org.appfuse.service.PersonManager;

public abstract class PersonList extends BasePage {
    public abstract PersonManager getPersonManager();

    public List getPersons() {
        return getPersonManager().getPeople();
    }
    
    public void edit(IRequestCycle cycle) {
        Object[] parameters = cycle.getListenerParameters();
        Long personId = (Longparameters[0];

        if (log.isDebugEnabled()) {
            log.debug("fetching person with personId: " + personId);
        }

        Person person = getPersonManager().getPerson(personId);

        PersonForm nextPage = (PersonFormcycle.getPage("personForm");
        nextPage.setPerson(person);
        cycle.activate(nextPage);
    }
}

Now if you run ant test-web -Dtestcase=PersonList, the test should pass.

Nice! BUILD SUCCESSFUL
Total time: 10 seconds

Create personList.html and Canoo test [#7]

There should already be a personList.html file in web/pages. If not, you can create it using AppGen. From the command-line, navigate to extras/appgen and run ant -Dobject.name=Person -Dappgen.type=pojo. This will generate both a personList.html and a personList.page in extras/appgen/build/gen/web/pages.

Copy personList.html and personList.page to web/pages and rename them people.html and people.page. Open them both for for editing. In personList.html, there should be a <table> with a "source" attribute of "ognl:persons". Change this to "ognl:people":

source="ognl:people"

This is necessary so OGNL (Tapestry's expression language) will call getPeople() on your PersonList class. There's also a "columns" attribute in this table that needs a bit of tweaking.

columns="person.id:id,person.firstName:firstName,person.lastName:lastName,"

The last comma is unecessary (but doesn't seem to effect anything). Remove it so you don't ask yourself why it's there later.

You should already have the title and heading keys (personList.title and personList.heading) for the personList.html page in web/WEB-INF/classes/ApplicationResources.properties. You should've added these keys in the previous tutorial. The column names automatically resolve to i18n messages based on their identifier (with the . in them).

Now add the page entry to web/WEB-INF/tapestry.application so that Tapestry knows where your pages and their specifications reside.

<page name="people" specification-path="pages/people.page"/>

At this point, you should be able to run ant clean deploy, start Tomcat and view this page in your browser at http://localhost:8080/appfuse/people.html.

Now that you have a List Screen, let's change the pages that are displayed after adding and deleting a new Person. In the src/web/org/**/action/PersonForm.java class, change the references to MainMenu/mainMenu to be PersonList/people.

In the last tutorial, the Canoo tests verify that the user sees the Main Menu after saving/editing a person. Since you've changed that logic, and now you're activating the PersonList, you need to update the tests. Open test/web/web-tests.xml and change any "mainMenu" references in the "PersonsTests" to be go to the people list.

<verifyTitle description="display Main Menu" text=".*${mainMenu.title}.*" regex="true"/>

Should become:

{{{<verifyTitle description="Person List appears if save successful" text=".*${personList.title}.*" regex="true"/>
}}}

You will also need to change the Canoo tests to use the list screen for editing, rather than the Main Menu's "Edit Person" link. Change the <clicklink> in "EditPerson", "SavePerson" and "DeletePerson" from

<clicklink label="Edit Person"/>

to:

<invoke description="View People" url="/people.html"/>
<clicklink description="edit first record in list" label="1"/>

To test that displaying this page works, create a new HTML test in test/web/web-tests.xml:


    <!-- 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="/people.html"/>
                <verifytitle description="we should see the personList title" 
                    text=".*${personList.title}.*" regex="true"/>
            </steps>
        </webtest>
    </target>

You'll also want to add the "SearchPeople" target to the "PersonTests" target so it will be executed along with all the other person-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 JSP tests!</echo>
    </target>

Now you can run ant test-canoo -Dtestcase=SearchPeople (or ant test-html if Tomcat isn't running) and hopefully it will result in "BUILD SUCCESSFUL". If so - nice work!

Add link to menu [#8]

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 web/pages/mainMenu.html:


    <li>
        <a href="people.html"><span key="menu.viewPeople"/></a>
    </li>

Where menu.viewPeople is an entry in web/WEB-INF/classes/ApplicationResources.properties.

menu.viewPeople=View People

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


<Menu name="PeopleMenu" title="menu.viewPeople" page="/people.html"/>

Make sure the above XML is inside the <Menus> tag, but not within another <Menu>. Then add this new menu to web/common/menu.jsp - which should now look as follows:


<%@ include file="/common/taglibs.jsp"%>

<div id="menu">
<menu:useMenuDisplayer name="ListMenu" permissions="rolesAdapter">
    <menu:displayMenu name="AdminMenu"/>
    <menu:displayMenu name="UserMenu"/>
    <menu:displayMenu name="PeopleMenu"/>
    <menu:displayMenu name="FileUpload"/>
    <menu:displayMenu name="FlushCache"/>
    <menu:displayMenu name="Clickstream"/>
</menu:useMenuDisplayer>
</div>

Now if you run ant clean deploy start Tomcat and go to http://localhost:8080/appfuse/mainMenu.html, you should see something like the screenshot below.

ValidationAndList/new-menu-item.png

Notice that there is a new link on the left side (from mainMenu.html) and on the right in our menu (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, stop tomcat and run ant clean test-all. This will run all the unit tests within your project. As a reminder, it should be easy to setup and test AppFuse from scratch using ant setup-db setup-tomcat test-all.

Happy Day!

BUILD SUCCESSFUL
Total time: 2 minutes 15 seconds

Attachments:


Go to top   Edit this page   More info...   Attach file...
This page last changed on 31-Jan-2007 02:00:38 MST by MattRaible.