Skip to content

14. Aplicação web MVC numa arquitetura de três camadas – Exemplo 1

14.1. Présentation

Até agora, limitámo-nos a exemplos com fins pedagógicos. Por isso, tinham de ser simples. Apresentamos agora uma aplicação básica, mas, ainda assim, mais rica do que todas as apresentadas até agora. Terá a particularidade de utilizar as três camadas de uma arquitetura de 3 camadas:

Image

Convidamos o leitor a rever os princípios de uma aplicação web MVC numa arquitetura de três camadas, caso os tenha esquecido, no parágrafo 4.

A aplicação web que vamos criar permitirá gerir um grupo de pessoas através de quatro operações:

  • lista das pessoas do grupo
  • adição de uma pessoa ao grupo
  • alteração de uma pessoa do grupo
  • eliminação de uma pessoa do grupo

Estas quatro operações correspondem às operações básicas de uma tabela de base de dados. Iremos desenvolver duas versões desta aplicação:

  • na versão 1, a camada [dao] não utilizará uma base de dados. As pessoas do grupo serão armazenadas num simples objeto [ArrayList], gerido internamente pela camada [dao]. Isto permitirá ao leitor testar a aplicação sem as restrições de uma base de dados.
  • Na versão 2, colocaremos o grupo de pessoas numa tabela da base de dados. Mostraremos que isso será feito sem impacto na camada web da versão 1, que permanecerá inalterada.

As capturas de ecrã que se seguem mostram as páginas que a aplicação troca com o utilizador.

Image

Image

Image

 

14.2. O projeto Eclipse

O projeto da aplicação chama-se [personnes-01]:

Image

Este projeto abrange as três camadas da arquitetura de três camadas da aplicação:

  • a camada [dao] está contida no pacote [istia.st.mvc.personnes.dao]
  • a camada [metier] ou [service] está contida no pacote [istia.st.mvc.personnes.service]
  • a camada [web] ou [ui] está contida no pacote [istia.st.mvc.personnes.web]
  • o pacote [istia.st.mvc.personnes.entites] contém os objetos partilhados entre diferentes camadas
  • o pacote [istia.st.mvc.personnes.tests] contém os testes Junit das camadas [dao] e [service]

Vamos explorar sucessivamente as três camadas [dao], [service] e [web]. Como seria demasiado demorado de escrever e talvez demasiado enfadonho de ler, poderemos, por vezes, ser um pouco rápidos nas explicações, exceto quando o que for apresentado for novo.

14.3. A representação de uma pessoa

A aplicação gere um grupo de pessoas. As capturas de ecrã do parágrafo 14.1 mostraram algumas das características de uma pessoa. Formalmente, estas são representadas por uma classe [Personne]:

Image

A classe [Personne] é a seguinte:

package istia.st.springmvc.personnes.entites;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Personne {

     // identificador único da pessoa
    private int id;
     // a versão atual
    private long version;
     // o apelido
    private String nom;
     // o nome próprio
    private String prenom;
     // data de nascimento
    private Date dateNaissance;
     // estado civil
    private boolean marie = false;
     // o número de filhos
    private int nbEnfants;

     // getters - setters
...

     // construtor por predefinição
    public Personne() {

    }

     // construtor com inicialização dos campos da pessoa
    public Personne(int id, String prenom, String nom, Date dateNaissance,
            boolean marie, int nbEnfants) {
        setId(id);
        setNom(nom);
        setPrenom(prenom);
        setDateNaissance(dateNaissance);
        setMarie(marie);
        setNbEnfants(nbEnfants);
    }

     // construtor de uma pessoa por cópia de outra pessoa
    public Personne(Personne p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setNom(p.getNom());
        setPrenom(p.getPrenom());
        setDateNaissance(p.getDateNaissance());
        setMarie(p.getMarie());
        setNbEnfants(p.getNbEnfants());
    }


     // toString
    public String toString() {
        return "[" + id + "," + version + "," + prenom + "," + nom + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance)
                + "," + marie + "," + nbEnfants + "]";
    }
}
  • uma pessoa é identificada pelas seguintes informações:
    • id: um número que identifica de forma única uma pessoa
    • apelido: o apelido da pessoa
    • nome próprio: o seu nome próprio
    • dateNaissance: a sua data de nascimento
    • casada: o seu estado civil (casada ou solteira)
    • nbEnfants: o número de filhos
  • O atributo [version] é um atributo adicionado artificialmente para as necessidades da aplicação. Do ponto de vista dos objetos, teria sido sem dúvida preferível adicionar este atributo numa classe derivada de [Personne]. A sua necessidade surge quando se analisam os casos de utilização da aplicação web. Um deles é o seguinte:

No momento T1, um utilizador U1 acede à edição de uma pessoa P. Nesse momento, o número de filhos é 0. Altera esse número para 1, mas antes de validar a sua alteração, um utilizador U2 acede à edição da mesma pessoa P. Uma vez que U1 ainda não validou a sua alteração, U2 vê o número de filhos como 0. U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 confirmam as suas alterações por esta ordem. É a alteração de U2 que prevalecerá: o nome passará a estar em maiúsculas e o número de filhos permanecerá em zero, mesmo que U1 pense ter-no alterado para 1.

O conceito de versão de pessoa ajuda-nos a resolver este problema. Retomemos o mesmo caso de utilização:

No momento T1, um utilizador U1 acede ao modo de edição de uma pessoa P. Nesse momento, o número de filhos é 0 e a versão é V1. Ele altera o número de filhos para 1, mas antes de validar a sua alteração, um utilizador U2 inicia a edição da mesma pessoa P. Uma vez que U1 ainda não validou a sua alteração, o utilizador U2 vê o número de filhos como 0 e a versão como V1. O utilizador U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 validam as suas alterações por esta ordem. Antes de validar uma alteração, verifica-se se quem altera uma pessoa P possui a mesma versão que a pessoa P atualmente registada. Este será o caso do utilizador U1. A sua alteração é, portanto, aceite e, em seguida, altera-se a versão da pessoa modificada de V1 para V2, para registar o facto de a pessoa ter sofrido uma alteração. Ao validar a alteração de U2, verificar-se-á que este possui uma versão V1 da pessoa P, quando, na realidade, a versão atual desta é V2. Será então possível informar ao utilizador U2 que alguém o antecedeu e que deve recomeçar a partir da nova versão da pessoa P. Ele fará isso, recuperará uma pessoa P com a versão V2, que agora tem um filho, colocará o nome em maiúsculas e validará. A sua alteração será aceite se a pessoa P registada ainda tiver a versão V2. No final, as alterações feitas por U1 e U2 serão tidas em conta, enquanto que, no caso de utilização sem versão, uma das alterações se perderia.

  • linhas 32-40: um construtor capaz de inicializar os campos de uma pessoa. O campo [version] é omitido.
  • linhas 43-51: um construtor que cria uma cópia da pessoa que lhe é passada como parâmetro. Ficamos, assim, com dois objetos com conteúdo idêntico, mas referenciados por dois ponteiros diferentes.
  • linha 55: o método [toString] é redefinido para devolver uma cadeia de caracteres que representa o estado da pessoa

14.4. A camada [dao]

A camada [dao] é constituída pelas seguintes classes e interfaces:

Image

  • [IDao] é a interface apresentada pela camada [dao]
  • [DaoImpl] é uma implementação desta, em que o grupo de pessoas está encapsulado num objeto [ArrayList]
  • [DaoException] é um tipo de exceções não verificadas (unchecked), lançadas pela camada [dao]

A interface [IDao] é a seguinte:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // lista de todas as pessoas
    Collection getAll();
     // obter uma pessoa específica
    Personne getOne(int id);
     // adicionar/editar uma pessoa
    void saveOne(Personne personne);
     // eliminar uma pessoa
    void deleteOne(int id);
}
  • A interface possui quatro métodos para as quatro operações que se pretende realizar sobre o grupo de pessoas:
    • getAll: para obter um conjunto de pessoas
    • getOne: para obter uma pessoa com um id específico
    • saveOne: para adicionar uma pessoa (id=-1) ou alterar uma pessoa existente (id <> -1)
    • deleteOne: para eliminar uma pessoa com um id específico

A camada [dao] pode lançar exceções. Estas serão do tipo [DaoException] :

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

     // código de erro
    private int code;

    public int getCode() {
        return code;
    }

// fabricante
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • linha 3: a classe [DaoException], derivada de [RuntimeException], é um tipo de exceção não controlada: o compilador não nos obriga a:
    • gerir este tipo de exceções com um try/catch quando chamamos um método que possa lançá-la
    • incluir o marcador «throws DaoException» na assinatura de um método suscetível de lançar a exceção

Esta técnica evita que tenhamos de assinar os métodos da interface [IDao] com exceções de um tipo específico. Qualquer implementação que lance exceções não controladas será, assim, aceitável, proporcionando maior flexibilidade à arquitetura.

  • linha 6: um código de erro. A camada [dao] lançará várias exceções que serão identificadas por códigos de erro diferentes. Isto permitirá que a camada responsável pelo tratamento da exceção conheça a origem exata do erro e, assim, tome as medidas adequadas. Existem outras formas de chegar ao mesmo resultado. Uma delas consiste em criar um tipo de exceção para cada tipo de erro possível, por exemplo, NomManquantException, PrenomManquantException, AgeIncorrectException, ...
  • linhas 13-16: o construtor que permitirá criar uma exceção identificada por um código de erro, bem como uma mensagem de erro.
  • linhas 8-10: o método que permitirá ao código de gestão de uma exceção recuperar o código de erro.

A classe [DaoImpl] implementa a interface [IDao]:

package istia.st.springmvc.personnes.dao;

import istia.st.springmvc.personnes.entites.Personne;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;

public class DaoImpl implements IDao {

     // uma lista de pessoas
    private ArrayList personnes = new ArrayList();

     // n.º da próxima pessoa
    private int id = 0;

     // inicializações
    public void init() {
        try {
            Personne p1 = new Personne(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("13/11/1984"),
                    true, 2);
            saveOne(p1);
            Personne p2 = new Personne(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Personne p3 = new Personne(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Erreur d'initialisation de la couche [dao] : "
                            + ex.toString(), 1);
        }
    }

     // lista de pessoas
    public Collection getAll() {
        return personnes;
    }

     // obter uma pessoa em particular
    public Personne getOne(int id) {
         // procura-se a pessoa
        int i = getPosition(id);
         // já a encontrámos?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

     // adicionar ou alterar uma pessoa
    public void saveOne(Personne personne) {
         // o parâmetro «pessoa» é válido?
        check(personne);
         // adição ou alteração?
        if (personne.getId() == -1) {
             // adição
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
         // alteração - está a procurar a pessoa
        int i = getPosition(personne.getId());
         // foi encontrada?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
         // Temos a versão correta do original?
        Personne original = (Personne) personnes.get(i);
        if (original.getVersion() != personne.getVersion()) {
            throw new DaoException("L'original de la personne [" + personne
                    + "] a changé depuis sa lecture initiale", 3);
        }
         // aguarda 10 ms
         //wait(10);
         // Está tudo bem — fazemos a alteração
        original.setVersion(original.getVersion()+1);
        original.setNom(personne.getNom());
        original.setPrenom(personne.getPrenom());
        original.setDateNaissance((personne.getDateNaissance()));
        original.setMarie(personne.getMarie());
        original.setNbEnfants(personne.getNbEnfants());
    }

     // eliminação de uma pessoa
    public void deleteOne(int id) {
         // procura-se a pessoa
        int i = getPosition(id);
         // Encontrámos?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
             // eliminamos a pessoa
            personnes.remove(i);
        }
    }

     // gerador de ID
    private int getNextId() {
        id++;
        return id;
    }

     // procurar uma pessoa
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
         // percorre-se a lista de pessoas
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
         // resultado?
        return trouvé ? i : -1;
    }

     // verificação de uma pessoa
    private void check(Personne p) {
         // pessoa p
        if (p == null) {
            throw new DaoException("Personne null", 10);
        }
         // ID
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Id [" + p.getId() + "] invalide", 11);
        }
         // data de nascimento
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
         // número de filhos
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
         // apelido
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
         // nome próprio
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

     // espera
    private void wait(int N) {
         // aguarda-se N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // exibe o registo da exceção
            e.printStackTrace();
            return;
        }
    }
}

Vamos apresentar apenas as linhas gerais deste código. No entanto, dedicaremos algum tempo às partes mais delicadas.

  • linha 13: o objeto [ArrayList], que irá conter o grupo de pessoas
  • linha 16: o identificador da última pessoa adicionada. A cada nova adição, este identificador será incrementado em 1.

A classe [DaoImpl] será instanciada numa única instância. É o que se denomina um singleton. Uma aplicação web atende os seus utilizadores de forma simultânea. Num determinado momento, existem várias threads a serem executadas pelo servidor web. Estas partilham os singletons:

  • o da camada [dao]
  • o da camada [service]
  • os das diferentes controlas, validadores de dados, etc., da camada web

Se um singleton tiver campos privados, é preciso questionar-se imediatamente sobre o motivo de os ter. Estão justificados? Com efeito, vão ser partilhados entre diferentes threads. Se forem de leitura única, isso não representa um problema, desde que possam ser inicializados num momento em que se tenha a certeza de que existe apenas um thread ativo. Em geral, sabemos identificar esse momento. É o momento do arranque da aplicação web, quando esta ainda não começou a servir clientes. Se forem de leitura/escrita, é necessário implementar uma sincronização do acesso aos campos; caso contrário, corremos para o desastre. Iremos ilustrar este problema quando testarmos a camada [dao].

  • A classe [DaoImpl] não tem construtor. Por isso, será utilizado o seu construtor por predefinição.
  • linhas 19-38: o método [init] será chamado no momento da instanciação do singleton da camada [dao]. Este método cria uma lista de três pessoas.
  • linhas 41-43: implementa o método [getAll] da interface [IDao]. Devolve uma referência à lista de pessoas.
  • linhas 46-55: implementa o método [getOne] da interface [IDao]. O seu parâmetro é o ID da pessoa procurada.

Para a recuperar, recorre-se a um método privado [getPosition] das linhas 113-126. Este método devolve a posição na lista da pessoa procurada ou -1 se a pessoa não tiver sido encontrada.

Se a pessoa tiver sido encontrada, o método [getOne] devolve uma referência (linha 51) a uma cópia dessa pessoa e não à própria pessoa. Com efeito, quando um utilizador pretender alterar uma pessoa, as informações sobre a mesma serão solicitadas à camada [dao] e encaminhadas até à camada [web] para alteração, sob a forma de uma referência a um objeto [Personne]. Esta referência servirá de contentor para os dados introduzidos no formulário de alteração. Quando, na camada web, o utilizador enviar as suas alterações, o conteúdo do contentor de dados será alterado. Se o contentor for uma referência à pessoa real do [ArrayList] da camada [dao], então esta é alterada, mesmo que as alterações não tenham sido apresentadas às camadas [service] e [dao]. Esta última é a única habilitada a gerir a lista de pessoas. Por isso, é necessário que a camada web trabalhe com uma cópia da pessoa a ser alterada. Neste caso, a camada [dao] fornece essa cópia.

Se a pessoa procurada não for encontrada, é lançada uma exceção do tipo [DaoException] com o código de erro 2 (linha 53).

  • linhas 94-104: implementam o método [deleteOne] da interface [IDao]. O seu parâmetro é o ID da pessoa a eliminar. Se a pessoa a eliminar não existir, é lançada uma exceção do tipo [DaoException] com o código de erro 2.
  • linhas 58-91: implementa o método [saveOne] da interface [IDao]. O seu parâmetro é um objeto [Personne]. Se este objeto tiver um id=-1, trata-se de uma adição de pessoa. Caso contrário, trata-se de modificar a pessoa da lista com esse id com os valores do parâmetro.
    • linha 60: a validade do parâmetro [Personne] é verificada por um método privado [check] definido nas linhas 129-155. Este método efetua verificações básicas sobre o valor dos diferentes campos de [Personne]. Sempre que é detetada uma anomalia, é lançado um [DaoException] com um código de erro específico. Como o método [saveOne] não trata esta exceção, esta será reenviada para o método chamador.
    • linha 62: se o parâmetro [Personne] tiver o seu id igual a -1, trata-se de uma adição. O objeto [Personne] é adicionado à lista interna de pessoas (linha 66), com o primeiro ID disponível (linha 64) e um número de versão igual a 1 (linha 65).
    • Se o parâmetro [Personne] tiver um [id] diferente de -1, trata-se de modificar a pessoa da lista interna que possui esse [id]. Em primeiro lugar, verifica-se (linhas 70-75) se a pessoa a modificar existe. Se não for o caso, lança-se uma exceção do tipo [DaoException] com o código de erro 2.
    • Se a pessoa estiver efetivamente presente, verifica-se se a sua versão atual é a mesma que a do parâmetro [Personne], que contém as alterações a introduzir no original. Se não for esse o caso, isso significa que quem pretende efetuar a alteração na pessoa não possui a versão mais recente. Isto é-lhe comunicado através do lançamento de uma exceção do tipo [DaoException] com o código de erro 3 (linhas 79-80).
    • Se tudo correr bem, as alterações são efetuadas no registo original da pessoa (linhas 85-90)

É evidente que este método tem de ser sincronizado. Por exemplo, entre o momento em que se verifica se a pessoa a modificar existe efetivamente e aquele em que a alteração vai ser efetuada, a pessoa pode ter sido eliminada da lista por outra pessoa. O método deveria, portanto, ser declarado como [synchronized], a fim de garantir que apenas um thread o execute de cada vez. O mesmo se aplica aos outros métodos da interface [IDao]. Não o fazemos, preferindo transferir essa sincronização para a camada [service]. Para destacar os problemas de sincronização, durante os testes da camada [dao], iremos interromper a execução de [saveOne] durante 10 ms (linha 83) entre o momento em que sabemos que podemos efetuar a alteração e o momento em que a efetuamos efetivamente. O thread que executa o [saveOne] perderá então o controlo do processador a favor de outro. Desta forma, aumentamos as nossas hipóteses de observar conflitos de acesso à lista de pessoas.

14.5. Testes da camada [dao]

É escrito um teste JUnit para a camada [dao]:

[TestDao] é o teste JUnit. Para evidenciar os problemas de acesso simultâneo à lista de pessoas, são criadas threads do tipo [ThreadDaoMajEnfants]. Estas têm como função aumentar em 1 o número de filhos de uma determinada pessoa.

O [TestDao] tem cinco testes, do [test1] ao [test5]. Apresentamos apenas dois deles, convidando o leitor a descobrir os restantes no código-fonte associado a este artigo.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

     // camada [dao]
    private DaoImpl dao;

     // construtor
    public TestDao() {
        dao = new DaoImpl();
        dao.init();
    }

     // lista de pessoas
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

     // teste1
    public void test1() throws ParseException {
...
    }

     // alteração-eliminação de um elemento inexistente
    public void test2() throws ParseException {
...
    }

     // gestão de versões de pessoas
    public void test3() throws ParseException, InterruptedException {
...
    }

     // bloqueio otimista - acesso multithread
    public void test4() throws Exception {
...
    }

     // testes de validade de saveOne
    public void test5() throws ParseException {
    ...
}
  • linha 9: referência à implementação da camada [dao] testada
  • linhas 12-15: o construtor do teste JUnit. Este cria uma instância do tipo [DaoImpl] da camada [dao] a ser testada e inicializa-a.

O método [test1] testa os quatro métodos da interface [IDao] da seguinte forma:

    public void test1() throws ParseException {
         // lista atual
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
         // visualização
        doListe(personnes);
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // verificação — ocorrerá uma falha se a pessoa não for encontrada
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
         // alteração
        p1.setNom("Y");
        dao.saveOne(p1);
         // verificação - ocorrerá uma falha se a pessoa não for encontrada
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
         // eliminação
        dao.deleteOne(id1);
         // verificação
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve ocorrer um erro de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // lista de pessoas
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • linha 3: solicita-se a lista de pessoas
  • linha 6: apresenta-se essa lista
[1,1,Joachim,Major,13/01/1984,true,2]
[2,1,Mélanie,Humbort,12/01/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]

Em seguida, o teste adiciona uma pessoa, altera-a e elimina-a. Desta forma, são utilizados os quatro métodos da interface [IDao].

  • linhas 8-10: adiciona-se uma nova pessoa (id=-1).
  • linha 11: recupera-se o id da pessoa adicionada, uma vez que a adição lhe atribuiu um. Antes, ela não tinha nenhum.
  • linhas 13-14: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser adicionada. É importante lembrar que, se a pessoa solicitada não for encontrada, a camada [dao] lança uma exceção. Nesse caso, ocorrerá uma falha na linha 13. Este caso poderia ter sido tratado de forma mais adequada. Na linha 14, verifica-se o nome da pessoa encontrada.
  • linhas 16-17: altera-se esse nome e solicita-se à camada [dao] que registe as alterações.
  • linhas 19-20: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser adicionada e verifica-se o seu novo nome.
  • linha 22: elimina-se a pessoa adicionada no início do teste.
  • linhas 23-34: solicita-se à camada [dao] uma cópia da pessoa que acabou de ser eliminada. Deve-se obter uma [DaoException] com o código 2.
  • linhas 36-37: a lista de pessoas é solicitada novamente. Deve ser obtida a mesma lista do início do teste.

O método [test4] procura evidenciar os problemas de acesso simultâneo aos métodos da camada [dao]. Recorde-se que estes não foram sincronizados. O código do teste é o seguinte:

    public void test4() throws Exception {
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // criação de N threads para atualizar o número de filhos
        final int N = 10;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            taches[i].start();
        }
         // aguarda-se a conclusão dos threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
         // recuperar a pessoa
        p1 = dao.getOne(id1);
         // ela deve ter N filhos
        assertEquals(N, p1.getNbEnfants());
         // eliminação da pessoa p1
        dao.deleteOne(p1.getId());
         // verificação
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // deve haver um erro de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • linhas 3-6: adiciona-se à lista uma pessoa P sem filhos. Regista-se o seu [id] (linha 6).
  • linhas 7-13: lançam-se N threads. Cada um deles irá incrementar o número de filhos da pessoa P em 1 unidade. No final, a pessoa P deverá ter N filhos.
  • linhas 15-17: o método [test4], que iniciou as N threads, aguarda que estas concluam o seu trabalho antes de verificar o novo número de filhos da pessoa P.
  • linhas 18-21: recupera-se a pessoa P e verifica-se se o seu número de filhos é N.
  • linhas 22-35: a pessoa P é eliminada e, em seguida, verifica-se se já não existe na lista.

Na linha 11, verifica-se que os threads são do tipo [ThreadDaoMajEnfants]. O construtor deste tipo tem três parâmetros:

  1. o nome atribuído ao thread, para que seja possível acompanhá-lo através dos registos
  2. uma referência à camada [dao] para que o thread tenha acesso à mesma
  3. o ID da pessoa sobre a qual o thread deve trabalhar

O tipo [ThreadDaoMajEnfants] é o seguinte:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

public class ThreadDaoMajEnfants extends Thread {
     // nome do thread
    private String name;
     // referência na camada [dao]
    private IDao dao;
     // o ID da pessoa em que vamos trabalhar
    private int idPersonne;

     // construtor
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        this.idPersonne = idPersonne;
    }

     // núcleo do tópico
    public void run() {
         // acompanhamento
        suivi("lancé");
         // o ciclo repete-se até que se consiga incrementar em 1
         // o número de filhos da pessoa idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
             // recupera-se uma cópia da pessoa de idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // acompanhamento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
             // espera de 10 ms para libertar o processador
            try {
                 // acompanhamento
                suivi("début attente");
                 // interrompe-se para libertar o processador
                Thread.sleep(10);
                 // acompanhamento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
             // espera concluída — tenta-se validar a cópia
             // entretanto, outras threads podem ter alterado o original
            int codeErreur = 0;
            try {
                 // incrementa em 1 o número de instâncias desta cópia
                personne.setNbEnfants(nbEnfants + 1);
                 // está a tentar alterar o original
                dao.saveOne(personne);
                 // conseguiu-se — o original foi alterado
                fini = true;
            } catch (DaoException ex) {
                 // recuperamos o código de erro
                codeErreur = ex.getCode();
                 // deve ser um erro da versão 3 — caso contrário, reinicia-se
                 // a exceção
                if (codeErreur != 3) {
                    throw ex;
                } else {
                     // acompanhamento
                    suivi(ex.getMessage());
                }
                 // o original foi alterado — recomeça-se tudo
            }
        }
         // acompanhamento
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

     // acompanhamento
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • linha 9: [ThreadDaoMajEnfants] é, de facto, um thread
  • linhas 18-22: o construtor que inicializa o thread com três informações
    1. o nome [name] atribuído ao thread
    2. uma referência [dao] à camada [dao]. Note-se que, mais uma vez, estamos a trabalhar com o tipo da interface [IDao] e não com o da implementação [DaoImpl].
    3. o identificador [id] da pessoa sobre a qual o thread deve trabalhar

Quando [test4] inicia um thread [ThreadDaoMajEnfants] (linha 12 de test4), o método [run] (linha 25) deste último é executado:

  • linhas 78-81: o método privado [suivi] permite efetuar registos no ecrã. O método [run] utiliza-o para permitir o acompanhamento da execução do thread.
  • o thread procura incrementar em 1 o número de filhos da pessoa P com o identificador [id]. Esta atualização pode exigir várias tentativas. Consideremos dois threads: [TH1] e [TH2]. [TH1] solicita uma cópia da pessoa P à camada [dao]. Obtém-na e constata que esta tem a versão V1. O [TH1] é interrompido. O [TH2], que se seguia a ele, faz o mesmo e obtém a mesma versão V1 da pessoa P. O [TH2] é interrompido. O [TH2] retoma o controlo, incrementa o número de filhos de P e guarda as suas alterações. Sabemos que, nessa altura, estas são guardadas e que a versão de P passará para V2. O [TH1] terminou o seu trabalho. [TH2] retoma o controlo e faz o mesmo. A sua atualização de P será recusada, pois possui uma cópia de P com a versão V1, enquanto o P original tem agora a versão V2. O [TH2] tem, então, de repetir todo o ciclo do [lecture -> mise à jour -> sauvegarde]. É por isso que encontramos o ciclo nas linhas 32-72. Nele, o thread:
  • solicita uma cópia da pessoa P a modificar (linha 34)
  • aguarda 10 ms (linha 43). Isto é artificial e visa interromper o thread entre a leitura da pessoa P e a sua atualização efetiva na lista de pessoas, de modo a aumentar a probabilidade de conflitos.
  • incrementa o número de filhos de P (linha 54) e guarda P (linha 56). Se o thread não tiver a versão correta de P, será lançada uma exceção pela camada [dao]. Recupera-se então o código da exceção (linha 61) para verificar se é efetivamente o código 3 (versão incorreta de P). Se não for esse o caso, a exceção é reenviada para o método chamador, que, no final, é o método de teste [test4]. Se ocorrer a exceção com código 3, então recomeça-se o ciclo [lecture -> mise à jour -> sauvegarde]. Se não ocorrer nenhuma exceção, significa que a atualização foi efetuada e o trabalho da thread está concluído.

Quais são os resultados dos testes?

Na primeira configuração testada:

  • comentamos a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
         // aguardamos 10 ms
         //wait(10);
  • o método [test4] cria 100 threads (linha 8, parágrafo 14.5).
         // criação de N threads para atualizar o número de filhos
        final int N = 100;

Obtêm-se os seguintes resultados:

Image

Os cinco testes foram bem-sucedidos.

Na segunda configuração testada:

  • descomenta-se a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
         // espera de 10 ms
        wait(10);
  • o método [test4] cria 2 threads (linha 8, parágrafo 14.5).
         // criação de N threads para atualizar o número de filhos
        final int N = 2;

Obteêm-se os seguintes resultados:

O teste [test4] falhou. Foram criadas duas threads, cada uma encarregada de incrementar em 1 o número de filhos de uma pessoa P que, inicialmente, tinha 0. Esperávamos, portanto, 2 filhos após a execução das duas threads, mas só temos um.

Vamos analisar os registos de ecrã do [test4] para compreender o que aconteceu:

thread n° 0 [1145536368171] : lancé
thread n° 0 [1145536368171] : 0 -> 1 pour la version 1
thread n° 0 [1145536368171] : début attente
thread n° 1 [1145536368171] : lancé
thread n° 1 [1145536368171] : 0 -> 1 pour la version 1
thread n° 1 [1145536368171] : début attente
thread n° 0 [1145536368187] : fin attente
thread n° 1 [1145536368187] : fin attente
thread n° 0 [1145536368187] : a terminé et passé le nombre d'enfants à 1
thread n° 1 [1145536368187] : a terminé et passé le nombre d'enfants à 1
  • linha 1: o thread n.º 0 inicia o seu trabalho
  • linha 2: recuperou uma cópia da pessoa P e verifica que o número de filhos é 0
  • linha 3: depara-se com o [Thread.sleep(10)] do seu método [run] e, por isso, pára no tempo [1145536368171] (ms)
  • linha 4: o thread n.º 1 recupera então o processador e inicia o seu trabalho
  • linha 5: recuperou uma cópia da pessoa P e verifica que o número de filhos é 0
  • linha 6: depara-se com o [Thread.sleep(10)] do seu método [run] e, por isso, pára
  • linha 7: o thread n.º 0 recupera o processador no momento [1145536368187] (ms), c.a.d. 16 ms depois de o ter perdido.
  • linha 8: o mesmo se aplica à thread n.º 1
  • linha 9: o thread n.º 0 efetuou a sua atualização e alterou o número de filhos para 1
  • linha 10: o thread n.º 1 fez o mesmo

A questão é saber por que razão o thread n.º 1 conseguiu efetuar a sua atualização, quando, normalmente, já não detinha a versão correta da pessoa P, que acabara de ser atualizada pelo thread n.º 0.

Em primeiro lugar, é possível observar uma anomalia entre as linhas 7 e 8: parece que o thread n.º 0 perdeu o controlo do processador entre estas duas linhas, cedendo-o ao thread n.º 1. O que estava ele a fazer nesse momento? Estava a executar o método [saveOne] da camada [dao]. Este método tem a seguinte estrutura (ver parágrafo 14.4):

    public void saveOne(Personne personne) {
...
         // alteração — procura-se a pessoa
....
         // temos a versão correta do original?
...
         // aguarda-se 10 ms
        wait(10);
         // Está tudo bem — estamos a efetuar a alteração
    ...
}
  • o thread n.º 0 executou o [saveOne] e chegou até à linha 8, onde foi obrigado a libertar o processador. Entretanto, leu a versão da pessoa P e esta era 1, porque a pessoa P ainda não tinha sido atualizada.
  • Como o processador ficou livre, foi o thread n.º 1 que o herdou. Este, por sua vez, executou o [saveOne] e chegou até à linha 8, onde foi obrigado a libertar o processador. Entretanto, leu a versão da pessoa P e esta era 1, porque a pessoa P ainda não tinha sido atualizada.
  • Como o processador ficou livre, foi o thread n.º 0 que o herdou. A partir da linha 9, efetuou a sua atualização e alterou o número de filhos para 1. Em seguida, o método [run] do thread n.º 0 terminou e o thread exibiu o registo que indicava que tinha alterado o número de filhos para 1 (linha 9).
  • Como o processador ficou livre, foi a thread n.º 1 que o herdou. A partir da linha 9, efetuou a sua atualização e alterou o número de filhos para 1. Porquê 1? Porque possui uma cópia de P com o número de filhos igual a 0. É o registo (linha 5) que o indica. Em seguida, o método [run] do thread n.º 1 terminou e o thread exibiu o registo que indicava que tinha alterado o número de filhos para 1 (linha 10).

De onde vem o problema? Vem do facto de o thread n.º 0 não ter tido tempo de validar a sua alteração e, portanto, de alterar a versão da pessoa P antes de o thread n.º 1 tentar ler essa versão para saber se a pessoa P tinha mudado. Este cenário é pouco provável, mas não impossível. Foi necessário forçar o thread n.º 0 a perder o controlo do processador para que este caso surgisse com apenas dois threads. Sem este artifício, a configuração anterior não tinha conseguido reproduzir este mesmo caso com 100 threads. O teste [test4] tinha sido bem-sucedido.

Qual é a solução? Existem, sem dúvida, várias. Uma delas, simples de implementar, consiste em sincronizar o método [saveOne]:


    public synchronized void saveOne(Personne personne)

A palavra-chave [synchronized] garante que apenas um thread de cada vez possa executar o método. Assim, o thread n.º 1 só será autorizado a executar o [saveOne] quando o thread n.º 0 tiver saído do mesmo. Assim, temos a certeza de que a versão da pessoa P terá sido alterada quando o thread n.º 1 entrar em [saveOne]. A sua atualização será então recusada, uma vez que não terá a versão correta de P.

São estes os quatro métodos da camada [dao] que deveriam ser sincronizados. Decidimos, no entanto, manter esta camada tal como foi descrita e adiar a sincronização para a camada [service]. Para tal, existem várias razões:

  • partimos do princípio de que o acesso à camada [dao] se faz sempre através de uma camada [service]. É o que acontece na nossa aplicação web.
  • pode ser necessário sincronizar também o acesso aos métodos da camada [service] por outras razões que não aquelas que nos levariam a sincronizar os da camada [dao]. Nesse caso, não é necessário sincronizar os métodos da camada [dao]. Se tivermos a certeza de que:
  • todo o acesso à camada [dao] passa pela camada [service]
  • que apenas um único thread de cada vez utiliza a camada [service]

então temos a garantia de que os métodos da camada [dao] não serão executados por dois threads ao mesmo tempo.

Vamos agora descobrir a camada [service].

14.6. A camada [service]

A camada [service] é constituída pelas seguintes classes e interfaces:

Image

  • [IService] é a interface apresentada pela camada [dao]
  • [ServiceImpl] é uma implementação desta

A interface [IService] é a seguinte:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // lista de todas as pessoas
    Collection getAll();
     // obter uma pessoa específica
    Personne getOne(int id);
     // adicionar/alterar uma pessoa
    void saveOne(Personne personne);
     // eliminar uma pessoa
    void deleteOne(int id);
}

É idêntica à interface [IDao].

A implementação [ServiceImpl] da interface [IService] é a seguinte:

package istia.st.springmvc.personnes.service;

import istia.st.springmvc.personnes.dao.IDao;
import istia.st.springmvc.personnes.entites.Personne;

import java.util.Collection;

public class ServiceImpl implements IService {

     // a camada [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

     // lista de pessoas
    public synchronized Collection getAll() {
        return dao.getAll();
    }

     // obter uma pessoa específica
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

     // adicionar ou alterar uma pessoa
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // eliminar uma pessoa
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • linhas 10-19: o atributo [IDao dao] é uma referência à camada [dao]. Será inicializado pelo Spring IoC.
  • linhas 22-24: implementação do método [getAll] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
  • linhas 27-29: implementação do método [getOne] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
  • linhas 32-34: implementação do método [saveOne] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
  • linhas 37-39: implementação do método [deleteOne] da interface [IService]. O método limita-se a delegar o pedido à camada [dao].
  • Todos os métodos são sincronizados (palavra-chave `synchronized`), garantindo que apenas um thread de cada vez possa utilizar a camada [service] e, consequentemente, a camada [dao].

14.7. Testes da camada [service]

É escrito um teste JUnit para a camada [service]:

[TestService] é o teste JUnit. Os testes realizados são estritamente idênticos aos realizados para a camada [dao]. A estrutura de [TestService] é a seguinte:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

     // camada [service]
    private ServiceImpl service;

     // construtor
    public TestService() {
        service = new ServiceImpl();
        DaoImpl dao=new DaoImpl();
        service.setDao(dao);
    }

     // lista de pessoas
    private void doListe(Collection personnes) {
...
    }

     // teste1
    public void test1() throws ParseException {
         // lista atual
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
         // visualização
        doListe(personnes);
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
         // verificação — ocorrerá uma falha se a pessoa não for encontrada
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

     // alteração/eliminação de um elemento inexistente
    public void test2() throws ParseException {
...
    }

     // gestão de versões de pessoas
    public void test3() throws ParseException, InterruptedException {
...
    }

     // bloqueio otimista — acesso multithread
    public void test4() throws Exception {
         // adição de uma pessoa
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
         // criação de N threads para atualizar o número de filhos
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadServiceMajEnfants("thread n° " + i, service,
                    id1);
            taches[i].start();
        }
...
    }

     // testes de validade de saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • linha 9: a camada [service] testada é do tipo [ServiceImpl].
  • linhas 11-15: o construtor do teste JUnit cria uma instância da camada [service] a ser testada (linha 12), cria uma instância da camada [dao] (linha 13) e indica à camada [service] que deve utilizar esta camada [dao] (linha 14).

O método [test1] testa os quatro métodos da interface [IService] da mesma forma que o método de teste da camada [dao] com o mesmo nome. Simplesmente, acede-se à camada [service] (linhas 25, 32, 35) em vez de à camada [dao].

O método [test4] visa identificar problemas de acesso simultâneo aos métodos da camada [service]. É, mais uma vez, idêntico ao método de teste [test4] da camada [dao]. No entanto, existem alguns detalhes que diferem:

  • recorre-se à camada [service] em vez da camada [dao] (linha 55)
  • passa-se aos threads uma referência à camada [service] em vez de à camada [dao] (linha 61)

O tipo [ThreadServiceMajEnfants] é também praticamente idêntico ao tipo [ThreadDaoMajEnfants], com a única diferença de que trabalha com a camada [service] e não com a camada [dao]:

package istia.st.springmvc.personnes.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.service.IService;

public class ThreadServiceMajEnfants extends Thread {

     // nome do thread
    private String name;
     // referência na camada [service]
    private IService service;
     // o ID da pessoa em que se vai trabalhar
    private int idPersonne;

    public ThreadServiceMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    public void run() {
...
    }

     // acompanhamento
    private void suivi(String message) {
        System.out.println(name + " : " + message);
    }

}
  • linha 12: o thread funciona com a camada [service]

Realizamos os testes com a configuração que causou o problema na camada [dao]:

  • descomentamos a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, parágrafo 14.4).
         // aguarda-se 10 ms
        wait(10);
  • O método [test4] cria 100 threads (linha 65, parágrafo 14.7).
         // criação de N threads para atualizar o número de filhos
        final int N = 100;

Os resultados obtidos são os seguintes:

Foi a sincronização dos métodos da camada [service] que permitiu o sucesso do teste [test4].

14.8. A camada [web]

Recorde-se a arquitetura de três camadas da nossa aplicação:

A camada [web] irá apresentar ecrãs ao utilizador para que este possa gerir o grupo de pessoas:

  • lista das pessoas do grupo
  • adição de uma pessoa ao grupo
  • alteração de uma pessoa do grupo
  • eliminação de uma pessoa do grupo

Para tal, irá basear-se na camada [service], que, por sua vez, recorrerá à camada [dao]. Já apresentámos os ecrãs geridos pela camada [web] (parágrafo 14.1). Para descrever a camada web, iremos apresentar sucessivamente:

  • a sua configuração
  • as suas vistas
  • o seu controlador
  • alguns testes

14.8.1. Configuração da aplicação web

O projeto Eclipse da aplicação é o seguinte:

Image

  • no pacote [istia.st.mvc.personnes.web], encontra-se o controlador [Application].
  • As páginas JSP / JSTL encontram-se em [WEB-INF/vues].
  • A pasta [lib] contém os ficheiros de terceiros necessários à aplicação. Estes estão visíveis na pasta [Web App Libraries].

[web.xml]


O ficheiro [web.xml] é o ficheiro utilizado pelo servidor web para carregar a aplicação. O seu conteúdo é o seguinte:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
    xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>mvc-personnes-01</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>
            istia.st.mvc.personnes.web.Application
        </servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Mapeamento de ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  ficheiros de início -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Página de erro inesperado -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • linhas 27-30: as URLs [/do/*] serão processadas pelo servlet [personnes]
  • linhas 9-12: o servlet [personnes] é uma instância da classe [Application], uma classe que iremos criar.
  • linhas 13-24: definem três parâmetros [urlList, urlEdit, urlErreurs] que identificam as URLs das páginas JSP das vistas [list, edit, erreurs].
  • linhas 32-34: a aplicação tem uma página inicial predefinida [index.jsp], que se encontra na raiz da pasta da aplicação web.
  • linhas 36-39: a aplicação tem uma página de erros predefinida que é apresentada quando o servidor web deteta uma exceção não gerida pela aplicação.
    • linha 37: a baliza <exception-type> indica o tipo de exceção gerida pela diretiva <error-page>, neste caso o tipo [java.lang.Exception] e derivados, ou seja, todas as exceções.
    • linha 38: a baliza <location> indica a página JSP a ser apresentada quando ocorre uma exceção do tipo definido por <exception-type>. A exceção ocorrida está disponível nessa página num objeto denominado «exception», se a página tiver a diretiva:

<%@ page isErrorPage="true" %>
  • (continuação)
    • se <exception-type> especificar um tipo T1 e se uma exceção do tipo T2, não derivada de T1, for reportada ao servidor web, este envia ao cliente uma página de exceção proprietária, geralmente pouco intuitiva. Daí a importância da baliza <error-page> no ficheiro [web.xml].

[index.jsp]


Esta página é apresentada se um utilizador solicitar diretamente o contexto da aplicação sem especificar um URL, c.a.d. Aqui, [/personnes-01]. O seu conteúdo é o seguinte:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp] redireciona o cliente para a URL [/do/list]. Esta URL apresenta a lista de pessoas do grupo.

14.8.2. As páginas JSP / JSTL da aplicação


A vista [list.jsp]


Serve para apresentar a lista de pessoas:

Image

O seu código é o seguinte:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.dateNaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbEnfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

  • Esta vista recebe um elemento no seu modelo:
  • o elemento [personnes] associado a um objeto do tipo [ArrayList] de objetos do tipo [Personne]
  • linhas 22-34: percorre-se a lista ${personas} para apresentar uma tabela HTML contendo as pessoas do grupo.
  • linha 31: o URL para o qual aponta o link [Modifier] é definido pelo campo [id] da pessoa atual, para que o controlador associado ao URL [/do/edit] saiba qual é a pessoa a modificar.
  • linha 32: o mesmo se aplica ao link [Supprimer].
  • linha 28: para apresentar a data de nascimento da pessoa no formato JJ/MM/AAAA, utiliza-se a baliza <dt> da biblioteca de balizas [DateTime] do projeto Apache [Jakarta Taglibs]:

Image

O ficheiro de descrição desta biblioteca de tags está definido na linha 3.

  • linha 37: o link [Ajout] para adicionar uma nova pessoa tem como destino o URL [/do/edit], tal como o link [Modifier] da linha 31. É o valor -1 do parâmetro [id] que indica que se trata de uma adição e não de uma modificação.

A vista [edit.jsp]


Serve para apresentar o formulário de adição de uma nova pessoa ou de alteração de uma pessoa existente:

O código da vista [edit.jsp] é o seguinte:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit != ''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${dateNaissance}" name="dateNaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbEnfants}" name="nbEnfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

Esta vista apresenta um formulário para adicionar uma nova pessoa ou atualizar uma pessoa existente. Daqui em diante, e para simplificar a redação, utilizaremos apenas o termo [mise à jour]. O botão [Valider] (linha 73) aciona o POST do formulário na URL [/do/validate] (linha 16). Se o POST falhar, a vista [edit.jsp] é novamente apresentada com o(s) erro(s) que ocorreram; caso contrário, é apresentada a vista [list.jsp].

  • A vista [edit.jsp], apresentada tanto num GET como num POST que falhe, recebe os seguintes elementos no seu modelo:
atributo
GET
POST
id
identificador da pessoa atualizada
idem
version
a sua versão
idem
prenom
o seu nome próprio
nome próprio introduzido
nom
o seu apelido
apelido introduzido
dateNaissance
data de nascimento
data de nascimento introduzida
marie
estado civil
estado civil introduzido
nbEnfants
o número de filhos
número de filhos introduzido
erreurEdit
em branco
uma mensagem de erro a indicar que a adição ou alteração falhou no momento do POST, provocada pelo botão [Envoyer]. Vazio se não houver erro.
erreurPrenom
vazio
indica um nome próprio incorreto – vazio caso contrário
erreurNom
vazio
indica um apelido incorreto – vazio caso contrário
erreurDateNaissance
vazio
indica uma data de nascimento incorreta – vazio caso contrário
erreurNbEnfants
vazio
indica um número de filhos incorreto – vazio caso contrário
  • linhas 11-15: se o POST do formulário falhar, teremos [erreurEdit!=''] e será exibida uma mensagem de erro.
  • linha 16: o formulário será enviado para o URL [/do/validate]
  • linha 20: o elemento [id] do modelo é apresentado
  • linha 24: o elemento [version] do modelo é apresentado
  • linhas 26-32: introdução do nome próprio da pessoa:
    • aquando da exibição inicial do formulário (GET), ${prenom} exibe o valor atual do campo [prenom] do objeto [Personne] atualizado e ${erreurPrenom} está vazio.
    • em caso de erro após o POST, volta a ser apresentado o valor introduzido ${prenom}, bem como a eventual mensagem de erro ${erreurPrenom}
  • linhas 33-39: introdução do apelido da pessoa
  • linhas 40-46: introdução da data de nascimento da pessoa
  • linhas 47-61: introdução do estado civil da pessoa através de um botão de opção. Utiliza-se o valor do campo [marie] do objeto [Personne] para determinar qual dos dois botões de opção deve ser selecionado.
  • linhas 62-68: introdução do número de filhos da pessoa
  • linha 71: um campo HTML oculto, denominado [id], cujo valor corresponde ao campo [id] da pessoa que está a ser atualizada; -1 para uma adição, outro valor para uma alteração.
  • linha 72: um campo oculto HTML, denominado [version], cujo valor corresponde ao campo [id] da pessoa que está a ser atualizada.
  • linha 73: o botão [Valider] do tipo [Submit] do formulário
  • linha 74: um link que permite regressar à lista de pessoas. Foi denominado [Annuler] porque permite sair do formulário sem o validar.

A vista [exception.jsp]


Serve para apresentar uma página a indicar que ocorreu uma exceção não gerida pela aplicação e que foi encaminhada para o servidor web.

Por exemplo, vamos eliminar uma pessoa que não existe no grupo:

O código da vista [exception.jsp] é o seguinte:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>

  • esta vista recebe uma chave no seu modelo, o elemento [exception], que corresponde à exceção que foi interceptada pelo servidor web. Para que este elemento seja incluído no modelo da página JSP pelo servidor web, é necessário que a página tenha definido a baliza da linha 3.
  • linha 6: define-se o código de estado HTTP da resposta como 200. Trata-se do primeiro cabeçalho HTTP da resposta. O código 200 indica ao cliente que o seu pedido foi atendido. Normalmente, um documento HTML foi incluído na resposta do servidor. É o que acontece neste caso. Se o código de estado HTTP da resposta não for definido como 200, terá aqui o valor 500, o que significa que ocorreu um erro. Com efeito, o servidor web, ao interceptar uma exceção não gerida, considera esta situação anómala e sinaliza-a através do código 500. A reação ao código 500 difere consoante os navegadores: o Firefox apresenta o documento que pode acompanhar esta resposta, enquanto o Firefox ignora esse documento e apresenta a sua própria página. É por esta razão que substituímos o código 500 pelo código 200.
  • linha 16: o texto da exceção é apresentado
  • linha 18: é apresentado ao utilizador um link para regressar à lista de pessoas

A vista [erreurs.jsp]


Serve para apresentar uma página que sinaliza os erros de inicialização da aplicação, c.a.d, e os erros detetados durante a execução do método [init] do servlet do controlador. Pode tratar-se, por exemplo, da ausência de um parâmetro no ficheiro [web.xml], tal como se pode ver no exemplo abaixo:

Image

O código da página [erreurs.jsp] é o seguinte:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

A página recebe no seu modelo um elemento [erreurs], que é um objeto do tipo [ArrayList] de objetos [String], sendo estes últimos mensagens de erro. São apresentadas pelo ciclo das linhas 13-15.

14.8.3. O controlador da aplicação

O controlador [Application] está definido no pacote [istia.st.mvc.personnes.web]:

Image


Estrut tura e inicialização do controlador


A estrutura do controlador [Application] é a seguinte:

package istia.st.mvc.personnes.web;

import istia.st.mvc.personnes.dao.DaoException;
...

@SuppressWarnings("serial")
public class Application extends HttpServlet {
     // parâmetros de instância
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

     // serviço
    ServiceImpl service=null;

     // inicialização
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
         // recuperam-se os parâmetros de inicialização do servlet
        ServletConfig config = getServletConfig();
         // processam-se os restantes parâmetros de inicialização
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
             // valor do parâmetro
            valeur = config.getInitParameter(paramètres[i]);
             // o parâmetro existe?
            if (valeur == null) {
                 // regista-se o erro
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                 // armazena-se o valor do parâmetro
                params.put(paramètres[i], valeur);
            }
        }
         // a URL da vista [erreurs] tem um tratamento específico
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
         // instanciação da camada [dao]
        DaoImpl dao = new DaoImpl();
        dao.init();
         // instanciação da camada [service]
        service = new ServiceImpl();
        service.setDao(dao);
    }

     // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
....
    }

     // exibição da lista de pessoas
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // alteração/adição de uma pessoa
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // confirmação da alteração/adição de uma pessoa
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // confirmação da alteração/adição de uma pessoa
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

     // exibição do formulário pré-preenchido
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

     // envio
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
         // passa o controlo para o GET
        doGet(request, response);    }
}
  • linhas 20-36: recuperam-se os parâmetros esperados no ficheiro [web.xml].
  • linhas 39-41: o parâmetro [urlErreurs] deve estar obrigatoriamente presente, pois indica o URL da vista [erreurs] capaz de apresentar eventuais erros de inicialização. Se não existir, a aplicação é interrompida através do lançamento de uma [ServletException] (linha 40). Esta exceção será encaminhada para o servidor web e tratada pela baliza <error-page> do ficheiro [web.xml]. A vista [exception.jsp] é, portanto, apresentada:

Image

O link [Retour à la liste] acima não funciona. Ao utilizá-lo, obtém-se a mesma resposta enquanto a aplicação não for alterada e recarregada. É útil para outros tipos de exceções, como já vimos.

  • linha 43: cria uma instância [DaoImpl] que implementa a camada [dao]
  • linha 44: inicializa esta instância (criação de uma lista inicial de três pessoas)
  • linha 46: cria uma instância [ServiceImpl] que implementa a camada [service]
  • linha 47: inicializa a camada [service], atribuindo-lhe uma referência à camada [dao]

Após a inicialização do controlador, os seus métodos dispõem de uma referência [service] à camada [service] (linha 15), que irão utilizar para executar as ações solicitadas pelo utilizador. Estas serão interceptadas pelo método [doGet], que as encaminhará para serem processadas por um método específico do controlador:

Url
Método HTTP
método do controlador
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

O método [doGet]


Este método tem como objetivo encaminhar o processamento das ações solicitadas pelo utilizador para o método correto. O seu código é o seguinte:

// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

         // verifica-se como decorreu a inicialização do servlet
        if (erreursInitialisation.size() != 0) {
             // passamos o controlo para a página de erros
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
             // fim
            return;
        }
         // recuperamos o método de envio da solicitação
        String méthode = request.getMethod().toLowerCase();
         // recupera-se a ação a executar
        String action = request.getPathInfo();
         // ação?
        if (action == null) {
            action = "/list";
        }
         // execução da ação
        if (méthode.equals("get") && action.equals("/list")) {
             // lista de pessoas
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
             // eliminação de uma pessoa
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
             // apresentação do formulário de adição/alteração de uma pessoa
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
             // validação do formulário de adição/alteração de uma pessoa
            doValidatePersonne(request, response);
            return;
        }
         // outros casos
        doListPersonnes(request, response);
    }
  • linhas 7-13: verifica-se se a lista de erros de inicialização está vazia. Se não for esse o caso, é apresentada a vista [erreurs(erreurs)], que irá sinalizar o(s) erro(s).
  • linha 15: recupera-se o método [get] ou [post] que o cliente utilizou para efetuar o seu pedido.
  • linha 17: recupera-se o valor do parâmetro [action] da consulta.
  • linhas 23-27: processamento da consulta [GET /do/list], que solicita a lista de pessoas.
  • linhas 28-32: processamento da solicitação [GET /do/delete], que solicita a eliminação de uma pessoa.
  • linhas 33-37: processamento da consulta [GET /do/edit], que solicita o formulário de atualização de uma pessoa.
  • linhas 38-42: processamento da solicitação [POST /do/validate], que solicita a validação da pessoa atualizada.
  • linha 44: se a ação solicitada não for uma das cinco anteriores, então procede-se como se fosse [GET /do/list].

O método [doListPersonnes]


Este método processa a solicitação [GET /do/list], que solicita a lista de pessoas:

Image

O seu código é o seguinte:

1
2
3
4
5
6
7
8
9
     // exibição da lista de pessoas
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // o modelo da vista [list]
        request.setAttribute("personnes", service.getAll());
         // Exibição da vista [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • linha 5: solicita-se à camada [service] a lista de pessoas do grupo e esta é inserida no modelo sob a chave «pessoas».
  • linha 7: exibe-se a vista [list.jsp] descrita no parágrafo 14.8.2.

O método [doDeletePersonne]


Este método processa a consulta [GET /do/delete?id=XX], que solicita a eliminação da pessoa com id=XX. A URL [/do/delete?id=XX] corresponde aos links [Supprimer] da vista [list.jsp]:

Image

cujo código é o seguinte:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

Na linha 12, vemos a URL [/do/delete?id=XX] do link [Supprimer]. O método [doDeletePersonne], que deve processar esta URL, deve remover a pessoa com id=XX e, em seguida, apresentar a nova lista de pessoas do grupo. O seu código é o seguinte:

     // validação da alteração/adição de uma pessoa
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recuperar o ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
         // eliminação da pessoa
        service.deleteOne(id);
         // redirecionamento para a lista de pessoas
        response.sendRedirect("list");
    }
  • linha 5: a URL processada tem o formato [/do/delete?id=XX]. Recupera-se o valor [XX] do parâmetro [id].
  • linha 7: solicita-se à camada [service] a eliminação da pessoa com o ID obtido. Não fazemos qualquer verificação. Se a pessoa que se pretende eliminar não existir, a camada [dao] lança uma exceção que é propagada pela camada [service]. Também não a tratamos aqui, no controlador. Por conseguinte, a exceção será encaminhada até ao servidor web, que, por configuração, fará com que seja apresentada a página [exception.jsp], descrita no parágrafo 14.8.2:

Image

  • linha 9: se a eliminação tiver ocorrido (sem exceção), solicita-se ao cliente que seja redirecionado para a URL relativa [list]. Como a página que acabou de ser processada é a [/do/delete], a URL de redirecionamento será [/do/list]. O navegador será, assim, levado a aceder à página [GET /do/list], o que provocará a exibição da lista de pessoas.

O método [doEditPersonne]


Este método processa a solicitação [GET /do/edit?id=XX], que solicita o formulário de atualização da pessoa com id=XX. A URL [/do/edit?id=XX] é a dos links [Modifier] e a do link [Ajout] da vista [list.jsp]:

Image

cujo código é o seguinte:

...
<html>
    <head>
        <title>MVC - personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>

Na linha 11, vemos o URL [/do/edit?id=XX] do link [Modifier] e, na linha 17, o URL [/do/edit?id=-1] do link [Ajout]. O método [doEditPersonne] deve apresentar o formulário de edição da pessoa com id=XX ou, caso se trate de uma adição, apresentar um formulário em branco.

O código do método [doEditPersonne] é o seguinte:

     // alteração/adição de uma pessoa
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recupera-se o ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
         // adição ou alteração?
        Personne personne = null;
        if (id != -1) {
             // alteração - recupera-se a pessoa a alterar
            personne = service.getOne(id);
        } else {
             // adição - cria-se um registo de pessoa vazio
            personne = new Personne();
            personne.setId(-1);
        }
         // insere-se o objeto [Personne] no modelo da vista [edit]
        request.setAttribute("erreurEdit", "");
        request.setAttribute("id", personne.getId());
        request.setAttribute("version", personne.getVersion());
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDateNaissance();
        if (dateNaissance != null) {
            request.setAttribute("dateNaissance", new SimpleDateFormat(
                    "dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("dateNaissance", "");
        }
        request.setAttribute("marie", personne.getMarie());
        request.setAttribute("nbEnfants", personne.getNbEnfants());
         // exibição da vista [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • o GET tem como destino uma URL do tipo [/do/edit?id=XX]. Na linha 5, recuperamos o valor de [id]. Em seguida, há dois casos:
  1. o id é diferente de -1. Nesse caso, trata-se de uma alteração e é necessário apresentar um formulário pré-preenchido com as informações da pessoa a ser alterada. Na linha 10, essa pessoa é solicitada à camada [service].
  2. Se o id for igual a -1, trata-se de uma adição e é necessário apresentar um formulário vazio. Para tal, é criado um registo vazio nas linhas 13-14.
  • O objeto [Personne] obtido é inserido no modelo da página [edit.jsp] descrito no parágrafo 14.8.2. Este modelo inclui os seguintes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Estes elementos são inicializados nas linhas 17 a 30, com exceção daqueles cujo valor é a cadeia vazia [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Sabe-se que, na sua ausência no modelo, a biblioteca JSTL exibirá uma cadeia vazia como seu valor. Embora o elemento [erreurEdit] também tenha como valor uma cadeia vazia, é, no entanto, inicializado, pois é efetuada uma verificação do seu valor na página [edit.jsp].
  • Assim que o modelo estiver pronto, o controlo passa para a página [edit.jsp], linhas 32-33, que irá gerar a vista [edit].

O método [doValidatePersonne]


Este método processa a solicitação [POST /do/validate] que valida o formulário de atualização. Esta solicitação POST é acionada pelo botão [Valider]:

Image

Recorde-se os campos de preenchimento do formulário HTML da vista acima:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${prenom}" name="prenom" size="20">
....
        <input type="text" value="${nom}" name="nom" size="20">
....
        <input type="text" value="${dateNaissance}" name="dateNaissance">
...
        <input type="radio" name="marie" value="true" checked>Oui
....
        <input type="text" value="${nbEnfants}" name="nbEnfants">
....
            <input type="hidden" value="${id}" name="id">
     <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
</form>

A consulta POST contém os parâmetros [prenom, nom, dateNaissance, marie, nbEnfants, id, version] e é enviada para o URL [/do/validate] (linha 1). É processada pelo seguinte método [doValidatePersonne]:

// validação da alteração/adição de uma pessoa
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
         // recuperação dos elementos enviados
        boolean formulaireErroné = false;
        boolean erreur;
         // o nome próprio
        String prenom = request.getParameter("prenom").trim();
         // o nome próprio é válido?
        if (prenom.length() == 0) {
             // registar o erro
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
         // o apelido
        String nom = request.getParameter("nom").trim();
         // nome próprio válido?
        if (nom.length() == 0) {
             // regista-se o erro
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
         // data de nascimento
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
             // regista-se o erro
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
         // estado civil
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
         // número de filhos
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
             // regista-se o erro
            erreur = true;
        }
         // número de filhos incorreto?
        if (erreur) {
             // o erro foi comunicado
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
         // ID da pessoa
        int id = Integer.parseInt(request.getParameter("id"));
         // versão
        long version = Long.parseLong(request.getParameter("version"));
         // O formulário está incorreto?
        if (formulaireErroné) {
             // reapresenta-se o formulário com as mensagens de erro
            showFormulaire(request, response, "");
             // concluído
            return;
        }
         // O formulário está correto — registar a pessoa
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
             // registo
            service.saveOne(personne);
        } catch (DaoException ex) {
             // o formulário é exibido novamente com a mensagem do erro ocorrido
            showFormulaire(request, response, ex.getMessage());
             // concluído
            return;
        }
         // redireciona para a lista de pessoas
        response.sendRedirect("list");
    }

     // exibição do formulário pré-preenchido
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
         // prepara-se o modelo da vista [edit]
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("dateNaissance", request.getParameter(
                "dateNaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
         // exibição da vista [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • linhas 8-14: o parâmetro [prenom] da solicitação POST é recuperado e a sua validade é verificada. Se estiver incorreto, o elemento [erreurPrenom] é inicializado com uma mensagem de erro e colocado nos atributos da consulta.
  • linhas 16-22: procede-se de forma semelhante para o parâmetro [nom]
  • linhas 24-32: procede-se de forma semelhante para o parâmetro [dateNaissance]
  • linha 34: recupera-se o parâmetro [marie]. Não se verifica a sua validade porque, a priori, provém do valor de um botão de opção. Dito isto, nada impede que um programa crie um [POST /personnes-01/do/validate] acompanhado de um parâmetro [marie] inventado. Devemos, portanto, testar a validade deste parâmetro. Aqui, contamos com o nosso mecanismo de gestão de exceções, que provoca a exibição da página [exception.jsp] caso o controlador não as gere por si próprio. Assim, se a conversão do parâmetro [marie] em booleano falhar na linha 34, será gerada uma exceção que resultará no envio da página [exception.jsp] ao cliente. Este funcionamento é o que pretendemos.
  • linhas 34-54: recuperamos o parâmetro [nbEnfants] e verificamos o seu valor.
  • linha 56: recupera-se o parâmetro [id] sem verificar o seu valor
  • linha 58: faz-se o mesmo com o parâmetro [version]
  • linhas 60-65: se o formulário estiver incorreto, é exibido novamente com as mensagens de erro criadas anteriormente
  • linhas 67-69: se for válido, cria-se um novo objeto [Personne] com os elementos do formulário
  • linhas 70-78: o utilizador é guardado. O processo de gravação pode falhar. Num ambiente multiutilizadores, o utilizador a ser alterado pode ter sido eliminado ou já ter sido alterado por outra pessoa. Neste caso, a camada [dao] irá lançar uma exceção que é tratada aqui.
  • linha 80: se não tiver ocorrido nenhuma exceção, redirecionamos o cliente para o URL [/do/list] para lhe apresentar o novo estado do grupo.
  • linha 75: se tiver ocorrido uma exceção durante o gravação, solicitamos novamente a exibição do formulário inicial, passando-lhe a mensagem de erro da exceção (3.º parâmetro).

O método [showFormulaire] (linhas 84-101) constrói o modelo necessário para a página [edit.jsp] com os valores introduzidos (request.getParameter(" ... ")). Recorde-se que as mensagens de erro já foram inseridas no modelo pelo método [doValidatePersonne]. A página [edit.jsp] é apresentada nas linhas 99-100.

14.9. Testes da aplicação web

Foram apresentados vários testes no parágrafo 14.1. Convidamos o leitor a repeti-los. Apresentamos aqui outras capturas de ecrã que ilustram casos de conflitos de acesso aos dados num ambiente multiutilizador:

[Firefox] será o navegador do utilizador U1. Este solicita o URL [http://localhost:8080/personnes-01]:

Image

[IE] será o navegador do utilizador U2. Este solicita a mesma URL:

Image

O utilizador U1 acede à edição do perfil da pessoa [Lemarchand]:

Image

O utilizador U2 faz o mesmo:

Image

O utilizador U1 efetua alterações e valida:

O utilizador U2 faz o mesmo:

O utilizador U2 volta à lista de pessoas através do link [Annuler] do formulário:

Image

Encontra a pessoa [Lemarchand] tal como U1 a alterou. Agora, U2 elimina [Lemarchand]:

U1 continua a ter a sua própria lista e pretende alterar [Lemarchand] novamente:

O U1 utiliza o link [Retour à la liste] para ver do que se trata:

Image

Descobre que, de facto, o [Lemarchand] já não faz parte da lista...

14.10. Conclusion

Implementámos a arquitetura MVC numa arquitetura de três camadas [web, metier, dao], utilizando um exemplo básico de gestão de uma lista de pessoas. Isto permitiu-nos utilizar os conceitos que tinham sido apresentados nas secções anteriores. Na versão analisada, a lista de pessoas era mantida na memória. Em breve, iremos analisar versões em que essa lista será mantida numa tabela de base de dados.

Mas, antes disso, vamos apresentar uma ferramenta chamada Spring IoC, que facilita a integração das diferentes camadas de uma aplicação ntier.