Skip to content

4. Rastreamento de 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 processo funciona da seguinte forma:

  • Passo 1
    • O cliente C1 estabelece uma ligação com o servidor e faz o seu pedido inicial.
    • O servidor envia o formulário F1 ao cliente C1 e encerra a ligação estabelecida no passo 1.
  • Passo 2
    • O cliente C1 preenche-o e envia-o de volta ao servidor. Para tal, o navegador abre uma nova ligação com o servidor.
    • O servidor processa os dados do formulário 1, calcula a informação I1 a partir deles, envia o formulário F2 ao cliente C1 e encerra a ligação estabelecida no passo 3.
  • Passo 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 terá calculado as informações I1 e I2 a partir deles.

O problema em questão é: como é que o servidor mantém o registo das informações I1 e I2 associadas ao cliente C1? Este problema é designado por acompanhamento da sessão do cliente C1. Para compreender a sua causa principal, vamos examinar o diagrama de uma aplicação de servidor TCP-IP que serve vários clientes simultaneamente:

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

  • o cliente estabelece uma ligação com o servidor
  • troca dados com o servidor através desta ligação
  • a ligação é encerrada por uma das duas partes

Os dois pontos-chave deste mecanismo são:

  1. é criada uma única ligação para cada cliente
  2. esta ligação é utilizada durante toda a duração do diálogo do servidor com o seu cliente

O que permite ao servidor saber, a qualquer momento, com que cliente está a trabalhar é a ligação — ou, por outras palavras, o «canal» — que o liga ao seu cliente. Uma vez que este canal é dedicado a um cliente específico, tudo o que passa por este canal tem origem nesse cliente, e tudo o que é enviado através deste canal chega ao cliente.

O mecanismo cliente-servidor HTTP segue de perto o modelo anterior, com a exceção de que o diálogo cliente-servidor se limita a uma única troca entre o cliente e o servidor:

  • o cliente abre uma ligação ao servidor e faz o seu pedido
  • o servidor envia a sua resposta e encerra a ligação

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

Imaginemos um sistema que funciona da seguinte forma:

  • Existe uma única fila
  • Existem vários balcões de atendimento. Isto significa que vários clientes podem ser atendidos ao mesmo tempo. Quando um balcão fica disponível, um cliente sai da fila para ser atendido nesse balcão
  • Se esta for a primeira visita do cliente, o funcionário entrega-lhe um bilhete com um número. O cliente pode fazer apenas uma pergunta. Assim que receber a resposta, deve sair da janela do funcionário e ir para o fim da fila. O funcionário regista as informações do cliente num ficheiro com o seu número.
  • Quando for novamente a sua vez, o cliente poderá ser atendido por um caixa diferente do da vez anterior. O caixa pede-lhe o seu token e recupera o ficheiro com o número do token. Mais uma vez, o cliente faz um pedido, recebe uma resposta e as informações são adicionadas ao seu ficheiro.
  • E assim por diante... Com o tempo, o cliente receberá respostas a todos os seus pedidos. O acompanhamento entre os diferentes pedidos é feito através do token e do ficheiro a ele associado.

O mecanismo de acompanhamento de sessões numa aplicação web cliente-servidor funciona de forma semelhante:

  • Ao fazer a sua primeira solicitação, o cliente recebe um token do servidor web
  • e apresentará esse token em cada pedido subsequente para se identificar

O token pode assumir várias formas:

  • um campo oculto num formulário
    • o cliente faz a sua primeira solicitação (o servidor reconhece-o porque o cliente não tem token)
    • O servidor envia a sua resposta (um formulário) e coloca o token num campo oculto dentro dele. Nesta altura, a ligação é encerrada (o cliente sai da sessão com o seu token). O servidor pode ter informações associadas a este token.
    • O cliente faz uma segunda solicitação reenviando o formulário. O servidor recupera o token do formulário. Pode então processar a segunda solicitação do cliente acedendo, através do token, às informações calculadas durante a primeira solicitação. 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 colocado de volta no formulário de resposta para que o utilizador o possa apresentar durante a sua próxima solicitação.
    • e assim por diante...

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

  • O método do cookie
    • O cliente faz a sua primeira solicitação (o servidor reconhece isso porque o cliente não tem token)
    • O servidor responde adicionando um cookie aos cabeçalhos HTTP. Isto é feito utilizando o comando HTTP Set-Cookie:

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

onde param1, param2, etc., são nomes de parâmetros e os seus valores correspondentes. Entre os parâmetros estará o token. Muitas vezes, apenas o token é incluído no cookie, com o servidor a armazenar as outras informações 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 sessão com o seu token).

  • (continuação)
    • O cliente faz a sua segunda solicitação ao servidor. Cada vez que uma solicitação é feita a um servidor, o navegador verifica entre todos os cookies que possui para ver se tem um do servidor solicitado. Se tiver, envia-o ao servidor, sempre na forma de um comando HTTP — o comando Cookie, que tem uma sintaxe semelhante à do comando Set-Cookie usado pelo servidor:

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

Entre os parâmetros enviados pelo navegador, o servidor encontrará o token que lhe permite reconhecer o cliente e recuperar as informações associadas a ele.

Esta é a forma de token mais utilizada. Tem uma desvantagem: um utilizador pode configurar o seu navegador para rejeitar cookies. Esses utilizadores não podem, então, aceder a aplicações web que utilizem cookies.

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

4.2. A API Java para rastreamento de sessões

Apresentaremos agora os principais métodos úteis para o rastreamento de sessões:

HttpSession [HttpServletRequest].getSession()
Recupera o objeto Session ao qual a solicitação atual pertence. Se a solicitação ainda não fazia parte de uma sessão, uma é 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, 00:00).
long [HttpSession].getLastAccessedTime()
data do último acesso do cliente à sessão
long [HttpSession].getMaxInactiveInterval()
duração máxima em segundos de inatividade para uma sessão. Após este período, a sessão é invalidada.
[HttpSession].setMaxInactiveInterval(int duration)
Define a duração máxima de inatividade de uma sessão em segundos. Após este período, a sessão é invalidada.
boolean [HttpSession].isNew()
true se a sessão acabou de ser criada
[HttpSession].setAttribute(String parameter, Object value)
associa um valor a um parâmetro numa determinada sessão. Este mecanismo permite armazenar informações que permanecerão disponíveis durante toda a sessão.
[HttpSession].removeAttribute(String parameter)
Remove o parâmetro dos dados da sessão.
Object [HttpSession].getAttribute(String parameter)
retorna o valor associado ao parâmetro na sessão. Retorna null se o parâmetro não existir.
Enumeration [HttpSession].getAttributeNames()
enumera todos os atributos da sessão atual como uma enumeração
[HttpSession].invalidate()
encerra a sessão atual. Todas as informações associadas a ela são destruídas.

4.3. Exemplo 1

Apresentamos um exemplo retirado do excelente livro «Programming with J2EE», publicado pela Wrox e distribuído pela Eyrolles. Este livro é um tesouro de informações de alto nível para desenvolvedores de soluções web em Java. A aplicação apresentada neste livro como um único servlet Java foi adaptada aqui como um servlet principal que chama páginas JSP para exibir 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, encontrará os seguintes elementos:

Image

Os ficheiros error.jsp, invalid.jsp e valid.jsp estão todos associados à aplicação sessions. Na pasta WEB-INF acima, encontramos:

Image

Acima está o ficheiro de configuração web.xml para a aplicação sessions. Na pasta classes, encontrará o ficheiro da classe 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>

O servlet principal chama-se cycledevie (servlet-name) e está associado ao ficheiro cycledevie.class (servlet-class). Tem um alias /cycledevie (servlet-mapping) que permite que seja chamado através do URL http://localhost:8080/sessions/cycledevie. Tem três parâmetros de inicialização:

urlSessionValide
URL da página que exibe as propriedades da sessão atual
urlSessionInvalid
URL da página exibida após a sessão atual ser invalidada
urlErreur
URL da página exibida em caso de erro de inicialização do servlet principal cycledevie

Os componentes da aplicação de sessões são os seguintes:

ciclo de vida
servlet principal - analisa o pedido do cliente:
  • Se esta página fizer parte de uma sessão, passa o controlo para a página valide.jsp, que apresenta os detalhes da sessão. A partir desta página, o utilizador pode:
    • recarregá-la
    • invalidá-la
  • Se a solicitação pedir para invalidar a sessão atual, o servlet passa o controle para a página invalide.jsp, que solicitará ao utilizador que crie uma nova sessão
  • Se o servlet encontrar erros durante a inicialização, ele passa o controle para a página error.jsp, que exibirá uma mensagem de erro.
valide.jsp
  • exibe os detalhes da sessão atual e fornece dois links:
    • um para recarregar a página e ver como o parâmetro do último acesso à sessão atual muda
    • o outro para invalidar a sessão atual
invalid.jsp
exibida quando o utilizador invalida a sessão atual. Em seguida, oferece a possibilidade de criar uma nova.
error.jsp
exibida quando o servlet principal encontra erros durante a inicialização.

O ciclo de vida do servlet principal é o seguinte:

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

public class cycledevie extends HttpServlet{

    // instance variables
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

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

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // retrieve the current session
        HttpSession session=request.getSession();

         // analyze the action to be taken
        String action=request.getParameter("action");
        // invalidate current session
        if(action!=null && action.equals("invalider")){
            // the current session is invalidated
            session.invalidate();
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // other cases
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // parameters ok?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Tenha em atenção os seguintes pontos:

  • No seu método de inicialização, o servlet recupera os seus três parâmetros
  • Ao processar (doGet) um pedido, o servlet:
    • primeiro verifica se não ocorreram erros durante a inicialização. Se houver algum, redireciona para a página error.jsp.
    • verifica o valor do parâmetro «action». Se este parâmetro tiver o valor «invalid», o servlet redireciona para a página «invalid.jsp»; caso contrário, redireciona para a página «valid.jsp».

A página JSP **valide.jsp** apresenta as características da sessão atual:

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

<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- top of page 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 que na linha

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

utilizamos um objeto de sessão que surge do nada. Na verdade, este objeto é um dos objetos implícitos disponibilizados às páginas JSP, tal como os objetos request, response, out, config (ServletConfig) e context (ServletContext) que já encontrámos. Os dois links na página remetem para o servlet de ciclo de vida 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=invalidate, que permite ao servlet de ciclo de vida reconhecer que o utilizador pretende invalidar a sessão atual. O outro link recarrega a página. Para impedir que o navegador recupere a página a partir de uma cache, a diretiva HTML:

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

foi utilizada. Ela instrui o navegador a não utilizar o cache para a página que recebe.

A página invalidate.jsp é a seguinte:

<!-- top of page 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>

Fornece um link para o servlet cycledevie sem o parâmetro de ação. Este link fará com que o servlet cycledevie crie uma nova sessão.

A página error.jsp é a seguinte:

<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- top of page 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 é enviada pelo servlet de ciclo de vida. 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 página»:

Image

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

Image

Repare no URL desta nova página com o parâmetro action=invalidate. Vamos utilizar o link «Criar uma nova sessão» para criar uma nova sessão:

Image

Podemos ver que uma nova sessão foi iniciada. Nos exemplos anteriores, a sessão depende do mecanismo de cookies. Vamos agora desativar os cookies no nosso navegador e repetir os testes. Os exemplos seguintes foram realizados utilizando o Netscape Communicator. Por alguma razão inexplicável, os testes realizados com o IE6 produziram resultados inesperados, como se o IE6 continuasse a utilizar cookies mesmo depois de estes terem sido desativados. O servlet cycledevie é solicitado pela primeira vez:

Image

Utilizamos agora o link «Reload Page»:

Image

Podemos 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 os cookies no seu navegador. Ele utiliza dois mecanismos para implementar o token mencionado no início deste parágrafo: cookies e reescrita de URL. Se o cookie de sessão não estiver disponível, ele tentará recuperar o token a partir da URL solicitada pelo cliente. Para que isto funcione, a URL deve conter o token. De um modo geral, todos os links gerados num documento HTML que apontem para a aplicação web devem conter o token da aplicação. Isto pode ser feito utilizando o método encodeURL:

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

Modificamos a nossa aplicação da seguinte forma:

  • No servlet cycledevie.java, os URLs são codificados:
             // we hand over to the error page
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • Na página valide.jsp, os URLs são codificados:
<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
  // encodage URL cycledevie
  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 URLs estão codificados:
<%
    // jspservice - on invalide la session en cours
  session.invalidate();
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

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

Image

e recarregamos a página utilizando o link «Reload page»:

Image

Podemos ver que:

  • a sessão não mudou (mesmo ID)
  • o URL do servlet cycledevie contém, de facto, o token, como se pode ver no campo Endereço acima
  • o servidor Tomcat recupera, portanto, o token da sessão a partir da URL solicitada (se o programador tiver 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. Aqui, a única informação será um contador que é incrementado cada vez que o utilizador acede à URL do servlet. Quando é acedido pela primeira vez, aparece a seguinte página:

Image

Se clicar na ligação «Atualizar página» acima, obtém a seguinte página nova:

Image

A aplicação tem três componentes:

  • um servlet que processa o pedido do cliente
  • uma página JSP que exibe o valor do contador
  • uma página JSP que apresenta eventuais erros

Estes três componentes estão instalados na aplicação web «sessions» já em uso. O seu ficheiro web.xml foi modificado 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>
  • O servlet chama-se counter (servlet-name) e está associado ao ficheiro de classe counter.class (servlet-class)
  • Tem dois parâmetros de inicialização:
    • displayCounterURL: URL da página JSP que exibe o contador
    • urlErreur: URL da página JSP que exibe eventuais erros
  • e um alias /compteur, o que significa que será chamado através da 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{

     // instance variables
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

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

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // retrieve the current session
        HttpSession session=request.getSession();
         // and the
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // counter incrementation
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // save counter in session
        session.setAttribute("compteur",compteur);
         // and in the query
        request.setAttribute("compteur",compteur);

         // hand over to counter display url
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

         // parameters ok?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Este servlet tem a mesma estrutura que os servlets que já vimos. Repare no tratamento do contador:

  • a sessão é recuperada através de request.getSession()
  • o contador é recuperado desta sessão através de session.getAttribute("counter")
  • se for recuperado um valor nulo, significa que a sessão acabou de começar. O contador é então definido para 0.
  • o contador é incrementado, armazenado novamente na sessão (session.setAttribute("counter", counter)) e colocado na solicitação que será passada para o servlet de exibição (request.setAttribute("counter", counter)).

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

<%
    // jspService
  // on récupère le compteur
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- top of page 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 simplesmente recupera o atributo contador (request.getAttribute("counter")) que lhe foi passado pelo servlet principal e apresenta-o.

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

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of page 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 de contador anterior. Esta chamá-la-ia N vezes seguidas, sendo que N é passado como parâmetro. O nosso objetivo é demonstrar um cliente web programado e como gerir cookies. O nosso ponto de partida será um cliente web genérico apresentado no folheto sobre Java do mesmo autor. É chamado da seguinte forma:

webclient URL GET/HEAD

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

Aqui está um exemplo utilizando a URL http://localhost:8080/sessions/compteur:


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/compteur 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
 
 
<!-- top of page 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 exibe tudo o que recebe do servidor. Acima, vemos o comando HTTP Set-cookie, que o servidor utiliza para enviar um cookie ao seu cliente. Aqui, o cookie contém duas informações:

  • JSESSIONID, que é o token da sessão
  • Path, que define o URL ao qual o cookie pertence. Path=/sessions indica ao navegador que deve reenviar o cookie ao servidor sempre que solicitar um URL que comece por /sessions. Na aplicação sessions, utilizámos vários servlets, incluindo os servlets /sessions/lifecycle e /sessions/counter. Se chamarmos o servlet /sessions/cycledevie, o navegador receberá um token J. Se, utilizando o mesmo navegador, chamarmos em seguida o servlet /sessions/compteur, o navegador enviará o token J de volta ao servidor, porque este aplica-se a todas as URLs que começam por /sessions. No nosso exemplo, os servlets cycledevie e compteur não precisam de partilhar o mesmo token de sessão. Por conseguinte, não deveriam ter sido colocados na mesma aplicação web. Este é um ponto importante a reter: todos os servlets dentro da mesma aplicação partilham o mesmo token de sessão.
  • Um cookie também pode ter um prazo de validade. Aqui, esta informação está em falta. O cookie será, portanto, eliminado quando o navegador for fechado. Um cookie pode ter um prazo de validade de N dias, por exemplo. Enquanto for válido, o navegador irá reenviá-lo sempre que uma das URLs no seu domínio (Path) for acedida. Considere uma loja de CDs online. Esta pode rastrear o percurso de navegação de um cliente pelo seu catálogo e determinar gradualmente as suas preferências — música clássica, por exemplo. Estas preferências podem ser armazenadas num cookie com uma duração de 3 meses. Se esse mesmo cliente regressar ao site após um mês, o navegador reenviará o cookie para a aplicação do servidor. Com base na informação contida no cookie, a aplicação do servidor pode então adaptar as páginas geradas às preferências do cliente.

Segue-se o código do cliente web. Mais tarde, servirá como ponto de partida para outro cliente.

// imported packages
import java.io.*;
import java.net.*;

public class clientweb{

    // requests a URL
     // displays its contents on the screen

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

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extract useful information from 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();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        try{
             // connect to the server
            client=new Socket(host,port);

            // create customer input/output flows TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // request URL - send HTTP headers
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // we read the answer
            while((réponse=IN.readLine())!=null){
                 // the answer is processed
                System.out.println(réponse);
            }//while
             // it's over
            client.close();
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

Criamos agora o programa clientCounter, que é chamado da seguinte forma:

clientCounter URL N [JSESSIONID]

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

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


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/counter 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 no formulário -->
  • os cabeçalhos HTTP que recebe
  • o valor do contador após cada chamada

Podemos ver que, durante a primeira chamada:

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

Para os pedidos subsequentes:

  • o cliente reenvia sistematicamente o cookie que recebeu do servidor durante a primeira solicitação. É isso que permite ao servidor reconhecê-lo e incrementar o seu contador.
  • o servidor já não envia um cookie

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


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/compteur 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

Aqui vemos que, assim que o cliente faz a sua primeira solicitação, o servidor recebe um cookie de sessão válido. É importante notar que, para o Tomcat, o tempo máximo de inatividade padrão para uma sessão é de 20 minutos (na verdade, isso é configurável). Se a segunda solicitação do programa enviar o cookie recebido durante a primeira solicitação com rapidez suficiente, o servidor irá tratá-la como a mesma sessão. Isto destaca uma potencial vulnerabilidade de segurança. Se eu conseguir interceptar um token de sessão na rede, posso então fazer-me passar pelo utilizador que iniciou a sessão. No nosso exemplo, a primeira chamada representa o utilizador que inicia a sessão (talvez com um nome de utilizador e uma palavra-passe que lhe concedem o direito de receber um token), e a segunda chamada representa o utilizador que «hackeou» o token de sessão da primeira chamada. Se a operação atual for uma transação bancária, isto pode tornar-se muito problemático...

O código do cliente é o seguinte:

// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // requests a URL
     // displays its contents on the screen

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

         // number of arguments
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // note the URL required
        String URLString=args[0];

        // URL validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // check number of calls N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // incorrect N argument
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // has the JSESSIONID token been passed as a parameter?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // extract useful information from 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();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
         // the model searched for in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // the model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // the result of the model comparison
        Matcher résultat=null;
         // a Boolean giving the result of the counter search
        boolean compteurTrouvé;

        try{
             // we make N calls to the server
            for(int i=0;i<N;i++){
                // connect to the server
                client=new Socket(host,port);

                // create customer input/output flows TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // request URL - send HTTP headers
                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,"");

                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // it's over
                client.close();
            }//for
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error

     // monitoring client-server exchanges
    public static void envoie(PrintWriter OUT,String msg){
        // sends message to server
        OUT.println(msg);
         // screen tracking
        System.out.println("--> "+msg);
    }//error
}//class

Vamos analisar os pontos-chave deste programa:

  • Precisamos de realizar N trocas cliente-servidor. É por isso que estão num ciclo
            for(int i=0;i<N;i++){
  • Para cada troca, o cliente abre uma ligação TCP/IP com o servidor. Assim que a ligação é estabelecida, envia os cabeçalhos HTTP do seu pedido ao servidor:
                // on demande l'URL - envoi des entêtes 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 como um cookie; caso contrário, não é enviado.

  • Assim que o pedido é enviado, o cliente aguarda a resposta do servidor. Começa por examinar os cabeçalhos HTTP dessa resposta para procurar um eventual cookie. Para o encontrar, compara as linhas que recebe com a expressão regular do cookie:
         // the model searched in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • Assim que o token for encontrado pela primeira vez, deixará de ser procurado nas chamadas subsequentes ao servidor. Depois de processados os cabeçalhos HTTP da resposta, passamos ao código HTML dessa mesma resposta. Nele, procuramos a linha que fornece o valor do contador. Esta pesquisa também é realizada utilizando uma expressão regular:
         // the counter model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Exemplo 4

No exemplo anterior, o cliente web devolve o token como um cookie. Vimos que também o poderia devolver dentro da própria URL solicitada, na forma URL;jsessionid=xxx. Vamos verificar isto. O programa clientCompteur.java é renomeado para clientCompteur2.java e modificado da seguinte forma:

....
                // on demande l'URL - envoi des entêtes 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 já não envia um cookie. Esta é a única alteração. Aqui estão os resultados de uma chamada inicial:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/counter 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 solicitação, o cliente solicita a URL sem um token de sessão. O servidor responde enviando o token. O cliente, então, solicita novamente a mesma URL, anexando o token recebido a ela. Podemos ver que o contador é incrementado, comprovando 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, às quais chamaremos página0, página1 e página2. O utilizador deve aceder-lhes nesta ordem:

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

Existem aqui três trocas cliente-servidor:

  • Na primeira troca, o formulário da página0 é solicitado pelo cliente e enviado pelo servidor
  • Na segunda troca, o formulário da página1 é solicitado pelo cliente e enviado pelo servidor. O cliente envia o nome para o servidor.
  • Na terceira troca, o documento da página 3 é solicitado pelo cliente e enviado pelo servidor. O cliente envia a idade para o servidor. O documento da página 3 deve exibir o nome e a idade. O nome foi obtido pelo servidor na segunda troca e, desde então, foi «esquecido». É utilizada uma sessão para armazenar o nome da troca 2, de modo a que este esteja disponível durante a troca 3.

O documento page0 obtido na primeira troca é o seguinte:

Image

Preenchemos o campo do nome:

Image

Clicamos no botão «Seguinte» e, em seguida, aparece a seguinte página1:

Image

Preenchemos o campo da idade:

Image

Clicamos no botão «Seguinte» e somos direcionados para a seguinte página 2:

Image

Quando a página 0 é enviada para o servidor, este pode devolvê-la com um código de erro se o campo «nome» estiver vazio:

Image

Quando enviar a página 1 para o servidor, este poderá devolver 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ágina1
page2.jsp
exibe a página2
error.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 para a 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 é denominado 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 URLs 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{

    // instance variables
    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{

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // we retrieve the step parameter
        String étape=request.getParameter("etape");
         // retrieve the current session
        HttpSession session=request.getSession();
         // the current step is processed
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // other cases are invalid
        étape0(request,response,session);
    }

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

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

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

     //-------- step0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // we set a few attributes
        request.setAttribute("nom","");
         // we present page 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- step1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the query
        String nom=request.getParameter("nom");
        // name positioned?
        if(nom==null) étape0(request,response,session);
         // remove any spaces from the name
        nom=nom.trim();
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // empty name?
        if(nom.equals("")){
             // it's a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 0
            étape0(request,response,session);
        }
         // valid name - stored in the current session
        session.setAttribute("nom",nom);
         // set the age attribute in the query
        request.setAttribute("age","");
         // we present page 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- step2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the session
        String nom=(String)session.getAttribute("nom");
         // name positioned?
        if(nom==null) étape0(request,response,session);
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // the age is retrieved from the query
        String age=request.getParameter("age");
        // age positioned?
        if(age==null){
            // back to page 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // the age is stored in the query
        age=age.trim();
        request.setAttribute("age",age);
        // valid age?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // this is a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // age valid - page 2 is presented
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • O método init recupera os quatro parâmetros de inicialização e define uma mensagem de erro caso algum deles esteja em falta
  • Vimos que o pedido consiste em três trocas. Para acompanhar a fase atual dessas trocas, os formulários page0 e page1 têm uma variável oculta "etape" com um valor de 1 (page0) ou 2 (page1). Este número pode ser interpretado como o número da próxima página a ser apresentada. No método doGet, este parâmetro é recuperado da solicitação e, dependendo do seu valor, o processamento é delegado a três outros métodos:
    • étape0 processa a solicitação inicial e envia a página0
    • étape1 processa o formulário na página0 e envia a página1 ou a página0 novamente se ocorrer um erro
    • étape2 processa o formulário na página1 e envia a página2 ou a página1 novamente se tiver ocorrido um erro
  • step0
    • exibe a página0 com um nome vazio
  • Etapa 1
    • Recupera o parâmetro «nome» do formulário da página0.
    • Verifica se o nome existe (não é nulo). Caso contrário, a página 0 é exibida novamente como se fosse a primeira chamada.
    • verifica se o nome não está vazio. Se estiver, a página0 é exibida novamente juntamente com uma mensagem de erro.
    • armazena o nome na sessão atual e exibe a página1 se o nome for válido.
  • passo2
    • Recupera o parâmetro «name» da sessão atual.
    • Verifica se o nome existe (não é nulo). Caso contrário, exibe a página0 novamente como se fosse a primeira chamada.
    • Recupera o parâmetro idade da solicitação atual enviada pela página1.
    • Verifica se a idade é válida. Caso contrário, exibe a página1 novamente com uma mensagem de erro.
    • Armazena o nome e a idade como atributos da solicitação e exibe a página2 se o nome e a idade forem válidos.

A página page0.jsp é a seguinte:

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

<% // page0.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributs valides ?
  if(nom==null){
      // retour à la servlet principale
    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>
    <% // erreurs ?
      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 page0, quando ocorre um erro
  • O parâmetro `nameToDisplay` é fornecido pelo servlet principal, juntamente com qualquer 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 «etape», que indica em que fase da aplicação o utilizador se encontra.

A página page1.jsp é a seguinte:

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

<% // page1.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributs valides ?
  if(nom==null || age==null){
      // retour à la servlet principale
    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>
    <% // erreurs ?
      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 exceção de que agora recebe dois atributos do servlet principal: nome e idade. Por fim, a página page2.jsp é a seguinte:

<% 
    // page2.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // attributs valides ?
  if(nom==null || age==null){
      // retour à la servlet principale
    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 name e age do servlet principal. Ela simplesmente os exibe. Por fim, a página error.jsp, responsável por exibir um erro no caso de inicialização incorreta do servlet, é a seguinte:

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of page HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

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

Em conclusão, podemos ver que, durante as três fases da aplicação, é sempre o servlet principal que é consultado em primeiro lugar pelo navegador. No entanto, não é o servlet principal que gera a resposta a ser apresentada, mas sim uma das quatro páginas JSP. O utilizador não repara nisto, uma vez que o navegador continua a apresentar o URL inicialmente solicitado — o do servlet principal — no seu campo «Endereço».