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


Referenced by
Articles
Articles_cn
Articles_de
Articles_pt
Articles_zh
CreateActions
CreateActions_de
CreateActions_it
CreateActions_pt
CreateActions_zh
...and 1 more




JSPWiki v2.2.33

[RSS]


Hide Menu

TapestryPages


This is version 3. It is not the current version, and thus it cannot be edited.
[Back to current version]   [Restore this version]


Part III: Creating Tapestry Pages and HTML Templates - A HowTo for creating Tapestry Pages and HTML Templates in an AppFuse project.
This tutorial depends on Part II: Creating new Managers.

About this Tutorial

This tutorial will show you how to create Tapestry Pages and HTML Templates. It'll also demonstrate writing a JUnit Test to test PersonForm. The Page class we create will talk to the PersonManager we created in the Creating Managers tutorial. In most web frameworks, the controller logic is contain in an "Action" of some sort. However, with Tapestry, they're commonly referred to as "Page". The methods withing these pages are called listeners. This tutorial is not going to teach you a whole lot about how Tapestry works, but it will get you up and running quickly with it. 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!
I will tell you how I do stuff in the Real World in text like this.

Let's get started by creating a new Page and HTML Template in AppFuse's architecture. If you haven't installed the Tapestry module at this point, do so by running ant install-tapestry.

Table of Contents

  • [1] Create pageForm.html using XDoclet
  • [2] Create PersonFormTest to test PersonForm
  • [3] Create PersonForm
  • [4] Run PersonFormTest
  • [5] View the form in your browser
  • [6] Create Canoo WebTests to test browser-like actions

Create pageForm.html Template using XDoclet [#1]

In this step, you'll generate a an HTML Template to display information from the Person object. It will contain Tapestry's syntax for rendering form elements - which is just HTML with "jwcid" attributes. The AppGen tool that's used to do this is based off a StrutsGen tool - which was originally written by Erik Hatcher. It's basically just a couple of classes and a bunch of XDoclet templates. All these files are located in extras/appgen.

Here are the simple steps to generating the HTML Template and a properties file containing the labels for the form elements:

  • From the command-line, navigate to "extras/appgen"
  • Execute ant -Dobject.name=Person -Dappgen.type=pojo to generate a bunch of files in extras/appgen/build/gen. In fact, it'll generate all the files you need to complete this tutorial. However, let's just grab the ones you need.
    • web/WEB-INF/classes/Person.properties (labels for your form elements)
    • web/pages/personForm.html (HTML Template file for viewing a single Person)
    • web/pages/personForm.page (Page Specification for previous page)
    • web/pages/personList.html (HTML Template file for viewing a list of People)
    • web/pages/personList.page (Page Specification for previous page)
  • Copy the contents of Person.properties into web/WEB-INF/classes/ApplicationResources_en.properties. These are all the keys you will need for titles/headings and form properties. Here is an example of what you should add to ApplicationResources_en.properties:
# -- person form --
personForm.id=Id
personForm.firstName=First Name
personForm.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
  • Copy personForm.html and personForm.page to web/pages/personForm.jsp and web/pages/personForm.page. Copy personList.html and personList.page to web/pages/personList.jsp and web/pages/personList.page. Notice that each of the new filename's first character is lowercase.
The files in the "pages" directory will end up in "WEB-INF/pages" at deployment time. The container provides security for all files below WEB-INF. This applies to client requests, but not to forwards from Tapestry's ApplicationServlet. Placing all HTML templates below WEB-INF ensures they are only accessed through Pages, and not directly by the client or each other. This allows security to be moved up into the Page, where it can be handled more efficiently, and out of the base presentation layer.

The web application security for AppFuse specifies that all *.html url-patterns should be protected (except for /signup.html and /passwordHint.html). This guarantees that clients must go through a Page to get to a template (or at least the ones in pages).

NOTE: If you want to customize the CSS for a particular page, you can add <body id="pageName"/> to the top of the file (right after the </content> tag). 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 PersonFormTest to test PersonForm [#2]

To create a JUnit Test for PersonForm, start by creating a PersonFormTest.java file in the test/web/**/action directory.


package org.appfuse.webapp.action;

import java.util.ResourceBundle;

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

public class PersonFormTest extends BasePageTestCase {
    private PersonForm page;
    private Manager manager;

    protected void setUp() throws Exception {    
        super.setUp();
        page = (PersonFormgetPage(PersonForm.class);
        // unfortunately this is a required step if you're calling 
        // getMessage in the page class
        page.setBundle(ResourceBundle.getBundle(MESSAGES));
        page.setValidationDelegate(new Validator());

        // this manager can be mocked if you want a more "pure" unit test
        manager = (Managerctx.getBean("manager");
        page.setManager(manager);
        // default request cycle
        page.setRequestCycle(getCycle(request, response));
    }

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

    public void testAdd() throws Exception {
        Person person = new Person();
        // set required fields
        person.setFirstName("firstName");
        person.setLastName("lastName");
        page.setPerson(person);

        page.save(page.getRequestCycle());
        assertFalse(page.hasErrors());
    }

    public void testEdit() throws Exception {
        MockRequestCycle cycle = (MockRequestCyclepage.getRequestCycle();
        cycle.addServiceParameter(new Long(1));
        
        page.edit(cycle);

        assertNotNull(page.getPerson());
        assertFalse(page.hasErrors());
    }
    
    public void testSave() {
        assertNotNull(manager);
        Person person = (Personmanager.getObject(Person.class, new Long(1));

        // update fields
        person.setFirstName("firstName");
        person.setLastName("lastName");
        page.setPerson(person);

        page.save(page.getRequestCycle());
        assertFalse(page.hasErrors());
    }

    public void testRemove() throws Exception {
        Person person = new Person();
        person.setId(new Long(2));
        page.setPerson(person);

        page.delete(page.getRequestCycle());
        assertFalse(page.hasErrors());
    }
}

Nothing will compile at this point because you need to create the PersonForm that you're referring to in this test.

Create PersonForm [#3]

In src/web/**/action, create a PersonForm.java file with the following contents:


package org.appfuse.webapp.action;

import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.event.PageEvent;
import org.apache.tapestry.event.PageRenderListener;

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

public abstract class PersonForm extends BasePage implements PageRenderListener {
    public abstract PersonManager getPersonManager();
    public abstract void setPersonManager(PersonManager mgr);
    public abstract void setPerson(Person person);
    public abstract Person getPerson();

    public void pageBeginRender(PageEvent event) {
        if ((getPerson() == null&& !event.getRequestCycle().isRewinding()) {
            setPerson(new Person());
        else if (event.getRequestCycle().isRewinding()) { // add
            setPerson(new Person());
        }
    }

    public void cancel(IRequestCycle cycle) {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'cancel' method");
        }

        cycle.activate("mainMenu");
    }

    public void delete(IRequestCycle cycle) {
        if (log.isDebugEnabled()) {
            log.debug("entered 'delete' method");
        }

        getPersonManager().removePerson(getPerson().getId().toString());

        MainMenu nextPage = (MainMenucycle.getPage("mainMenu");
        nextPage.setMessage(getMessage("person.deleted"));
        cycle.activate(nextPage);
    }

    public void edit(IRequestCycle cycle) {
        Object[] parameters = cycle.getServiceParameters();
        Long id = (Longparameters[0];
        
        if (log.isDebugEnabled()) {
            log.debug("getting person with id: " + id);
        }
        
        setPerson(getPersonManager().getPerson(id.toString()));
        cycle.activate(this);
    }
    
    public void save(IRequestCycle cycle) {
        if (getValidationDelegate().getHasErrors()) {
            return;
        }

        boolean isNew = (getPerson().getId() == null);

        getPersonManager().savePerson(getPerson());

        String key = (isNew"person.added" "person.updated";

        if (isNew) {
            MainMenu nextPage = (MainMenucycle.getPage("mainMenu");
            nextPage.setMessage(getMessage(key));
            cycle.activate(nextPage);
        else {
            PersonForm nextPage = (PersonFormcycle.getPage("personForm");
            nextPage.setMessage(getMessage(key));
            cycle.activate("personForm")// return to current page
        }
    }
}

You'll 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_en.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 setMessage(format(key, stringtoreplace)) method.

You might notice that the code we're using to call the PersonManager is the same as the code we used in our PersonManagerTest. Both PersonForm and PersonManagerTest are clients of PersonManagerImpl, so this makes perfect sense.

Now you need to tell Tapestry that this page exists. To do this, add a page entry in the web/WEB-INF/tapestry.application file.


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

If you keep your HTML templates in the WEB-INF directory, the above step is unnecessary. Hopefully a future version of Tapestry will allow you to set a global specification-path.

The PersonForm returns the "MainMenu" page from the cancel()(, delete() and save() methods. In the next tutorial, you will change this to be the PersonList.

Run the PersonFormTest [#4]

If you look at our PersonFormTest, all the tests depend on having a record with id=1 in the database (and testRemove depends on id=2), so add those records to your sample data file (metadata/sql/sample-data.xml). I'd just add it at the bottom - order is not important since it (currently) does not relate to any other tables.
  <table name='person'>
    <column>id</column>
    <column>first_name</column>
    <column>last_name</column>
    <row>
      <value>1</value>
      <value>Matt</value>
      <value>Raible</value>
    </row>
    <row>
      <value>2</value>
      <value>James</value>
      <value>Davidson</value>
    </row>
  </table>

DBUnit loads this file before we running any of the tests, so this record will be available to your Form test.

Make sure are in the base directory of your project and all files are saved. If you run ant test-web -Dtestcase=PersonForm - everything should work as planned.

BUILD SUCCESSFUL
Total time: 12 seconds

View the form in your browser [#5]

Now if you execute ant db-load deploy, start Tomcat and point your browser to http://localhost:8080/appfuse/personForm.html, you should see something like this:
JSFBeans/personForm-final.png
NOTE: Tapestry will automatically put focus on the first required field of the form. If you want to change this, see the mailing list archives.

With Tapestry, the URLs are kinda ugly, but they hold a lot of information in them. Unlike other frameworks, where you typically call methods in an Action, with Tapestry - you call listeners in a Page class. To call the "edit" listener in the PersonForm class, add the following to web/pages/mainMenu.html.

    <a jwcid="@DirectLink" listener="ognl:requestCycle.getPage('personForm').listeners.edit" 
        parameters="ognl:new java.lang.Long(1)">Edit Person</a>

Finally, to make this page more user friendly, you may want to add a message for your users at the top of the form, but this can easily be done by adding text (using <span key="..."/>) at the top of the personForm.html page.

[Optional] Create a Canoo WebTest to test browser-like actions [#6]

The final (optional) step in this tutorial is to create a Canoo WebTest to test the HTML Templates.
I say this step is optional, because you can run the same tests through your browser.

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

  • Add - http://localhost:8080/appfuse/personForm.html.
  • Edit - Use the link you created on the Main Menu (make sure and run ant db-load first).
  • Delete - Use the edit link above and click on the Delete button.
  • Save - Click the edit link on the Main Menu (run ant db-load first if you deleted already) and then click the Save button.

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 test/web/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.

I use CamelCase target names (vs. the traditional lowercase, dash-separated) because when you're typing -Dtestcase=Name, I've found that I'm used to doing CamelCase for my JUnit Tests.


    <!-- runs person-related tests -->
    <target name="PersonTests"
        depends="EditPerson,SavePerson,AddPerson,DeletePerson"
        description="Call and executes all person test cases (targets)">
        <echo>Successfully ran all Person HTML Template tests!</echo>
    </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;
                <clicklink label="Edit Person"/>
                <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;
                <clicklink label="Edit Person"/>
                <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="Canoo"/>
                <setinputfield description="set lastName" name="lastNameField" value="WebTest"/>
                <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="View Person Form" 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="Jack"/>
                <setinputfield description="set lastName" name="lastNameField" value="Raible"/>
                <clickbutton label="${button.save}" description="Click button 'Save'"/>
                <verifytitle description="Main Menu appears if save successful" 
                    text=".*${mainMenu.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;
                <clicklink label="Edit Person"/>
                <clickbutton label="${button.delete}" description="Click button 'Delete'"/>
                <verifytitle description="display Main Menu" text=".*${mainMenu.title}.*" regex="true"/>
                <verifytext description="verify success message" text="${person.deleted}"/>
            </steps>
        </webtest>
    </target>

After adding this, you should be able to run ant test-canoo -Dtestcase=PersonTests with Tomcat running or ant test-html -Dtestcase=PersonTests if you want Ant to start/stop Tomcat for you. To include the PersonTests when all Canoo tests are run, add it as a dependency to the "run-all-tests" target.

You'll notice that there's no logging in the client-side window by Canoo. If you'd like to see what it's doing, you can add tweak the log4j settings in web/WEB-INF/classes/log4j.properties.

BUILD SUCCESSFUL
Total time: 27 seconds


Next Up: Part IV: Adding Validation and List Screen - Explains the validation logic on the personForm and how the firstName and lastName are required fields. You'll also add a list screen to display all person records in the database.



Go to top   More info...   Attach file...
This particular version was published on 06-Nov-2006 13:52:42 MST by MattRaible.