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_pt
CreateManager_pt
ValidationAndList_pt




JSPWiki v2.2.33

[RSS]


Hide Menu

CreateActions_pt


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


Parte III: Criando Actions e JSPs - Um tutorial para a criação de Actions Struts e JSPs na arquitetura AppFuse.
Este tutorial depende da Parte II: Criando novos Managers.

Sobre este Tutorial

Este tutorial vai lhe mostrar como criar Actions Struts, um teste JUnit (utilizando StrutsTestCase), e um JSP para o formulário. A Action que criaremos se comunicará com o PersonManager criado no tutorial Criando Managers.

Por default, o AppFuse disponibiliza o Struts como seu framework web. Desde a versão 1.6+, podemos utilizar Spring ou WebWork como framework web. Na versão 1.7, foi adicionado suporte à utilização do JSF ou Tapestry.

Para instalar qualquer destes frameworks web ao invés de Struts, simplesmente devemos navegar para o diretório extras e entrar no diretório do framework que desejamos instalar. O arquivo README.txt deste diretório possui instruções mais detalhadas. Os tutoriais para estes frameworks estão listados abaixo.

Vamos começar criando uma nova Action Struts e JSP para nosso projeto AppFuse.

Vou dizer a vocês como faço as coisas no Mundo Real em textos como este.

Tabela de Conteúdo

  • [1] Adicionar Tags XDoclet à classe Person para gerar PersonForm
  • [2] Criar esqueletos JSPs usando XDoclet
  • [3] Criar a classe PersonActionTest para testar PersonAction
  • [4] Criar a classe PersonAction
  • [5] Rodar PersonActionTest
  • [6] Formatar o JSP para deixá-lo apresentável
  • [7] Criar WebTests Canoo para testar ações baseadas no browser

Adicionar Tags XDoclet à classe Person para gerar PersonForm [#1]

Agora vamos gerar nosso objeto PersonForm para Struts e nossa camada web. Para fazê-lo, é necessário adicionar tags XDoclet à classe Person.java para criar nosso ActionForm Struts. No JavaDoc para o arquivo Person.java, adicione as seguintes tags @struts.form (verifique o arquivo User.java se necessita de exemplos):


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

Estendemos org.appfuse.webapp.form.BaseForm porque ele possui um método toString() que nos permite chamar log.debug(formName) para nos mostrar um formato amigável do objeto Form em console.
Se você ainda não renomeou os seus pacotes "org.appfuse" para "com.company" ou se você não possui sua classe de modelo no pacote default, você precisará de uma referência completa para org.appfuse.webapp.form.BaseForm na tag @struts.form.

Criar esqueletos JSPs usando XDoclet [#2]

Nesta parte, geraremos uma página JSP para mostrar informações de um objeto Person. Esta página deve conter tags JSP Struts para renderizar linhas de tabela para cada propriedade do arquivo Person.java. A ferramenta AppGen que é utilizada para isto, baseada na ferramenta StrutsGen - que foi originalmente escrita por Erik Hatcher. São basicamente algumas classes e alguns templates XDoclet. Todos estes arquivos estão localizados no diretório extras/appgen.

Aqui estão passos simples para gerar os JSPs e arquivos de propriedades que contém os rótulos para os elementos do Formulário:

  • Da linha de comando, navegue para "extras/appgen"
  • Execute ant -Dobject.name=Person -Dappgen.type=pojo para gerar os arquivos em extras/appgen/build/gen. Na verdade, serão gerados todos os arquivos que você precisa para completar o tutorial. Mas por enquanto, vamos pegar apenas os arquivos necessários.
    • web/WEB-INF/classes/Person.properties (Rótulos para nossos elementos de formulário)
    • web/pages/personForm.jsp (arquivo JSP de detalhamento de um único objeto Person)
    • web/pages/personList.jsp (arquivo JSP para mostrar uma lista de objetos Person)
  • Copie o conteúdo do arquivo Person.properties no arquivo web/WEB-INF/classes/ApplicationResources.properties. Estas são todas as chaves que precisaremos para títulos/cabeçalhos e propriedades de formulário. Aqui está um exemplo do que deve ser adicionado ao arquivo ApplicationResources.properties (no nosso caso, ApplicationResources_pt.properties):
# --  formulário person --
personForm.id=Id
personForm.firstName=Primeiro Nome
personForm.lastName=Sobrenome

person.added=Person foi adicionado com sucesso.
person.updated=Person foi alterado com sucesso.
person.deleted=Person foi removido com sucesso.

# -- página da lista de Person --
personList.title=Lista de Pessoas
personList.heading=Pessoas

# -- página de detalhamento de Person --
personDetail.title=Detalhamento da Pessoa
personDetail.heading=Informações da Pessoa
  • Copie o arquivo personForm.jsp para web/pages/personForm.jsp. Copie o arquivo personList.jsp para web/pages/personList.jsp. Perceba que cada um dos novos nomes de arquivo são iniciados com caracteres minúsculos.
Os arquivos no diretório "pages" terminarão em "WEB-INF/pages" em tempo de publicação. O container fornece segurança para todos os arquivos dentro de WEB-INF. Isto se aplica a requisições de cliente, mas não a envios para o ActionServlet do Struts. Deixando todos os JSPs dentro de WEB-INF assegura-se que eles serão acessados apenas por Actions, e não diretamente pelo cliente ou por outrém. Isto permite que a segurança seja movida para a Action, onde pode ser tratada com mais eficiência, e fora da base da camada de apresentação.

A segurança da aplicação web para o AppFuse especifica que todos os padrões *.html devem ser protegidos (exceto /signup.html e /passwordHint.html). Isto garante que o cliente deve acessar uma Action para receber um JSP (ou ao menos os JSPs em pages).

NOTA: Se você deseja customizar o CSS para uma página em particular, você pode adicionar <body id="nomeDaPagina"/> no início do arquivo. Isto será pego pelo SiteMesh e colocado ao final da página. Você pode então customizar seu CSS em uma base página-por-página utilizando algo assim:
body#nomeDaPagina element.class { background-color: blue } 
  • Adicione chaves no arquivo ApplicationResources.properties (no nosso caso, ApplicationResources_pt.properties) para os títulos e cabeçalhos nos JSPs
Nos JSPs gerados, existem duas chaves: uma para o título (parte superior da janela do browser) e outra para o cabeçalho (cabeçalho na página). Estes campos são preenchidos acima com os nomes de chave de personDetail.title e personDetail.heading.

Acima, nós adicionamos chaves "personForm.*" para este arquivo, então porque utilizar personDetail ao invés de personForm para os títulos e cabeçalhos? A melhor razão é porque isto nos dá uma boa separação entre rótulos de formulários e textos na página. Uma outra razão é que todos os *Form.* nos dão uma boa representação de todos os campos em nossa base de dados.

Eu recentemente tive um cliente que queria que todos os campos da base de dados fossem consultáveis. Isto foi razoavelmente fácil de fazer. Eu apenas procurei todas as chaves no arquivo ApplicationResources.properties que contém "Form." e então os coloquei em um drop-down(


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.

Create PersonAction [#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!

Run PersonActionTest [#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

Clean up the JSP to make it presentable [#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).

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] Create a Canoo WebTest to test browser-like actions [#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   More info...   Attach file...
This particular version was published on 06-Nov-2006 13:52:49 MST by RafaelNami.