Desenvolver a camada apresentação de uma aplicação Web, em Java, de forma simples e rápida.
O Stripes é uma framework open source que implementa o padrão arquitectural model-view-controller (MVC). Este padrão é frequentemente utilizado no desenvolvimento de aplicações web. O model corresponde ao modelo de domínio da aplicação (representação dos dados + funcionalidades), o controller é o responsável por receber os pedidos do cliente e o view é o responsável por apresentar os dados ao cliente.
Algumas das funcionalidades suportadas pelo Stripes são:
Antes de avançar mais na apresentação do Stripes convém relembrar o modelo tradicional de funcionamento de uma aplicação web.
As aplicações web assentam no protocolo HTTP que funciona num modelo de pedido-resposta. Os clientes (browsers) começam por efectuar um pedido ao servidor. O servidor processa esse pedido e em seguida responde de volta. A figura seguinte resume o funcionamento do protocolo HTTP.
Os dados do pedido permitem indicar: a) tipo de pedido (habitualmente GET ou POST), b) identificação do recurso pretendido e c) parâmetros do pedido.
Os dados da resposta contêm: a) código de resultado da operação (200=OK, 404=Not found, etc.) e em caso de sucesso b) o recurso pedido.
As Servlets são classes Java que se executam no contexto de um servidor web. Estas permitem executar código Java que computará a resposta a dar a um pedido web.
A versão complementar das Servlets são as JavaServer Pages (JSP). Estas últimas são particularmente úteis na construção de interfaces web. Os ficheiros JSP contêm código HTML, no qual se pode embeber código Java. Este código é igualmente executado no servidor e permite também gerar dinamicamente conteúdo a ser enviado na resposta para o cliente.
A actual arquitectura de utilização de JSPs (arquitectura JSP Modelo 2) concretiza o padrão MVC: as Servlets são os controladores e as JSPs são as vistas. O modelo pode ser acedido quer pelo controlador quer pela vista. No entanto o controlador é o único que deve invocar código que afecta o modelo de objectos; a vista limita-se a aceder ao modelo para o apresentar. É ainda possível encadear vários controladores antes de enviar para a vista os dados a apresentar.
A sequência de interacções desde a recepção do pedido até ao envio da resposta é indicada pelos números na imagem seguinte.
O modelo de interacção do protocolo HTTP limita o contexto das interacções aos pares pedido-resposta. No entanto isto não é adequado para implementar aplicações web mais complexas. Considere-se, por exemplo, os típicos carrinhos de compras: estes mantêm o seu estado por mais de um pedido sem que os dados sejam perdidos.
Assim sendo, existem, pelo menos, os seguintes contextos:
Nome do contexto | Duração do contexto |
---|---|
Pedido (request) | Desde que chega o pedido até que termina o envio da resposta |
Sessão (session) | Durante toda a interacção (vários pares pedido-resposta) entre o cliente e o servidor (uma sessão por cliente) |
Aplicação (application) | Durante a execução da aplicação web (partilhado por todos os clientes) |
Estes contextos são disponibilizados aos programadores através de objectos Java nos quais é possível guardar e obter outros objectos.
Vamos começar por desenvolver uma aplicação web simples para exemplificar a utilizando o Stripes.
O objectivo é fazer uma página web que demonstra a utilização dos vários contextos para manutenção de estado. Esta página permite submeter uma mensagem de texto através de um formulário.
Após a submissão do formulário volta-se automaticamente para a mesma página. Adicionalmente a página apresenta, se disponível, a seguinte informação:
Vamos começar por definir o ecrã inicial. Como o que queremos fazer é apresentar algo ao utilizador, então de acordo com o MVC, temos de construir uma vista, pelo que vamos fazê-lo utilizando uma JSP.
Para apresentar uma página com o seguinte aspecto...
...podemos escrever o seguinte JSP.
<%@ page contentType="text/html;charset=ISO-8859-1" language="java" %> <%@ taglib prefix="stripes" uri="http://stripes.sourceforge.net/stripes.tld" %> <html> <head><title>An Example of using Contexts and Stripes</title></head> <body> <h1>Hello!</h1> <stripes:form action="/ClickCountEcho.action" focus=""> <p> Say something, please: <stripes:text name="message"/> <stripes:submit name="countClicksAndEcho" value="Go!"/> </p> </stripes:form> </body> </html>
A primeira linha contém informação sobre o /encoding/4 da página e a linguagem de programação usada. A segunda linha importa a biblioteca de etiquetas (taglib) do Stripes que iremos usar na construção do JSP5.
Depois é uma questão de escrever a página HTML pretendida. As únicas
diferenças são na forma como geramos o formulário. Em vez de usar os
elementos originais de html "form
", "input
" e "submit
", usam-se os
equivalentes do Stripes ("form
", "text
" e "submit
").
Vamos analisar estes elementos um a um:
<stripes:form action="/ClickCountEcho.action" focus="">
Esta linha produz um formulário HTML que, quando submetido, será processado
por uma classe Java no servidor. O atributo focus
indica qual o campo do
formulário que terá o cursor (se for definido como "" é o mesmo que indicar
"first").
<p> Say something, please: <stripes:text name="message"/>
Aqui é gerado o campo de texto do formulário. O nome do campo é definido
como sendo message
.
<stripes:submit name="countClicksAndEcho" value="Go!"/>
Finalmente é gerado o botão de submissão. O nome countClicksAndEcho
está
associado ao método que será executado no servidor para tratar o pedido. O
valor Go!
corresponde ao texto que será apresentado no botão.
Em Stripes uma instância de ActionBean é um objecto que recebe os dados submetidos num pedido e os processa. Ou seja, em Stripes os controladores do MVC são os ActionBeans.
Vamos então fazer um ActionBean para o nosso caso:
package scopes.action; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; public class ClickCountEchoAction implements ActionBean { private ActionBeanContext context; public ActionBeanContext getContext() { return context; } public void setContext(ActionBeanContext context) { this.context = context; } ... }
Como é que o Stripes descobre que é esta classe que irá processar o
formulário e não outra qualquer? O Stripes simplesmente assume uma relação
entre o nome da classe e o atributo action
do formulário, fazendo o seguinte:
Ou seja a classe scopes.action.ClickCountEchoAction
corresponde ao URL
/ClickCountEcho.action
que é exactamente o que está escrito no atributo
action
do formulário na JSP. Como a geração do formulário (na JSP) foi
realizada com uma tag do Stripes, esta encarrega-se de gerar o formulário
HTML com os links correctos.
A implementação da interface ActionBean obriga à implementação dos métodos
getContext
e setContext
. O ActionBeanContext encapsula informação
acerca do pedido HTTP actual e permite acesso directo à API das Servlets, caso
isso seja necessário.
Falta ainda preencher a classe com, pelo menos:
No código que se segue, o atributo message
corresponde ao nome do campo de
texto do formulário e o método clickCountsAndEcho()
corresponde ao nome do
botão de submissão do formulário.
public class ClickCountEchoAction implements ActionBean { ... private String message; public String getMessage() { return this.message; } public void setMessage(String message) { this.message = message; } public Resolution countClicksAndEcho() { processSessionClicks(); processApplicationClicks(); return new ForwardResolution("/default.jsp"); } ... }
O método countClicksAndEcho()
retorna uma instância de Resolution
indicando quem é o próximo elemento na cadeia de processamento do pedido.
Neste caso, o que pretendemos é apresentar ao utilizador a resposta, pelo que
reenviamos para uma vista (/default.jsp
).
A classe de processamento do pedido completa fica então assim:
package scopes.action; import javax.servlet.ServletContext; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.DefaultHandler; import net.sourceforge.stripes.action.ForwardResolution; import net.sourceforge.stripes.action.Resolution; public class ClickCountEchoAction implements ActionBean { private ActionBeanContext context; private String message; public ActionBeanContext getContext() { return context; } public void setContext(ActionBeanContext context) { this.context = context; } public String getMessage() { return this.message; } public void setMessage(String message) { this.message = message; } private void processSessionClicks() { Integer sessionClicks = (Integer)getContext().getRequest().getSession().getAttribute("sessionClicks"); if (sessionClicks == null) { sessionClicks = 0; } sessionClicks++; getContext().getRequest().getSession().setAttribute("sessionClicks", sessionClicks); } private void processApplicationClicks() { ServletContext applicationContext = getContext().getServletContext(); synchronized (applicationContext) { Integer applicationClicks = (Integer)applicationContext.getAttribute("applicationClicks"); if (applicationClicks == null) { applicationClicks = 0; } applicationClicks++; applicationContext.setAttribute("applicationClicks", applicationClicks); } } public Resolution countClicksAndEcho() { processSessionClicks(); processApplicationClicks(); return new ForwardResolution("/default.jsp"); } }
Os métodos auxiliares para processar os cliques não fazem mais que incrementar nos respectivos contextos o número de cliques, tendo em atenção que pode ser o primeiro e, nesse caso, é necessário inicializar os valores.
Importante: O contexto aplicação é partilhado por todos os clientes da aplicação web. Logo, para garantir a correcção do contador o código é synchronized no objecto que representa o contexto aplicação, garantindo assim que não se perde a contagem de nenhum clique. O código para processar cliques de sessão sofre do mesmo problema, mas limitado a pedidos concorrentes do mesmo cliente, pois a sessão é única por cliente. Neste caso, se o mesmo cliente efectuar múltiplos pedidos concorrentes é possível que se perca a contagem de alguns cliques. Como exercício pode tentar corrigir este problema.
No caso deste exemplo vamos apresentar o resultado na mesma página em que está o formulário. Após o primeiro pedido deste cliente, mas tendo já ocorrido outros pedidos, um aspecto possível é este:
O que necessitamos de fazer é editar o JSP (a vista) para acrescentar a tabela após o formulário. Mas como a tabela nem sempre aparece temos de escrever uma instrução condicional. Se existir um ActionBean já criado, então isso significa que o formulário já foi processado: só nesse caso é que queremos apresentar a tabela. Para realizar instruções condicionais (entre muitas outras), temos à disposição a JavaServer Pages Standard Tag Library (JSTL).
Assim sendo começamos por acrescentar o core da JSTL ao nosso JSP inicial:
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
Em seguida acrescentamos o seguinte código após a escrita do formulário:
<core:if test="${actionBean != null}"> <table border="1"> <tr><th>Context</th><th>Information</th></tr> <tr> <td>Application</td> <td>${applicationScope.applicationClicks} submission(s) overall</td> </tr> <tr> <td>Session</td> <td>${sessionScope.sessionClicks} submission(s) in this session</td> </tr> <tr><td>Request</td> <td>You said: <i>${actionBean.message}</i></td> </tr> </table> </core:if>
O código ou HTML escrito dentro de <core:if> ... </core:if>
só é executado
caso a condição indicada em test
seja verdadeira, neste caso, se o
actionBean
for diferente de null
.
A nossa aplicação está pronta para ser colocada num servidor web que suporte JSPs, como é o caso do Tomcat. Apenas faltam alguns detalhes de configuração. É necessário configurar e activar a framework do Stripes no ficheiro de configuração do Tomcat (web.xml).
<filter> <display-name>Stripes Filter</display-name> <filter-name>StripesFilter</filter-name> <filter-class>net.sourceforge.stripes.controller.StripesFilter</filter-class> <init-param> <param-name>ActionResolver.UrlFilters</param-name> <param-value>WEB-INF/classes</param-value> </init-param> <init-param> <param-name>ActionResolver.PackageFilters</param-name> <param-value>scopes.action</param-value> </init-param> </filter> <filter-mapping> <filter-name>StripesFilter</filter-name> <url-pattern>*.jsp</url-pattern> <dispatcher>REQUEST</dispatcher> </filter-mapping> <filter-mapping> <filter-name>StripesFilter</filter-name> <servlet-name>StripesDispatcher</servlet-name> <dispatcher>REQUEST</dispatcher> </filter-mapping>
O StripesFilter é responsável pelo processamento inicial dos pedidos. Homogeniza o acesso a várias configurações, ao Locale e aos dados do pedido.1 Neste ponto ou o pedido é directo para um JSP ou para um ActionBean. Neste último caso, o StripesFilter irá direccioná-lo primeiramente para a DispatcherServlet.
<servlet> <servlet-name>StripesDispatcher</servlet-name> <servlet-class>net.sourceforge.stripes.controller.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>StripesDispatcher</servlet-name> <url-pattern>*.action</url-pattern> </servlet-mapping>
A Servlet DispatcherServlet é responsável por identificar para qual ActionBean é o pedido e agulhar para lá. Antes disso, instancia o ActionBeanContext e colocá lá os dados do pedido. Ver o ciclo de vida de um pedido.
Pode obter aqui o exemplo completo para experimentar. Basta:
Até aqui demonstrámos o funcionamento básico do Stripes. Vamos agora analisar mais algumas características úteis no desenvolvimento de uma aplicação Web.
A maioria das aplicações web tem mais de uma página e os programadores pretendem dar um aspecto comum às várias páginas da aplicação. O Stripes facilita a escrita de páginas web com aspectos comuns através da utilização de layouts.
Existem 3 etiquetas para gerir layouts:
<stripes:layout-definition>
: define um layout reutilizável
<stripes:layout-component>
: define um componente dentro de um layout
<stripes:layout-render>
: apresenta um layout dentro de uma página
Ao apresentar um layout o Stripes segue as seguintes regras:
<stripes:layout-definition>
é ignorado
<stripes:layout-definition>
, mas fora de
<stripes:layout-component>
é sempre apresentado
<stripes:layout-component>
o seu conteúdo é
apresentado, a menos que tenha sido redefinido (overridden). Nesse caso é
o valor redefinido que é apresentado.
Exemplo: Utilização de layouts.
Para saber mais ver:
As interfaces com o utilizador têm por norma que se preocupar com a validação dos dados de entrada e com a apresentação de erros.
A etiqueta principal de apresentação de erros é <stripes:errors/>
(javadoc). Esta etiqueta tem várias opções de utilização. As mais comuns
são:
<stripes:errors/>
Se estiver dentro de um formulário esta etiqueta apresenta todos os erros de validação desse formulário. Se estiver fora, apresenta todos os erros de validação de qualquer formulário que tenha sido submetido.
Em alternativa pode-se pedir para apresentar os erros campo a campo de um formulário:
<stripes:form action="/login.action"> <stripes:errors globalErrorsOnly="true"/> <table> <tr> <td>Username:</td> <td> <stripes:text name="username"/> <stripes:errors field="username"/> </td> </tr> <tr> <td>Password:</td> <td> <stripes:text name="password"/> <stripes:errors field="password"/> </td> </tr> </table> </stripes:form>
A primeira utilização com globalErrorsOnly="true" indica que ali apenas serão apresentados os erros não associados a nenhum campo específico de um formulário.
Dado que as excepções podem ocorrer em diversos pontos do tratamento de um
pedido (no controlador, no modelo de domínio, na vista), o Stripes efectua o
catch
das excepções ao nível do StripesFilter. Relembre que esta classe é a
primeira a receber o pedido web, ou seja, é a que está mais acima no que
respeita à cadeia de processamento de um pedido em Stripes. Desta forma, onde
quer que a excepção possa ocorrer, esta será apanhada e pode ser tratada pelo
mecanismo de tratamento de excepções do Stripes.
Este mecanismo tem várias formas de funcionamento. Iremos apenas descrever uma das mais utilizadas.
Através do ficheiro web.xml é possível activar o DelegatingExceptionHandler do Stripes. Este exception handler delega o tratamento de excepções em classes que implementam a interface AutoExceptionHandler.
<init-param> <param-name>ExceptionHandler.Class</param-name> <param-value>net.sourceforge.stripes.exception.DelegatingExceptionHandler</param-value> </init-param> <init-param> <param-name>DelegatingExceptionHandler.UrlFilters</param-name> <param-value>WEB-INF/classes</param-value> </init-param> <init-param> <param-name>DelegatingExceptionHandler.PackageFilters</param-name> <param-value>errors.exception</param-value> </init-param>
Os parâmetros DelegatingExceptionHandler.UrlFilters
e
DelegatingExceptionHandler.PackageFilters
servem apenas para limitar o
espaço de procura de classes que implementam AutoExceptionHandlers.
Um AutoExceptionHandler pode ter vários métodos no formato:
public Resolution metodo(Exception e, HttpServletRequest req, HttpServletResponse res);
onde metodo
é um nome de método qualquer e o primeiro argumento pode ser de
qualquer subclasse de Exception
(ou até de Throwable
na realidade).
Sempre que ocorrer uma excepção, o DelegatingExceptionHandler irá procurar o método mais específico capaz de tratar a excepção e invocá-lo. O objectivo do método será tratar a excepção e devolver o próximo elemento na cadeia de processamento.
Cada vez mais as aplicações são utilizadas a um nível global. Não só as cadeias de texto variam consoante a língua do utilizador, como também o formato de introdução dos dados depende da região2. Eis os principais aspectos a considerar para tornar uma aplicação em Stripes localizada:
Quanto à configuração é tão simples quanto indicar no web.xml quais os locales disponíveis. Por exemplo:
<init-param> <param-name>LocalePicker.Locales</param-name> <param-value>pt, en_US</param-value> </init-param>
O Stripes tem uma classe que é responsável por identificar para cada pedido qual o locale a utilizar. Este é decido tendo em conta os locales configurados e os locales pedidos pelo cliente (enviado pelo browser).
As mensagens localizadas são procuradas, por omissão, no ficheiro de recursos
StripesResources.properties
. Ficheiros específicos para cada locale têm o
locale no nome, por exemplo: StripesResources_pt.properties
.
Nestes ficheiros cada linha tem o formato nome=valor, sendo o "nome" a chave da mensagem e "valor" o texto a utilizar.
Existem vários pontos onde se pode fazer uso de mensagens localizadas:
Exemplo: Tratamento de excepções e mensagens de erro. Neste exemplo altere nas configurações do seu browser a linguagem preferida. Experimente inglês [en] e português [pt].
Para saber mais ver:
É possível utilizar o Stripes para validar automaticamente os dados enviados num pedido. No contexto da validação, é também possível efectuar a conversão dos dados do pedido para tipos de Java. Em primeiro lugar é necessário anotar os campos do formulário que estão declarados no ActionBean com os requisitos de validação pretendidos.
Por exemplo, considere-se o típico formulário de login com dois campos: username e password. O ActionBean correspondente poderia ter o seguinte aspecto:
public class LoginAction implements ActionBean { @Validate(required=true, converter=EmailTypeConverter.class) private String username; @Validate(required=true, mask="[A-Za-z0-9]{6,}") private String password; ... }
Isto significa que ambos os campos são obrigatórios, o username tem de ter o formato de um email e a password tem de ter no mínimo 6 caracteres sendo composta por letras (maiúsculas e minúsculas) e/ou algarismos.
Caso a validação falhe o utilizador verá novamente o formulário com a
informação do(s) erro(s) ocorrido(s). Para mostrar os erros utiliza-se a
etiqueta stripes:errors
nos JSPs.
O processo de validação de um formulário é o seguinte:
A validação tenta avançar o mais possível, ou seja, mesmo que a validação falhe em algum passo para um determinado campo, os campos que ainda válidos continuam o processo de validação. O objectivo é identificar o maior número de erros de validação de uma só vez.
A criação de regras de validação "à medida" (validação "costumizada") é muito
fácil. Basta criar métodos3 no ActionBean e anotá-los com @ValidationMethod
.
Exemplo: Validação de um formulário e conversão dos campos.
Para saber mais ver:
Pretende-se desenvolver uma aplicação web para calcular a raíz quadrada de um número.
A aplicação deve ter 2 ecrãs. No primeiro é pedido o número do qual calcular a raiz quadrada e no segundo deve ser apresentado o resultado, com um link dando a hipótese de voltar à página inicial.
A aplicação só deve aceitar números não negativos para o cálculo, mas pode aceitar números não inteiros.
Em ambos os ecrãs deve aparecer o nome da aplicação (invente-o, p.e. "Square root application").
Sugestão 1: baseie-se na estrutura dos exemplos fornecidos para começar o desenvolvimento da aplicação.
Sugestão 2: faça uma versão inicial da aplicação que não use layouts. Quando esta estiver a funcionar altere-a para usar layouts. A parte comum (o nome da aplicação) deve ser reutilizado para construir os dois ecrãs.
Documentação das tags do Stripes
Tutorial JavaEE 5, Part II: The Web Tier - http://java.sun.com/javaee/5/docs/tutorial/doc/bnadp.html
Tutorial JavaEE 5, Part II, Chapter 5, Section 6: Unified Expression Language - http://java.sun.com/javaee/5/docs/tutorial/doc/bnahq.html
Tutorial JavaEE 5, Part II, Chapter 7: JavaServer Pages Standard Tag Library - http://java.sun.com/javaee/5/docs/tutorial/doc/bnakc.html
1 Ver http://www.stripesframework.org/display/stripes/LifeCycles+Etc. O ciclo de vida de um pedido realizado a um ActionBean pode ser definido resumidamente da seguinte forma:
Resolution
), executá-lo
2 Um exemplo muito comum é a diferença na escrita de datas entre europeus e americanos (23/02/2008 vs. 02/23/2008)
3 Estes métodos não necessitam de receber nem devolver nada. Em alternativa
podem receber um único argumento do tipo
net.sourceforge.stripes.validation.ValidationErrors
, o que apenas serve para
facilitar o acesso a estes dados caso seja necessário adicionar mensagens de
erros.
4 O encoding indicado deve ser aquele no qual o ficheiro é gravado. Os encodings mais habituais são ISO-8859-1 e UTF-8.
5 Os elementos de uma taglib devem ser sempre precedidos pelo prefixo definido aquando da importação da biblioteca.
Data: 2008/04/30 11:55:10