Skip to content

4. Acompanhamento da sessão

4.1. O problema

Uma aplicação web pode consistir em várias trocas de formulários entre o servidor e o cliente. O funcionamento é então o seguinte:

  • etapa 1
    • o cliente C1 estabelece uma ligação com o servidor e efetua o seu pedido inicial.
    • O servidor envia o formulário F1 ao cliente C1 e encerra a ligação estabelecida na etapa 1.
  • etapa 2
    • O cliente C1 preenche-o e reenvia-o ao servidor. Para tal, o navegador estabelece uma nova ligação com o servidor.
    • Este processa os dados do formulário 1, calcula as informações I1 a partir desses dados, envia um formulário F2 ao cliente C1 e encerra a ligação aberta na etapa 3.
  • Etapa 3
    • O ciclo das etapas 3 e 4 repete-se nas etapas 5 e 6. No final da etapa 6, o servidor terá recebido dois formulários F1 e F2 e, a partir destes, terá calculado as informações I1 e I2.

A questão que se coloca é: como é que o servidor consegue manter as informações I1 e I2 associadas ao cliente C1? A este problema chama-se «acompanhamento da sessão do cliente C1». Para compreender a sua origem, analisemos o esquema de uma aplicação de servidor TCP-IP que atende simultaneamente vários clientes:

Numa aplicação cliente-servidor TCP-IP clássica:

  • o cliente estabelece uma ligação com o servidor
  • troca dados com o servidor através dessa ligação
  • a ligação é encerrada por um dos dois parceiros

Os dois pontos importantes deste mecanismo são:

  1. é criada uma ligação única para cada um dos clientes
  2. esta ligação é utilizada durante todo o período de diálogo entre o servidor e o seu cliente

O que permite ao servidor saber, num determinado momento, com que cliente está a trabalhar é a ligação ou, por outras palavras, o «canal» que o liga ao seu cliente. Sendo este canal dedicado a um determinado cliente, tudo o que chega por esse canal provém desse cliente e tudo o que é enviado por esse canal chega ao cliente.

O mecanismo cliente-servidor HTTP segue fielmente o esquema anterior, com a particularidade de que a comunicação cliente-servidor se limita a uma única troca entre o cliente e o servidor:

  • o cliente abre uma ligação com o servidor e faz o seu pedido
  • o servidor responde e encerra a ligação

Se, no momento T1, um cliente C efetuar uma solicitação ao servidor, obtém uma ligação C1 que servirá para a única troca de solicitação-resposta. Se, no momento T2, esse mesmo cliente efetuar uma segunda solicitação ao servidor, obterá uma ligação C2 diferente da ligação C1. Para o servidor, não há, portanto, qualquer diferença entre esta segunda solicitação do utilizador C e a sua solicitação inicial: em ambos os casos, o servidor considera o cliente como um novo cliente. Para que exista uma ligação entre as diferentes ligações do cliente C ao servidor, é necessário que o cliente C seja «reconhecido» pelo servidor como um «cliente habitual» e que o servidor recupere as informações que possui sobre esse cliente habitual.

Imaginemos um sistema de gestão que funcionasse da seguinte forma:

  • Existe uma única fila de espera
  • Existem vários balcões. Assim, vários clientes podem ser atendidos simultaneamente. Quando um balcão fica livre, um cliente sai da fila de espera para ser atendido nesse balcão
  • Se for a primeira vez que o cliente se apresenta, a pessoa no balcão entrega-lhe uma ficha com um número. O cliente só pode fazer uma pergunta. Quando obtém a resposta, deve sair do balcão e passar para o fim da fila de espera. O funcionário do balcão regista as informações desse cliente num processo com o seu número.
  • Quando chega novamente a sua vez, o cliente pode ser atendido por um funcionário diferente do da vez anterior. Este pede-lhe a ficha e recupera o processo com o número da ficha. Mais uma vez, o cliente faz um pedido, obtém uma resposta e são adicionadas informações ao seu processo.
  • E assim sucessivamente... Com o passar do tempo, o cliente terá a resposta a todas as suas solicitações. O acompanhamento entre as diferentes solicitações é feito através do ficha e do processo a ele associado.

O mecanismo de acompanhamento de sessão numa aplicação web cliente-servidor é análogo ao funcionamento anterior:

  • na sua primeira solicitação, um cliente recebe um token do servidor web
  • ele apresentará esse token em cada uma das suas solicitações subsequentes para se identificar

O token pode assumir diferentes formas:

  • um campo oculto num formulário
    • o cliente faz a sua primeira solicitação (o servidor reconhece-o pelo facto de o cliente não ter um token)
    • o servidor envia a sua resposta (um formulário) e coloca o token num campo oculto do mesmo. Nesse momento, a ligação é encerrada (o cliente sai do servidor com o seu token). O servidor terá, eventualmente, associado informações a esse token.
    • o cliente faz o seu segundo pedido, reenviando o formulário. O servidor recupera o token desse formulário. Pode então processar o segundo pedido do cliente, tendo acesso, graças ao token, às informações calculadas durante o primeiro pedido. São adicionadas novas informações ao ficheiro associado ao token, é enviada uma segunda resposta ao cliente e a ligação é encerrada pela segunda vez. O token foi novamente incluído no formulário da resposta para que o utilizador o possa apresentar na sua próxima solicitação.
    • E assim sucessivamente...

A principal desvantagem desta técnica é que o token tem de ser inserido num formulário. Se a resposta do servidor não for um formulário, o método do campo oculto deixa de ser utilizável.

  • A técnica do cookie
    • o cliente faz a sua primeira solicitação (o servidor reconhece-o pelo facto de o cliente não ter um token)
    • o servidor responde adicionando um cookie aos cabeçalhos HTTP da resposta. Isto é feito através do comando HTTP Set-Cookie:

Set-Cookie: param1=valor1;param2=valor2;....

onde parami são nomes de parâmetros e valeursi os respetivos valores. Entre os parâmetros, estará o token. Muitas vezes, apenas o token consta no cookie, sendo as restantes informações registadas pelo servidor na pasta associada ao token. O navegador que recebe o cookie irá armazená-lo num ficheiro no disco. Após a resposta do servidor, a ligação é encerrada (o cliente sai da janela com o seu token).

  • (continuação)
    • o cliente faz a sua segunda solicitação ao servidor. Sempre que é feita uma solicitação a um servidor, o navegador verifica, entre todos os cookies que possui, se existe algum proveniente do servidor solicitado. Se sim, envia-o ao servidor sempre sob a forma de um comando HTTP, o comando «Cookie», cuja sintaxe é análoga à do comando Set-Cookie utilizado pelo servidor:

Cookie: param1=valor1;param2=valor2;....

Entre os parâmetros enviados pelo navegador, o servidor irá identificar o token que lhe permite reconhecer o cliente e recuperar as informações que lhe estão associadas.

Esta é a forma mais utilizada de token. Apresenta uma desvantagem: um utilizador pode configurar o seu navegador para que não aceite cookies. Este tipo de utilizador não tem, então, acesso às aplicações web que utilizam cookies.

  • Reescrita de URL
    • o cliente faz o seu primeiro pedido (o servidor reconhece-o pelo facto de o cliente não ter um token)
    • o servidor envia a sua resposta. Esta contém links que o utilizador deve utilizar para continuar a aplicação. No URL de cada um desses links, o servidor adiciona o token na forma URL;token=valor.
    • Quando o utilizador clica num dos links para continuar a aplicação, o navegador envia o seu pedido ao servidor web, incluindo nos cabeçalhos HTTP o token solicitado na forma URL URL;token=valor. O servidor consegue então recuperar o token.

4.2. Java para o acompanhamento de sessões

Apresentamos agora os principais métodos úteis para o acompanhamento da sessão:

HttpSession [HttpServletRequest].getSession()
obtém o objeto Session ao qual pertence o pedido em curso. Se este ainda não fizesse parte de uma sessão, esta é criada.
String [HttpSession].getId()
identificador da sessão atual
long [HttpSession].getCreationTime()
data de criação da sessão atual (número de milissegundos decorridos desde 1 de janeiro de 1970, 0h).
long [HttpSession].getLastAccessedTime()
data do último acesso à sessão pelo cliente
long [HttpSession].getMaxInactiveInterval()
tempo máximo, em segundos, de inatividade de uma sessão. Após esse período, a sessão é invalidada.
[HttpSession].setMaxInactiveInterval(int durée)
define, em segundos, a duração máxima de inatividade de uma sessão. Após esse período, a sessão é invalidada.
boolean [HttpSession].isNew()
verdadeiro se a sessão tiver acabado de ser criada
[HttpSession].setAttribute(String paramètre, Object valeur)
associa um valor a um parâmetro numa determinada sessão. É este mecanismo que permite memorizar informações que permanecerão disponíveis ao longo de toda a sessão.
[HttpSession].removeAttribute(String paramètre)
remove parametre dos dados da sessão.
Object [HttpSession].getAttribute(String paramètre)
valor associado ao parâmetro paramètre da sessão. Retorna null caso este não exista.
Enumeration [HttpSession].getAttributeNames()
lista, sob a forma de enumeração, de todos os atributos da sessão atual
[HttpSession].invalidate()
encerra a sessão atual. Todas as informações associadas à mesma são eliminadas.

4.3. Exemplo 1

Apresentamos um exemplo retirado do excelente livro «Programação com J2EE», publicado pela editora Wrox e distribuído pela Eyrolles. Este livro é uma fonte de informação de alto nível para os programadores de soluções Web em Java. A aplicação apresentada neste livro sob a forma de um único servlet Java foi aqui reproduzida sob a forma de um servlet principal que recorre a páginas JSP para apresentar as várias respostas possíveis ao cliente.

A aplicação chama-se «sessions» e está configurada da seguinte forma no ficheiro <tomcat>\conf\server.xml:

                <Context path="/sessions" docBase="e:/data/serge/servlets/sessions" />

Na pasta docBase acima, encontram-se os seguintes elementos:

Image

Os ficheiros erreur.jsp, invalide.jsp e valide.jsp estão todos associados à aplicação «sessions». Na pasta WEB-INF acima referida, encontram-se:

Image

Acima, vemos o ficheiro web.xml de configuração da aplicação «sessions». Na pasta classes, encontra-se o ficheiro de classe do servlet:

Image

O ficheiro web.xml da aplicação é o seguinte:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
      <servlet-name>cycledevie</servlet-name>
    <servlet-class>cycledevie</servlet-class>
    <init-param>
          <param-name>urlSessionValide</param-name>
        <param-value>/valide.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlSessionInvalide</param-name>
        <param-value>/invalide.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
      <servlet-name>cycledevie</servlet-name>
    <url-pattern>/cycledevie</url-pattern>
  </servlet-mapping>
</web-app>

A servlet principal chama-se cycledevie (servlet-name) e está associada ao ficheiro de classe cycledevie.class (servlet-class). Possui um alias /cycledevie (servlet-mapping) que permite chamá-la através do URL http://localhost:8080/sessions/cycledevie. Possui três parâmetros de inicialização:

urlSessionValide
URL da página que apresenta as características da sessão atual
urlSessionInvalide
URL da página apresentada após a invalidação da sessão atual
urlErreur
URL da página apresentada em caso de erro de inicialização do servlet principal «cycledevie»

Os componentes da aplicação «sessions» são os seguintes:

cycledevie
servlet principal — analisa o pedido do cliente:
  • se esta fizer parte de uma sessão, redireciona para a página valide.jsp, que irá apresentar as características dessa sessão. A partir desta página, o utilizador pode:
    • recarregá-la
    • anulá-la
  • se a solicitação pedir para invalidar a sessão atual, o servlet redireciona para a página invalide.jsp, que irá propor ao utilizador que recrie uma nova sessão
  • se, durante a sua inicialização, a servlet encontrar erros, ela redireciona para a página erreur.jsp, que exibirá uma mensagem de erro.
valide.jsp
  • apresenta as características da sessão atual e oferece dois links:
    • um para recarregar a página e ver assim a evolução do parâmetro do último acesso à sessão atual
    • outro para invalidar a sessão atual
invalide.jsp
exibida quando o utilizador invalidou a sessão atual. Sugere então que se crie uma nova sessão.
erreur.jsp
exibida quando o servlet principal encontra erros durante a sua inicialização.

A servlet principal cycledevie é a seguinte:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class cycledevie extends HttpServlet{

    // variáveis de instância
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // A inicialização decorreu corretamente?
        if(msgErreur!=null){
             // passamos o controlo para a página de erro
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // recuperamos a sessão atual
        HttpSession session=request.getSession();

         // analisamos a ação a realizar
        String action=request.getParameter("action");
        // invalidar a sessão atual
        if(action!=null && action.equals("invalider")){
            // invalida-se a sessão atual
            session.invalidate();
             // passa o controlo para o URL urlSessionInvalide
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // outros casos
         // passa o controlo para o URL urlSessionInvalide
        getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // recuperam-se os parâmetros de inicialização
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // parâmetros corretos?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

É de salientar o seguinte:

  • no seu método de inicialização, o servlet recupera os seus três parâmetros
  • no processamento (doGet) de um pedido, a servlet:
    • verifica, em primeiro lugar, se não ocorreu nenhum erro durante a inicialização. Caso tenha ocorrido algum erro, a servlet passa o controlo para a página erreur.jsp.
    • verifica o valor do parâmetro action. Se este tiver o valor «invalider», a servlet passa o controlo para a página invalide.jsp; caso contrário, para a página valide.jsp.

A página JSP valide.jsp para a visualização das características da sessão atual:

<%@ page import="java.util.*" %>

<%
     // jspService
   // aqui estamos no caso em que é necessário descrever a sessão em curso
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- início da página HTML -->
  <html>
      <meta http-equiv="pragma" content="no-cache">
    <head>
        <title>Cycle de vie d'une session</title>
    </head>
    <body>
        <h3>Cycle de vie d'une session</h3>
        <hr>
        <br>Etat session : <%= etat %>
      <br>ID session : <%= session.getId() %>
      <br>Heure de création : <%= new Date(session.getCreationTime()) %>
      <br>Heure du dernier accès : <%= new Date(session.getLastAccessedTime()) %>
      <br>Intervalle maximum d'inactivité : <%= session.getMaxInactiveInterval() %>
      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>
    <body>
  </html>

Note-se que na linha

  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";

é utilizado um objeto de sessão que surge do nada. Na verdade, este objeto faz parte dos objetos implícitos disponibilizados às páginas JSP, tal como os objetos request, response, out, config (ServletConfig), context (ServletContext), que já foram mencionados. Os dois links da página remetem para o servlet cycledevie apresentado anteriormente:

      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>

O link para invalidar a sessão inclui o parâmetro action=invalider, que permitirá que o servlet cycledevie reconheça que o utilizador pretende invalidar a sessão atual. O outro link permite recarregar a página. Para que o navegador não a recupere da cache, a diretiva HTML:

      <meta http-equiv="pragma" content="no-cache">

foi utilizada. Esta indica ao navegador para não utilizar o cache para a página que recebe.

A página invalide.jsp é a seguinte:

<!-- início da página HTML -->
<html>
  <head>
      <title>Cycle de vie d'une session</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session</h3>
      <hr>
    Votre session a été invalidée
    <a href="/sessions/cycledevie">Créer une nouvelle session</a>
  </body>
</html>

Esta página apresenta um link que aponta para o servlet cycledevie sem o parâmetro action. Este link fará com que o servlet cycledevie crie uma nova sessão.

A página erreur.jsp é a seguinte:

<%
     // jspService
   // aqui estamos no caso em que é necessário descrever a sessão em curso
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- início da página HTML -->
<html>
  <head>
      <title>Cycle de vie d'une session</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

A sua função é apresentar a mensagem de erro que lhe foi transmitida pelo servlet cycledevie. Vejamos agora alguns exemplos de execução. O servlet é solicitado pela primeira vez:

Image

A página acima indica que estamos numa nova sessão. Utilizamos o link «Atualizar a página»:

Image

O resultado anterior indica que continuamos na mesma sessão da página anterior (o mesmo ID). Repare-se que a hora do último acesso a esta sessão mudou. Agora, utilizemos o link «Anular a sessão»:

Image

Repare-se no URL desta nova página, que difere do parâmetro action=invalider. Utilizemos o link «Criar uma nova sessão» para criar uma nova sessão:

Image

Verifica-se que foi iniciada uma nova sessão. Nos exemplos anteriores, a sessão baseia-se no mecanismo dos cookies. Vamos agora desativar a utilização de cookies no nosso navegador e repetir os testes. Os exemplos seguintes foram realizados com o Netscape Communicator. Por uma razão inexplicável, os testes realizados com IE6 apresentavam resultados inesperados, como se IE6 continuasse a utilizar cookies, apesar de estes terem sido desativados. A servlet cycledevie é solicitada uma primeira vez:

Image

Utilizamos agora o link «Atualizar a página»:

Image

É possível observar duas coisas:

  • o ID da sessão mudou
  • o servlet deteta a sessão como uma nova sessão

O servidor Tomcat oferece uma solução para o problema dos utilizadores que desativam a utilização de cookies no seu navegador. Utiliza dois mecanismos para implementar o token de que falámos no início deste parágrafo: os cookies e a reescrita do URL. Se o cookie de sessão não estiver disponível, tentará obter o token a partir do URL solicitado pelo cliente. Para tal, é necessário que esta contenha o token. De um modo geral, todos os links gerados num documento HTML para a aplicação web devem conter o token da mesma. Isto pode ser feito com o método encodeURL:

String [HttpResponse].encodeURL(String URL)
adiciona o token da sessão atual ao URL passado como parâmetro na forma URL;jsessionid=xxxx

Alteramos a nossa aplicação da seguinte forma:

  • no servlet cycledevie.java, os URL são codificados:
             // passamos o controlo para a página de erro
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // passamos o controlo para o URL urlSessionInvalide
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // passa-se o controlo para o URL urlSessionInvalide
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • na página valide.jsp, os URL estão codificados:
<%
     // jspService
   // aqui estamos no caso em que é necessário descrever a sessão em curso
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
   // codificação URL ciclo de vida
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • na página invalide.jsp, os URL estão codificados:
<%
     // jspservice — invalida-se a sessão atual
  session.invalidate();
   // codificação URL ciclo de vida
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

Agora estamos prontos para os testes. Estamos a utilizar o Netscape 4.5 e os cookies foram desativados. Acedemos pela primeira vez ao servlet cycledevie:

Image

e atualizamos a página através do link «Atualizar a página»:

Image

Podemos verificar que:

  • a sessão não se alterou (continua a ser ID)
  • o URL do servlet cycledevie contém efetivamente o token, tal como mostra o campo Adresse acima
  • o servidor Tomcat recupera, portanto, o token de sessão no URL solicitado (desde que o programador tenha tido o cuidado de o codificar).

4.4. Exemplo 2

Apresentamos agora um exemplo que mostra como armazenar informações na sessão de um cliente. Neste caso, a única informação será um contador que será incrementado sempre que o utilizador chamar o URL do servlet. Quando este é chamado pela primeira vez, obtém-se a seguinte página:

Image

Se clicarmos na ligação «Atualizar a página» acima, obtemos a seguinte nova página:

Image

A aplicação tem três componentes:

  • um servlet que processa o pedido do cliente
  • uma página JSP que apresenta o valor do contador
  • uma página JSP que apresenta um eventual erro

Estes três componentes estão instalados na aplicação web «sessions» já utilizada. O ficheiro web.xml desta aplicação foi alterado para configurar os novos servlets:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
...
  <servlet>
      <servlet-name>compteur</servlet-name>
    <servlet-class>compteur</servlet-class>
    <init-param>
          <param-name>urlAffichageCompteur</param-name>
        <param-value>/compteur.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreurcompteur.jsp</param-value>
    </init-param>
  </servlet>
...
  <servlet-mapping>
      <servlet-name>compteur</servlet-name>
    <url-pattern>/compteur</url-pattern>
  </servlet-mapping>
</web-app>
  • A servlet chama-se «contador» (servlet-name) e está associada ao ficheiro de classe compteur.class (servlet-class)
  • Possui dois parâmetros de inicialização:
    • urlAffichageCompteur: URL da página JSP de visualização do contador
    • urlErreur: URL da página JSP de exibição de um eventual erro
  • e um alias /contador que faz com que seja chamado através do URL http://localhost:8080/sessions/compteur

O servlet compteur.java é o seguinte:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class compteur extends HttpServlet{

     // variáveis de instância
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // A inicialização decorreu corretamente?
        if(msgErreur!=null){
             // passamos o controlo para a página de erro
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // recuperamos a sessão atual
        HttpSession session=request.getSession();
         // e o contador
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // incremento do contador
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // armazenamento do contador na sessão
        session.setAttribute("compteur",compteur);
         // e na consulta
        request.setAttribute("compteur",compteur);

         // passa-se o controlo para a URL de exibição do contador
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // recuperam-se os parâmetros de inicialização
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

         // parâmetros corretos?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Este servlet tem a mesma estrutura dos servlets já abordados. Destaca-se apenas a gestão do contador:

  • a sessão é recuperada através de request.getSession()
  • o contador é recuperado nesta sessão através de session.getAttribute("contador")
  • se for recuperado um valor null, significa que a sessão acabou de começar. O contador é então colocado a 0.
  • o contador é incrementado, colocado de volta na sessão (session.setAttribute("contador",contador)) e incluído na solicitação que será enviada ao servlet de visualização (request.setAttribute("contador",contador)).

A página de visualização compteur.jsp é a seguinte:

<%
     // jspService
   // a recuperar o contador
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- início da página HTML -->
<html>
  <head>
      <title>Comptage au fil d'une session</title>
  </head>
  <body>
      <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
      <hr>
    compteur = (<%= compteur %>)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

A página acima limita-se a recuperar o atributo compteur (request.getAttribute("contador")) que lhe foi passado pelo servlet principal e a exibi-lo.

A página de erro erreurcompteur.jsp é a seguinte:

<%
     // jspService
   // ocorreu um erro
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- início da página HTML -->
<html>
  <head>
      <title>Comptage au fil d'une session</title>
  </head>
  <body>
      <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

4.5. Exemplo 3

Propomos escrever uma aplicação Java que funcione como cliente da aplicação compteur anterior. Esta chamá-la-ia N vezes consecutivas, sendo que N seria passado como parâmetro. O nosso objetivo é mostrar um cliente web programado e a forma de gerir os cookies. O nosso ponto de partida será um cliente web genérico apresentado no folheto sobre Java do mesmo autor. É chamado da seguinte forma:

clientweb URL GET/HEAD

  • URL: URL solicitada
  • GET/HEAD: GET para solicitar o código HTML da página, HEAD para se limitar apenas aos cabeçalhos HTTP

Eis um exemplo com o URL e o http://localhost:8080/sessions/compteur:


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/contador GET

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 14:21:18 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=B8A9076E552945009215C34A97A0EC5D;Path=/sessions


<!-- início da página HTML -->
<html>
  <head>
        <title>Comptage au fil d'une session</title>
  </head>
  <body>
        <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
        <hr>
    compteur = (1)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

O programa clientweb apresenta tudo o que recebe do servidor. Acima, vemos o comando HTTP Set-cookie, com o qual o servidor envia um cookie ao seu cliente. Neste caso, o cookie contém duas informações:

  • JSESSIONID, que é o token da sessão
  • Path, que define a URL à qual o cookie pertence. Path=/sessions indica ao navegador que deverá reenviar o cookie ao servidor sempre que solicitar uma URL que comece por /sessions. Na aplicação sessions, utilizámos diferentes servlets, incluindo os servlets /sessions/cycledevie e /sessions/compteur. Se chamarmos o servlet /sessions/cycledevie, o navegador receberá um token J. Se, com esse mesmo navegador, chamar em seguida a servlet /sessions/compteur, o navegador reenviará ao servidor o token J, uma vez que este é válido para todas as servlets URL que começam por /sessions. No nosso exemplo, as servlets cycledevie e compteur não têm de partilhar o mesmo token de sessão. Por isso, não deveriam ter sido colocadas na mesma aplicação web. Este é um ponto a reter: todas as servlets de uma mesma aplicação partilham o mesmo token de sessão.
  • Um cookie também pode definir um período de validade. Neste caso, essa informação não está presente. O cookie será, portanto, eliminado quando o navegador for fechado. Um cookie pode ter um período de validade de N dias, por exemplo. Enquanto estiver válido, o navegador irá reenviá-lo sempre que uma das páginas URL do seu domínio (Path) for consultada. Tomemos como exemplo um site de vendas online de CD. Este pode acompanhar o percurso do cliente no seu catálogo e determinar, gradualmente, as suas preferências: música clássica, por exemplo. Essas preferências podem ser armazenadas num cookie com uma validade de 3 meses. Se esse mesmo cliente regressar ao site ao fim de um mês, o navegador reenviará o cookie para a aplicação do servidor. Esta, com base nas informações contidas no cookie, poderá então adaptar as páginas geradas às preferências do seu cliente.

Segue-se o código do cliente web. Este servirá posteriormente de ponto de partida para outro cliente.

// pacotes importados
import java.io.*;
import java.net.*;

public class clientweb{

    // solicita um URL
     // exibe o conteúdo da mesma no ecrã

    public static void main(String[] args){
        // sintaxe
        final String syntaxe="pg URI GET/HEAD";

        // número de argumentos
        if(args.length != 2)
            erreur(syntaxe,1);

         // regista-se o URI solicitado
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // verificação da validade do URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//exceção
         // verificação do pedido
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // encomenda incorreta
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extraímos as informações úteis do URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // é possível prosseguir
        Socket  client=null;                        // o cliente
        BufferedReader IN=null;                    // o fluxo de leitura do cliente
        PrintWriter OUT=null;                        // o fluxo de escrita do cliente
        String réponse=null;                        // resposta do servidor
        try{
             // estabelece-se a ligação ao servidor
            client=new Socket(host,port);

            // criam-se os fluxos de entrada e saída do cliente TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // solicitação do URL - envio dos cabeçalhos HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // lê-se a resposta
            while((réponse=IN.readLine())!=null){
                 // processa-se a resposta
                System.out.println(réponse);
            }//enquanto
             // terminou
            client.close();
        } catch(Exception e){
            // trata-se a exceção
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // exibição de erros
    public static void erreur(String msg, int exitCode){
         // exibição do erro
        System.err.println(msg);
         // encerramento com erro
        System.exit(exitCode);
    }//erro
}//classe

Vamos agora criar o programa clientCompteur, chamado da seguinte forma:

clientCompteur URL N [JSESSIONID]

  • URL: URL do servlet contador
  • N: número de chamadas a efetuar a esta servlet
  • JSESSIONID: parâmetro opcional — token de uma sessão

O objetivo do programa é chamar a servlet contador N vezes, gerindo o cookie de sessão e apresentando, em cada chamada, o valor do contador devolvido pelo servidor. No final das N chamadas, o valor do contador deve ser N. Eis um primeiro exemplo de execução:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/contador 3
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A;Path=/sessions
cookie trouvÚ : 92DB3808CE8FCB47D47D997C8B52294A

compteur : 1

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 2

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 3

O programa apresenta:

  • os cabeçalhos HTTP que envia ao servidor na forma -->
  • os cabeçalhos HTTP que recebe
  • o valor do contador após cada chamada

Vê-se que, na primeira chamada:

  • o cliente não envia nenhum cookie
  • o servidor envia um

Nas chamadas seguintes:

  • o cliente reenvia sistematicamente o cookie que recebeu do servidor na primeira chamada. É isso que permitirá ao servidor reconhecê-lo e incrementar o seu contador.
  • o servidor, por sua vez, já não reenvia o cookie

Reexecutamos o programa anterior, passando o token acima como terceiro parâmetro:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/contador 3 92DB3808CE8FCB47D47D997C8B52294A

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 4

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 5

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 6

Vemos aqui que, logo na primeira chamada do cliente, o servidor recebe um cookie de sessão válido. É importante saber que, no Tomcat, o tempo máximo de inatividade de uma sessão é, por predefinição, de 20 minutos (na verdade, é configurável). Se a segunda chamada do programa enviar rapidamente o cookie recebido na primeira chamada, para o servidor trata-se da mesma sessão. Isto aponta para uma potencial falha de segurança. Se eu conseguir interceptar na rede um token de sessão, posso então fazer-me passar por quem iniciou essa sessão. No nosso exemplo, a primeira chamada representa quem inicia a sessão (talvez com um nome de utilizador e palavra-passe que lhe conferem o direito a obter um token) e a segunda chamada representa quem «hackeou» o token de sessão da primeira chamada. Se a operação em curso for uma operação bancária, isto pode tornar-se muito problemático...

O código do cliente é o seguinte:

// pacotes importados
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // solicita um URL
     // exibe o conteúdo da mesma no ecrã

    public static void main(String[] args){
        // sintaxe
        final String syntaxe="pg URL-COMPTEUR N [JSESSIONID]";

         // número de argumentos
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // regista-se o URL solicitado
        String URLString=args[0];

        // verificação da validade do URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorreto
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//exceção
         // verificação do número de chamadas N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // argumento N incorreto
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // O token JSESSIONID foi passado como parâmetro?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // extraem-se as informações úteis do URL
        String path=url.getPath();
        if(path.equals("")) path="/";
        String query=url.getQuery();
        if(query!=null) query="?"+query; else query="";
        String host=url.getHost();
        int port=url.getPort();
        if(port==-1) port=url.getDefaultPort();

         // é possível prosseguir
        Socket  client=null;                        // o cliente
        BufferedReader IN=null;                    // o fluxo de leitura do cliente
        PrintWriter OUT=null;                        // o fluxo de escrita do cliente
        String réponse=null;                        // resposta do servidor
         // o modelo procurado nos cabeçalhos HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // o padrão procurado no código HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // o resultado da comparação com o modelo
        Matcher résultat=null;
         // um valor booleano que indica o resultado da pesquisa do contador
        boolean compteurTrouvé;

        try{
             // são efetuadas as N chamadas ao servidor
            for(int i=0;i<N;i++){
                // estabelece-se a ligação ao servidor
                client=new Socket(host,port);

                // criam-se os fluxos de entrada e saída do cliente TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // solicita-se o URL - envio dos cabeçalhos HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

                 // lê-se a resposta até ao fim dos cabeçalhos, procurando um eventual cookie
                while((réponse=IN.readLine())!=null){
                     // acompanhamento da resposta
                    System.out.println(réponse);
                     // linha vazia?
                    if(réponse.equals("")) break;
                     // linha HTTP não vazia
                     // se não tivermos o token da sessão, procuramo-lo
                    if (JSESSIONID.equals("")){
                        // comparamos a linha HTTP com o modelo do cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // encontrou-se o cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//enquanto

                 // terminou a verificação dos cabeçalhos HTTP — passa-se para o código HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // a linha atual contém o contador?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // encontrámos o contador — exibimo-lo
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // terminou
                client.close();
            }//for
        } catch(Exception e){
            // tratamos a exceção
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // exibição de erros
    public static void erreur(String msg, int exitCode){
         // exibição do erro
        System.err.println(msg);
         // encerramento com erro
        System.exit(exitCode);
    }//erro

     // acompanhamento das trocas cliente-servidor
    public static void envoie(PrintWriter OUT,String msg){
        // envia mensagem ao servidor
        OUT.println(msg);
         // acompanhamento do ecrã
        System.out.println("--> "+msg);
    }//erro
}//classe

Vamos analisar os pontos importantes deste programa:

  • é necessário efetuar N trocas cliente-servidor. É por isso que estas se encontram num ciclo
            for(int i=0;i<N;i++){
  • em cada troca, o cliente abre uma ligação TCP-IP com o servidor. Assim que a ligação é estabelecida, o cliente envia ao servidor os cabeçalhos HTTP do seu pedido:
                 // solicita-se o URL - envio dos cabeçalhos HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

Se o token JSESSIONID estiver disponível, é enviado na forma de um cookie; caso contrário, não é enviado.

  • Depois de enviar o seu pedido, o cliente aguarda a resposta do servidor. Começa por analisar os cabeçalhos HTTP dessa resposta, à procura de um eventual cookie. Para o encontrar, compara as linhas que recebe com a expressão regular do cookie:
         // o modelo procurado nos cabeçalhos HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // lê-se a resposta até ao fim dos cabeçalhos, procurando o eventual cookie
                while((réponse=IN.readLine())!=null){
                     // acompanhamento da resposta
                    System.out.println(réponse);
                     // linha vazia?
                    if(réponse.equals("")) break;
                     // linha HTTP não vazia
                     // se não tivermos o token da sessão, procuramo-lo
                    if (JSESSIONID.equals("")){
                        // comparamos a linha HTTP com o modelo do cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // encontrou-se o cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//enquanto
  • Quando o token for encontrado pela primeira vez, não será novamente procurado nas chamadas seguintes ao servidor. Depois de processados os cabeçalhos HTTP da resposta, passa-se para o código HTML dessa mesma resposta. Nesta, procura-se a linha que fornece o valor do contador. Esta pesquisa é feita também com uma expressão regular:
         // o padrão do contador procurado no código HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // terminou a análise dos cabeçalhos HTTP — passa-se para o código HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // a linha atual contém o contador?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // Encontrámos o contador — exibimo-lo
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Exemplo 4

No exemplo anterior, o cliente web devolve o token sob a forma de um cookie. Vimos que também o pode devolver no próprio URL solicitado, sob a forma URL;jsessionid=xxx. Vamos verificar isso. O programa clientCompteur.java é transformado em clientCompteur2.java e alterado da seguinte forma:

....
                 // solicitamos o URL - envio dos cabeçalhos HTTP
                if(JSESSIONID.equals(""))
                    envoie(OUT,"GET " + path + query + " HTTP/1.1");
                else envoie(OUT,"GET " + path + query + ";jsessionid=" + JSESSIONID + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                envoie(OUT,"Connection: close");
                envoie(OUT,"");
....

O cliente solicita, portanto, o URL do contador através de GET URL;jsessionid=xx HTTP/1.1 e deixa de enviar o cookie. Esta é a única alteração. Eis os resultados de uma primeira chamada:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/contador 2

--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=48A6DBA8357D808EC012AAF3A2AFDA63;Path=/sessions
cookie trouvÚ : 48A6DBA8357D808EC012AAF3A2AFDA63

compteur : 1

--> GET /sessions/compteur;jsessionid=48A6DBA8357D808EC012AAF3A2AFDA63 HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)

compteur : 2

Na primeira chamada, o cliente solicita o URL sem token de sessão. O servidor responde-lhe enviando-lhe o token. O cliente volta então a consultar o mesmo URL, anexando-lhe o token recebido. Verifica-se que o contador foi efetivamente incrementado, o que comprova que o servidor reconheceu corretamente que se tratava da mesma sessão.

4.7. Exemplo 5

Este exemplo mostra uma aplicação composta por três páginas, a que chamaremos page0, page1 e page2. O utilizador deve aceder a elas nesta ordem:

  • a página0 é um formulário que solicita uma informação: um nome
  • a página1 é um formulário obtido em resposta ao envio do formulário da página0. Solicita uma segunda informação: uma idade
  • a página2 é um documento HTML que apresenta o nome obtido pela página0 e a idade obtida pela página1.

Existem aqui três trocas cliente-servidor:

  • na primeira interação, o formulário da página0 é solicitado pelo cliente e enviado pelo servidor
  • na segunda troca, o formulário página1 é solicitado pelo cliente e enviado pelo servidor. O cliente envia o nome ao servidor.
  • na terceira troca, o documento page3 é solicitado pelo cliente e enviado pelo servidor. O cliente envia a idade ao servidor. O documento page3 deve apresentar o nome e a idade. O nome foi obtido pelo servidor na segunda troca e, desde então, foi «esquecido». Utiliza-se uma sessão para registar o nome na troca 2, para que este esteja disponível na troca 3.

A página «page0», obtida na primeira troca, é a seguinte:

Image

Preenche-se o campo do nome:

Image

Utiliza-se o botão Suite e obtém-se então a seguinte página page1:

Image

Preenche-se o campo da idade:

Image

Utiliza-se o botão Suite e obtém-se então a seguinte página «page2»:

Image

Quando se envia a página «page0» para o servidor, este pode devolvê-la com um código de erro se o nome estiver em branco:

Image

Quando se envia a página page1 para o servidor, este pode devolvê-la com um código de erro se a idade for inválida:

Image

A aplicação é composta por um servlet e quatro páginas JSP:

page0.jsp
exibe a página0
page1.jsp
exibe a página 1
page2.jsp
exibe a página 2
erreur.jsp
exibe uma página de erro

A aplicação web chama-se «suitedepages» e está configurada da seguinte forma no ficheiro server.xml do Tomcat:

                <Context path="/suitedepages" docBase="e:/data/serge/servlets/suitedepages" />

O ficheiro de configuração web.xml da aplicação suitedepages é o seguinte:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
      <servlet-name>main</servlet-name>
    <servlet-class>main</servlet-class>
    <init-param>
          <param-name>urlPage0</param-name>
        <param-value>/page0.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlPage1</param-name>
        <param-value>/page1.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlPage2</param-name>
        <param-value>/page2.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>    
  </servlet>
  <servlet-mapping>
      <servlet-name>main</servlet-name>
    <url-pattern>/main</url-pattern>
  </servlet-mapping>
</web-app>

O servlet principal chama-se «main» e, graças ao seu alias (servlet-mapping), é acessível através do URL http://localhost:8080/suitedepages/main. Possui quatro parâmetros de inicialização, que correspondem aos URL das quatro páginas JSP utilizadas para as diferentes visualizações. O código do servlet «main» é o seguinte:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.util.regex.*;

public class main extends HttpServlet{

    // variáveis de instância
    String msgErreur=null;
    String urlPage0=null;
    String urlPage1=null;
    String urlPage2=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // A inicialização decorreu corretamente?
        if(msgErreur!=null){
             // passamos o controlo para a página de erro
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // recuperamos o parâmetro de etapa
        String étape=request.getParameter("etape");
         // recuperamos a sessão atual
        HttpSession session=request.getSession();
         // processa-se a etapa atual
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // outros casos são inválidos
        étape0(request,response,session);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // recuperam-se os parâmetros de inicialização
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

         // parâmetros ok?
        if(urlPage0==null || urlPage1==null || urlPage2==null){
            msgErreur="Configuration incorrecte";
        }
    }

     //-------- etapa 0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // definem-se alguns atributos
        request.setAttribute("nom","");
         // apresenta-se a página 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- etapa 1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // recuperamos o nome na consulta
        String nom=request.getParameter("nom");
        // nome definido?
        if(nom==null) étape0(request,response,session);
         // removem-se os eventuais espaços do nome
        nom=nom.trim();
         // coloca-se num atributo da consulta
        request.setAttribute("nom",nom);
         // nome vazio?
        if(nom.equals("")){
             // é um erro
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // colocam-se os erros na consulta
            request.setAttribute("erreurs",erreurs);
             // regresso à página 0
            étape0(request,response,session);
        }
         // nome válido — é guardado na sessão atual
        session.setAttribute("nom",nom);
         // define-se o atributo «idade» na consulta
        request.setAttribute("age","");
         // apresenta-se a página 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- etapa 2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // recupera-se o nome da sessão
        String nom=(String)session.getAttribute("nom");
         // nome definido?
        if(nom==null) étape0(request,response,session);
         // coloca-se num atributo da consulta
        request.setAttribute("nom",nom);
         // recuperamos a idade da consulta
        String age=request.getParameter("age");
        // idade definida?
        if(age==null){
            // regresso à página 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // a idade é memorizada na consulta
        age=age.trim();
        request.setAttribute("age",age);
        // idade válida?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // é um erro
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // colocamos os erros na consulta
            request.setAttribute("erreurs",erreurs);
             // regresso à página 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // idade válida - apresenta-se a página 2
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • O método init recupera os quatro parâmetros de inicialização e apresenta uma mensagem de erro se algum deles estiver em falta
  • vimos que a consulta incluía três trocas de dados. Para saber em que ponto nos encontramos nessas trocas, os formulários page0 e page1 têm uma variável oculta etape que tem o valor 1 (page0) ou 2 (page1). Este número pode ser considerado como o número da próxima página a apresentar. No método doGet, este parâmetro é recuperado da solicitação e, consoante o seu valor, o processamento é delegado a três outros métodos:
    • étape0 processa o pedido inicial e envia page0
    • étape1 processa o formulário de page0 e envia page1 ou, novamente, page0, caso tenha ocorrido um erro
    • A etapa 2 processa o formulário page1 e envia o page2 ou, novamente, o page1, caso tenha ocorrido um erro
  • etapa 0
    • exibe o page0 com um nome vazio
  • etapa 1
    • recupera o parâmetro nom do formulário page0.
    • verifica se o nome existe (não é nulo). Se não for esse o caso, exibe novamente page0 como se fosse a primeira chamada.
    • Verifica se o nome não está vazio. Se não for esse o caso, volta a apresentar o page0 com uma mensagem de erro.
    • armazena o nome na sessão atual e apresenta page1 se o nome for válido.
  • etapa 2
    • Recupera o parâmetro nom na sessão atual.
    • Verifica se o nome existe (não é nulo). Se não for esse o caso, volta a apresentar page0 como se fosse a primeira chamada.
    • Recupera o parâmetro age na solicitação atual enviada por page1.
    • verifica se a idade é válida. Se não for o caso, volta a apresentar page1 com uma mensagem de erro.
    • armazena o nome e a idade como atributos da consulta e apresenta page2 se o nome e a idade forem válidos.

A página page0.jsp é a seguinte:

<%@ page import="java.util.*" %>

<% // page0.jsp
     // recuperam-se os atributos da consulta
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
   // atributos válidos?
  if(nom==null){
       // regresso ao servlet principal
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 0</title>
  </head>
  <body>
    <h3>Page 0/2</h3>
    <form name="frmNom" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="1">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<%= nom %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erros?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>
  • A página page0.jsp pode ser chamada pelo servlet principal em dois casos:
    • durante o pedido inicial
    • após o processamento do formulário de page0, caso ocorra um erro
  • o parâmetro nom a apresentar é-lhe fornecido pelo servlet principal, bem como a eventual lista de erros. O servlet page0.jsp começa, portanto, por recuperar estas duas informações.
  • O formulário é «enviado» para o servlet principal com o campo oculto (hidden) etape, que indica em que etapa da aplicação nos encontramos.

A página page1.jsp é a seguinte:

<%@ page import="java.util.*" %>

<% // page1.jsp
     // recuperam-se os atributos da solicitação
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // atributos válidos?
  if(nom==null || age==null){
      // regresso ao servlet principal
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/2</h3>
    <form name="frmAge" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="2">    
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<%= age %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erros?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>

A página page1.jsp tem uma estrutura semelhante à da página page0.jsp, com a diferença de que agora recebe dois atributos do servlet principal: nom e age. Por fim, a página page2.jsp é a seguinte:

<% 
     // page2.jsp
     // recuperam-se os atributos da solicitação
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // atributos válidos?
  if(nom==null || age==null){
      // regresso ao servlet principal
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  


<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/2</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><%= age %></font></td>
        </tr>
      </table>
  </body>
</html>

A página page2.jsp também recebe os atributos nom e age do servlet principal. Limita-se a exibi-los. Por fim, a página erreur.jsp, responsável por apresentar um erro em caso de inicialização incorreta do servlet, é a seguinte:

<%
     // jspService
   // ocorreu um erro
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- início da página HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Exibe o atributo msgErreur que lhe foi passado pelo servlet principal.

Em conclusão, pode-se observar que, ao longo das três etapas da aplicação, é sempre a servlet principal que é consultada em primeiro lugar pelo navegador. No entanto, não é ela que gera a resposta a apresentar, mas sim uma das quatro páginas JSP. O utilizador não percebe este pormenor, uma vez que o navegador continua a apresentar no seu campo «Endereço» a URL inicialmente solicitada, ou seja, a da servlet principal.