[linu.com.br] - [parte 1] - [parte 2] - [parte 3] - [parte 4] - lgapontes@gmail.com |
Por fim, a camada de apresentação.
Clique aqui caso você queira ler a 1ª parte desse material. Ou clique aqui para ler a 2ª parte. Para acessar a 4ª parte, clique aqui.
Vamos lá...
Vamos primeiro salientar os novos recursos que o Java EE 6 oferece para a camada View de sistemas enterprise. Todos esses recursos foram projetados dentro da nova versão do JavaServer Faces: a JSF 2.0. As anotações oferecidas pelo pacote javax.validation.constraints nós já havíamos comentado. Trata-se de anotações que são inseridas no modelo do sistema e validadas na comunicação entre as páginas e os Beans. Utilizamos nesse projeto, como exemplo, as anotações @NotNull e @Size. Veremos na prática como as mensagens associadas ao não cumprimento dessas anotações serão disparadas, automaticamente, para o componentes de mensagens (como o <h:messages>) do JSF.
A versão anterior do JSF (JSF 1.2) trabalhava com taglibs JSF embutidos em páginas JSP. Essa nova versão, porém, sugere que toda a camada view será construída em Facelets, com arquivos XHTML. O Facelets já era um conhecido de aplicações EE 5, disponível através de libraries (veja mais detalhes num interessante material disponibilizado pelo site JavaWora em http://javawora.blogspot.com/2007/08/tutorial-faceltes.html). Os arquivos XHTML também oferecem mais conformidade e padronização para a construção de páginas HTML (veja http://pt.wikipedia.org/wiki/XHTML). Ou seja, agora, o que usávamos por conta própria passou a ser encorajado pela própria convenção do Java EE 6.
O suporte a AJAX, que antes era possível com o uso do Ajax4Jsf do pacote RichFaces (JBoss), agora está amplamente atendido pela própria especificação JSF. Foi também acrescido recursos de navegação condicional (que coloca um IF na <navegation-rule>) e navegação implícita. Vou ressaltar a flexibilidade da navegação implícita. Antes, a construção de <navegation-rule> para conduzir a navegação entre duas páginas JSF era necessária. Agora, com a navegação implícita, caso não exista uma regra a ser considerada para o outcome definido na JSF, uma página com o mesmo nome (acrescida com .xhtml) será, automaticamente buscada (se você boiou nessa explicação, não se preocupe, veremos isso na prática mais adiante).
Outro importante detalhe gira em torno do suporte natural ao GET. Antes, para se construir páginas JSF bookmarkables, nós precisávamos fazer muitos intervenções na codificação natural do JSF. Agora, com o suporte ao GET, isso se tornará muito mais fácil - você verá como! Só para constar, uma página pode ser considerada bookmarkable quando o usuário pode guardá-la, de forma plena, na opção Favoritos de seu navegador. Como o JSF só dava suporte ao POST, isso não era possível.
Existem ainda outros recursos que precisariam de maior atenção, com é o caso do Project Stages (definição de qual estágio a aplicação se encontra - um possível estágio, por exemplo, seria o de Development, para indicar que estamos desenvolvendo a aplicação e assim, um número maior de detalhes seria apresentado nos erros). Neste artigo, porém, vamos tratar dos principais recursos que essa fantástica versão do JSF nos oferece. Inclusive, o recurso que eu acho mais pertinente, eu ainda não comentei. Não precisamos mais indicar quais Beans ficarão acessíveis na JSF (via EL) pelo faces-config.xml. Agora existem anotações específicas para isso! Veremos isso na prática também.
Bom gente, achei interessante divulgar um pouco mais sobre o assunto para mostrar como a Sun (aliás, a Oracle) investiu bastante nos recursos da camada View nessa nova versão do Java EE. Mas chega de blablabla... voltemos ao projeto!
Desenvolvendo a camada View (projeto SuperForumWeb)
O primeiro passo é apagar a página index.jsp. Ham? Bom, nosso Eclipse ainda não está 100% Java EE 6, portanto, quando criamos nosso projeto SuperForumWeb, uma página index.jsp foi automaticamente criada. Vamos excluí-la. Entre no projeto SuperForumWeb, na pasta WebContent. Clique com o botão direito sobre o arquivo index.jsp e selecione a opção delete.
Clique em OK
Vamos agora criar um pacote para a camada View. Clique com o botão direito sobre o projeto SuperForumWeb, selecione New, Package. Dê ao pacote o nome de br.com.linu.forum.view. Esse pacote conterá todas as classes dessa camada.
Assim como nas versões anteriores do JSF, é comum a construção de Beans associados às páginas JSP. Aqui, a ideia permanece a mesma, porém, não será mais necessário trabalhar com o arquivo faces-config.xml para definir os Managed Beans e as páginas serão construídas em XHTML.
Importante: Não precisaremos declarar os beans gerenciados, mas vamos codificar um Exception Handler (vou explicar em breve) e para ele funcionar, sua declaração será necessária no faces-config.xml.
Importando a biblioteca Commons Lang
Bom, em determinado momento da camada de apresentação, nós precisaremos percorrer toda a sequência de uma Exception até encontrar a exceção causadora do disparo. Isso poderia ser, de certa forma, facilmente implementado. Porém, como bons programadores OO, não vamos reinventar a roda! Faremos uso da classe ExceptionUtils do pacote Commons Lang da Apache Commons. Nesse momento mostrarei como configurar o Eclipse e o Glassfish para utilizarem esse pacote.
Entre no endereço http://commons.apache.org/lang/download_lang.cgi

Clique no link commons-lang-2.5-bin.zip para baixar o arquivo de mesmo nome. Após terminado o download, você deverá descompactar o arquivo. Vários arquivos serão descompactados dentro de uma pasta chamada commons-lang-2.5. Vamos utilizar o arquivo commons-lang-2.5.jar
Com o servidor Glassfish desligado, copie o arquivo commons-lang-2.5.jar para o diretório sges-v3/glassfish/lib (onde sges-v3 é o diretório de instalação do Glassfish). O servidor de aplicação já estará configurado na próxima vez que for iniciado. Vamos agora configurar o ambiente de desenvolvimento.
No Eclipse, clique no menu Window, sub-menu Preferences.

Vamos definir as bibliotecas do usuário. Clique (na TreeView à esquerda da janela) na opção Java, Build Path, User Libraries.
Detalhe: não estranhe caso seu Eclipse não possua a definição (javaee, jdbc e richfaces) igual à imagem. Essa imagem foi extraída de um Eclipse que já era utilizado para outros projetos.
Clique então no botão New...

Defina o valor de User library name como commons-lang e clique em OK

Selecione (no quadrado à direita) a definição commons-lang e clique no botão Add JARs...

No FileChooser, selecione o arquivo commons-lang-2.5.jar no local onde o ZIP foi descompactado. Clique em Abrir

Dessa forma, nosso Eclipse já possui uma User Library pronta para o uso do Commons Lang. Vamos adicioná-la ao projeto Web.
Clique com o botão direito sobre o projeto SuperForumWeb, selecione a opção Build Path, item Configure Build Path...

Na Aba Libraries, clique no botão Add Library...

Selecione a opção User Library e clique em Next

Marque a biblioteca commons-lang e clique em Finish

Dessa forma nosso projeto já está configurado para trabalhar com a Commons Lang. Clique em OK
Projetando um Exception Handler
Pessoal, precisamos entender alguns conceitos de funcionamento do servidor de aplicação EE antes de programarmos esses recursos. Tenha em mente que, todo o gerenciamento dos beans será realizado pelo servidor de aplicação Glassfish. Image, por exemplo, que nosso projeto SuperForumWeb resgatou o Session Bean br.com.linu.forum.services.ServiceFacade (implementado com @Stateless) do projeto SuperForumEJB. Esse Session Bean estará sendo integralmente gerenciado pelo servidor EE.

Beleza! Veja agora o código abaixo:
public void createTopic(Topic topic) throws BusinessException {
topic.setCreationDate( new Date() );
business.create( topic );
}
Esse código foi implementado na classe TopicService e será utilizado para cadastrar tópicos. Essa classe, contudo, será acessada pelo método getTopicService() do nosso SessionBean ServiceFacade. Em nosso projeto SuperForumWeb criaremos classes associadas aos XHTML e cada uma delas fará uma chamada (via JNDI) ao EJB publicado. Abaixo temos um exemplo do código de uma dessas classes (não se preocupe em implementá-la agora, faremos isso com detalhes em seguida).
(...)
@EJB(name="ejb/facade")
private ServiceFacade facade;
(...)
public void create(ActionEvent event) {
try {
facade.getTopicService().createTopic(this.topic);
} catch (BusinessException e) {
/* Nada precisa ser feito aqui */
}
}
(...)
Conforme comentamos, o ServiceFacade foi obtido via anotação @EJB. Veja que fazemos uso do método createTopic() dentro do método create(). Daí, precisamos entender algumas questões:
Se o ServiceFacade está sendo gerenciado pelo Glassfish, esse método pegará (via catch) uma BusinessException disparada?
Não. Minha nossa senhora!!!! E agora? ...
O que acontece quando uma BusinessException é disparada pelo método createTopic()?
Pois é, em nossa aplicação SuperForumWeb, as exceções disparadas pelos Session Beans serão capturadas por um Exception Hander padrão.

Então o que vamos fazer agora?
Vamos precisar criar um ExceptionHandler específico para capturar uma AbortProcessingException (exceção gerada pelo servidor de aplicação quando ocorre uma Exception nos métodos dos Session Beans), percorrer toda sua cadeia de exceções (através do recursos oferecidos pelo Commons Lang da Apache) até encontrar uma BusinessException. Em seguida, enviaremos para as páginas XHTML a mensagem contida na exceção de negócios.
Caraca véi!!! Tudo isso?
Bom, ainda nessa versão do Java EE, a codificação manual das Exceptions Handlers são necessárias. Em frameworks já consagrados no mercado (e, aliás, mais antigos) como o Struts, pode-se fazer isso via configuração em arquivo XML. Aqui, porém, não é possível.
Como vamos fazer?
Ai que entra a parte boa: a programação! Vamos lá!
O servidor EE busca sua Exception Handler numa fábrica de Exceptions Handlers (um pouco de Design Pattern GoF pra variar). Nós vamos então criar nossa própria handler de exceções chamada ExceptionHandlerForum e distribuí-la para o Glassfish através uma fábrica (também implementada por nós) chamada ExceptionHandlerForumFactory. Por fim, vamos configurar o arquivo faces-config.xml para indicar que nossa aplicação possui uma fábrica de handlers própria. Ufa!
Vamos criar um novo pacote. Clique com o botão direito sobre a aplicação SuperForumWeb, selecione a opção New, Package. Defina o nome do pacote como br.com.linu.forum.view.util
Nesse pacote nós vamos criar três classes: ExceptionHandlerForum, ExceptionHandlerForumFactory e ViewUtilities.
Vamos iniciar com a classe de utilidades (que será simples). Crie uma classe chamada ViewUtilities no pacote br.com.linu.forum.view.util
package br.com.linu.forum.view.util;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
public class ViewUtilities implements Serializable {
private static final long serialVersionUID = 1L;
public static String formatDate(Date date) {
if ( date == null ) return null;
Locale ptBR = new Locale("pt", "BR");
DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.MEDIUM, ptBR);
return dateFormat.format( date );
}
public static void sendMessage(String message) {
FacesContext.getCurrentInstance().addMessage
( null , new FacesMessage( FacesMessage.SEVERITY_WARN , null , message ) );
}
}
Essa classe oferece recursos estáticos para as outras classes da camada View. O método formatDate() formata uma data, através de Internationalization, para o padrão brasileiro. Existem muitas maneiras de fazer isso. Como nossa simples aplicação só precisa desse recurso para as datas, optei em fazer assim - totalmente Java SE... hehehe! O outro método, sendMessage() é o que nos interessa no momento. A String obtida do getMessage() da BusinessException capturada deverá ser exibida na tela XHTML. Através do addMessage() obtido da instância do FacesContext podemos fazer isso. Observe que todas as mensagens do nosso sistema serão SEVERITY_WARN (mensagens de atenção). Em aplicações maiores convém habilitarmos recursos para o uso de mensagens de aviso e erro. Agora que nossa extensa classe de utilidades ficou pronta, vamos criar o mecanismo de Handler.
Crie uma nova classe chamada ExceptionHandlerForum dentro do pacote br.com.linu.forum.view.util. O código dessa classe é apresentado abaixo.
Crie uma nova classe chamada ExceptionHandlerForum dentro do pacote br.com.linu.forum.view.util. O código dessa classe é apresentado abaixo.
package br.com.linu.forum.view.util;
import java.io.Serializable;
import java.util.Iterator;
import javax.faces.FacesException;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ExceptionQueuedEvent;
import javax.faces.event.ExceptionQueuedEventContext;
import org.apache.commons.lang.exception.ExceptionUtils;
import com.sun.faces.context.ExceptionHandlerImpl;
public class ExceptionHandlerForum extends ExceptionHandlerImpl implements Serializable {
private static final long serialVersionUID = 1L;
@Override
public void handle() throws FacesException {
Iterator<ExceptionQueuedEvent> iterator = getUnhandledExceptionQueuedEvents().iterator();
while (iterator.hasNext()) {
ExceptionQueuedEvent event = iterator.next();
ExceptionQueuedEventContext context = (ExceptionQueuedEventContext) event.getSource();
if ( context.getException() instanceof AbortProcessingException ) {
/*
* Utilização do ExceptionUtils do Apache Commons
*/
String message = ExceptionUtils.getRootCauseMessage( context.getException() );
ViewUtilities.sendMessage( message );
}
}
super.handle();
}
}
Veja que esta classe herda uma ExceptionHandlerImpl criada no Java EE para facilitar a vida daqueles malucos (como nós) que querem implementar sua própria Exception Handler. O único método que precisamos sobrescrever é o handle(). Nesse método, obtemos a fila de exceções (de outro método interno). Em seguida, vasculhamos cada exceção a procura da AbortProcessingException. Essa exceção será disparada quando outra exceção qualquer é obtida do Session Bean gerenciado pelo container. Isso significa que nossa BusinessException, quando ocorrer, será a causa inicial da AbortProcessingException. Deixei claro o momento em que a Apache Commons foi utilizada. Com ela obtemos a causa inicial da exceção (uma BusinessException). Por fim, através de um dos métodos disponíveis em nossa ViewUtilities enviamos uma mensagem para as páginas XHTML - que serão automaticamente exibidas pelo componente <h:messages />.
Já temos nossa Handler. Precisamos agora criar a fábrica. Dentro do pacote br.com.linu.forum.view.util crie uma classe chamada ExceptionHandlerForumFactory.
package br.com.linu.forum.view.util;
import java.io.Serializable;
import javax.faces.context.ExceptionHandler;
import javax.faces.context.ExceptionHandlerFactory;
public class ExceptionHandlerForumFactory extends ExceptionHandlerFactory implements Serializable {
private static final long serialVersionUID = 1L;
@Override
public ExceptionHandler getExceptionHandler() {
return new ExceptionHandlerForum();
}
}
Veja que nada de avançado existe aqui. Apenas tenha o cuidado de estender a classe ExceptionHandlerFactory e sobrescrever o método getExceptionHandler() - retornando nossa própria handler, é claro. Pronto! Todas as classes necessárias foram programadas. Vamos agora configurar o container para trabalhar com a fábrica criada. Para isso, será necessário criar o arquivo faces-config.xml.
Clique com o botão direito sobre o diretório SuperForumWeb/WebContent/WEB-INF, selecione a opção New, Other...

Dentro da pasta XML, selecione o tipo de arquivo XML e clique em Next

Defina o valor de File name como faces-config.xml e clique em Next

Selecione a opção Create XML file from an XML schema file e clique em Next

Selecione a opção Select XML Catalog entry e selecione o catálogo marcado com a key http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd. Em seguida, clique em Next

Nessa próxima janela, apenas clique em Finish
Pronto! O arquivo de configuração foi criado. Abra-o e deixo com o código igual ao modelo abaixo:
<?xml version="1.0" encoding="UTF-8"?>
<javaee:faces-config version="2.0" xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:xml="http://www.w3.org/XML/1998/namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd ">
<javaee:factory>
<javaee:exception-handler-factory>
br.com.linu.forum.view.util.ExceptionHandlerForumFactory
</javaee:exception-handler-factory>
</javaee:factory>
</javaee:faces-config>
Apenas atente-se que a versão do faces-config agora é sinalizada como 2.0. Só precisamos acrescentar a sintaxe indicativa da fábrica de Exception Hanlder utilizada. Salve e feche o arquivo. Enfim, agora nossa aplicação estará apta a capturar BusinessExceptions e enviá-las para as páginas XHTML que serão criadas.
Configurando o arquivo web.xml
O arquivo web.xml, muito comum para desenvolvedores de aplicações Web em Java, foi criado pelo Eclipse no diretório SuperForumWeb/WebContent/WEB-INF. Altere seu conteúdo para que fique igual ao código abaixo.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<context-param>
<param-name>javax.faces.PROJECT_STAGE</param-name>
<param-value>Development</param-value>
</context-param>
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>
30
</session-timeout>
</session-config>
<welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Aqui também existem novos recursos. Através da tag <context-param> estamos definindo em que período a aplicação se encontra. Neste caso ela está em período de Development (desenvolvimento). Esse tipo de definição trás diferentes comportamentos para a aplicação (como maior ênfase aos LOGs, maior performance para ambiente de produção, etc). As outras configurações tratam do load do FacesServlet e de onde os páginas Faces estarão disponíveis (/faces/*). Com exceção dos elementos de contexto, os outros recursos já eram comuns no arquivo web.xml da versão antiga do Java EE. Você pode verificar mais detalhes sobre a criação desse arquivo (como também de todos os recursos do JSF 2) no excelente material disponibilizado por Edson Gonçalves em http://www.edsongoncalves.com.br/2010/01/18/javaserver-faces-2-0-na-pratica-parte-1/
Criando o arquivo de folha de estilo padrão
Vamos agora criar o CSS das páginas. Para maiores detalhes de como construir documentos web formatados pelo CSS, veja o excelente material disponibilizado em http://maujor.com/ pelo amigável Maurício Samy (em português, é o melhor site de CSS - na minha opinião). Criaremos uma pasta própria (chamada util) para guardar os recursos utilizados pela camada de apresentação. Clique com o botão direito sobre a pasta WebContent do projeto SuperForumWeb, selecione a opção New, opção Folder. Chame a pasta de util.
Clique com o botão direito sobre a pasta util, selecione a opção New, Other...
Vamos agora criar o CSS das páginas. Para maiores detalhes de como construir documentos web formatados pelo CSS, veja o excelente material disponibilizado em http://maujor.com/ pelo amigável Maurício Samy (em português, é o melhor site de CSS - na minha opinião). Criaremos uma pasta própria (chamada util) para guardar os recursos utilizados pela camada de apresentação. Clique com o botão direito sobre a pasta WebContent do projeto SuperForumWeb, selecione a opção New, opção Folder. Chame a pasta de util.
Clique com o botão direito sobre a pasta util, selecione a opção New, Other...

Dentro da pasta Web, selecione CSS e clique em Next

Defina o valor de File name como style.css e clique em Finish
Codifique (leia-se cole) o texto abaixo dentro de seu arquivo.
@CHARSET "iso-8859-1";
/* CSS Geral */
body {
font-family: Verdana, Arial, Sans-serif;
font-size: 10px;
}
img {
border: 0px;
}
a {
font-family: Verdana, Arial, Sans-serif;
text-decoration: none;
color: green;
font-size: 10px;
}
a:hover {
text-decoration: underline;
}
div.bottom {
width: 100%;
text-align: center;
}
div.message {
font-size: 10px;
text-align: left;
color: #660;
width: 602px;
padding: 8px;
margin: 8px;
font-weight: bold;
text-indent: 5px;
}
/* CSS Geral */
/* CSS para tabelas */
table {
border: 1px solid gray;
padding: 2px;
margin: 8px;
width: 618px;
}
table td {
background-color: #e5efe5;
text-align: center;
padding: 4px;
}
table td.describe {
text-align: left;
width: 350px;
}
table td.normal {}
table td.comment {
text-align: left;
}
table th {
background-color: #c5cfc5;
padding: 4px;
}
table th.topic {
text-align: left;
font-weight: normal;
}
font.date {
font-weight: bold;
color: green;
}
div.topic {
border: 1px solid gray;
padding: 8px;
margin: 8px;
background-color: #c5cfc5;
width: 600px;
height: 85px;
}
font.userName {
font-weight: bold;
}
/* CSS para tabelas */
/* CSS para formulários */
br {
clear: left;
}
label {
float: left;
width: 120px;
text-align: right;
margin-top: 5px;
}
input,textarea {
width: 300px;
margin-bottom: 5px;
float: left;
}
textarea {
width: 440px;
height: 150px;
}
textarea.comment {
width: 590px;
height: 50px;
background-color: #f5f5f5;
}
.submit {
margin-left: 120px;
margin-top: 5px;
width: 90px;
}
.searchButton {
width: 90px;
margin-left: 5px;
}
h1 {
font-size: 12px;
color: green;
width: 602px;
background-color: #c5cfc5;
padding: 8px;
margin: 8px;
}
div.form {
border: 1px solid gray;
padding: 8px;
margin: 8px;
background-color: #e5efe5;
width: 600px;
}
/* CSS para formulários */
Esse será nosso padrão CSS para o SuperForum. Parto do princípio que o leitor conheça programação HTML, por isso não entrarei em detalhes sobre a sintaxe do código CSS mostrado. Em caso de dúvidas, veja o já comentado http://maujor.com/. Utilizei também os conceitos de Tableless para construir os formulários. Um simples exemplo, que inclusive foi utilizado para a montagem desse layout, pode ser encontrado em http://www.cssdrive.com/index.php/examples/exampleitem/tableless_forms/. Realizei testes satisfatórios desse CSS nos navegadores Firefox 3, Internet Explorer 8 e Google Chrome.
Ahhh. Mais um detalhe: lembra-se daquela imagem do SuperFórum que foi mostrada na parte 1 desse material? Pois é, vamos precisar dela aqui também. Por conveniência, preferi mostrá-la novamente:

Baixe para seu computador com o nome logo.png e a salve dentro da pasta SuperForumWeb/WebContent/util (a pasta que criamos anteriormente).
Bom, já temos nosso CSS e a imagem do sistema. Vamos agora criar o template das páginas XHTML do Facelets.
Criando o arquivo template.xhtml
Conforme vimos, o Java EE 6 adota o Facelets como padrão para a camada de apresentação. Os Facelets permitem a construção de templates. Criaremos então um template com o layout padrão das páginas XHTML. O Eclipse, por default, ainda não está respirando os padrões do Facelets. Portanto precisaremos criar nossos arquivos XHTML de forma manual.
Clique com o botão direito sobre o diretório util (dentro de WebContent) do projeto SuperForumWeb, selecione a opção New, File

Defina File name com o nome template.xhtml e clique em Finish. Copie o código abaixo para o arquivo criado.
<?xml version='1.0' encoding='iso-8859-1' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<link rel="stylesheet" type="text/css" href="util/style.css" />
<title><ui:insert name="title">SuperFórum</ui:insert></title>
</h:head>
<h:body>
<ui:insert name="header">
<hr /> <img src="util/logo.png" /> <hr />
[<h:link outcome="index"> Buscar Tópicos </h:link>] -
[<h:link outcome="topics"> Cadastrar Tópicos </h:link>]
<br /><br />
</ui:insert>
<ui:insert name="body">
Definir corpo.
</ui:insert>
<ui:insert name="message">
<div class="message">
<h:messages showSummary="false" showDetail="true" />
</div>
</ui:insert>
<ui:insert name="footer">
<br /><hr />
</ui:insert>
</h:body>
</html>
Vamos analisar o código. A taq inicial denota que este, trata-se, de um arquivo XML. A segunda tag (DOCTYPE) indica que estamos trabalhando com um XHTML Transitional (que oferece os mesmos recursos do HTML 4.01 Transitional). Existem os seguintes DOCTYPES: Strict, Transitional e Frameset. O tipo de documento Frameset é o mais flexível. Podemos utilizar marcações para frames, elementos deprecated e estilo (CSS) definido nas próprias tags. O tipo Transitional é parecido com o Frameset, porém restringe o uso de marcações para frames. Pode ser considerado como o caso "mais ou menos" dos três tipos de documentos. O Strict não permite CSS dentro das tags e a utilização de elementos deprecated. Para os padrões sugeridos pela W3C, esse tipo garante totalmente a padronização. Para pessoas como eu, que ainda não são 100% conhecidas do XHTML, recomendo o uso de meio termo (o Transitional). Certamente com o tempo o uso do Strict se tornará, por nossa própria opinião, preferível.
Para a criação de um XHTML correto precisamos atentar-se aos padrões comuns já definidos ao XML. As considerações abaixo devem ser observadas:
- Usar tags em caixa-baixa (letras minúsculas);
- Sempre utilizar o fechamento de tags, mesmo quando sejam vazias (como por exemplo, a tag <br/>);
- Aninhamento das tags correto (por exemplo, <div><p> devem ser finalizadas com </p></div> - e não com </div></p>);
- Os documentos devem possuir boa formatação.
Essas são as regras básicas para se construir um XHTML. Para maiores detalhes recomendo a leitura de http://maujor.com/tutorial/xhtml.php e http://pt.wikipedia.org/wiki/XHTML.
Na definição da tag html, nós declaramos as namespaces html, core e facelets utilizadas pela JSF 2.0. Veja também que a definição do CHARSET e do arquivo CSS utilizado foram criadas na tag <h:head>. O template Facelets permite definir elementos padrões para as páginas através da sintaxe <ui:insert>, onde o parâmetro name serve como indicador. Dentro dessas definições colocamos os códigos XHTML e JSF necessários. A partir daí, por default, todas as páginas XHTML que fizerem referência a esse template terão, automaticamente, esses campos. Observe que criamos o title, o header, o body, um espaço para mensagens (message) e o footer. Bom, fui breve, mas espero ter sido claro em relação à codificação desse template. Você pode tirar outras dúvidas em relação ao uso dos templates através do artigo http://www.coreservlets.com/JSF-Tutorial/jsf2/ (procure pelo tutorial Page Templating with Facelets).
O que existe de novo, em relação ao JSF 2.0, nessa página?
Claro que o uso de template e XHTML, por si só, já é uma inovação. Mas foi também utilizada a nova tag <h:link/>. O JSF 2.0 oferece grandes recursos para a utilização de comunicação via GET. Na versão 1.2, era necessária muita programação para obter um parâmetro enviado via GET. Não existiam também tags JSF puras que forneciam esses recursos. Essa nova versão trouxe as tags <h:button> e <h:link> que possibilitam justamente esse mecanismo. Veja que através dos comandos <h:link outcome="index"> e <h:link outcome="topics"> estamos utilizando essa notação. Aqui, no entanto, não foram passados parâmetros. Estamos apenas indicando o outcome da navegação.
Outra inovação é o usa da Navegação Implícita. Na antiga versão, todo outcome declarado nos elementos JSF nas páginas precisa conter uma navegation-rule relacionada. Aqui, quando não existe uma navegation-rule com o nome do outcome (em nosso caso, index e topics), páginas XHTML com o nome declarado serão automaticamente buscadas. Ou seja, não criamos nenhuma regra de navegação. Quando o link oferecido pelo componente <h:link outcome="index"> for utilizado, a página index.xhtml será referenciada automaticamente. Muito boa essa navegação implícita né!? Evita declarações de navegações básicas (e chatas) nos arquivos XML do servidor de aplicação.
É isso ai galera, estamos pronto para criar nossas páginas!!!
Desenvolvendo os objetos Viewables
As vezes o formato dos dados mantidos no modelo não são totalmente apropriados para a exibição na camada de apresentação. Por conta disso, existem recursos variados (inclusive do próprio JSF) para formatação desses dados. Eu, particularmente, prefiro criar componentes intermediários para fabricar a exibição dos valores da forma esperada. Na verdade, esses componentes não passam de Wrappers (um cara que empacota outro objeto ou primitivo - tire suas dúvidas sobre isso em http://en.wikipedia.org/wiki/Primitive_wrapper_class) para as entidades do modelo. Prefiro trabalhar dessa forma para facilitar o acesso aos dados formatados de forma transparente, como se estivéssemos trabalhando com os próprios pojos da camada Model.
Tenho o costume de chamar esses elementos de Viewable. Podemos criar wrappers para cada elemento do modelo - caso haja necessidade. Nesta aplicação nós vamos criar dois wrappers: CommentViewable e TopicViewable. Nesse caso, a entidade Metadata não precisa desse encapsulamento, pois não será exibida explicitamente para os usuários.
Crie um pacote no projeto SuperForumWeb chamado br.com.linu.forum.view.viewable. Crie então uma classe chamada TopicViewable. Veja seu código abaixo.
package br.com.linu.forum.view.viewable;
import java.io.Serializable;
import br.com.linu.forum.model.Topic;
import br.com.linu.forum.view.util.ViewUtilities;
public class TopicViewable implements Serializable {
private static final long serialVersionUID = 1L;
private Topic topic;
private int countComments;
public TopicViewable(Topic topic, int countComments) {
this.topic = topic;
this.countComments = countComments;
}
public int getCountComments() {
return this.countComments;
}
public String getCreationDateFormated() {
return ViewUtilities.formatDate( this.topic.getCreationDate() );
}
public Topic getTopic() {
return this.topic;
}
}
Não existe complexidade nesse código. Existem duas propriedades manipuladas por esse objeto: a creationDate e o número de comentários do tópico. O banco de dados guardará em creationDate um Timestamp com a hora da criação do registro. Caso não haja controle de intercionalização, um valor sob o Locale "en" seria exibido. No SuperFórum, porém, precisamos apenas exibir a data de criação. O método getCreationDateFormated() foi criado justamente para isso: ele obtém a data da entidade Topic e, através de ViewUtilities.formatDate(), converte para o formato necessário.
Sobre o campo countComments, vale ressaltar que nosso modelo (por opção) não contempla uma associação bidirecional entre Topic e Comment. Isso significa que não temos forma de acessar diretamente um elemento que identifique a quantidade de comentários relacionados aos tópicos (através da chamada do método size() de uma List, por exemplo, seria possível). Dessa forma, criamos aqui um atributo responsável por guardar esse quantitativo. Dessa forma, sempre que um TopicViewable for criado, precisaremos informar (além do Topic encapsulado) a quantidade de comentários associados. Isso será feito em outra classe, quando necessário.
Pronto! Dessa forma, todos os dados do tópico necessários para a camada de apresentação estão disponíveis e formatados nessa classe. Vamos fazer o mesmo para Comment.
Crie agora uma classe CommentViewable no pacote br.com.linu.forum.view.viewable com o código abaixo.
package br.com.linu.forum.view.viewable;
import java.io.Serializable;
import br.com.linu.forum.model.Comment;
import br.com.linu.forum.view.util.ViewUtilities;
public class CommentViewable implements Serializable {
private static final long serialVersionUID = 1L;
private Comment comment;
public CommentViewable(Comment comment) {
this.comment = comment;
}
public String getCreationDateFormated() {
return ViewUtilities.formatDate( this.comment.getCreationDate() );
}
public Comment getComment() {
return this.comment;
}
}
Esse Wrapper é mais simples. Ele só possui o método para formatação da data de criação. Assim como o anterior, seu construtor precisa receber o objeto encapsulado - neste caso, um Comment. Esses objetos serão intermediários aos elementos do modelo. As classes associadas aos XHTML trabalharão com eles quando isso se fizer necessário.
Vamos agora para as últimas classes do nosso sistema!
Desenvolvendo a camada de apresentação
Optei em construir um Bean para cada página! Isso não é uma regra. Pode haver vários Beans para uma página, como também várias páginas para um único Bean. Cada caso é um caso (poético isso né?!). Vamos lá!
Primeiro precisamos descobrir quais páginas vão existir em nosso sistema. Serão necessárias três páginas: a página inicial, que apresentará a listagem de tópicos e possibilitará a consulta através do mecanismo de busca; a página de visualização dos tópicos e seus comentários; e a página de cadastro de tópicos. Portanto vamos criar os arquivos index.xhtml, comments.xhtml e topics.xhtml para atender as páginas citadas acima, respectivamente.
Observação: Nos procedimentos anteriores, nós criamos dois pacotes (viewable e util) dentro do prefixo br.com.linu.forum.view. As classes definidas a seguir devem pertencer ao pacote br.com.linu.forum.view, ou seja, ao prefixo já definido desses pacotes.
Iniciaremos pela classe de criação de tópicos. Crie uma classe chamada TopicView no pacote br.com.linu.forum.view e codifique o conteúdo abaixo.
package br.com.linu.forum.view;
import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped;
import javax.faces.event.ActionEvent;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.model.Topic;
import br.com.linu.forum.services.ServiceFacade;
@ManagedBean
@RequestScoped
public class TopicView implements Serializable {
private static final long serialVersionUID = 1L;
@EJB(name="ejb/facade")
private ServiceFacade facade;
private Topic topic;
@PostConstruct
public void construct() {
this.topic = new Topic();
topic.setTitle("Novo Tópico");
}
public Topic getTopic() {
return this.topic;
}
public void create(ActionEvent event) {
try {
facade.getTopicService().createTopic(this.topic);
} catch (BusinessException e) {
/* Nada precisa ser feito aqui */
}
}
}
Presenciamos aqui várias novidades do JSF 2.0. Observe que essa classe será considerada como um bean gerenciado apenas por conter a anotação @ManagedBean. Conforme vimos, a última versão do JSF nos obrigava a declarar esse tipo de comportamento através do faces-config.xml. Quando anotamos uma classe com @ManagedBean e não especificamos explicitamente um escopo, ela será definida no escopo de requisição (Request). Aqui, porém, preferi colocar explicitamente a anotação @RequestScoped para indicar essa configuração.
Como sabemos, o escopo de requisição fará com que o objeto seja criado apenas para a requisição do momento. Ou seja, a classe basicamente recebe uma instância, executa seus procedimentos, devolve o resultado para a página e depois é destruída - existem mais comportamentos dentro do servidor de aplicação, mas aqui, não nos aprofundaremos nesse sentido.
Note que obtemos o SessionBean ServiceFacade através da anotação @EJB. Através dele teremos acesso ao serviço createTopic() disponível no TopicService. Existe também o detalhe de que alguns procedimentos precisam ser realizados logo após a criação desse objeto. Esses procedimentos são declarados num método anotado com @PostConstruct (que aqui, por acaso, é chamado de construct() - nome que não é obrigatório). No método create(), o procedimento de criação do tópico será realizado. Veja que não precisamos repassar a mensagem obtida de uma possível exceção para a página XHTML - pois caso ocorra uma BusinessException, nosso Exception Handler construído dará conta do recado.
Vamos agora criar a página XHTML associada a esse Managed Bean.
Clique com o botão direito sobre a pasta WebContent do projeto SuperForumWeb, selecione a opção New, Other...

Selecione a opção General, item File e clique em Next

Como a pasta WebContent já está selecionada, precisamos apenas definir o nome do arquivo em File name. Digite topics.xhtml e clique em Finish
Digite o conteúdo abaixo no arquivo criado.
<?xml version='1.0' encoding='iso-8859-1' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="util/template.xhtml">
<ui:define name="body">
<h1>Cadastrar Tópicos</h1>
<div class="form">
<h:form>
<label> Título: </label>
<h:inputText value="#{topicView.topic.title}" /><br />
<label> Usuário: </label>
<h:inputText value="#{topicView.topic.userName}" /> <br />
<label> Tópico: </label>
<h:inputTextarea value="#{topicView.topic.text}" /><br />
<h:commandButton action="index" actionListener="#{topicView.create}"
styleClass="submit" value="Postar"/> <br />
</h:form>
</div>
</ui:define>
</ui:composition>
</html>
Note que através dos recursos disponíveis no Facelets, o código da página ficou bem reduzido. A única área que precisamos redefinir é a body. Isso pode ser realizado através da <ui:define name="body">. Observe também que foi necessário (através da <ui:composition>) definir o template Facelets utilizado - nós usamos o template.xhtml criado anteriormente. Só pelo fato da classe TopicView ter sido anotada pela @ManagedBean nós já somos capazes de acessá-la via EL. Como nenhum nome foi definido, ela estará disponível pelo seu próprio nome - com a primeira letra minúscula. Ou seja, quando chamamos o valor #{topicView.topic.title} estamos justamente acessando o método getTopic() e, em seguida, o método getTitle(). Tenho como premissa o fato de que o leitor já conheça esse padrão de comportamento de acesso. Caso haja dúvidas, recomendo a leitura do artigo http://linubr.blogspot.com.br/2012/08/desenvolvendo-um-crud-para-web-com-java.html.
Todos os dados associados às propriedades do bean Topic serão enviados para o servidor no momento em que o botão Postar for acionado. Neste caso, deve-se ter em mente o ciclo de vida natural dos JSF. Veja abaixo uma figura (retirada e adaptada de http://www.laliluna.de/articles/jsf-2-evaluation-test.html) que representa esse comportamento.
![]() | Quando uma requisição é identificada, a classe em questão é instanciada. Em seguida ela recebe os valores da requisição. Caso existam validações definidas pela JSF, elas serão aplicadas. Em nosso caso, lembre-se que existem as anotações @NotNull e @Size para alguns campos da classe Topic do modelo. Como essa classe está sendo diretamente acessada através do método getTopic(), caso algum valor definido nos <h:inputText> ou <h:inputTextarea> esteja inválido, a mensagem (também definida na própria classe do modelo através do atributo message) será enviada para a página XHTML é exibida no <h:messages>. Neste caso, os demais procedimentos não seriam realizados. Caso não haja erros na validação (ou conversão de valores - que em nosso estudo, não foi aplicada), o objeto do modelo será, enfim, atualizado. Nesse momento, a instância de Topic (que foi criada no @PostConstruct - já comentado) já possui os valores digitados pelo usuário nos campos da página. Em seguida, os Listeners associados aos componentes da tela (em nosso caso, apenas ao botão) serão acionados. O código da página topics.xhtml associa ao <h:commandButton> o comportamento definido no método create() da classe TopicView através do parâmetro actionListener="#{topicView.create}". Para o leitor familiarizado com o comportamento padrão do JSF 1.2, esse ciclo de vida é conhecido. Para uma total abordagem sobre os conceitos inerentes ao comportamento dos componentes JSF, seu ciclo de vida, maiores detalhes sobre validações, conversões e eventos, recomendo a leitura completa do material disponibilizado em http://www.laliluna.de/articles/jsf-2-evaluation-test.html - de onde, inclusive, a figura ao lado foi adaptada. Só para constar, devemos nos lembrar que, quando o método createTopic() de TopicService é chamado dentro do método create(), um procedimento de validação padrão para os tópicos (definido no método polimórfico validate()) é acionado. Dessa forma, todas as outras regras de negócio (como por exemplo, o fato de que o título do tópico deve ser único) serão validadas. Isso fará com que somente os tópicos totalmente íntegros sejam passados para a camada de persistência e, consequentemente, salvos no banco de dados. |
Veja ainda que através do action="index" do componente <h:commandButton> estamos utilizando o recurso de navegação implícita. Neste caso, se todas as atividades referente à gravação de dados ocorrerem com sucesso, o usuário será redirecionado para a página index.xhtml. Vamos criar agora os componentes associados a essa página.
Crie uma classe chamada SearchView no pacote br.com.linu.forum.view.
package br.com.linu.forum.view;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
import javax.faces.event.ActionEvent;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.model.Topic;
import br.com.linu.forum.services.ServiceFacade;
import br.com.linu.forum.view.util.ViewUtilities;
import br.com.linu.forum.view.viewable.TopicViewable;
@ManagedBean
@ViewScoped
public class SearchView implements Serializable {
private static final long serialVersionUID = 1L;
@EJB(name="ejb/facade")
private ServiceFacade facade;
private List<TopicViewable> topics;
private String word;
public void setWord(String word) { this.word = word.trim(); }
public String getWord() { return word; }
public List<TopicViewable> getTopics() {
return this.topics;
}
private void search() {
try {
if ( this.getWord().equals("") )
topics = this.convert( facade.getTopicService().getTopics() );
else
topics = this.convert( facade.getTopicService().searchTopics( this.getWord() ) );
if ( this.topics.size() == 0 )
throw new BusinessException( "Nenhum tópico foi encontrado." );
} catch (BusinessException ex) { ViewUtilities.sendMessage( ex.getMessage() ); }
}
public void search(ActionEvent e) {
this.search();
}
public boolean isEmptyList() {
return this.topics.isEmpty();
}
private int getCountComments(Topic topic) throws BusinessException {
return this.facade.getCommentService().getComments(topic).size();
}
private List<TopicViewable> convert(List<Topic> topics) throws BusinessException {
List<TopicViewable> viewables = new ArrayList<TopicViewable>();
for (Topic topic : topics) {
int count = this.getCountComments(topic);
viewables.add( new TopicViewable(topic,count) );
}
return viewables;
}
@PostConstruct
public void construct() {
this.setWord("");
this.search();
}
@PreDestroy
public void destroy() {
topics = null;
}
}
Essa classe também será publicada para as páginas XHTML através da anotação @ManagedBean, porém, aqui estamos definindo um outro tipo de escopo: @ViewScoped. Esse é um novo tipo de escopo disponível no JSF 2.0. Ele trás um intermédio entre os escopos de requisição e sessão (já conhecidos no desenvolvimento de sistemas web). Como sabemos, o escopo de requisição cria o objeto somente para a chamada do usuário, não mantendo quaisquer estados entre duas requisições. O escopo de sessão, entretanto, é capaz de manter os dados do usuário entre várias chamadas, se mantendo ativo durante o período em que o usuário estiver trabalhando com esta sessão. Ele será fechado somente quando o usuário fechar a sessão ou o tempo previsto terminar. Esse novo escopo, disponível pela @ViewScoped, é capaz de manter os dados da requisição até o momento em que uma nova página é chamada. Em outras palavras, em quanto o usuário estiver apontando para esse bean e enquanto nenhum outcome for executado, o escopo estará disponível. Essa nova atração favorece a criação de páginas onde é necessário se manter um estado de conversação entre algumas requisições, sem que esses dados se mantenham ativos por todo o tempo de vida da sessão. Outro detalhe importante se refere ao fato de que as chamadas @PostConstruct e @PreDestroy só serão chamadas uma vez, durante todo o tempo de vida do bean - diferente do comportamento de um @RequestScoped - em que esses comportamentos seriam executados em todas as requisições ao bean.
Essa classe SearchView que estamos criando, por exemplo, é um típico caso de onde seria interessante utilizar o @ViewScoped. Note que sempre que esse bean for publicado, o método construct() será executado. Nesse método, definimos como padrão a String "" para o atributo word. Esse atributo guarda a palavra que o usuário deseja pesquisar. Ao definirmos "", estamos indicando que não existe palavra a ser pesquisada. Ainda neste método, temos uma chamada ao método polimórfico privado search() (sem o parâmetro ActionEvent). Esse método guarda uma listagem de tópicos (encapsulados na nossa Wrapper TopicViewable) no atributo topics de acordo com o valor de word. Se word for vazia (""), uma lista de todos os tópicos do sistema será obtida através do método getTopics() de TopicService. Caso word não esteja vazia, a listagem será obtida através do método searchTopics(). Ambos os métodos estão disponíveis através da ServiceFacade resgatada via @EJB do projeto SuperForumEJB. A condição seguinte verifica a listagem e, caso size() seja igual a 0 (ou seja, não existem elementos), dispara uma BusinessException que será exibida na página XHTML. Veja que para popular o atributo topics, é usado o método privado convert() que converte uma listagem de tópicos em TopicViewable. Esse método ainda utiliza o método getCountComments() para os tópicos. Esse recurso é usado para instanciar um TopicViewable - que, conforme vimos, necessita tanto do tópico quanto da quantidade de comentários a ele associados.
Esse objeto sempre guardará uma listagem de tópicos (inteira ou limitada através da word). Essa listagem está disponível para a página através do getTopics(). Existem também o Getter e Setter para o atributo word. Dessa forma, podemos construir nossa página XHTML.
Crie um novo arquivo XHTML (conforme procedimento já mostrado) no diretório SuperForumWeb/WebContent chamado index.xhtml. O código desse arquivo é mostrado abaixo.
<?xml version='1.0' encoding='iso-8859-1' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="util/template.xhtml">
<ui:define name="body">
<f:view>
<h:form>
<div class="form"><label> Digite uma palavra: </label>
<h:inputText value="#{searchView.word}" />
<h:commandButton actionListener="#{searchView.search}"
styleClass="searchButton" value="Pesquisar"/>
<br />
</div>
<h:dataTable value="#{searchView.topics}" var="item"
rendered="#{!searchView.emptyList}"
columnClasses="describe,normal,normal">
<h:column>
<f:facet name="header">
<h:outputText value="Tópico" />
</f:facet>
<h:link outcome="comments">
<f:param name="topic" value="#{item.topic.id}"/>
<h:outputText value="#{item.topic.title}" />
</h:link>
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Comentários" />
</f:facet>
<h:outputText value="#{item.countComments}" />
</h:column>
<h:column>
<f:facet name="header">
<h:outputText value="Postagem" />
</f:facet>
<h:outputText value="#{item.creationDateFormated}" />
</h:column>
</h:dataTable>
</h:form>
</f:view>
</ui:define>
</ui:composition>
</html>
Veremos os detalhes de sua implementação agora. Tudo que nos interessa está compreendido entre as tags <h:form> e </h:form>. A primeira <div> define o formulário para que o usuário digite uma palavra a ser pesquisada. Repare que o único <h:inputText> contido nesse item está ligado ao atributo word de SearchView. O <h:commandButton> possui sua actionListener apontando para o método search() assinada com ActionEvent. Ou seja, quando esse botão for pressionado, o valor digitado pelo usuário será enviado para word e em seguida, o método search() atualizará a listagem de tópicos. Observe que não foi definida uma action para o <h:commandButton>. Lembre-se que, mesmo que apontássemos a action para o mesmo bean gerenciado (no caso, SearchView), o escopo @ViewScoped seria finalizado - o @ViewScoped é finalizado quando um outcome é acionado. Por isso omitimos o recurso de redirecionamento do botão para que nosso escopo view fosse mantido.
A exibição dos tópicos está concentrada na <h:dataTable>. Veja que ela só será exibida se a negação do retorno de isEmptyList() for verdadeira. Esse método retorna true se a lista de tópicos for vazia. Então, se for vazia, a tabela não será exibida. Os elementos serão exibidos na tabela através de <h:column> e <h:outputText>, como já fazíamos nas páginas JSP do antigo JSF 1.2. Porém, existe aqui um novo elemento a ser considerado: <h:link>.
Conforme vimos, a nova versão da JSF trouxe vários recursos de requisições via GET. Esse componente faz exatamente isso. Ele é capaz de realizar um redirecionamento (pela navegação implícita definida no outcome) para a página comments.xhtml passando um parâmetro, definido no elemento <f:param> na URL. Ou seja, supondo que o usuário clique no link correspondente ao tópico de código 5, o navegador será redirecionado para a página http://localhost:8080/SuperForumWeb/faces/comments.xhtml?topic=5 - levando em consideração que a aplicação esteja rodando em localhost. Certamente a página destino será construída de forma apropriada para receber esse parâmetro via URL. O grande avanço nesse recurso gira em torno da possibilidade que o usuário do sistema tem de guardar essa página nos favoritos, ou seja, é uma página Bookmarkable.
Vamos agora a nossa última página. Crie uma classe chamada CommentView no pacote br.com.linu.forum.view. Seu código é mostrado abaixo.
package br.com.linu.forum.view;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;
import javax.faces.event.ActionEvent;
import br.com.linu.forum.business.BusinessException;
import br.com.linu.forum.model.Comment;
import br.com.linu.forum.model.Topic;
import br.com.linu.forum.services.ServiceFacade;
import br.com.linu.forum.view.util.ViewUtilities;
import br.com.linu.forum.view.viewable.CommentViewable;
import br.com.linu.forum.view.viewable.TopicViewable;
@ManagedBean
@SessionScoped
public class CommentView implements Serializable {
private static final long serialVersionUID = 1L;
@EJB
private ServiceFacade facade;
private String topicId;
private Comment comment;
private List<CommentViewable> comments;
private TopicViewable topicViewable;
/* Métodos associados ao GET */
public String getTopicId() {
return this.topicId;
}
public void setTopicId(String topicId) {
this.topicId = topicId;
this.refresh();
}
/* Métodos associados ao GET */
public void add(ActionEvent e) {
try {
this.facade.getCommentService().addComment(
this.topicViewable.getTopic(), this.comment);
this.comment = new Comment();
this.refresh();
} catch (BusinessException ex) { /* Nada precisa ser feito aqui */
}
}
private void refresh() {
try {
/* Definição do código do tópico */
long id = 0;
if ((this.topicId != null) && (!this.topicId.equals(""))) {
try {
this.comment = new Comment();
id = Long.parseLong(this.topicId);
} catch (Exception e) {
throw new BusinessException("Código do tópico inválido.");
}
} else {
if ( this.topicViewable != null )
id = this.topicViewable.getTopic().getId();
}
/* Definição do tópico */
List<Comment> commentList = new ArrayList<Comment>();
Topic topic = this.facade.getTopicService().getTopic(id);
if (topic == null) {
topic = new Topic();
topic.setTitle("não encontrado");
this.topicViewable = new TopicViewable(topic, 0);
} else {
commentList = this.facade.getCommentService().getComments(topic);
this.topicViewable = new TopicViewable(topic, commentList.size());
}
/* Definição da listagem de comentários */
this.comments = new ArrayList<CommentViewable>();
for (Comment comment : commentList) {
this.comments.add(new CommentViewable(comment));
}
} catch (BusinessException e) {
ViewUtilities.sendMessage(e.getMessage());
}
}
@PostConstruct
public void construct() {
this.comment = new Comment();
this.comments = new ArrayList<CommentViewable>();
this.topicViewable = new TopicViewable(new Topic(), 0);
}
public boolean isEmptyList() {
return ( this.comments.size() == 0 ) ? true : false;
}
public List<CommentViewable> getComments() {
return comments;
}
public Comment getComment() {
return comment;
}
public TopicViewable getTopicViewable() {
return topicViewable;
}
}
Certamente essa é a classe da camada View mais complicada. Primeiramente note que teremos acesso à camada de serviços através do atributo facade. Existe ainda os atributos topicId (que guardará o código do ID passado pela URL), comment (instância de um comentário), comments (lista de comentários relacionados ao tópico - encapsulados em uma List<CommentViewable>) e topicViewable (instância do tópico referenciado). Existem Getters comuns para todos esses atributos. Veja que esse bean será disponível no escopo @SessionScoped, ou seja, ele persistirá durante várias interações com o usuário. Para garantir que sempre existirão instâncias dos atributos, foi definido o comportamento @PostConstruct para o método construct(). Dessa forma, os atributos, mesmo que vazios, sempre existirão. Opto em trabalhar assim para evitar problemas chatos de NullPointerException (http://java.sun.com/j2se/1.4.2/docs/api/java/lang/NullPointerException.html). Existe o método isEmptyList() que retorna true se a lista de comentários for fazia. O método add() salvará a instância de comment no banco de dados (através addComment() disponível no serviço CommentService). Veja que também existe um Setter para o atributo topicId. Já falamos de todos os métodos, exceto o mais importante: refresh().
O método refresh() foi criado para controlar todo o comportamento de exibição dos dados dessa página. Veja que ele é explicitamente chamado em dois momentos: sempre quando um topicId é definido (em setTopicId()) e sempre que um comentário é adicionado com sucesso (em add()). É fácil de entender a necessidade de atualizar (refresh) a tela quando esses comportamentos forem executados. Veja bem, essa página será construída para receber o id do tópico pela URL. Sempre que informarmos um novo id, toda a listagem de comentários, como também os dados do próprio tópico, devem ser atualizadas. De forma semelhante, quando um novo comentário é adicionado ao tópico, precisamos atualizar a lista de comentários - para que essa seja atualizada na página XHTML para o usuário.
Vamos então entender como esse método trabalha. Primeiro ele define um id (do tipo long) como 0 - ou seja, um tópico inválido. Ele verifica então se existe um valor válido para topicId (se esse, por exemplo, chegou via URL). Caso exista um valor, ele define o long id com esse valor (se isso for possível - a letra Z, por exemplo, não poderia ser convertida para um long - ainda bem né!? hehe). Se não existir, ele vai obter o id do tópico que já está guardado em topicViewable. Isso significa que: se não estamos buscando outro tópico (valor de topicId inválido), vamos trabalhar com o tópico que já temos. Em seguida, ele faz uma busca de um tópico através do método getTopic() de TopicService. Se nenhum tópico for encontrado, ele cria um novo tópico e guarda-o no atributo topicViewable. Repare que esse tópico foi criado com o título não encontrado. Isso será exibido para o usuário caso o tópico realmente não exista. No fluxo correto, o tópico irá existir, portanto, uma lista de tópicos é obtida de getComments() de CommentService e o tópico é encapsulado no atributo topicViewable. Por fim, de posse de uma listagem de comentários associada ao tópico, guardamos em comments uma lista de commentViewable. Certamente que qualquer problema ocorrido nesses comandos será capturado pela catch e exibido na tela.
Ainda estou com uma dúvida!
Ainda estou com uma dúvida!
Você disse que no fluxo normal um tópico sempre seria encontrado em getTopic(). Por quê?
Lembre-se que essa página será acessada via o link encontrado em index.xhtml e que o código do tópico será enviado pela URL. Isso significa que topicId terá um valor válido. É claro que se você digitar no navegador a URL completa e informar o código de um tópico não válido, você cairia nessa situação alternativa. Faremos esses testes depois, ok?!
Bom, tudo que precisamos está aqui. Já podemos agora criar a página. Crie um novo arquivo chamado comments.xhtml na pasta SuperForumWeb/WebContent e digite (leia-se cole) o texto abaixo.
<?xml version='1.0' encoding='iso-8859-1' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets">
<ui:composition template="util/template.xhtml">
<ui:define name="body">
<f:view>
<h:form>
<f:metadata>
<f:viewParam name="topic" value="#{commentView.topicId}" />
</f:metadata>
<h1>
Tópico: <h:outputText value="#{commentView.topicViewable.topic.title}" />
</h1>
<div class="topic">
<h:outputText value="#{commentView.topicViewable.creationDateFormated}" /> -
<font class="userName"><h:outputText value="#{commentView.topicViewable.topic.userName}" /></font>
<br /><br />
<h:inputTextarea value="#{commentView.topicViewable.topic.text}"
readonly="true" styleClass="comment"/>
</div>
<h:dataTable value="#{commentView.comments}" var="item"
rendered="#{!commentView.emptyList}" columnClasses="comment"
headerClass="topic">
<h:column>
<h:outputText value="#{item.creationDateFormated}" /> -
<font class="userName"><h:outputText value="#{item.comment.userName}" /></font>
<br />
<br />
<h:inputTextarea value="#{item.comment.text}"
readonly="true" styleClass="comment"/>
</h:column>
</h:dataTable>
<br /><hr /><br />
<h1>Adicionar Comentário</h1>
<div class="form"><label> Usuário: </label> <h:inputText
value="#{commentView.comment.userName}" /> <br />
<label> Comentário: </label> <h:inputTextarea
value="#{commentView.comment.text}" /><br />
<h:commandButton actionListener="#{commentView.add}"
action="comments" styleClass="submit" value="Postar"/>
<br />
</div>
</h:form>
</f:view>
</ui:define>
</ui:composition>
</html>
Basicamente temos nessa página os seguintes recursos: exibir os dados do tópico; exibir a relação de comentários desse tópico e; inserir um comentário ao tópico. Os dados do tópico estão disponíveis nos primeiros <h1> e <div>. Em seguida, vamos exibir os comentários na <h:dataTable>, somente se a listagem de tópicos não for vazia (lembre-se do isEmptyList()). Por fim, através do último <div> temos o formulário para inserção de comentários novos ao tópico.
Além disso, existe um componente que merece destaque: <f:metadata>. Esse cara é responsável por agrupar os elementos que receberão os valores passados pela URL. Aqui, como estamos passando um único parâmetro pela URL, temos uma única tag <f:viewParam>. Dessa forma, o valor de topic (indicado no item name) será guardado automaticamente em #{commentView.topicId} (definido no parâmetro value). Isso faz parte dos mecanismos novos relacionados ao GET que foram acrescentados nessa nova versão do Java EE.
Tenha em mente que, logo que a página XHTML está sendo convertida pelo servidor de aplicações (porque no fundo no fundo, tudo vira um Servlet), ainda antes de enviar o valor para topicId, ela precisa buscar o valor em getTopicId() - lembre que, a página busca os valores pelo Getter e define pelo Setter. Logo após a obtenção desse valor, ela verifica que existe um parâmetro na URL chamado topic, busca o valor desse parâmetro e envia através do setTopicId(). Não entrarei em mais detalhes sobre como se comporta esse quesito dentro do ciclo de vidas da JSF. O que realmente importa para nós é que: se você quiser receber parâmetros pela URL, use as tags <f:metadata> e <f:viewParam>.
Publicando a aplicação no GlassFish (dentro do Eclipse)
Todas as classes foram programadas! Ótimo.
Caso você tenha optado em trabalhar sem o recurso de Build Automatically do Eclipse (desmarcando o sub-menu Build Automatically dentro do menu Project), lembre-se de fazer o build da aplicação manualmente. No meu caso, como eu não gosto do build automático (na verdade, meu micro que não gosta muito disso, fica uma carroça!), vou mostrar aqui esse processo. Vamos fazer um deploy da aplicação no Glassfish (dentro do Eclipse mesmo, só para testar).
Clique na aba Server (item localizado na parte inferior do Eclipse)

Clique com o botão direito no servidor GlassFish v3 Java EE 6 at localhost e selecione a opção Start
Aguarde até que o servidor de aplicação seja carregado.
Isso pode demorar...
Clique com o botão direito sobre o projeto SuperForum (agora já podemos trabalhar pelo projeto EAR) e selecione a opção Build Project. Isso construirá o projeto EAR (em conjunto com os projetos Web e EJB).
Clique então com o botão direito sobre o projeto SuperForumWeb, selecione a opção Run As, Run on Server
(dependendo da sua configuração, a tela abaixo pode ou não ser exibida)

Selecione a opção GlassFish v3 Java EE 6 at localhost, marque o item Always use this server when running this project e clique em Finish
Nesse momento, a aplicação está sendo publicada no GlassFish e o banco de dados está sendo criado. Você será enviado para um navegador (interno no Eclipse ou do seu Sistema Operacional). Caso apareça o navegador interno do Eclipse, sugiro acessar através de seu navegador o endereço: http://localhost:8080/SuperForumWeb/

Bom galera, nosso sistema está pronto!
Quanto ao desenvolvimento, está terminado. Vamos agora, para a última parte de nossa jornada, onde faremos todos os testes no sistema, validando um a um, os requisitos.
"Minha mãe sempre disse: a vida é como uma caixa de chocolates. Você nunca sabe o que vai achar." - Forrest Gump
Att, Guilherme Pontes
[linu.com.br] - [parte 1] - [parte 2] - [parte 3] - [parte 4] - lgapontes@gmail.com |
Nenhum comentário:
Postar um comentário