CreateActions_pt |
|
Your trail: |
This is version 8.
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
- Adicionar Tags XDoclet à classe Person para gerar PersonForm
- Criar esqueletos JSPs usando XDoclet
- Criar a classe PersonActionTest para testar PersonAction
- Criar a classe PersonAction
- Rodar PersonActionTest
- Formatar o JSP para deixá-lo apresentável
- Criar WebTests Canoo para testar ações baseadas no browser
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
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(os selects html). Na UI(User Interface, ou interface para o usuário), o usuário era capaz de entrar um termo de busca e selecionar a coluna que ele queria consultar. Fiquei feliz de ter seguido esta distinção Form vs. Detail naquele projeto!
Criar a classe PersonActionTest para testar PersonAction
Para criar um teste StrutsTestCase para PersonAction, comece criando um arquivo PersonActionTest.java no diretório test/web/**/action:
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 =
(PersonForm) request.getAttribute(Constants.PERSON_KEY);
assertTrue(personForm != null);
setRequestPathInfo("/savePerson");
addRequestParameter("method", "Save");
// altera o form da edição e o adiciona novamente à requisição
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();
}
}
|
Será necessário adicionar PERSON_KEY como uma variável na classe src/dao/**/Constants.java . O nome, "personForm", combina com o nome dado ao form no arquivo struts-config.xml.
/**
* O atributo de escopo de requisição que mantém o formulário da pessoa.
*/
public static final String PERSON_KEY = "personForm";
|
Se tentarmos executar este teste, teremos muitos NoSuchMethodErrors - então vamos definir os métodos edit, save, e delete na classe PersonAction.
Criar a classe PersonAction
Em src/web/**/action, crie um arquivo PersonAction.java com o seguinte conteúdo:
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("Entrando no método 'delete'");
}
ActionMessages messages = new ActionMessages();
PersonForm personForm = (PersonForm) form;
// Exceções são pegas pelo ActionExceptionHandler
PersonManager mgr = (PersonManager) getBean("personManager");
mgr.removePerson(personForm.getId());
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.deleted"));
// salva a mensagem na sessão para ela sobreviver ao redirecionamento
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("Entrando no método 'edit'");
}
PersonForm personForm = (PersonForm) form;
// se um id é passado, procura o objeto Person - senão
// não faz nada - o usuário está fazendo uma adição
if (personForm.getId() != null) {
PersonManager mgr = (PersonManager) getBean("personManager");
Person person = mgr.getPerson(personForm.getId());
personForm = (PersonForm) convert(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("Entrando no método 'save'");
}
// Extraindo atributos e parâmetros que iremos precisar
ActionMessages messages = new ActionMessages();
PersonForm personForm = (PersonForm) form;
boolean isNew = ("".equals(personForm.getId()));
if (log.isDebugEnabled()) {
log.debug("salvando person: " + personForm);
}
PersonManager mgr = (PersonManager) getBean("personManager");
Person person = (Person) convert(personForm);
mgr.savePerson(person);
// adiciona as mensagens de sucesso
if (isNew) {
messages.add(ActionMessages.GLOBAL_MESSAGE,
new ActionMessage("person.added"));
// salva a mensagem de sucesso na sessão, para sobreviver ao redirecionamento
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");
}
}
}
|
Perceba que o código acima possui várias chamadas a um método convert para um PersonForm ou para um objeto Person. O método convert está na classe BaseAction.java (que chama ConvertUtil.convert()) e
usa
BeanUtils.copyProperties
para a conversão POJOs → ActionForms e ActionForms → POJOs.
- Se utilizarmos Eclipse, devemos atualizar ("refresh") o projeto para poder ver o PersonForm. Ele está em build/web/gen, que é uma das pastas de código do projeto. Esta é a única maneira do Eclipse ver e importar PersonForm, pela razão desta classe ser gerada pelo XDoclet e não pertencer à árvore normal de pastas de código do projeto. Podemos encontrar esta classe em build/web/gen/org/appfuse/webapp/form/PersonForm.java.
- Na classe BaseAction podemos registrar conversores adicionais (i.e. DateConverter) para que o BeanUtils.copyProperties saiba como fazer a conversão Strings → Objects. Se temos Lists em nossos POJOs (i.e. para relacionamentos pai-filho), teremos que converter manualmente estes utilizando o método convertLists(java.lang.Object).
Agora temos que adicionar os action-mapping de envio edit e savePerson, ambos especificados em PersonActionTest. Para fazê-lo, adicionaremos algumas tags XDoclet na parte superior do arquivo PersonAction.java. Devemos fazer isto logo acima da declaração de classe. Já devemos ter a tag XDoclet para o action-mapping editPerson, mas o mostrarei aqui para que todos possamos ver todas as tag XDoclet na parte superior da classe.
/**
* @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 {
|
A maior diferença entre os action-mappings editPerson e savePerson é que savePerson possui a validação ligada (veja validation="true") na tag XDoclet acima. Note que o atributo "input" deve se referir a um envio, não podendo ser um caminho(i.e. /editPerson.html). Se preferirmos utilizar o caminho para salvar tanto para edição quanto para persistência, isto é possível também. Apenas devemos nos certificar que o validate="false", e que no método "save" - devemos chamar form.validate() e tratar os erros adequadamente.
Você perceberá que o código que estamos utilizando para chamar o PersonManager é o mesmo código utilizado em PersonManagerTest. Tanto PersonAction quanto PersonManagerTest são clientes de PersonManagerImpl, então isto faz sentido perfeitamente.
Tudo está quase pronto neste tutorial, vamos então rodar os testes!
Rodar PersonActionTest
Se olharmos ao nosso PersonActionTest, todos os testes dependem da tupla com o id=1 no banco de dados (e o testRemove depende do id=2), então adicione ao nosso arquivo de amostra de dados (metadata/sql/sample-data.xml). Eu adicionaria ao final do arquivo - ordem não é importante porque (atualmente) não se relaciona com outras tabelas.
<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>
O DBUnit carrega este arquivo antes de rodar qualquer teste nosso, então estas tuplas serão disponíveis para o PersonActionTest.
Agora se rodarmos ant test-web -Dtestcase=PersonAction - tudo deve rodar como planejado. Certifique-se que o Tomcat não está rodando antes de tentar isto.
BUILD SUCCESSFUL
Total time: 1 minute 21 seconds
Agora formataremos o personForm.jsp gerado. Mudaremos o atributo action do <html:form> para "savePerson" para ligar a validação quanto formos persistir. Mudaremos também o atributo focus de focus="" para focus="firstName" para o cursor estar no campo firstName quanto a página carregar (isto é feito via Javascript).
Agora se executarmos ant db-load deploy, iniciarmos o Tomcat e apontarmos o browser para http://localhost:8080/appfuse/editPerson.html?id=1, veremos algo como:
NOTA: Utilize o alvo(target) deploy-web se foram alterados apenas arquivos do diretórios web. Senão, use deploy que compila e publica.
Finalmente, para tornarmos a página mais amigável, poderemos adicionar mensagens aos nossos usuários na parte superior do formulário, o que podemos fazer facilmente adicionando texto (utilizando <fmt:message>) na parte superior da página personForm.jsp.
[Opcional] Criar WebTests Canoo para testar ações baseadas no browser
A parte final (opcional) para este tutorial é criar um Canoo WebTest para testar os JSPs.
- Digo que estes testes são opcionais, porque podemos rodar os mesmos testes no browser.
Podemos utilizar as seguintes URLs para testar as diferentes ações para adicionar, editar e persistir um Person.
Testes Canoo são simples, configurados apenas por um arquivo XML. Para adicionarmos testes para as operações CRUD(add, edit, save e delete), abriremos test/web/web-tests.xml e adicionaremos o seguinte XML. Note que este fragmento possui um alvo(target) nomeado PersonTests que executa todos os testes relacionados.
- Utilizo notação composta(CamelCase) para os nomes de alvos (target)(vs. letras minúsculas tradicionais, separadas por hífen) por causa das execuções utilizando o JUnit, como por exemplo, -Dtestcase=Name, Percebi que estou acostumado com a notação composta para meus testes JUnit.
<!-- roda os testes relacionados com Person -->
<target name="PersonTests"
depends="EditPerson,SavePerson,AddPerson,DeletePerson"
description="Chama e executa todos os casos de teste relacionados com Person (targets)">
<echo>Rodou todos os testes JSP de Person com sucesso!</echo>
</target>
<!-- Verifica se a tela de edição da pessoa mostra sem erros -->
<target name="EditPerson"
description="Testa a edição das informações de uma pessoa existente">
<webtest name="editPerson">
&config;
<steps>
&login;
<invoke description="clica no link de edição de pessoa" url="/editPerson.html?id=1"/>
<verifytitle description="devemos ver o título do personDetail"
text=".*${personDetail.title}.*" regex="true"/>
</steps>
</webtest>
</target>
<!-- Edita a pessoa e então salva -->
<target name="SavePerson"
description="Testes editando e salvando a pessoa">
<webtest name="savePerson">
&config;
<steps>
&login;
<invoke description="clica no link de edição de pessoas" url="/editPerson.html?id=1"/>
<verifytitle description="devemos ver o título da edição de pessoas"
text=".*${personDetail.title}.*" regex="true"/>
<setinputfield description="seta o último nome" name="lastName" value="Canoo"/>
<clickbutton label="Save" description="Clica no botão Save"/>
<verifytitle description="A página reaparece se tudo ocorreu com sucesso"
text=".*${personDetail.title}.*" regex="true"/>
<verifytext description="verifica a mensagem de sucesso" text="${person.updated}"/>
</steps>
</webtest>
</target>
<!-- Adiciona uma nova Pessoa -->
<target name="AddPerson"
description="Adiciona uma nova Pessoa">
<webtest name="addPerson">
&config;
<steps>
&login;
<invoke description="clica no botão de Adição" url="/editPerson.html"/>
<verifytitle description="deveremos ver o título do detalhamento da pessoa"
text=".*${personDetail.title}.*" regex="true"/>
<setinputfield description="seta o firstName" name="firstName" value="Abbie"/>
<setinputfield description="seta o lastName" name="lastName" value="Raible"/>
<clickbutton label="${button.save}" description="Clica no botão 'Salvar'"/>
<verifytitle description="O menu principal reaparece se tudo ocorreu com sucesso"
text=".*${mainMenu.title}.*" regex="true"/>
<verifytext description="verifica a mensagem de sucesso" text="${person.added}"/>
</steps>
</webtest>
</target>
<!-- Deleta uma pessoa existente -->
<target name="DeletePerson"
description="Deleta uma pessoa existente">
<webtest name="deletePerson">
&config;
<steps>
&login;
<invoke description="clica no link de edição da pessoa" url="/editPerson.html?id=1"/>
<clickbutton label="${button.delete}" description="Clica no botão 'Deletar'"/>
<verifytitle description="Mostra o menu principal se tudo ocorreu com sucesso" text=".*${mainMenu.title}.*" regex="true"/>
<verifytext description="verifica a mensagem de sucesso" text="${person.deleted}"/>
</steps>
</webtest>
</target>
|
Após adicionar isto, seremos capazes de rodar ant test-canoo -Dtestcase=PersonTests com o Tomcat rodando ou ant test-jsp -Dtestcase=PersonTests se quisermos que o Ant inicialize e pare o Tomcat. Para incluir o PersonTests quando todos os testes canoo são executados, adicionaremos sua dependência ao alvo(target) "run-all-tests".
Você perceberá que não há registro(logging) na janela cliente do canoo. Se desejarmos ver o que o Canoo está fazendo, podemos adicionar o seguinte código entre </canoo> e </target> no final do alvo(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
Próximo: Parte IV: Adicionando Validação e Tela de Listagem - Adicionando lógica de validação para o personForm para que o firstName e o lastName sejam campos obrigatórios, e adicionando uma tela de listagem para mostrar todas as tuplas de pessoas no banco de dados.
Attachments:
|