Skip to content

14. Aplicação Web MVC numa arquitetura de 3 camadas – Exemplo 1

14.1. Introdução

Até este momento, limitámo-nos a exemplos destinados a fins educativos. Por esse motivo, tiveram de ser simples. Apresentamos agora uma aplicação básica que, no entanto, é mais rica em funcionalidades do que qualquer uma das apresentadas até agora. Será única na medida em que utiliza as três camadas de uma arquitetura de três camadas:

Image

Recomenda-se aos leitores que revisem os princípios de uma aplicação Web MVC numa arquitetura de três camadas na Secção 4, caso os tenham esquecido.

A aplicação web que vamos escrever permitir-nos-á gerir um grupo de pessoas utilizando quatro operações:

  • lista de pessoas no grupo
  • adicionar uma pessoa ao grupo
  • alterar uma pessoa no grupo
  • remover uma pessoa do grupo

Estas são as quatro operações básicas numa tabela de base de dados. Vamos escrever duas versões desta aplicação:

  • Na versão 1, a camada [DAO] não utilizará uma base de dados. Os membros do grupo serão armazenados num objeto [ArrayList] simples, 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 de base de dados. Demonstraremos que isto pode ser feito sem afetar a camada web da versão 1, que permanecerá inalterada.

As seguintes capturas de ecrã 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 [people-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 [business] 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.entities] contém objetos partilhados entre diferentes camadas
  • o pacote [istia.st.mvc.people.tests] contém os testes JUnit para as camadas [DAO] e [service]

Iremos explorar as três camadas [dao], [service] e [web] sucessivamente. Uma vez que demoraria demasiado tempo a escrever e poderia ser demasiado tedioso de ler, por vezes poderemos avançar rapidamente pelas explicações, exceto quando o material apresentado for novo.

14.3. Representação de uma pessoa

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

Image

A classe [Person] é a seguinte:

package istia.st.springmvc.personnes.entites;

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

public class Personne {

    // unique personal identifier
    private int id;
    // the current version
    private long version;
    // the name
    private String nom;
    // first name
    private String prenom;
    // date of birth
    private Date dateNaissance;
    // marital status
    private boolean marie = false;
    // number of children
    private int nbEnfants;

    // getters - setters
...

    // default builder
    public Personne() {

    }

    // constructor with initialization of person fields
    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);
    }

    // builder of a person by copying another person
    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 identificador único para uma pessoa
    • last_name: o apelido da pessoa
    • firstName: o seu nome próprio
    • dateOfBirth: a data de nascimento
    • maritalStatus: se é casada ou não
    • nbChildren: o número de filhos
  • O atributo [version] é um atributo adicionado artificialmente para efeitos da aplicação. Numa perspetiva orientada para objetos, teria provavelmente sido preferível adicionar este atributo a uma classe derivada de [Person]. A sua necessidade torna-se evidente quando se consideram os casos de utilização da aplicação web. Um desses casos de utilização é o seguinte:

No momento T1, o utilizador U1 começa a editar uma pessoa P. Nesta altura, o número de filhos é 0. U1 altera este número para 1, mas antes de validar a alteração, o utilizador U2 começa a editar a mesma pessoa P. Como 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 guardam as suas alterações nessa ordem. A alteração de U2 terá precedência: o nome ficará em maiúsculas e o número de filhos permanecerá em zero, mesmo que U1 acredite ter alterado esse número para 1.

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

No momento T1, um utilizador U1 começa a editar uma pessoa P. Neste momento, o número de filhos é 0 e a versão é V1. Ele altera o número de filhos para 1, mas antes de confirmar a sua edição, um utilizador U2 entra no modo de edição para a mesma pessoa P. Como U1 ainda não confirmou a sua edição, U2 vê o número de filhos como 0 e a versão como V1. U2 altera o nome da pessoa P para maiúsculas. Em seguida, U1 e U2 confirmam as suas edições nessa ordem. Antes de confirmar uma alteração, verificamos se o utilizador que está a modificar a pessoa P tem a mesma versão que a versão atualmente guardada da pessoa P. Este será o caso do utilizador U1. A sua alteração é, portanto, aceite e, em seguida, alteramos a versão da pessoa modificada de V1 para V2 para indicar que a pessoa sofreu uma alteração. Ao validar a modificação de U2, vamos perceber que este tem a versão V1 da pessoa P, enquanto a versão atual é a V2. Podemos então informar o utilizador U2 de que outra pessoa agiu antes dele e que deve começar com a nova versão da pessoa P. Ele fará isso, recuperará a versão V2 da pessoa P, que agora tem um filho, colocará o nome em maiúsculas e validará. A sua modificação será aceite se a pessoa P registada ainda tiver a versão V2. Em última análise, as modificações feitas por U1 e U2 serão tidas em conta, enquanto que no caso de utilização sem versões, uma das modificações se perdeu.

  • linhas 32–40: um construtor capaz de inicializar os campos de uma pessoa. O campo [versão] é omitido.
  • linhas 43–51: um construtor que cria uma cópia da pessoa que lhe é passada como parâmetro. Temos agora 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] é composta pelas seguintes classes e interfaces:

Image

  • [IDao] é a interface apresentada pela camada [dao]
  • [DaoImpl] é uma implementação desta interface, na qual o grupo de pessoas é encapsulado num objeto [ArrayList]
  • [DaoException] é um tipo de exceção não verificada lançada 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 {
    // list of all persons
    Collection getAll();
    // get a specific person
    Personne getOne(int id);
    // add/modify a person
    void saveOne(Personne personne);
    // delete a person
    void deleteOne(int id);
}
  • A interface possui quatro métodos para as quatro operações que pretendemos realizar no grupo de pessoas:
    • getAll: para recuperar uma coleção de pessoas
    • getOne: para recuperar uma pessoa com um ID específico
    • saveOne: para adicionar uma pessoa (id=-1) ou modificar 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 {

    // error code
    private int code;

    public int getCode() {
        return code;
    }

// manufacturer
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • Linha 3: A classe [DaoException], que deriva de [RuntimeException], é um tipo de exceção não tratada: o compilador não exige que:
    • tratar este tipo de exceção com um bloco try/catch ao chamar um método que possa lançá-la
    • incluirmos a palavra-chave "throws DaoException" na assinatura de um método que possa 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 verificadas será então aceitável, conferindo assim flexibilidade à arquitetura.

  • Linha 6: um código de erro. A camada [dao] lançará várias exceções identificadas por diferentes códigos de erro. Isto permitirá que a camada responsável pelo tratamento da exceção determine a origem exata do erro e tome as medidas adequadas. Existem outras formas de alcançar o mesmo resultado. Uma delas é criar um tipo de exceção para cada tipo de erro possível, por exemplo, MissingLastNameException, MissingFirstNameException, IncorrectAgeException, ...
  • linhas 13–16: o construtor que permite criar uma exceção identificada por um código de erro e uma mensagem de erro.
  • Linhas 8–10: O método que permite ao manipulador de exceções 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 {

    // a list of people
    private ArrayList personnes = new ArrayList();

    // next person's no
    private int id = 0;

    // initializations
    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);
        }
    }

    // list of persons
    public Collection getAll() {
        return personnes;
    }

    // get a specific person
    public Personne getOne(int id) {
        // we're looking for the person
        int i = getPosition(id);
        // have we found?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

    // add or modify a person
    public void saveOne(Personne personne) {
        // is the person parameter valid?
        check(personne);
        // addition or modification?
        if (personne.getId() == -1) {
            // add
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
        // modification - we're looking for the person
        int i = getPosition(personne.getId());
        // have we found?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
        // do we have the right version of the 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);
        }
        // wait 10 ms
        //wait(10);
        // that's it - make the change
        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());
    }

    // deleting a person
    public void deleteOne(int id) {
        // we're looking for the person
        int i = getPosition(id);
        // have we found?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
            // we delete the person
            personnes.remove(i);
        }
    }

    // id generator
    private int getNextId() {
        id++;
        return id;
    }

    // find a person
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
        // browse the list of people
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
        // result?
        return trouvé ? i : -1;
    }

    // person verification
    private void check(Personne p) {
        // person 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);
        }
        // date of birth
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
        // number of children
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
        // name
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
        // first name
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

    // waiting
    private void wait(int N) {
        // we wait for N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
            // display the exception trace
            e.printStackTrace();
            return;
        }
    }
}

Vamos apenas dar uma visão geral deste código. No entanto, vamos dedicar algum tempo às partes mais complexas.

  • Linha 13: o objeto [ArrayList] que irá conter o grupo de pessoas
  • Linha 16: o ID da última pessoa adicionada. Cada vez que uma nova pessoa for adicionada, este ID será incrementado em 1.

A classe [DaoImpl] será instanciada como uma única instância. Isto é conhecido como singleton. Uma aplicação web serve os seus utilizadores simultaneamente. A qualquer momento, existem múltiplas threads a executar-se no servidor web. Estas threads partilham os singletons:

  • o da camada [dao]
  • o da camada [service]
  • os dos vários controladores, validadores de dados, etc., na camada web

Se um singleton tiver campos privados, deve perguntar-se imediatamente por que razão os tem. Estão justificados? Na verdade, serão partilhados entre diferentes threads. Se forem de leitura apenas, isto não é um problema se puderem ser inicializados num momento em que tenha a certeza de que existe apenas uma thread ativa. Geralmente sabemos como identificar esse momento. É quando a aplicação web é iniciada, mas ainda não começou a servir clientes. Se forem de leitura/gravação, então a sincronização de acesso aos campos deve ser implementada; caso contrário, o desastre é inevitável. Iremos ilustrar este problema quando testarmos a camada [dao].

  • A classe [DaoImpl] não tem construtor. Por isso, será utilizado o seu construtor padrão.
  • Linhas 19–38: O método [init] será chamado quando o singleton da camada [dao] for instanciado. Ele cria uma lista de três pessoas.
  • Linhas 41–43: Implementa o método [getAll] da interface [IDao]. Retorna 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 que está a ser pesquisada.

Para a recuperar, chamamos um método privado [getPosition] nas linhas 113–126. Este método devolve a posição na lista da pessoa que está a ser procurada, ou -1 se a pessoa não for encontrada.

Se a pessoa for encontrada, o método [getOne] retorna uma referência (linha 51) a uma cópia dessa pessoa, não à própria pessoa. Na verdade, quando um utilizador pretende editar uma pessoa, as informações sobre essa pessoa são solicitadas à camada [dao] e passadas para a camada [web] para modificação, na forma de uma referência a um objeto [Person]. Esta referência serve como o contentor de entrada no formulário de edição. Quando o utilizador submete as suas alterações na camada web, o conteúdo do contentor de entrada será modificado. Se o contentor for uma referência à pessoa real na [ArrayList] da camada [dao], então essa pessoa é modificada, mesmo que as alterações não tenham sido apresentadas às camadas [service] e [dao]. Esta última é a única camada autorizada a gerir a lista de pessoas. Portanto, a camada web deve trabalhar numa cópia da pessoa a ser modificada. Aqui, a camada [dao] fornece essa cópia.

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

  • linhas 94–104: implementa o método [deleteOne] da interface [IDao]. O seu parâmetro é o ID da pessoa a ser eliminada. Se a pessoa a ser eliminada não existir, é lançada uma [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 [Person]. Se este objeto tiver um id de -1, trata-se de uma nova pessoa a ser adicionada. Caso contrário, modifica a pessoa na lista com esse id utilizando os valores no parâmetro.
    • Linha 60: A validade do parâmetro [Person] é verificada por um método privado [check] definido nas linhas 129–155. Este método realiza verificações básicas dos valores dos vários campos de [Person]. Sempre que uma anomalia é detetada, é lançada uma [DaoException] com um código de erro específico. Uma vez que o método [saveOne] não trata esta exceção, esta será propagada para o método de chamada.
    • Linha 62: Se o parâmetro [Person] tiver um id igual a -1, trata-se de uma adição. O objeto [Person] é 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 [Person] tiver um [id] diferente de -1, isso implica a modificação da pessoa na lista interna com esse [id]. Primeiro, verificamos (linhas 70–75) se a pessoa a ser modificada existe. Se não for esse o caso, lançamos uma [DaoException] com o código de erro 2.
    • Se a pessoa existir, verificamos se a sua versão atual corresponde à do parâmetro [Person], que contém as alterações a aplicar ao original. Se não for esse o caso, significa que o utilizador que tenta modificar a pessoa não possui a versão mais recente. Informamo-lo disso lançando uma [DaoException] com o código de erro 3 (linhas 79–80).
    • Se tudo correr bem, as alterações são feitas no registo original da pessoa (linhas 85–90)

É evidente que este método deve ser sincronizado. Por exemplo, entre o momento em que verificamos se a pessoa a ser modificada está de facto presente e o momento em que a modificação é efetuada, a pessoa poderia ter sido removida da lista por outra pessoa. O método deve, portanto, ser declarado [synchronized] para garantir que apenas um thread o execute de cada vez. O mesmo se aplica aos outros métodos da interface [IDao]. Não fazemos isso, preferindo transferir essa sincronização para a camada [service]. Para destacar as questões de sincronização, durante o teste da camada [dao], vamos pausar a execução de [saveOne] por 10 ms (linha 83) entre o momento em que sabemos que podemos fazer a modificação e o momento em que a fazemos efetivamente. A thread que executa [saveOne] perderá então a CPU para outra thread. Isto aumenta as nossas hipóteses de observar conflitos de acesso na lista de pessoas.

14.5. Testes da Camada [DAO]

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

[TestDao] é o teste JUnit. Para destacar problemas de acesso simultâneo à lista de pessoas, são criadas threads do tipo [ThreadDaoMajEnfants]. Estas são responsáveis por aumentar em 1 o número de filhos de uma determinada pessoa.

[TestDao] tem cinco testes, [test1] a [test5]. Apresentamos aqui apenas dois deles; convidamos os leitores a explorar 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 {

    // layer [dao]
    private DaoImpl dao;

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

    // list of persons
    private void doListe(Collection personnes) {
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }

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

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
...
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
    ...
}
  • linha 9: referência à implementação da camada [dao] que está a ser testada
  • linhas 12–15: o construtor de teste JUnit. 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 {
        // current list
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
        // display
        doListe(personnes);
        // add a person
        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();
        // verification - a crash will occur if the person is not found
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
        // modification
        p1.setNom("Y");
        dao.saveOne(p1);
        // verification - a crash will occur if the person is not found
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
        // delete
        dao.deleteOne(id1);
        // check
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // list of persons
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • Linha 3: Solicitar a lista de pessoas
  • linha 6: exibimos a 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]

O teste adiciona então uma pessoa, modifica-a e elimina-a. Assim, são utilizados os quatro métodos da interface [IDao].

  • Linhas 8–10: É adicionada uma nova pessoa (id=-1).
  • Linha 11: Recuperamos o ID da pessoa adicionada, pois a adição atribuiu-lhe um. Antes disso, ela não tinha nenhum.
  • Linhas 13–14: Solicitamos à camada [dao] uma cópia da pessoa que acabou de ser adicionada. Tenha em mente que, se a pessoa solicitada não for encontrada, a camada [dao] lança uma exceção. Isto causará uma falha na linha 13. Poderíamos ter tratado este caso de forma mais elegante. Na linha 14, verificamos o nome da pessoa recuperada.
  • Linhas 16–17: Modificamos este nome e pedimos à camada [DAO] para guardar as alterações.
  • Linhas 19–20: Solicitamos à camada [DAO] uma cópia da pessoa que acabou de ser adicionada e verificamos o seu novo nome.
  • Linha 22: Elimine a pessoa adicionada no início do teste.
  • Linhas 23–34: Solicitamos uma cópia da pessoa que acabou de ser eliminada à camada [dao]. Deverá receber uma [DaoException] com o código 2.
  • Linhas 36–37: A lista de pessoas é solicitada novamente. Devemos obter a mesma lista que no início do teste.

O método [test4] tem como objetivo destacar problemas com o acesso simultâneo aos métodos da camada [dao]. Lembre-se de que estes métodos não foram sincronizados. O código de teste é o seguinte:

    public void test4() throws Exception {
        // add a person
        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();
        // creation of N threads for updating the number of children
        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();
        }
        // we wait for the end of threads
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // we pick up the person
        p1 = dao.getOne(id1);
        // she must have N children
        assertEquals(N, p1.getNbEnfants());
        // delete person p1
        dao.deleteOne(p1.getId());
        // check
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // we must have a code 2 error
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • linhas 3–6: adicionamos uma pessoa P sem filhos à lista. Registamos o seu [id] (linha 6).
  • linhas 7–13: Iniciamos N threads. Cada um deles incrementará o número de filhos da pessoa P em 1. 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: Recuperamos a pessoa P e verificamos se o seu número de filhos é N.
  • Linhas 22–35: A pessoa P é removida e verificamos se já não existe na lista.

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

  1. o nome dado à thread, usado para a rastrear através de registos
  2. uma referência à camada [dao] para que o thread possa aceder-lhe
  3. o ID da pessoa com quem 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 {
    // thread name
    private String name;
    // reference on the [dao] layer
    private IDao dao;
    // the id of the person we're going to work on
    private int idPersonne;

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

    // thread core
    public void run() {
        // follow-up
        suivi("lancé");
        // we loop until we have succeeded in incrementing by 1
        // person's number of children idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // a copy of the idPersonne person is retrieved
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
            // follow-up
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
            // 10 ms wait to abandon processor
            try {
                // follow-up
                suivi("début attente");
                // we pause to let the processor
                Thread.sleep(10);
                // follow-up
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // waiting complete - try to validate the copy
            // in the meantime, other threads may have modified the original
            int codeErreur = 0;
            try {
                // increments by 1 the number of children in this copy
                personne.setNbEnfants(nbEnfants + 1);
                // we try to modify the original
                dao.saveOne(personne);
                // we passed - the original has been modified
                fini = true;
            } catch (DaoException ex) {
                // we retrieve the error code
                codeErreur = ex.getCode();
                // must be a version 3 error - otherwise it will be re-run
                // the exception
                if (codeErreur != 3) {
                    throw ex;
                } else {
                    // follow-up
                    suivi(ex.getMessage());
                }
                // the original has changed - start all over again
            }
        }
        // follow-up
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // follow-up
    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 de interface [IDao] e não com o tipo de implementação [DaoImpl].
    3. o identificador [id] da pessoa em que o thread irá trabalhar

Quando [test4] inicia uma thread [ThreadDaoMajEnfants] (linha 12 de test4), o seu método [run] (linha 25) é executado:

  • linhas 78–81: o método privado [suivi] permite o registo no ecrã. O método [run] utiliza-o para acompanhar a execução da thread.
  • O thread tenta 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 regista que tem a versão V1. [TH1] é interrompido. [TH2], que a seguia, faz o mesmo e obtém a mesma versão V1 da pessoa P. [TH2] é interrompida. [TH2] retoma o controlo, incrementa o número de filhos da P e guarda as suas alterações. Sabemos que estas alterações estão agora guardadas e que a versão da P mudará para V2. [TH1] terminou o seu trabalho. [TH2] retoma o controlo e faz o mesmo. A sua atualização de P será rejeitada porque mantém uma cópia de P na versão V1, enquanto o P original está agora na versão V2. [TH2] deve então repetir todo o ciclo [ler -> atualizar -> guardar]. É por isso que encontramos o loop nas linhas 32–72. Neste loop, o thread:
  • solicita uma cópia da pessoa P para modificar (linha 34)
  • espera 10 ms (linha 43). Isto é artificial e visa interromper a thread entre a leitura da pessoa P e a sua atualização efetiva na lista de pessoas, a fim de aumentar a probabilidade de conflitos.
  • incrementa o número de filhos de P (linha 54) e guarda P (linha 56). Se a thread não tiver a versão correta de P, será lançada uma exceção pela camada [dao]. Recuperamos então o código da exceção (linha 61) para verificar se é de facto o código 3 (versão incorreta de P). Se não for esse o caso, a exceção é lançada novamente para o método de chamada, em última instância o método de teste [test4]. Se tivermos a exceção de código 3, reiniciamos o ciclo [ler -> atualizar -> guardar]. Se não houver exceção, a atualização foi concluída e o trabalho do thread está terminado.

O que mostram os testes?

Na primeira configuração testada:

  • comentamos a instrução de espera no método [saveOne] de [DaoImpl] (linha 83, secção 14.4).
        // on attend 10 ms
        //wait(10);
  • O método [test4] cria 100 threads (linha 8, secção 14.5).
        // création de N threads de mise à jour du nombre d'enfants
        final int N = 100;

Os seguintes resultados são obtidos:

Image

Todos os cinco testes foram bem-sucedidos.

Na segunda configuração testada:

  • A instrução «wait» no método [saveOne] de [DaoImpl] está descomentada (linha 83, secção 14.4).
        // on attend 10 ms
        wait(10);
  • o método [test4] cria 2 threads (linha 8, secção 14.5).
        // création de N threads de mise à jour du nombre d'enfants
        final int N = 2;

Os seguintes resultados são obtidos:

O teste [test4] falhou. Criámos duas threads, cada uma com a tarefa de incrementar em 1 o número de filhos de uma pessoa P que inicialmente tinha 0. Por isso, esperávamos 2 filhos após a execução das duas threads, mas temos apenas um.

Vamos examinar 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: encontra o [Thread.sleep(10)] no seu método [run] e, por isso, faz uma pausa no momento [1145536368171] (ms)
  • linha 4: a thread #1 assume então o controlo do 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: Encontra o [Thread.sleep(10)] no seu método [run] e, por isso, faz uma pausa
  • Linha 7: A thread 0 recupera a CPU no momento [1145536368187] (ms), ou seja, 16 ms após a ter perdido.
  • linha 8: o mesmo para o thread #1
  • linha 9: o thread #0 atualizou-se e definiu o número de filhos para 1
  • linha 10: o thread #1 fez o mesmo

A questão é: por que razão a 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 pela thread n.º 0?

Primeiro, podemos observar uma anomalia entre as linhas 7 e 8: parece que a thread #0 perdeu a CPU entre estas duas linhas para a thread #1. O que estava a fazer nesse momento? Estava a executar o método [saveOne] da camada [dao]. Este método tem a seguinte estrutura (ver secção 14.4):

    public void saveOne(Personne personne) {
...
        // modification - we're looking for the person
....
        // do we have the right version of the original?
...
        // wait 10 ms
        wait(10);
        // that's it - make the change
    ...
}
  • O thread #0 executou [saveOne] e avançou para a linha 8, onde foi forçado a libertar o processador. Entretanto, leu a versão da pessoa P, que era 1 porque a pessoa P ainda não tinha sido atualizada.
  • Como a CPU ficou livre, a thread #1 assumiu o controlo. Por sua vez, executou [saveOne] e chegou à linha 8, onde foi forçada a libertar a CPU. Entretanto, leu a versão da pessoa P, que era 1 porque a pessoa P ainda não tinha sido atualizada.
  • Como o processador ficou livre, o thread #0 adquiriu-o. A partir da linha 9, realizou a sua atualização e definiu o número de filhos para 1. Em seguida, o método [run] do thread #0 terminou, e o thread exibiu o registo indicando que tinha definido o número de filhos para 1 (linha 9).
  • Como o processador ficou livre, a thread #1 herdou-o. A partir da linha 9, realizou a sua atualização e definiu o número de filhos para 1. Porquê 1? Porque mantém uma cópia de P com o número de filhos definido para 0. Isto é indicado pelo registo (linha 5). Em seguida, o método [run] da thread #1 terminou e a thread apresentou o registo indicando que tinha definido o número de filhos para 1 (linha 10).

De onde vem o problema? Decorre do facto de a thread #0 não ter tido tempo de confirmar a sua alteração e, assim, atualizar a versão da pessoa P antes de a thread #1 tentar ler essa versão para verificar se a pessoa P tinha mudado. Este cenário é improvável, mas não impossível. Tivemos de forçar a thread #0 a perder a CPU para que parecesse haver apenas duas threads. Sem esta solução alternativa, a configuração anterior não tinha conseguido reproduzir este mesmo cenário com 100 threads. O teste [test4] tinha sido bem-sucedido.

Qual é a solução? Sem dúvida, existem várias. Uma delas, que é 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 #1 só poderá executar [saveOne] depois de o thread #0 ter saído do método. Podemos então ter a certeza de que a versão da pessoa P terá sido alterada quando o thread #1 entrar em [saveOne]. A sua atualização será então rejeitada, pois não terá a versão correta de P.

Estes são os quatro métodos da camada [dao] que precisariam de ser sincronizados. No entanto, decidimos manter esta camada tal como descrita e transferir a sincronização para a camada [service]. Existem várias razões para isso:

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

então podemos ter a certeza de que os métodos da camada [DAO] não serão executados por duas threads ao mesmo tempo.

Vamos agora explorar a camada [service].

14.6. A camada [service]

A camada [service] é composta pelas seguintes classes e interfaces:

Image

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

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 {
    // list of all persons
    Collection getAll();
    // find a specific person
    Personne getOne(int id);
    // add/modify a person
    void saveOne(Personne personne);
    // delete a person
    void deleteOne(int id);
}

É idêntico à 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 {

    // the [dao] layer
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

    // list of persons
    public synchronized Collection getAll() {
        return dao.getAll();
    }

    // get a specific person
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

    // add or modify a person
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

    // deleting a person
    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 simplesmente delega a solicitação à camada [dao].
  • linhas 27–29: implementação do método [getOne] da interface [IService]. O método simplesmente delega a solicitação à camada [dao].
  • Linhas 32–34: Implementação do método [saveOne] da interface [IService]. O método simplesmente delega a solicitação à camada [dao].
  • Linhas 37–39: Implementação do método [deleteOne] da interface [IService]. O método simplesmente delega a solicitação à camada [dao].
  • Todos os métodos são sincronizados (utilizando a palavra-chave `synchronized`), garantindo que apenas um thread de cada vez possa utilizar a camada [service] e, consequentemente, a camada [dao].

14.7. Testes para a camada [service]

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

[TestService] é o teste JUnit. Os testes realizados são exatamente os mesmos que os realizados para a camada [dao]. O esqueleto de [TestService] é o seguinte:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

    // service] layer
    private ServiceImpl service;

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

    // list of persons
    private void doListe(Collection personnes) {
...
    }

    // test1
    public void test1() throws ParseException {
        // current list
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
        // display
        doListe(personnes);
        // add a person
        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();
        // verification - a crash will occur if the person is not found
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

    // modification-deletion of a non-existent element
    public void test2() throws ParseException {
...
    }

    // person version management
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
        // add a person
        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();
        // creation of N child update threads
        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();
        }
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • Linhas 9: A camada [service] que está a ser testada é do tipo [ServiceImpl].
  • linhas 11–15: o construtor de teste JUnit cria uma instância da camada [service] a ser testada (linha 12), cria uma instância da camada [dao] (linha 13) e instrui a camada [service] a 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. A única diferença é que acede à camada [service] (linhas 25, 32, 35) em vez de à camada [dao].

O método [test4] tem como objetivo destacar problemas com o 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:

  • abordamos a camada [service] em vez da camada [dao] (linha 55)
  • passamos uma referência à camada [service] para os threads em vez de para a camada [dao] (linha 61)

O tipo [ThreadServiceMajEnfants] é também quase idêntico ao tipo [ThreadDaoMajEnfants], com a exceção de que funciona com a camada [service] em vez de 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 {

    // thread name
    private String name;
    // reference on the [service] layer
    private IService service;
    // the id of the person we're going to work on
    private int idPersonne;

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

    public void run() {
...
    }

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

}
  • Linha 12: O thread funciona com a camada [service]

Estamos a executar os testes com a configuração que causou problemas na camada [dao]:

  • descomentamos a instrução wait no método [saveOne] de [DaoImpl] (linha 83, secção 14.4).
        // on attend 10 ms
        wait(10);
  • O método [test4] cria 100 threads (linha 65, secção 14.7).
        // création de N threads de mise à jour du nombre d'enfants
        final int N = 100;

Os resultados obtidos são os seguintes:

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

14.8. A camada [web]

Vamos rever a arquitetura de três camadas da nossa aplicação:

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

  • lista de pessoas no grupo
  • adicionar uma pessoa ao grupo
  • editar uma pessoa no grupo
  • remover uma pessoa do grupo

Para tal, irá recorrer à camada [service], que, por sua vez, irá invocar a camada [DAO]. Já apresentámos os ecrãs geridos pela camada [web] (secção 14.1). Para descrever a camada web, iremos apresentar o seguinte, por ordem:

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

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

O projeto Eclipse para a aplicação é o seguinte:

Image

  • No pacote [istia.st.mvc.personnes.web], encontrará o controlador [Application].
  • As páginas JSP/JSTL encontram-se em [WEB-INF/views].
  • A pasta [lib] contém as bibliotecas de terceiros necessárias para a aplicação. Estas 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>
    <!--  Mapping ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  welcome files -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Unexpected error page -->
    <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 tratadas pelo servlet [people]
  • 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, urlErrors] que identificam as URLs das páginas JSP para as vistas [list, edit, errors].
  • linhas 32–34: A aplicação tem uma página de entrada predefinida [index.jsp] localizada na raiz da pasta da aplicação web.
  • linhas 36–39: A aplicação tem uma página de erro predefinida que é apresentada quando o servidor web encontra uma exceção não tratada pela aplicação.
    • Linha 37: A tag <exception-type> especifica o tipo de exceção tratada pela diretiva <error-page>; aqui, é o tipo [java.lang.Exception] e os seus subtipos, ou seja, todas as exceções.
    • Linha 38: A tag <location> especifica a página JSP a ser exibida quando ocorre uma exceção do tipo definido por <exception-type>. A exceção que ocorreu está disponível nesta página num objeto chamado exception, se a página tiver a diretiva:

<%@ page isErrorPage="true" %>
  • (continuação)
    • Se <exception-type> especificar um tipo T1 e uma exceção do tipo T2 (não derivada de T1) for propagada até ao servidor web, o servidor envia ao cliente uma página de exceção proprietária, que geralmente não é muito intuitiva. Daí a importância da tag <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, ou seja, 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 o URL [/do/list]. Este URL apresenta a lista de pessoas do grupo.

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


A vista [ list.jsp]


É utilizada 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 [people] associado a uma [ArrayList] de objetos [Person]
  • linhas 22–34: percorremos a lista ${people} para apresentar uma tabela HTML contendo as pessoas do grupo.
  • linha 31: o URL apontado pelo link [Edit] é definido utilizando o campo [id] da pessoa atual, para que o controlador associado ao URL [/do/edit] saiba qual a pessoa a editar.
  • linha 32: o mesmo é feito para o link [Delete].
  • linha 28: Para apresentar a data de nascimento da pessoa no formato DD/MM/AAAA, utilizamos a tag <dt> da biblioteca de tags [DateTime] do projeto Apache [Jakarta Taglibs]:

Image

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

  • Linha 37: O link [Adicionar] para adicionar uma nova pessoa aponta para a URL [/do/edit], tal como o link [Editar] na linha 31. O valor -1 para o parâmetro [id] indica que se trata de uma adição e não de uma edição.

A visualização [ edit.jsp]


É utilizada para apresentar o formulário para adicionar uma nova pessoa ou modificar uma já existente:

O código para a 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 já existente. A partir de agora, para simplificar o texto, utilizaremos o termo único [atualizar]. O botão [Submit] (linha 73) aciona um pedido POST para o URL [/do/validate] (linha 16). Se o POST falhar, a vista [edit.jsp] é exibida novamente com o(s) erro(s) que ocorreu(ram); caso contrário, a vista [list.jsp] é exibida.

  • A vista [edit.jsp], que é exibida tanto numa solicitação GET como numa solicitação POST com falha, recebe os seguintes elementos no seu modelo:
atributo
GET
POST
id
ID da pessoa que está a ser atualizada
o mesmo
versão
a sua versão
igual
nome próprio
nome
Nome introduzido
apelido
o apelido dele/dela
Apelido introduzido
data de nascimento
data de nascimento
data de nascimento introduzida
casado
estado civil
Estado civil introduzido
n.º de filhos
número de filhos
Número de filhos introduzido
erroEditar
vazio
Uma mensagem de erro indicando que a adição ou modificação falhou durante o POST acionado pelo botão [Submeter]. Vazio se não houver erro.
errorFirstName
vazio
indica um nome próprio incorreto – vazio caso contrário
errorName
vazio
indica um nome incorreto – vazio caso contrário
erroDataNascimento
vazio
indica uma data de nascimento incorreta – vazio caso contrário
errorNumberOfChildren
vazio
indica um número incorreto de filhos – vazio caso contrário
  • linhas 11-15: se o envio do formulário falhar, [errorEdit!=''] será devolvido e será exibida uma mensagem de erro.
  • linha 16: o formulário será enviado para a URL [/do/validate]
  • linha 20: o elemento [id] do modelo é exibido
  • linha 24: o elemento [version] do modelo é exibido
  • linhas 26-32: introdução do nome próprio da pessoa:
    • Quando o formulário é exibido inicialmente (GET), ${firstName} exibe o valor atual do campo [firstName] do objeto [Person] atualizado, e ${firstNameError} está vazio.
    • em caso de erro após o POST, o valor introduzido ${firstName} é exibido novamente, juntamente com qualquer mensagem de erro ${firstNameError}
  • 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 utilizando um botão de opção. Utilizamos o valor do campo [married] do objeto [Person] 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 chamado [id] com um valor igual ao campo [id] da pessoa que está a ser atualizada, -1 para uma adição ou outro valor para uma modificação.
  • linha 72: um campo HTML oculto chamado [version] com um valor igual ao campo [id] da pessoa que está a ser atualizada.
  • Linha 73: O botão [Submit] do formulário
  • linha 74: um link para voltar à lista de pessoas. Está rotulado como [Cancelar] porque permite ao utilizador sair do formulário sem o enviar.

A vista [ exception.jsp]


É utilizada para apresentar uma página indicando que ocorreu uma exceção não tratada pela aplicação e que foi propagada para o servidor web.

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

O código para a 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, a página deve ter definido a tag na linha 3.
  • Linha 6: Definimos o código de estado HTTP da resposta como 200. Este é o primeiro cabeçalho HTTP da resposta. O código de estado 200 indica ao cliente que o seu pedido foi bem-sucedido. Normalmente, um documento HTML foi incluído na resposta do servidor. É o que acontece aqui. Se o código de estado HTTP da resposta não for definido como 200, terá o valor 500, o que significa que ocorreu um erro. De facto, quando o servidor web intercepta uma exceção não tratada, considera esta situação anormal e sinaliza-a com um código 500. A resposta a um código HTTP 500 varia consoante o navegador: o Firefox apresenta o documento HTML que pode acompanhar esta resposta, enquanto o IE ignora este documento e apresenta a sua própria página. É por isso que substituímos o código 500 pelo código 200.
  • Linha 16: O texto da exceção é exibido
  • Linha 18: É oferecido ao utilizador um link para regressar à lista de pessoas

A visualização [ -errors .jsp]


É utilizado para apresentar uma página que informa erros de inicialização da aplicação, ou seja, erros detetados durante a execução do método [init] do servlet controlador. Isto pode ser, por exemplo, a ausência de um parâmetro no ficheiro [web.xml], como se pode ver no exemplo abaixo:

Image

O código da página [errors.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 um elemento [errors] no seu modelo, que é uma [ArrayList] de objetos [String]; estes são mensagens de erro. São apresentadas pelo ciclo nas linhas 13–15.

14.8.3. O controlador da aplicação

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

Image


Estrutura e inicialização do controlador


A estrutura básica 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 {
    // instance parameters
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // service
    ServiceImpl service=null;

    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // retrieve servlet initialization parameters
        ServletConfig config = getServletConfig();
        // other initialization parameters are processed
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // parameter value
            valeur = config.getInitParameter(paramètres[i]);
            // present parameter?
            if (valeur == null) {
                // we note the error
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                // parameter value is stored
                params.put(paramètres[i], valeur);
            }
        }
        // the [errors] view url has a special treatment
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
        // dao] layer instantiation
        DaoImpl dao = new DaoImpl();
        dao.init();
        // instantiation of the [service] layer
        service = new ServiceImpl();
        service.setDao(dao);
    }

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

    // display list of persons
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // modify / add a person
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // validation modification / addition of a person
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // display pre-filled form
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

    // post
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        // we hand over to GET
        doGet(request, response);    }
}
  • linhas 20–36: recuperam os parâmetros especificados no ficheiro [web.xml].
  • Linhas 39–41: O parâmetro [urlErrors] deve estar presente, pois especifica a URL da vista [errors], que exibe quaisquer erros de inicialização. Se não existir, a aplicação é encerrada através do lançamento de uma [ServletException] (linha 40). Esta exceção será propagada para o servidor web e tratada pela tag <error-page> no ficheiro [web.xml]. A vista [exception.jsp] é, portanto, exibida:

Image

O link [Voltar à lista] acima está inativo. Clicar nele retorna a mesma resposta, desde que a aplicação não tenha sido modificada 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 uma lista inicial de três pessoas)
  • linha 46: cria uma instância de [ServiceImpl] que implementa a camada [service]
  • linha 47: inicializa a camada [service], fornecendo-lhe uma referência à camada [dao]

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

Url
Método HTTP
Método do controlador
/do/list
GET
doListPeople
/do/edit
GET
doEditPerson
/do/validate
POST
doValidatePerson
/do/delete
GET
executarDeletePessoa

O método [doGet]


O objetivo deste método é 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 {

        // check how the servlet was initialized
        if (erreursInitialisation.size() != 0) {
            // we hand over to the error page
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // end
            return;
        }
        // retrieve the request sending method
        String méthode = request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action = request.getPathInfo();
        // action?
        if (action == null) {
            action = "/list";
        }
        // execution action
        if (méthode.equals("get") && action.equals("/list")) {
            // list of persons
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // deleting a person
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // presentation form add / modify a person
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // validation form add / modify a person
            doValidatePersonne(request, response);
            return;
        }
        // other cases
        doListPersonnes(request, response);
    }
  • linhas 7–13: Verificamos se a lista de erros de inicialização está vazia. Se não estiver, exibimos a vista [errors(errors)], que irá relatar o(s) erro(s).
  • linha 15: Recuperamos o método [get] ou [post] que o cliente utilizou para efetuar o pedido.
  • linha 17: recuperamos o valor do parâmetro [action] da solicitação.
  • Linhas 23–27: Processamos a solicitação [GET /do/list], que solicita a lista de pessoas.
  • Linhas 28–32: Processamos a solicitação [GET /do/delete], que solicita a exclusão de uma pessoa.
  • Linhas 33–37: Processamos a solicitação [GET /do/edit], que solicita o formulário para atualizar uma pessoa.
  • linhas 38–42: processar a 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, tratamo-la como se fosse [GET /do/list].

O método [doListPersonnes]


Este método trata 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
    // display list of persons
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // the [list] view model
        request.setAttribute("personnes", service.getAll());
        // list] view display
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • Linha 5: Solicitamos a lista de pessoas do grupo à camada [service] e guardamo-la no modelo sob a chave "people".
  • Linha 7: A vista [list.jsp] descrita na secção 14.8.2 é apresentada.

O método [doDeletePerson]


Este método trata do pedido [GET /do/delete?id=XX], que solicita a eliminação da pessoa com id=XX. A URL [/do/delete?id=XX] é a dos links [Delete] na 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>

A linha 12 mostra o URL [/do/delete?id=XX] para o link [Eliminar]. O método [doDeletePerson], que trata deste URL, deve eliminar a pessoa com id=XX e, em seguida, apresentar a lista atualizada de pessoas no grupo. O seu código é o seguinte:

    // validation modification / addition of a person
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // we delete the person
        service.deleteOne(id);
        // redirects to the list of persons
        response.sendRedirect("list");
    }
  • Linha 5: A URL que está a ser processada tem o formato [/do/delete?id=XX]. Recuperamos o valor [XX] do parâmetro [id].
  • linha 7: solicitamos à camada [service] que elimine a pessoa com o ID obtido. Não realizamos qualquer validação. Se a pessoa que estamos a tentar eliminar não existir, a camada [dao] lança uma exceção que é propagada até à camada [service]. Também não a tratamos aqui no controlador. Por conseguinte, propagar-se-á até ao servidor web, que, por configuração, exibirá a página [exception.jsp], descrita na secção 14.8.2:

Image

  • Linha 9: Se a eliminação foi bem-sucedida (sem exceção), o cliente é redirecionado para o URL relativo [list]. Uma vez que o URL acabado de processar foi [/do/delete], o URL de redirecionamento será [/do/list]. O navegador irá, portanto, efetuar um pedido [GET /do/list], que irá apresentar a lista de pessoas.

O método [doEditPerson]


Este método trata da solicitação [GET /do/edit?id=XX], que solicita o formulário para atualizar a pessoa com id=XX. A URL [/do/edit?id=XX] é a utilizada para os links [Editar] e [Adicionar] na visualização [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] para o link [Editar] e, na linha 17, o URL [/do/edit?id=-1] para o link [Adicionar]. O método [doEditPersonne] deve apresentar o formulário de edição para a pessoa com id=XX ou, se for uma adição, apresentar um formulário vazio.

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

    // modify / add a person
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's id
        int id = Integer.parseInt(request.getParameter("id"));
        // addition or modification?
        Personne personne = null;
        if (id != -1) {
            // modification - the person to be modified is retrieved
            personne = service.getOne(id);
        } else {
            // add - create an empty person
            personne = new Personne();
            personne.setId(-1);
        }
        // we put the [Person] object in the [edit] view model
        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());
        // view display [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • A solicitação GET tem como destino uma URL no formato [/do/edit?id=XX]. Na linha 5, recuperamos o valor de [id]. Existem então dois casos:
  1. Se id não for igual a -1, trata-se de uma atualização, e precisamos de apresentar um formulário pré-preenchido com as informações da pessoa a ser editada. Na linha 10, esta pessoa é solicitada à camada [service].
  2. Se id for igual a -1, trata-se de uma adição e deve ser apresentado um formulário vazio. Para tal, é criada uma pessoa vazia nas linhas 13–14.
  • O objeto [Person] é colocado no modelo de página [edit.jsp] descrito na Secção 14.8.2. Este modelo inclui os seguintes elementos: [errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, dateOfBirth, errorDateOfBirth, spouse, numberOfChildren, errorNumberOfChildren]. Estes elementos são inicializados nas linhas 17–30, com exceção daqueles cujo valor é uma cadeia vazia [firstNameError, lastNameError, birthDateError, childrenCountError]. Sabemos que, se estiverem em falta no modelo, a biblioteca JSTL apresentará uma cadeia de caracteres vazia como valor. Embora o elemento [errorEdit] também tenha uma cadeia de caracteres vazia como valor, é, no entanto, inicializado porque é realizada uma verificação do seu valor na página [edit.jsp].
  • Assim que o modelo estiver pronto, o controlo é transferido para a página [edit.jsp], linhas 32–33, que irá gerar a vista [edit].

O método [doValidatePersonne]


Este método trata do pedido [POST /do/validate], que valida o formulário de atualização. Este POST é acionado pelo botão [Validate]:

Image

Vamos rever os elementos de entrada do formulário HTML na 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>

O pedido POST contém os parâmetros [firstName, lastName, dateOfBirth, spouse, numberOfChildren, id, version] e é enviado para o URL [/do/validate] (linha 1). É processado pelo seguinte método [doValidatePerson]:

// validation modification / addition of a person
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve posted items
        boolean formulaireErroné = false;
        boolean erreur;
        // first name
        String prenom = request.getParameter("prenom").trim();
        // valid first name?
        if (prenom.length() == 0) {
            // we note the error
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // the name
        String nom = request.getParameter("nom").trim();
        // valid first name?
        if (nom.length() == 0) {
            // we note the error
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // date of birth
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
            // we note the error
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // marital status
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
        // number of children
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // we note the error
            erreur = true;
        }
        // wrong number of children?
        if (erreur) {
            // we report the error
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // pERSON ID
        int id = Integer.parseInt(request.getParameter("id"));
        // version
        long version = Long.parseLong(request.getParameter("version"));
        // is the form incorrect?
        if (formulaireErroné) {
            // redisplay the form with error messages
            showFormulaire(request, response, "");
            // finish
            return;
        }
        // the form is correct - the person is registered
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
            // registration
            service.saveOne(personne);
        } catch (DaoException ex) {
            // redisplay the form with the error message
            showFormulaire(request, response, ex.getMessage());
            // finish
            return;
        }
        // redirects to the list of persons
        response.sendRedirect("list");
    }

    // display pre-filled form
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
        // prepare the view model [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());
        // view display [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • linhas 8-14: o parâmetro [firstName] da solicitação POST é recuperado e a sua validade é verificada. Se estiver incorreto, o elemento [firstNameError] é inicializado com uma mensagem de erro e colocado nos atributos da solicitação.
  • linhas 16–22: o mesmo processo é seguido para o parâmetro [lastName]
  • linhas 24–32: o mesmo processo é aplicado ao parâmetro [dateOfBirth]
  • Linha 34: O parâmetro [spouse] é recuperado. Não verificamos a sua validade porque, em princípio, provém do valor de um botão de opção. Dito isto, nada impede que um programa faça uma solicitação [POST /people-01/do/validate] acompanhada de um parâmetro [spouse] fictício. Devemos, portanto, testar a validade deste parâmetro. Aqui, contamos com o nosso tratamento de exceções, que faz com que a página [exception.jsp] seja exibida se o controlador não tratar a exceção por si próprio. Assim, se a conversão do parâmetro [marie] para um booleano falhar na linha 34, será lançada uma exceção, resultando no envio da página [exception.jsp] para o cliente. Este comportamento funciona para nós.
  • Linhas 34–54: Recuperamos o parâmetro [nbEnfants] e verificamos o seu valor.
  • Linha 56: Recuperamos o parâmetro [id] sem verificar o seu valor
  • Linha 58: Fazemos o mesmo para o parâmetro [version]
  • Linhas 60–65: Se o formulário for inválido, ele é exibido novamente com as mensagens de erro geradas anteriormente
  • Linhas 67–69: Se for válido, criamos um novo objeto [Person] utilizando os campos do formulário
  • Linhas 70–78: a pessoa é guardada. A operação de gravação pode falhar. Num ambiente multiutilizador, a pessoa a ser modificada pode ter sido eliminada ou já ter sido modificada por outra pessoa. Neste caso, a camada [dao] irá lançar uma exceção, que tratamos aqui.
  • Linha 80: Se não ocorreu nenhuma exceção, o cliente é redirecionado para o URL [/do/list] para exibir o novo estado do grupo.
  • Linha 75: Se ocorreu uma exceção durante o salvamento, solicitamos que o formulário inicial seja exibido novamente, 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] utilizando os valores introduzidos (request.getParameter(" ... ")). Recorde-se que as mensagens de erro já foram adicionadas ao modelo pelo método [doValidatePersonne]. A página [edit.jsp] é apresentada nas linhas 99–100.

14.9. Teste da aplicação Web

Foram apresentados vários testes na Secção 14.1. Convidamos o leitor a executá-los novamente. Aqui mostramos capturas de ecrã adicionais que ilustram casos de conflitos de acesso a dados num ambiente multiutilizador:

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

Image

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

Image

O utilizador U1 começa a editar o registo de [Lemarchand]:

Image

O utilizador U2 faz o mesmo:

Image

O utilizador U1 faz alterações e guarda:

O utilizador U2 faz o mesmo:

O utilizador U2 volta à lista de pessoas que utilizam o link [Cancelar] no formulário:

Image

Encontra a pessoa [Lemarchand] tal como foi modificada por U1. Agora, U2 elimina [Lemarchand]:

O U1 ainda tem a sua própria lista e quer editar [Lemarchand] novamente:

O U1 usa o link [Voltar à lista] para ver o que se passa:

Image

Ele descobre que [Lemarchand] já não consta, de facto, da lista...

14.10. Conclusão

Implementámos a arquitetura MVC no âmbito de uma arquitetura de três camadas [web, lógica de negócio, DAO] utilizando um exemplo básico de gestão de uma lista de pessoas. Isto permitiu-nos aplicar os conceitos apresentados nas secções anteriores. Na versão que analisámos, a lista de pessoas era mantida na memória. Em breve exploraremos versões em que esta lista é armazenada numa tabela de base de dados.

Mas, primeiro, iremos apresentar uma ferramenta chamada Spring IoC, que facilita a integração das diferentes camadas de uma aplicação de n camadas.