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_de
CreateManager_de




JSPWiki v2.2.33

[RSS]


Hide Menu

CreateActions_de


Teil III: Actions and JSPs erstellen - Eine Anleitung zu Erstellung von Struts Actions und JSPs in der AppFuse Architektur.

Dieses Tutorial benöötigt Teil II: Neue Manager erstellen.

Über dieses Tutorial

Dieses Tutorial erläutert, wie man eine Struts Action, einen JUnit Test (unter Verwendung von StrutsTestCase) und eine JSP Datei für die Form erstellt. Die Action, die wir erzeugen werden, wird mit dem PersonManager, den wir im Tutorial Manager erstellen erstellt haben, kommunizieren.

Standardmäßig wird AppFuse mit Struts als Web Framework ausgeliefert. Seit der Version 1.6+ kann man auch Spring oder WebWork als Web Framework verwenden. In Version 1.7 wurde die Unterstützung für den Einsatz von JSF und Tapestry hinzugefügt.

Um eines dieser Web Frameworks an der Stelle von Struts zu nutzen, wechselt man einfach in das Verzeichnis extras und dort in das Verzeichnis des Frameworks, welches man installieren will. Die README.txt Datei in diesem Verzeichnis enthält weitere Informationen. Die Tutorials sind im folgenden aufgelistet.

Beginnen wir mit dem Erzeugen einer neuen Struts Action und einer JSP Seite.

In diesem Textformat werde ich erläutern, wie man Dinge wirklich macht.

Inhaltsverszeichnis

  • [1] XDoclet Tags zum Person Objekt hinzufügen, um die PersonForm zu erzeugen
  • [2] Das Gerüst der JSPs mit der Hilfe von XDoclet erzeugen
  • [3] PersonActionTest erzeugen, um die PersonAction zu testen
  • [4] PersonAction erzeugen
  • [5] PersonActionTest ausführen
  • [6] Die JSP säubern, um sie präsentabel zu machen
  • [7] Canoo WebTests erstellen, um browserähnliche Aktionen zu testen

XDoclet Tags zum Person Objekt hinzufügen, um die PersonForm zu erzeugen [#1]

Beginnen wir mit dem Erzeugen unserer PersonForm für Struts und unserem unserer Web Schicht. Um dies zu tun, müssen wir zu unserem Person Objekt XDoclet Tags hinzufügen, um unsere Struts ActionForm zu erzeugen. Hierzu fügt man in den JavaDocbereich der Person.java Datei folgenden @struts.form Tag hinzu (Ein Beispiel findet man in der Datei User.java):


* @struts.form include-all="true" extends="BaseForm"

We extend org.appfuse.webapp.form.BaseForm because it has a toString() method that allows us to call log.debug(formName) to print out a reader-friendly view of the Form object.
If you haven't renamed the "org.appfuse" packages to "com.company" or otherwise don't have your model class in the default package, you may need to fully-qualify the reference to org.appfuse.webapp.form.BaseForm in the @struts.form tag.

Das Gerüst der JSPs mit der Hilfe von XDoclet erzeugen [#2]

In this step, you'll generate a JSP page to display information from the Person object. It will contain Struts' JSP tags that render table rows for each property in Person.java. 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 JSP 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.jsp (JSP file for viewing a single Person)
    • web/pages/personList.jsp (JSP file for viewing a list of People)
  • Copy the contents of Person.properties into web/WEB-INF/classes/ApplicationResources.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.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.jsp to web/pages/personForm.jsp. Copy personList.jsp to web/pages/personList.jsp.
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 Struts' ActionServlet. Placing all JSPs below WEB-INF ensures they are only accessed through Actions, and not directly by the client or each other. This allows security to be moved up into the Action, 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 an Action to get to a JSP (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. 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 } 
  • Add keys in ApplicationResources.properties the titles and headings in the JSPs
In the generated JSPs, there are two keys for the title (top of the browser window) and the header (heading in the page). These fields are provided above with key names of personDetail.title and personDetail.heading.

Just above, we added "personForm.*" keys to this file, so why do I use personDetail instead of personForm for the titles and headings? The best reason is because it gives a nice separation between form labels and text on the page. Another reason is because all the *Form.* give you a nice representation of all the fields in your database.

I recently had a client who wanted all fields in the database searchable. This was fairly easy to do. I just looked up all the keys in ApplicationResources.properties which contained "Form." and then put them into a drop-down. On the UI, the user was able to enter a search term and select the column they wanted to search. I was glad I followed this Form vs. Detail distinction on that project!

PersonActionTest erzeugen, um die PersonAction zu testen [#3]

To create a StrutsTestCase Test for PersonAction, start by creating a PersonActionTest.java file in the test/web/**/action directory:


package org.appfuse.webapp.action;

import org.appfuse.Constants;
import org.appfuse.webapp.form.PersonForm;

public class PersonActionTest extends BaseStrutsTestCase {
    
    public PersonActionTest(String name) {
        super(name);
    }

    public void testEdit() throws Exception {
        setRequestPathInfo("/editPerson");
        addRequestParameter("method""Edit");
        addRequestParameter("id""1");
        actionPerform();

        verifyForward("edit");
        assertTrue(request.getAttribute(Constants.PERSON_KEY!= null);
        verifyNoActionErrors();
    }

    public void testSave() throws Exception {
        setRequestPathInfo("/editPerson");
        addRequestParameter("method""Edit");
        addRequestParameter("id""1");

        actionPerform();

        PersonForm personForm =
            (PersonFormrequest.getAttribute(Constants.PERSON_KEY);
        assertTrue(personForm != null);
        
        setRequestPathInfo("/savePerson");
        addRequestParameter("method""Save");

        // update the form from the edit and add it back to the request
        personForm.setLastName("Feltz");
        request.setAttribute(Constants.PERSON_KEY, personForm);

        actionPerform();

        verifyForward("edit");
        verifyNoActionErrors();
    }

    public void testRemove() throws Exception {
        setRequestPathInfo("/editPerson");
        addRequestParameter("method""Delete");
        addRequestParameter("id""2");
        actionPerform();

        verifyForward("mainMenu");

        verifyNoActionErrors();
    }
}

You will need to add PERSON_KEY as a variable to the src/dao/**/Constants.java class. The name, "personForm", matches the name given to the form in the struts-config.xml file.


    /**
     * The request scope attribute that holds the person form.
     */
    public static final String PERSON_KEY = "personForm";

If you try to run this test, you will get a number of NoSuchMethodErrors - so let's define the edit, save, and delete methods in the PersonAction class.

PersonAction erzeugen [#4]

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


package org.appfuse.webapp.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.action.ActionMessage;
import org.apache.struts.action.ActionMessages;

import org.appfuse.model.Person;
import org.appfuse.service.PersonManager;
import org.appfuse.webapp.form.PersonForm;

/**
 * @struts.action name="personForm" path="/editPerson" scope="request"
 *  validate="false" parameter="method" input="mainMenu"
 */
public final class PersonAction extends BaseAction {
    
    public ActionForward cancel(ActionMapping mapping, ActionForm form,
                                HttpServletRequest request,
                                HttpServletResponse response)
    throws Exception {
        return mapping.findForward("mainMenu");
    }

    public ActionForward delete(ActionMapping mapping, ActionForm form,
                                HttpServletRequest request,
                                HttpServletResponse response)
    throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'delete' method");
        }

        ActionMessages messages = new ActionMessages();
        PersonForm personForm = (PersonFormform;

        // Exceptions are caught by ActionExceptionHandler
        PersonManager mgr = (PersonManagergetBean("personManager");
        mgr.removePerson(personForm.getId());

        messages.add(ActionMessages.GLOBAL_MESSAGE,
                     new ActionMessage("person.deleted"));

        // save messages in session, so they'll survive the redirect
        saveMessages(request.getSession(), messages);

        return mapping.findForward("mainMenu");
    }

    public ActionForward edit(ActionMapping mapping, ActionForm form,
                              HttpServletRequest request,
                              HttpServletResponse response)
    throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'edit' method");

        }

        PersonForm personForm = (PersonFormform;

        // if an id is passed in, look up the user - otherwise
        // don't do anything - user is doing an add
        if (personForm.getId() != null) {
            PersonManager mgr = (PersonManagergetBean("personManager");
            Person person = mgr.getPerson(personForm.getId());
            personForm = (PersonFormconvert(person);
            updateFormBean(mapping, request, personForm);
        }

        return mapping.findForward("edit");
    }

    public ActionForward save(ActionMapping mapping, ActionForm form,
                              HttpServletRequest request,
                              HttpServletResponse response)
    throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Entering 'save' method");
        }

        // Extract attributes and parameters we will need
        ActionMessages messages = new ActionMessages();
        PersonForm personForm = (PersonFormform;
        boolean isNew = ("".equals(personForm.getId()));

        if (log.isDebugEnabled()) {
            log.debug("saving person: " + personForm);
        }

        PersonManager mgr = (PersonManagergetBean("personManager");
        Person person = (Personconvert(personForm);
        mgr.savePerson(person);

        // add success messages
        if (isNew) {
            messages.add(ActionMessages.GLOBAL_MESSAGE,
                         new ActionMessage("person.added"));

            // save messages in session to survive a redirect
            saveMessages(request.getSession(), messages);

            return mapping.findForward("mainMenu");
        else {
            messages.add(ActionMessages.GLOBAL_MESSAGE,
                         new ActionMessage("person.updated"));
            saveMessages(request, messages);

            return mapping.findForward("edit");
        }
    }
}

You'll notice in the code above that there are many calls to to convert a PersonForm or a Person object. The convert method is in BaseAction.java (which calls ConvertUtil.convert()) and uses BeanUtils.copyProperties to convert POJOs → ActionForms and ActionForms → POJOs.

If you are running Eclipse, you might have to "refresh" the project in order to see PersonForm. It lives in build/web/gen, which should be one of your project's source folders. This is the only way for Eclipse to see and import PersonForm, since it is generated by XDoclet and does not live in your regular source tree. You can find it at build/web/gen/org/appfuse/webapp/form/PersonForm.java.
In BaseAction you can register additional Converters (i.e. DateConverter) so that BeanUtils.copyProperties knows how to convert Strings → Objects. If you have Lists on your POJOs (i.e. for parent-child relationships), you will need to manually convert those using the convertLists(java.lang.Object) method.

Now you need to add the edit forward and the savePerson action-mapping, both which are specified in in the PersonActionTest. To do this, add a couple more XDoclet tags to the top of the PersonAction.java file. Do this right above the class declaration. You should already have the XDoclet tag for the editPerson action-mapping, but I'm showing it here so you can see all the XDoclet tags at the top of this class.


/**
 * @struts.action name="personForm" path="/editPerson" scope="request"
 *  validate="false" parameter="method" input="mainMenu"
 
 * @struts.action name="personForm" path="/savePerson" scope="request"
 *  validate="true" parameter="method" input="edit"
 
 * @struts.action-forward name="edit" path="/WEB-INF/pages/personForm.jsp"
 */
public final class PersonAction extends BaseAction {

The main difference between the editPerson and savePerson action-mappings is that savePerson has validation turned on (see validation="true") in the XDoclet tag above. Note that the "input" attribute must refer to a forward, and cannot be a path (i.e. /editPerson.html). If you'd prefer to use the save path for both edit and save, that's possible too. Just make sure validate="false", and then in your "save" method - you'll need to call form.validate() and handle errors appropriately.

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

Everything is almost done for this tutorial, let's get to running the tests!

PersonActionTest ausführen [#5]

If you look at our PersonActionTest, all the tests depend on having a record with id=1 in the database (and testRemove depends on id=2), so add that to our sample data file (metadata/sql/sample-data.xml). I'd 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 run any of our tests, so this record will be available to the PersonActionTest.

Now if you run ant test-web -Dtestcase=PersonAction - everything should work as planned. Make sure Tomcat isn't running before you try this.

BUILD SUCCESSFUL
Total time: 1 minute 21 seconds

Die JSP säubern, um sie präsentabel zu machen [#6]

Now let's clean up the generated personForm.jsp. Change the action of the <html:form> to be "savePerson" so validation will be turned on when saving. Also, change the focus attribute from focus="" to focus="firstName" so the cursor will be in the firstName field when the page loads (this is done with JavaScript).

Another thing you will need to do is comment out the following lines at the bottom of the personForm.jsp. This is because the Validator will throw an exception if a formName is specified and no validation rules exist for it.

Personally, I think this is a bug, but the Struts Committers disagreed.
<html:javascript formName="personForm" cdata="false"
    dynamicJavascript="true" staticJavascript="false"/>
<script type="text/javascript" 
    src="<html:rewrite page="/scripts/validator.jsp"/>"></script>

Now if you execute ant db-load deploy, start Tomcat and point your browser to http://localhost:8080/appfuse/editPerson.html?id=1, you should see something like this:

personForm-final.png

NOTE: Use the deploy-web target if you've changed any files under the web directory. Otherwise, use deploy which compiles and deploys.

Finally, to make this page more user friendly, you may want to add a message for your users at the top of the form, which can easily be done by adding text (using <fmt:message>) at the top of the personForm.jsp page.

[Optional] Canoo WebTests erstellen, um browserähnliche Aktionen zu testen [#7]

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

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

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 JSP 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;
            <invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
            <verifytitle description="we should see the personDetail title"
                text=".*${personDetail.title}.*" regex="true"/>
        </steps>
    </webtest>
</target>

<!-- Edit a person and then save -->
<target name="SavePerson"
    description="Tests editing and saving a user">
    <webtest name="savePerson">
        &config;
        <steps>
            &login;
            <invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
            <verifytitle description="we should see the personDetail title"
                text=".*${personDetail.title}.*" regex="true"/>
            <setinputfield description="set lastName" name="lastName" value="Canoo"/>
            <clickbutton label="Save" description="Click Save"/>
            <verifytitle description="Page re-appears if save successful"
                text=".*${personDetail.title}.*" regex="true"/>
            <verifytext description="verify success message" text="${person.updated}"/>
        </steps>
    </webtest>
</target>

<!-- Add a new Person -->
<target name="AddPerson"
    description="Adds a new Person">
    <webtest name="addPerson">
        &config;
        <steps>
            &login;
            <invoke description="click Add Button" url="/editPerson.html"/>
            <verifytitle description="we should see the personDetail title"
                text=".*${personDetail.title}.*" regex="true"/>
            <setinputfield description="set firstName" name="firstName" value="Abbie"/>
            <setinputfield description="set lastName" name="lastName" 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;
            <invoke description="click Edit Person link" url="/editPerson.html?id=1"/>
            <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-jsp -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 the following between </webtest> and </target> at the end of each target.

<loadfile property="web-tests.result" 
    srcFile="${test.dir}/data/web-tests-result.xml"/>
<echo>${web-tests.result}</echo>
BUILD SUCCESSFUL
Total time: 11 seconds


Next Up: Part IV: Adding Validation and List Screen - Adding validation logic to the personForm so that firstName and lastName are required fields and adding a list screen to display all person records in the database.


Attachments:


Go to top   Edit this page   More info...   Attach file...
This page last changed on 06-Nov-2006 13:53:00 MST by PeterSchneider-Manzell.