Skip to content

14. Aplicación web MVC en una arquitectura de tres capas – Ejemplo 1

14.1. Introducción

Hasta ahora, nos hemos limitado a ejemplos con fines didácticos. Por ello, debían ser sencillos. Ahora presentamos una aplicación básica, pero más completa que todas las presentadas hasta ahora. Tendrá la particularidad de utilizar las tres capas de una arquitectura de tres capas:

Image

Se invita al lector a volver a leer los principios de una aplicación web MVC en una arquitectura de tres capas, si los ha olvidado, en el apartado 4.

La aplicación web que vamos a desarrollar permitirá gestionar un grupo de personas mediante cuatro operaciones:

  • lista de personas del grupo
  • añadir una persona al grupo
  • modificación de una persona del grupo
  • eliminación de una persona del grupo

Estas cuatro operaciones son las básicas que se realizan en una tabla de base de datos. Escribiremos dos versiones de esta aplicación:

  • en la versión 1, la capa [dao] no utilizará ninguna base de datos. Las personas del grupo se almacenarán en un simple objeto [ArrayList] gestionado internamente por la capa [dao]. Esto permitirá al lector probar la aplicación sin las limitaciones de una base de datos.
  • En la versión 2, colocaremos el grupo de personas en una tabla de la base de datos. Demostraremos que esto se hará sin que afecte a la capa web de la versión 1, que permanecerá sin cambios.

Las siguientes capturas de pantalla muestran las páginas que la aplicación intercambia con el usuario.

Image

Image

Image

 

14.2. El proyecto Eclipse

El proyecto de la aplicación se llama [personnes-01]:

Image

Este proyecto abarca las tres capas de la arquitectura de tres capas de la aplicación:

  • la capa [dao] está contenida en el paquete [istia.st.mvc.personnes.dao]
  • la capa [metier] o [service] está incluida en el paquete [istia.st.mvc.personnes.service]
  • La capa [web] o [ui] está incluida en el paquete [istia.st.mvc.personnes.web]
  • El paquete [istia.st.mvc.personnes.entites] contiene los objetos compartidos entre diferentes capas
  • el paquete [istia.st.mvc.personnes.tests] contiene las pruebas JUnit de las capas [dao] y [service]

Vamos a explorar sucesivamente las tres capas [dao], [service] y [web]. Como sería demasiado largo de escribir y quizá demasiado aburrido de leer, es posible que a veces seamos un poco breves en las explicaciones, salvo cuando lo que se presente sea nuevo.

14.3. La representación de una persona

La aplicación gestiona un grupo de personas. Las capturas de pantalla del apartado 14.1 mostraban algunas de las características de una persona. Formalmente, estas se representan mediante una clase [Personne]:

Image

La clase [Personne] es la siguiente:

package istia.st.springmvc.personnes.entites;

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

public class Personne {

    // identificador único de la persona
    private int id;
    // la versión actual
    private long version;
    // el apellido
    private String nom;
    // el nombre
    private String prenom;
    // fecha de nacimiento
    private Date dateNaissance;
    // estado civil
    private boolean marie = false;
    // el número de hijos
    private int nbEnfants;

    // getters - setters
...

    // constructor por defecto
    public Personne() {

    }

    // constructor con inicialización de los campos de la persona
    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);
    }

    // constructor de una persona a partir de otra
    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 + "]";
    }
}
  • Una persona se identifica mediante la siguiente información:
    • id: un número que identifica de forma única a una persona
    • apellido: el apellido de la persona
    • nombre: su nombre
    • dateNaissance: su fecha de nacimiento
    • estado civil: si está casada o soltera
    • nbEnfants: el número de hijos que tiene
  • El atributo [version] es un atributo añadido artificialmente para las necesidades de la aplicación. Desde el punto de vista de los objetos, sin duda habría sido preferible añadir este atributo en una clase derivada de [Personne]. Su necesidad surge al analizar los casos de uso de la aplicación web. Uno de ellos es el siguiente:

En el momento T1, un usuario U1 accede a la edición de una persona P. En ese momento, el número de hijos es 0. Cambia este número a 1, pero antes de que valide su modificación, un usuario U2 accede para modificar la misma persona P. Dado que U1 aún no ha validado su modificación, U2 ve que el número de hijos es 0. U2 cambia el nombre de la persona P a mayúsculas. A continuación, U1 y U2 validan sus modificaciones en ese orden. La modificación de U2 será la que prevalezca: el nombre se escribirá en mayúsculas y el número de hijos se mantendrá en cero, aunque U1 crea haberlo cambiado a 1.

El concepto de versión de persona nos ayuda a resolver este problema. Volvamos al mismo caso de uso:

En el momento T1, un usuario U1 accede a la edición de una persona P. En ese momento, el número de hijos es 0 y la versión es V1. Cambia el número de hijos a 1, pero antes de que valide su modificación, un usuario U2 accede para modificar la misma persona P. Dado que U1 aún no ha validado su modificación, U2 ve que el número de hijos es 0 y que la versión es V1. U2 cambia el nombre de la persona P a mayúsculas. A continuación, U1 y U2 validan sus modificaciones en ese orden. Antes de validar una modificación, se comprueba que quien modifica a una persona P tenga la misma versión que la persona P registrada actualmente. Este será el caso del usuario U1. Por lo tanto, se acepta su modificación y se cambia la versión de la persona modificada de V1 a V2 para indicar que la persona ha sufrido un cambio. Al validar la modificación de U2, nos daremos cuenta de que tiene una versión V1 de la persona P, mientras que actualmente la versión de esta es V2. Entonces podremos indicar al usuario U2 que alguien le ha adelantado y que debe volver a partir de la nueva versión de la persona P. Lo hará, recuperará una persona P de la versión V2 que ahora tiene un hijo, escribirá el nombre en mayúsculas y validará. Su modificación se aceptará si la persona P registrada sigue teniendo la versión V2. Al final, se tendrán en cuenta las modificaciones realizadas por U1 y U2, mientras que en el caso de uso sin versión, una de las modificaciones se perdía.

  • Líneas 32-40: un constructor capaz de inicializar los campos de una persona. Se omite el campo [version].
  • líneas 43-51: un constructor que crea una copia de la persona que se le pasa como parámetro. De este modo, se obtienen dos objetos con el mismo contenido, pero a los que hacen referencia dos punteros diferentes.
  • línea 55: se redefine el método [toString] para devolver una cadena de caracteres que representa el estado de la persona

14.4. La capa [dao]

La capa [dao] está formada por las siguientes clases e interfaces:

Image

  • [IDao] es la interfaz que presenta la capa [dao]
  • [DaoImpl] es una implementación de esta en la que el grupo de personas está encapsulado en un objeto [ArrayList]
  • [DaoException] es un tipo de excepciones no controladas (unchecked), lanzadas por la capa [dao]

La interfaz [IDao] es la siguiente:

package istia.st.springmvc.personnes.dao;

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

import java.util.Collection;

public interface IDao {
    // lista de todas las personas
    Collection getAll();
    // obtener una persona concreta
    Personne getOne(int id);
    // Añadir/modificar una persona
    void saveOne(Personne personne);
    // eliminar una persona
    void deleteOne(int id);
}
  • La interfaz cuenta con cuatro métodos para las cuatro operaciones que se desean realizar sobre el grupo de personas:
    • getAll: para obtener una colección de personas
    • getOne: para obtener una persona con un id concreto
    • saveOne: para añadir una persona (id=-1) o modificar una persona existente (id <> -1)
    • deleteOne: para eliminar una persona con un id concreto

La capa [dao] puede generar excepciones. Estas serán del tipo [DaoException] :

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

    // código de error
    private int code;

    public int getCode() {
        return code;
    }

// fabricante
    public DaoException(String message,int code) {
        super(message);
        this.code=code;
    }
}
  • línea 3: la clase [DaoException], derivada de [RuntimeException], es un tipo de excepción no controlada: el compilador no nos obliga a:
    • gestionar este tipo de excepciones con un try/catch al llamar a un método que pueda lanzarla
    • incluir el marcador «throws DaoException» en la firma de un método que pueda lanzar la excepción

Esta técnica nos evita tener que declarar los métodos de la interfaz [IDao] con excepciones de un tipo concreto. Cualquier implementación que lance excepciones no controladas será, por tanto, aceptable, lo que aporta flexibilidad a la arquitectura.

  • línea 6: un código de error. La capa [dao] lanzará diversas excepciones que se identificarán mediante distintos códigos de error. Esto permitirá a la capa que decida gestionar la excepción conocer el origen exacto del error y, de este modo, tomar las medidas adecuadas. Hay otras formas de conseguir el mismo resultado. Una de ellas es crear un tipo de excepción para cada tipo de error posible, por ejemplo, NomManquantException, PrenomManquantException, AgeIncorrectException, ...
  • líneas 13-16: el constructor que permitirá crear una excepción identificada por un código de error y un mensaje de error.
  • líneas 8-10: el método que permitirá al código de gestión de una excepción recuperar el código de error.

La clase [DaoImpl] implementa la interfaz [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 {

    // una lista de personas
    private ArrayList personnes = new ArrayList();

    // N.º de la siguiente persona
    private int id = 0;

    // inicializaciones
    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 personas
    public Collection getAll() {
        return personnes;
    }

    // obtener a una persona en concreto
    public Personne getOne(int id) {
        // ¿A quién se busca?
        int i = getPosition(id);
        // ¿Se ha encontrado?
        if (i != -1) {
            return new Personne(((Personne) personnes.get(i)));
        } else {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }

    // añadir o modificar una persona
    public void saveOne(Personne personne) {
        // ¿Es válido el parámetro «persona»?
        check(personne);
        // ¿Añadir o modificar?
        if (personne.getId() == -1) {
            // Añadir
            personne.setId(getNextId());
            personne.setVersion(1);
            personnes.add(personne);
            return;
        }
        // modificación: se busca la persona
        int i = getPosition(personne.getId());
        // ¿Se ha encontrado?
        if (i == -1) {
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] qu'on veut modifier n'existe pas", 2);
        }
        // ¿Tenemos la versión correcta del 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);
        }
        // esperamos 10 ms
        //wait(10);
        // Todo bien, se realiza la modificación
        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());
    }

    // eliminación de una persona
    public void deleteOne(int id) {
        // se busca a la persona
        int i = getPosition(id);
        // ¿La hemos encontrado?
        if (i == -1) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        } else {
            // se elimina a la persona
            personnes.remove(i);
        }
    }

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

    // buscar a una persona
    private int getPosition(int id) {
        int i = 0;
        boolean trouvé = false;
        // recorrer la lista de personas
        while (i < personnes.size() && !trouvé) {
            if (id == ((Personne) personnes.get(i)).getId()) {
                trouvé = true;
            } else {
                i++;
            }
        }
        // ¿Resultado?
        return trouvé ? i : -1;
    }

    // verificación de una persona
    private void check(Personne p) {
        // persona 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);
        }
        // fecha de nacimiento
        if (p.getDateNaissance() == null) {
            throw new DaoException("Date de naissance manquante", 12);
        }
        // número de hijos
        if (p.getNbEnfants() < 0) {
            throw new DaoException("Nombre d'enfants [" + p.getNbEnfants()
                    + "] invalide", 13);
        }
        // apellidos
        if (p.getNom() == null || p.getNom().trim().length() == 0) {
            throw new DaoException("Nom manquant", 14);
        }
        // nombre
        if (p.getPrenom() == null || p.getPrenom().trim().length() == 0) {
            throw new DaoException("Prénom manquant", 15);
        }
    }

    // espera
    private void wait(int N) {
        // se espera N ms
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
            // se muestra el seguimiento de la excepción
            e.printStackTrace();
            return;
        }
    }
}

Solo vamos a ofrecer una visión general de este código. Sin embargo, nos detendremos un poco en las partes más delicadas.

  • línea 13: el objeto [ArrayList], que contendrá el grupo de personas
  • línea 16: el identificador de la última persona añadida. Cada vez que se añada una nueva persona, este identificador se incrementará en 1.

La clase [DaoImpl] se instanciará en un único ejemplar. Es lo que se denomina un singleton. Una aplicación web atiende a sus usuarios de forma simultánea. En un momento dado, hay varios subprocesos ejecutados por el servidor web. Estos comparten los singletons:

  • el de la capa [dao]
  • el de la capa [service]
  • los de los distintos controladores, validadores de datos, etc., de la capa web

Si un singleton tiene campos privados, hay que preguntarse inmediatamente por qué los tiene. ¿Están justificados? De hecho, van a ser compartidos entre diferentes subprocesos. Si son de solo lectura, no supone ningún problema siempre que puedan inicializarse en un momento en el que se tenga la certeza de que solo hay un subproceso activo. Por lo general, sabemos cómo encontrar ese momento. Es el del inicio de la aplicación web, cuando aún no ha comenzado a atender a los clientes. Si son de lectura y escritura, hay que implementar una sincronización del acceso a los campos; de lo contrario, nos abocamos al desastre. Ilustraremos este problema cuando probemos la capa [dao].

  • La clase [DaoImpl] no tiene constructor. Por lo tanto, se utilizará su constructor por defecto.
  • Líneas 19-38: el método [init] se invocará en el momento de la instanciación del singleton de la capa [dao]. Crea una lista de tres personas.
  • líneas 41-43: implementa el método [getAll] de la interfaz [IDao]. Devuelve una referencia a la lista de personas.
  • líneas 46-55: implementa el método [getOne] de la interfaz [IDao]. Su parámetro es el ID de la persona buscada.

Para recuperarla, se invoca un método privado [getPosition] de las líneas 113-126. Este método devuelve la posición en la lista de la persona buscada o -1 si no se ha encontrado.

Si se ha encontrado a la persona, el método [getOne] devuelve una referencia (línea 51) a una copia de dicha persona y no a la persona en sí. De hecho, cuando un usuario quiera modificar una persona, la información sobre ella se solicitará a la capa [dao] y se transmitirá hasta la capa [web] para su modificación, en forma de referencia a un objeto [Personne]. Esta referencia servirá como contenedor de los datos introducidos en el formulario de modificación. Cuando, en la capa web, el usuario envíe sus modificaciones, se modificará el contenido del contenedor de datos. Si el contenedor es una referencia a la persona real del objeto [ArrayList] de la capa [dao], entonces esta se modifica aunque los cambios no se hayan aplicado a las capas [service] y [dao]. Esta última es la única autorizada para gestionar la lista de personas. Por lo tanto, la capa web debe trabajar con una copia de la persona que se va a modificar. En este caso, la capa [dao] proporciona dicha copia.

Si no se encuentra la persona buscada, se lanza una excepción de tipo [DaoException] con el código de error 2 (línea 53).

  • líneas 94-104: implementa el método [deleteOne] de la interfaz [IDao]. Su parámetro es el ID de la persona que se va a eliminar. Si la persona que se va a eliminar no existe, se lanza una excepción de tipo [DaoException] con el código de error 2.
  • líneas 58-91: implementa el método [saveOne] de la interfaz [IDao]. Su parámetro es un objeto [Personne]. Si este objeto tiene un id=-1, se trata de añadir una persona. En caso contrario, se trata de modificar la persona de la lista que tiene ese id con los valores del parámetro.
    • Línea 60: la validez del parámetro [Personne] se comprueba mediante un método privado [check] definido en las líneas 129-155. Este método realiza comprobaciones básicas sobre el valor de los distintos campos de [Personne]. Cada vez que se detecta una anomalía, se lanza un [DaoException] con un código de error específico. Dado que el método [saveOne] no gestiona esta excepción, la remitirá al método que lo ha llamado.
    • Línea 62: si el parámetro [Personne] tiene un ID igual a -1, se trata de una adición. El objeto [Personne] se añade a la lista interna de personas (línea 66), con el primer ID disponible (línea 64) y un número de versión igual a 1 (línea 65).
    • Si el parámetro [Personne] tiene un [id] distinto de -1, se trata de modificar la persona de la lista interna que tiene ese [id]. En primer lugar, se comprueba (líneas 70-75) que la persona que se va a modificar existe. Si no es así, se lanza una excepción de tipo [DaoException] con el código de error 2.
    • Si la persona sí está presente, se comprueba que su versión actual sea la misma que la del parámetro [Personne], que contiene las modificaciones que hay que aplicar al original. Si no es así, significa que quien desea modificar la persona no dispone de la última versión. Se le avisa lanzando una excepción de tipo [DaoException] con el código de error 3 (líneas 79-80).
    • Si todo va bien, las modificaciones se aplican al registro original de la persona (líneas 85-90).

Es evidente que este método debe estar sincronizado. Por ejemplo, entre el momento en que se comprueba que la persona que se va a modificar está realmente ahí y el momento en que se va a realizar la modificación, es posible que otra persona haya eliminado a esa persona de la lista. Por lo tanto, el método debería declararse como [synchronized] para garantizar que solo lo ejecute un hilo a la vez. Lo mismo ocurre con los demás métodos de la interfaz [IDao]. No lo hacemos así, ya que preferimos trasladar esta sincronización a la capa [service]. Para poner de manifiesto los problemas de sincronización, durante las pruebas de la capa [dao] detendremos la ejecución de [saveOne] durante 10 ms (línea 83) entre el momento en que sabemos que podemos realizar la modificación y el momento en que la realizamos realmente. El hilo que ejecuta [saveOne] cederá entonces el procesador a otro. De este modo, aumentamos las posibilidades de que se produzcan conflictos de acceso a la lista de personas.

14.5. Pruebas de la capa [dao]

Se ha escrito una prueba JUnit para la capa [dao]:

[TestDao] es la prueba JUnit. Para poner de manifiesto los problemas de acceso concurrente a la lista de personas, se crean subprocesos de tipo [ThreadDaoMajEnfants]. Su función es incrementar en 1 el número de hijos de una persona determinada.

[TestDao] cuenta con cinco pruebas, desde [test1] hasta [test5]. Solo presentamos dos de ellas; invitamos al lector a descubrir las demás en el código fuente asociado a este artículo.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

    // capa [dao]
    private DaoImpl dao;

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

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

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

    // modificación-eliminación de un elemento inexistente
    public void test2() throws ParseException {
...
    }

    // gestión de versiones de personas
    public void test3() throws ParseException, InterruptedException {
...
    }

    // bloqueo optimista: acceso multihilo
    public void test4() throws Exception {
...
    }

    // pruebas de validez de saveOne
    public void test5() throws ParseException {
    ...
}
  • línea 9: referencia a la implementación de la capa [dao] sometida a prueba
  • líneas 12-15: el constructor de la prueba JUnit. Crea una instancia de tipo [DaoImpl] de la capa [dao] que se va a probar y la inicializa.

El método [test1] prueba los cuatro métodos de la interfaz [IDao] de la siguiente manera:

    public void test1() throws ParseException {
        // lista actual
        Collection personnes = dao.getAll();
        int nbPersonnes = personnes.size();
        // Visualización
        doListe(personnes);
        // Añadir una persona
        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();
        // verificación: se producirá un error si no se encuentra la persona
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getNom());
        // modificación
        p1.setNom("Y");
        dao.saveOne(p1);
        // verificación: se producirá un error si no se encuentra la persona
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getNom());
        // eliminación
        dao.deleteOne(id1);
        // verificación
        int codeErreur = 0;
        boolean erreur = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // debe producirse un error de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
        // lista de personas
        personnes = dao.getAll();
        assertEquals(nbPersonnes, personnes.size());
    }
  • línea 3: se solicita la lista de personas
  • línea 6: se muestra dicha 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]

A continuación, la prueba añade una persona, la modifica y la elimina. De este modo, se utilizan los cuatro métodos de la interfaz [IDao].

  • líneas 8-10: se añade una nueva persona (id=-1).
  • línea 11: se recupera el id de la persona añadida, ya que al añadirla se le ha asignado uno. Antes no lo tenía.
  • Líneas 13-14: se solicita a la capa [dao] una copia de la persona que acaba de añadirse. Hay que recordar que, si no se encuentra la persona solicitada, la capa [dao] lanza una excepción. En ese caso, se producirá un fallo en la línea 13. Se podría haber gestionado este caso de forma más adecuada. En la línea 14, se comprueba el nombre de la persona encontrada.
  • Líneas 16-17: se modifica este nombre y se solicita a la capa [dao] que guarde los cambios.
  • Líneas 19-20: se solicita a la capa [dao] una copia de la persona que se acaba de añadir y se comprueba su nuevo nombre.
  • línea 22: se elimina la persona añadida al inicio de la prueba.
  • líneas 23-34: se solicita a la capa [dao] una copia de la persona que acaba de eliminarse. Debe obtenerse un [DaoException] con código 2.
  • líneas 36-37: se vuelve a solicitar la lista de personas. Debemos obtener la misma que al inicio de la prueba.

El método [test4] pretende poner de manifiesto los problemas de acceso concurrente a los métodos de la capa [dao]. Recordemos que estos no se han sincronizado. El código de la prueba es el siguiente:

    public void test4() throws Exception {
        // Añadir una persona
        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();
        // creación de N subprocesos para actualizar el número de hijos
        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();
        }
        // se espera a que finalicen los subprocesos
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // se recupera la persona
        p1 = dao.getOne(id1);
        // debe tener N hijos
        assertEquals(N, p1.getNbEnfants());
        // eliminación de la persona p1
        dao.deleteOne(p1.getId());
        // verificación
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
        // debe haber un error de código 2
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }
  • líneas 3-6: se añade a la lista una persona P sin hijos. Se anota su [id] (línea 6).
  • líneas 7-13: se inician N subprocesos. Cada uno de ellos incrementará en 1 unidad el número de hijos de la persona P. Al final, la persona P deberá tener N hijos.
  • líneas 15-17: el método [test4], que ha iniciado los N subprocesos, espera a que estos hayan terminado su trabajo antes de comprobar el nuevo número de hijos de la persona P.
  • líneas 18-21: se recupera la persona P y se comprueba que su número de hijos sea N.
  • Líneas 22-35: se elimina a la persona P y, a continuación, se comprueba que ya no existe en la lista.

En la línea 11, vemos que los hilos son de tipo [ThreadDaoMajEnfants]. El constructor de este tipo tiene tres parámetros:

  1. el nombre asignado al hilo, para poder realizar su seguimiento mediante registros
  2. una referencia a la capa [dao] para que el hilo tenga acceso a ella
  3. el ID de la persona en la que debe trabajar el hilo

El tipo [ThreadDaoMajEnfants] es el siguiente:

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 {
    // nombre del hilo
    private String name;
    // referencia en la capa [dao]
    private IDao dao;
    // el ID de la persona en la que vamos a trabajar
    private int idPersonne;

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

    // núcleo del hilo
    public void run() {
        // seguimiento
        suivi("lancé");
        // se repite el bucle hasta que se consiga incrementar en 1
        // el número de hijos de la persona idPersonne
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // se recupera una copia de la persona de idPersonne
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
            // seguimiento
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version "+personne.getVersion());
            // espera de 10 ms para liberar el procesador
            try {
                // seguimiento
                suivi("début attente");
                // se interrumpe para liberar el procesador
                Thread.sleep(10);
                // seguimiento
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // espera finalizada: se intenta validar la copia
            // Mientras tanto, otros subprocesos han podido modificar el original
            int codeErreur = 0;
            try {
                // se incrementa en 1 el número de hijos de esta copia
                personne.setNbEnfants(nbEnfants + 1);
                // Se está intentando modificar el original
                dao.saveOne(personne);
                // Se ha superado: el original ha sido modificado
                fini = true;
            } catch (DaoException ex) {
                // se obtiene el código de error
                codeErreur = ex.getCode();
                // debe ser un error de la versión 3; de lo contrario, se vuelve a ejecutar
                // la excepción
                if (codeErreur != 3) {
                    throw ex;
                } else {
                    // seguimiento
                    suivi(ex.getMessage());
                }
                // el original ha cambiado; se vuelve a empezar desde el principio
            }
        }
        // seguimiento
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // seguimiento
    private void suivi(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • línea 9: [ThreadDaoMajEnfants] es, efectivamente, un hilo
  • líneas 18-22: el constructor que inicializa el hilo con tres datos
    1. el nombre [name] asignado al hilo
    2. una referencia [dao] a la capa [dao]. Cabe señalar que, una vez más, trabajamos con el tipo de la interfaz [IDao] y no con el de la implementación [DaoImpl].
    3. el identificador [id] de la persona en la que debe trabajar el hilo

Cuando [test4] inicia un hilo [ThreadDaoMajEnfants] (línea 12 de test4), se ejecuta el método [run] (línea 25) de este último:

  • líneas 78-81: el método privado [suivi] permite generar registros en pantalla. El método [run] lo utiliza para permitir el seguimiento del hilo durante su ejecución.
  • El hilo intentará incrementar en 1 el número de hijos de la persona P con el identificador [id]. Esta actualización puede requerir varios intentos. Tomemos dos hilos: [TH1] y [TH2]. [TH1] solicita una copia de la persona P a la capa [dao]. La obtiene y comprueba que tiene la versión V1. [TH1] se interrumpe. [TH2], que le seguía, hace lo mismo y obtiene la misma versión V1 de la persona P. [TH2] se interrumpe. [TH2] recupera el control, incrementa el número de hijos de P y guarda sus modificaciones. Sabemos que, en ese momento, estas se guardan y que la versión de P pasará a ser V2. [TH1] ha terminado su trabajo. [TH2] retoma el control y hace lo mismo. Su actualización de P será rechazada porque tiene una copia de P con la versión V1, mientras que el P original tiene ahora la versión V2. [TH2] debe entonces repetir todo el ciclo [lecture -> mise à jour -> sauvegarde]. Por eso encontramos el bucle de las líneas 32-72. En él, el hilo:
  • solicita una copia de la persona P para modificarla (línea 34)
  • espera 10 ms (línea 43). Esto es artificial y tiene como objetivo interrumpir el hilo entre la lectura de la persona P y su actualización efectiva en la lista de personas, con el fin de aumentar la probabilidad de conflictos.
  • incrementa el número de hijos de P (línea 54) y guarda P (línea 56). Si el hilo no tiene la versión correcta de P, la capa [dao] lanzará una excepción. A continuación, se recupera el código de la excepción (línea 61) para comprobar que se trata efectivamente del código 3 (versión incorrecta de P). Si no es así, se vuelve a lanzar la excepción al método que la invocó, que en este caso es el método de prueba [test4]. Si se produce la excepción con código 3, se vuelve a iniciar el ciclo [lecture -> mise à jour -> sauvegarde]. Si no se produce ninguna excepción, significa que la actualización se ha realizado y que el trabajo del hilo ha finalizado.

¿Qué resultados arrojan las pruebas?

En la primera configuración probada:

  • se comenta la instrucción de espera del método [saveOne] de [DaoImpl] (línea 83, apartado 14.4).
        // esperamos 10 ms
        //wait(10);
  • El método [test4] crea 100 subprocesos (línea 8, apartado 14.5).
        // creación de N subprocesos para actualizar el número de hijos
        final int N = 100;

Se obtienen los siguientes resultados:

Image

Las cinco pruebas se han superado con éxito.

En la segunda configuración probada:

  • se descomenta la instrucción de espera en el método [saveOne] de [DaoImpl] (línea 83, apartado 14.4).
        // espera de 10 ms
        wait(10);
  • El método [test4] crea dos subprocesos (línea 8, apartado 14.5).
        // creación de N subprocesos para actualizar el número de hijos
        final int N = 2;

Se obtienen los siguientes resultados:

La prueba [test4] ha fallado. Se crearon dos subprocesos, cada uno encargado de incrementar en 1 el número de hijos de una persona P que, al principio, tenía 0. Por lo tanto, se esperaban 2 hijos tras la ejecución de los dos subprocesos, pero solo hay uno.

Analicemos los registros de pantalla de [test4] para comprender qué ha ocurrido:

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
  • línea 1: el hilo n.º 0 comienza su trabajo
  • línea 2: ha obtenido una copia de la persona P y comprueba que el número de hijos es 0
  • línea 3: se encuentra con el [Thread.sleep(10)] de su método [run] y, por lo tanto, se detiene en el tiempo [1145536368171] (ms)
  • línea 4: el hilo n.º 1 recupera entonces el procesador y comienza su trabajo
  • línea 5: ha recuperado una copia de la persona P y comprueba que tiene 0 hijos
  • línea 6: llega al [Thread.sleep(10)] de su método [run] y, por lo tanto, se detiene
  • línea 7: el hilo n.º 0 recupera el procesador en el momento [1145536368187] (ms), c.a.d, 16 ms después de haberlo perdido.
  • línea 8: lo mismo ocurre con el hilo n.º 1
  • línea 9: el hilo n.º 0 ha realizado su actualización y ha aumentado el número de hijos a 1
  • línea 10: el hilo n.º 1 ha hecho lo mismo

La pregunta es: ¿por qué el hilo n.º 1 pudo realizar su actualización cuando, en teoría, ya no disponía de la versión correcta de la persona P, que acababa de ser actualizada por el hilo n.º 0?

En primer lugar, se observa una anomalía entre las líneas 7 y 8: parece que el hilo n.º 0 perdió el control del procesador entre estas dos líneas en favor del hilo n.º 1. ¿Qué estaba haciendo en ese momento? Estaba ejecutando el método [saveOne] de la capa [dao]. Este tiene la estructura siguiente (véase el apartado 14.4):

    public void saveOne(Personne personne) {
...
        // modificación: se busca a la persona
....
        // ¿Tenemos la versión correcta del original?
...
        // se espera 10 ms
        wait(10);
        // Todo listo: se realiza la modificación
    ...
}
  • El hilo n.º 0 ejecutó [saveOne] y llegó hasta la línea 8, donde se vio obligado a liberar el procesador. Mientras tanto, leyó la versión de la persona P y era 1, ya que la persona P aún no se había actualizado.
  • Al quedar libre el procesador, el hilo n.º 1 lo heredó. A su vez, ejecutó [saveOne] y llegó hasta la línea 8, donde se vio obligado a liberar el procesador. Mientras tanto, leyó la versión de la persona P y era 1 porque la persona P aún no se había actualizado.
  • Al quedar libre el procesador, el hilo n.º 0 lo heredó. A partir de la línea 9, realizó su actualización y cambió el número de hijos a 1. A continuación, el método [run] del hilo n.º 0 finalizó y el hilo mostró el registro que indicaba que había cambiado el número de hijos a 1 (línea 9).
  • Al quedar libre el procesador, el hilo n.º 1 lo heredó. A partir de la línea 9, realizó su actualización y cambió el número de hijos a 1. ¿Por qué 1? Porque tiene una copia de P con un número de hijos igual a 0. Así lo indica el registro (línea 5). A continuación, el método [run] del hilo n.º 1 finalizó y el hilo mostró el registro que indicaba que había cambiado el número de hijos a 1 (línea 10).

¿De dónde viene el problema? Se debe a que el hilo n.º 0 no tuvo tiempo de validar su modificación y, por lo tanto, de cambiar la versión de la persona P antes de que el hilo n.º 1 intentara leer esa versión para saber si la persona P había cambiado. Este caso es poco probable, pero no imposible. Ha sido necesario forzar al hilo n.º 0 a perder el control del procesador para que se produjera con tan solo dos hilos. Sin este artificio, la configuración anterior no había logrado reproducir este mismo caso con 100 hilos. La prueba [test4] se había superado con éxito.

¿Cuál es la solución? Sin duda hay varias. Una de ellas, fácil de implementar, consiste en sincronizar el método [saveOne]:


    public synchronized void saveOne(Personne personne)

La palabra clave [synchronized] garantiza que solo un hilo a la vez pueda ejecutar el método. De este modo, el hilo n.º 1 solo podrá ejecutar [saveOne] cuando el hilo n.º 0 haya salido de él. De este modo, se garantiza que la versión de la persona P habrá cambiado cuando el hilo n.º 1 entre en [saveOne]. Su actualización será entonces rechazada, ya que no tendrá la versión correcta de P.

Son los cuatro métodos de la capa [dao] los que habría que sincronizar. Sin embargo, decidimos mantener esta capa tal y como se ha descrito y trasladar la sincronización a la capa [service]. Hay varias razones para ello:

  • partimos de la hipótesis de que el acceso a la capa [dao] siempre se realiza a través de una capa [service]. Así ocurre en nuestra aplicación web.
  • puede ser necesario sincronizar también el acceso a los métodos de la capa [service] por motivos distintos a los que nos llevarían a sincronizar los de la capa [dao]. En este caso, no es necesario sincronizar los métodos de la capa [dao]. Si se garantiza que:
  • todo acceso a la capa [dao] pasa por la capa [service]
  • que solo un hilo a la vez utiliza la capa [service]

entonces tenemos la garantía de que los métodos de la capa [dao] no serán ejecutados por dos subprocesos al mismo tiempo.

Ahora descubrimos la capa [service].

14.6. La capa [service]

La capa [service] está formada por las siguientes clases e interfaces:

Image

  • [IService] es la interfaz que presenta la capa [dao]
  • [ServiceImpl] es una implementación de la anterior

La interfaz [IService] es la siguiente:

package istia.st.springmvc.personnes.service;

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

import java.util.Collection;

public interface IService {
    // lista de todas las personas
    Collection getAll();
    // buscar a una persona concreta
    Personne getOne(int id);
    // añadir/modificar una persona
    void saveOne(Personne personne);
    // eliminar una persona
    void deleteOne(int id);
}

Es idéntica a la interfaz [IDao].

La implementación [ServiceImpl] de la interfaz [IService] es la siguiente:

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 {

    // la capa [dao]
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

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

    // buscar a una persona en concreto
    public synchronized Personne getOne(int id) {
        return dao.getOne(id);
    }

    // Añadir o modificar una persona
    public synchronized void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

    // eliminar una persona
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • líneas 10-19: el atributo [IDao dao] es una referencia a la capa [dao]. Será inicializado por Spring IoC.
  • líneas 22-24: implementación del método [getAll] de la interfaz [IService]. El método se limita a delegar la solicitud a la capa [dao].
  • líneas 27-29: implementación del método [getOne] de la interfaz [IService]. El método se limita a delegar la solicitud a la capa [dao].
  • líneas 32-34: implementación del método [saveOne] de la interfaz [IService]. El método se limita a delegar la solicitud a la capa [dao].
  • Líneas 37-39: implementación del método [deleteOne] de la interfaz [IService]. El método se limita a delegar la solicitud a la capa [dao].
  • Todos los métodos están sincronizados (palabra clave «synchronized»), lo que garantiza que solo un hilo a la vez pueda utilizar la capa [service] y, por lo tanto, la capa [dao].

14.7. Pruebas de la capa [service]

Se escribe una prueba JUnit para la capa [service]:

[TestService] es la prueba JUnit. Las pruebas realizadas son exactamente iguales a las realizadas para la capa [dao]. La estructura de [TestService] es la siguiente:

package istia.st.springmvc.personnes.tests;

...

public class TestService extends TestCase {

    // capa [service]
    private ServiceImpl service;

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

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

    // prueba1
    public void test1() throws ParseException {
        // lista actual
        Collection personnes = service.getAll();
        int nbPersonnes = personnes.size();
        // visualización
        doListe(personnes);
        // añadir una persona
        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();
        // verificación: se producirá un error si no se encuentra la persona
        p1 = service.getOne(id1);
        assertEquals("X", p1.getNom());
...
    }

    // modificación o eliminación de un elemento inexistente
    public void test2() throws ParseException {
...
    }

    // gestión de versiones de personas
    public void test3() throws ParseException, InterruptedException {
...
    }

    // bloqueo optimista: acceso multihilo
    public void test4() throws Exception {
        // adición de una persona
        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();
        // creación de N subprocesos para actualizar el número de hijos
        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();
        }
...
    }

    // pruebas de validez de saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • línea 9: la capa [service] probada es del tipo [ServiceImpl].
  • líneas 11-15: el generador de pruebas JUnit crea una instancia de la capa [service] que se va a probar (línea 12), crea una instancia de la capa [dao] (línea 13) e indica a la capa [service] que debe utilizar esta capa [dao] (línea 14).

El método [test1] comprueba los cuatro métodos de la interfaz [IService] de la misma forma que el método de prueba de la capa [dao] con el mismo nombre. La única diferencia es que se accede a la capa [service] (líneas 25, 32 y 35) en lugar de a la capa [dao].

El método [test4] tiene como objetivo poner de manifiesto los problemas de acceso concurrente a los métodos de la capa [service]. De nuevo, es idéntico al método de prueba [test4] de la capa [dao]. Sin embargo, hay algunos detalles que cambian:

  • se hace referencia a la capa [service] en lugar de a la capa [dao] (línea 55)
  • se pasa a los subprocesos una referencia a la capa [service] en lugar de a la capa [dao] (línea 61)

El tipo [ThreadServiceMajEnfants] también es prácticamente idéntico al tipo [ThreadDaoMajEnfants], con la única diferencia de que trabaja con la capa [service] y no con la capa [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 {

    // nombre del hilo
    private String name;
    // referencia en la capa [service]
    private IService service;
    // ID de la persona en la que se va a trabajar
    private int idPersonne;

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

    public void run() {
...
    }

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

}
  • línea 12: el hilo trabaja con la capa [service]

Realizamos las pruebas con la configuración que ha dado problemas en la capa [dao]:

  • descomentamos la instrucción de espera en el método [saveOne] de [DaoImpl] (línea 83, apartado 14.4).
        // se espera 10 ms
        wait(10);
  • El método [test4] crea 100 subprocesos (línea 65, apartado 14.7).
        // creación de N subprocesos para actualizar el número de hijos
        final int N = 100;

Los resultados obtenidos son los siguientes:

La sincronización de los métodos de la capa [service] fue lo que permitió que la prueba [test4] se realizara con éxito.

14.8. La capa [web]

Recordemos la arquitectura de tres capas de nuestra aplicación:

La capa [web] ofrecerá pantallas al usuario para que pueda gestionar el grupo de personas:

  • lista de personas del grupo
  • añadir una persona al grupo
  • modificación de una persona del grupo
  • eliminación de una persona del grupo

Para ello, se basará en la capa [service], que a su vez recurrirá a la capa [dao]. Ya hemos presentado las pantallas gestionadas por la capa [web] (apartado 14.1). Para describir la capa web, vamos a presentar sucesivamente:

  • su configuración
  • sus vistas
  • su controlador
  • algunas pruebas

14.8.1. Configuración de la aplicación web

El proyecto Eclipse de la aplicación es el siguiente:

Image

  • En el paquete [istia.st.mvc.personnes.web] se encuentra el controlador [Application].
  • Las páginas JSP / JSTL se encuentran en [WEB-INF/vues].
  • La carpeta [lib] contiene los archivos de terceros necesarios para la aplicación. Se pueden ver en la carpeta [Web App Libraries].

[web.xml]


El archivo [web.xml] es el que utiliza el servidor web para cargar la aplicación. Su contenido es el siguiente:


<?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>
    <!--  Asignación de ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  archivos de inicio -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Página de error inesperado -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • líneas 27-30: las URL [/do/*] serán procesadas por el servlet [personnes]
  • líneas 9-12: el servlet [personnes] es una instancia de la clase [Application], una clase que vamos a crear.
  • líneas 13-24: definen tres parámetros [urlList, urlEdit, urlErreurs] que identifican las URL de las páginas JSP de las vistas [list, edit, erreurs].
  • Líneas 32-34: la aplicación tiene una página de inicio por defecto, [index.jsp], que se encuentra en la raíz de la carpeta de la aplicación web.
  • Líneas 36-39: la aplicación tiene una página de error predeterminada que se muestra cuando el servidor web detecta una excepción no gestionada por la aplicación.
    • línea 37: la etiqueta <exception-type> indica el tipo de excepción gestionada por la directiva <error-page>; en este caso, el tipo [java.lang.Exception] y sus derivados, es decir, todas las excepciones.
    • línea 38: la etiqueta <location> indica la página JSP que se mostrará cuando se produzca una excepción del tipo definido por <exception-type>. La excepción que se ha producido está disponible en esta página en un objeto denominado «exception» si la página contiene la directiva:

<%@ page isErrorPage="true" %>
  • (continuación)
    • si <exception-type> especifica un tipo T1 y una excepción de tipo T2 no derivada de T1 se remite al servidor web, este envía al cliente una página de excepción propia que, por lo general, resulta poco intuitiva. De ahí la utilidad de la etiqueta <error-page> en el archivo [web.xml].

[index.jsp]


Esta página se muestra si un usuario solicita directamente el contexto de la aplicación sin especificar una URL, c.a.d. Aquí, [/personnes-01]. Su contenido es el siguiente:


<%@ 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] redirige al cliente a la URL [/do/list]. Esta URL muestra la lista de personas del grupo.

14.8.2. Las páginas JSP / JSTL de la aplicación


La vista [list.jsp]


Sirve para mostrar la lista de personas:

Image

Su código es el siguiente:


<%@ 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 recibe un elemento en su modelo:
  • el elemento [personnes] asociado a un objeto de tipo [ArrayList] de objetos de tipo [Personne]
  • líneas 22-34: se recorre la lista ${personas} para mostrar una tabla HTML que contiene a las personas del grupo.
  • línea 31: la URL a la que apunta el enlace [Modifier] se configura mediante el campo [id] de la persona actual, para que el controlador asociado a la URL [/do/edit] sepa qué persona debe modificarse.
  • línea 32: se hace lo mismo con el enlace [Supprimer].
  • línea 28: para mostrar la fecha de nacimiento de la persona en el formato JJ/MM/AAAA, se utiliza la etiqueta <dt> de la biblioteca de etiquetas [DateTime] del proyecto Apache [Jakarta Taglibs]:

Image

El archivo de descripción de esta biblioteca de etiquetas se define en la línea 3.

  • Línea 37: el enlace [Ajout] para añadir una nueva persona tiene como destino la URL [/do/edit], al igual que el enlace [Modifier] de la línea 31. Es el valor -1 del parámetro [id] el que indica que se trata de una adición y no de una modificación.

La vista [edit.jsp]


Sirve para mostrar el formulario de alta de una nueva persona o de modificación de una persona ya existente:

El código de la vista [edit.jsp] es el siguiente:


<%@ 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 presenta un formulario para añadir una nueva persona o actualizar una ya existente. A partir de ahora, y para simplificar la redacción, utilizaremos únicamente el término [mise à jour]. El botón [Valider] (línea 73) activa el POST del formulario en la URL [/do/validate] (línea 16). Si el POST falla, se vuelve a mostrar la vista [edit.jsp] con el error o los errores que se hayan producido; de lo contrario, se muestra la vista [list.jsp].

  • La vista [edit.jsp], que se muestra tanto en un GET como en un POST que falla, recibe los siguientes elementos en su plantilla:
atributo
GET
POST
id
identificador de la persona actualizada
ídem
version
su versión
ídem
prenom
su nombre
nombre introducido
nom
su apellido
apellido introducido
dateNaissance
su fecha de nacimiento
fecha de nacimiento introducida
marie
su estado civil
estado civil introducido
nbEnfants
Número de hijos
Número de hijos introducido
erreurEdit
vacío
Un mensaje de error que indica que se ha producido un error al añadir o modificar datos al pulsar el botón [Envoyer]. Vacío si no hay error.
erreurPrenom
vacío
indica un nombre de pila incorrecto; en caso contrario, está vacío
erreurNom
vacío
indica un apellido erróneo; si no, está vacío
erreurDateNaissance
vacío
indica una fecha de nacimiento errónea; si no, se deja en blanco
erreurNbEnfants
vacío
indica un número de hijos erróneo; si no, se deja en blanco
  • líneas 11-15: si el POST del formulario falla, aparecerá [erreurEdit!=''] y se mostrará un mensaje de error.
  • línea 16: el formulario se enviará a la URL [/do/validate]
  • línea 20: se muestra el elemento [id] de la plantilla
  • línea 24: se muestra el elemento [version] de la plantilla
  • líneas 26-32: introducción del nombre de la persona:
    • al mostrar el formulario por primera vez (GET), ${nombre} muestra el valor actual del campo [prenom] del objeto [Personne] actualizado y ${erreurPrenom} está vacío.
    • En caso de error tras el campo POST, se vuelve a mostrar el valor introducido ${prenom}, así como el posible mensaje de error ${erreurPrenom}
  • líneas 33-39: introducción del apellido de la persona
  • líneas 40-46: introducción de la fecha de nacimiento de la persona
  • líneas 47-61: introducción del estado civil de la persona mediante un botón de opción. Se utiliza el valor del campo [marie] del objeto [Personne] para determinar cuál de los dos botones de opción debe marcarse.
  • líneas 62-68: introducción del número de hijos de la persona
  • línea 71: un campo oculto HTML denominado [id] y cuyo valor es el campo [id] de la persona que se está actualizando; -1 para una alta, cualquier otro valor para una modificación.
  • línea 72: un campo oculto HTML denominado [version] y cuyo valor es el campo [id] de la persona que se está actualizando.
  • línea 73: el botón [Valider], de tipo [Submit], del formulario
  • línea 74: un enlace que permite volver a la lista de personas. Se ha denominado [Annuler] porque permite salir del formulario sin validarlo.

La vista [exception.jsp]


Sirve para mostrar una página que indica que se ha producido una excepción no gestionada por la aplicación y que se ha transmitido al servidor web.

Por ejemplo, eliminemos una persona que no existe en el grupo:

El código de la vista [exception.jsp] es el siguiente:


<%@ 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 recibe una clave en su modelo, el elemento [exception], que es la excepción que ha sido interceptada por el servidor web. Para que el servidor web incluya este elemento en el modelo de la página JSP, es necesario que la página tenga definida la etiqueta de la línea 3.
  • línea 6: se establece en 200 el código de estado HTTP de la respuesta. Se trata del primer encabezado HTTP de la respuesta. El código 200 indica al cliente que su solicitud ha sido atendida. Por lo general, se ha incluido un documento HTML en la respuesta del servidor. Este es el caso aquí. Si no se establece en 200 el código de estado HTTP de la respuesta, tendrá aquí el valor 500, lo que significa que se ha producido un error. De hecho, al haber interceptado el servidor web una excepción no gestionada, considera que esta situación es anómala y la señala mediante el código 500. La reacción ante el código HTTP 500 varía según el navegador: Firefox muestra el documento HTML que puede acompañar a esta respuesta, mientras que IE ignora dicho documento y muestra su propia página. Por este motivo, hemos sustituido el código 500 por el código 200.
  • línea 16: se muestra el texto de la excepción
  • línea 18: se ofrece al usuario un enlace para volver a la lista de personas

La vista [erreurs.jsp]


Sirve para mostrar una página que indica los errores de inicialización de la aplicación, c.a.d, y los errores detectados durante la ejecución del método [init] del servlet del controlador. Puede tratarse, por ejemplo, de la ausencia de un parámetro en el archivo [web.xml], tal y como se muestra en el ejemplo siguiente:

Image

El código de la página [erreurs.jsp] es el siguiente:


<%@ 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>

La página recibe en su plantilla un elemento [erreurs], que es un objeto de tipo [ArrayList] de objetos [String]; estos últimos son mensajes de error. Se muestran mediante el bucle de las líneas 13-15.

14.8.3. El controlador de la aplicación

El controlador [Application] se define en el paquete [istia.st.mvc.personnes.web]:

Image


Estruct tura e inicialización del controlador


El esqueleto del controlador [Application] es el siguiente:

package istia.st.mvc.personnes.web;

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

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

    // servicio
    ServiceImpl service=null;

    // inicialización
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // se recuperan los parámetros de inicialización del servlet
        ServletConfig config = getServletConfig();
        // se procesan los demás parámetros de inicialización
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // valor del parámetro
            valeur = config.getInitParameter(paramètres[i]);
            // ¿Existe el parámetro?
            if (valeur == null) {
                // se registra el error
                erreursInitialisation.add("Le paramètre [" + paramètres[i]
                        + "] n'a pas été initialisé");
            } else {
                // se almacena el valor del parámetro
                params.put(paramètres[i], valeur);
            }
        }
        // la URL de la vista [erreurs] tiene un tratamiento especial
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException(
                    "Le paramètre [urlErreurs] n'a pas été initialisé");
        // instanciación de la capa [dao]
        DaoImpl dao = new DaoImpl();
        dao.init();
        // instanciación de la capa [service]
        service = new ServiceImpl();
        service.setDao(dao);
    }

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

    // visualización de la lista de personas
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // modificación/adición de una persona
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // confirmación de la modificación o alta de una persona
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // Validación de la modificación o alta de una persona
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

    // Visualización del formulario prellenado
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit) throws ServletException, IOException{
...
    }

    // envío
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        // se pasa el control a GET
        doGet(request, response);    }
}
  • líneas 20-36: se recuperan los parámetros esperados del archivo [web.xml].
  • líneas 39-41: el parámetro [urlErreurs] debe estar presente obligatoriamente, ya que indica la URL de la vista [erreurs], capaz de mostrar los posibles errores de inicialización. Si no existe, se interrumpe la aplicación ejecutando una [ServletException] (línea 40). Esta excepción se transmitirá al servidor web y será gestionada por la etiqueta <error-page> del archivo [web.xml]. Por lo tanto, se muestra la vista [exception.jsp]:

Image

El enlace [Retour à la liste] anterior no funciona. Al utilizarlo se obtiene la misma respuesta mientras la aplicación no se haya modificado y vuelto a cargar. Resulta útil para otros tipos de excepciones, como ya hemos visto.

  • línea 43: crea una instancia [DaoImpl] que implementa la capa [dao]
  • línea 44: inicializa esta instancia (creación de una lista inicial de tres personas)
  • línea 46: crea una instancia [ServiceImpl] que implementa la capa [service]
  • línea 47: inicializa la capa [service] proporcionándole una referencia a la capa [dao]

Tras la inicialización del controlador, sus métodos disponen de una referencia [service] a la capa [service] (línea 15), que utilizarán para ejecutar las acciones solicitadas por el usuario. Estas serán interceptadas por el método [doGet], que las remitirá a un método específico del controlador:

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

El método [doGet]


El objetivo de este método es dirigir el procesamiento de las acciones solicitadas por el usuario hacia el método adecuado. Su código es el siguiente:

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

        // se comprueba cómo ha ido la inicialización del servlet
        if (erreursInitialisation.size() != 0) {
            // se pasa el control a la página de errores
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // fin
            return;
        }
        // se recupera el método de envío de la solicitud
        String méthode = request.getMethod().toLowerCase();
        // se recupera la acción que se debe ejecutar
        String action = request.getPathInfo();
        // ¿acción?
        if (action == null) {
            action = "/list";
        }
        // ejecución de la acción
        if (méthode.equals("get") && action.equals("/list")) {
            // lista de personas
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // eliminación de una persona
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // visualización del formulario de alta/modificación de una persona
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // validación del formulario de alta/modificación de una persona
            doValidatePersonne(request, response);
            return;
        }
        // otros casos
        doListPersonnes(request, response);
    }
  • líneas 7-13: se comprueba que la lista de errores de inicialización esté vacía. Si no es así, se muestra la vista [erreurs(erreurs)], que indicará el error o los errores.
  • línea 15: se recupera el método [get] o [post] que el cliente ha utilizado para realizar su solicitud.
  • línea 17: se recupera el valor del parámetro [action] de la consulta.
  • líneas 23-27: procesamiento de la solicitud [GET /do/list], que solicita la lista de personas.
  • líneas 28-32: procesamiento de la solicitud [GET /do/delete], que solicita la eliminación de una persona.
  • líneas 33-37: procesamiento de la consulta [GET /do/edit], que solicita el formulario de actualización de una persona.
  • líneas 38-42: procesamiento de la solicitud [POST /do/validate], que solicita la validación de la persona actualizada.
  • línea 44: si la acción solicitada no es ninguna de las cinco anteriores, se procede como si se tratara de [GET /do/list].

El método [doListPersonnes]


Este método procesa la solicitud [GET /do/list], que solicita la lista de personas:

Image

Su código es el siguiente:

1
2
3
4
5
6
7
8
9
    // Visualización de la lista de personas
    private void doListPersonnes(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // la plantilla de la vista [list]
        request.setAttribute("personnes", service.getAll());
        // Visualización de la vista [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • línea 5: se solicita a la capa [service] la lista de personas del grupo y se incluye esta en la plantilla bajo la clave «personas».
  • línea 7: se muestra la vista [list.jsp] descrita en el apartado 14.8.2.

El método [doDeletePersonne]


Este método procesa la consulta [GET /do/delete?id=XX], que solicita la eliminación de la persona con id=XX. La URL [/do/delete?id=XX] es la de los enlaces [Supprimer] de la vista [list.jsp]:

Image

cuyo código es el siguiente:

...
<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>

En la línea 12, se ve la URL [/do/delete?id=XX] del enlace [Supprimer]. El método [doDeletePersonne], que debe procesar esta URL, debe eliminar a la persona con id=XX y, a continuación, mostrar la nueva lista de personas del grupo. Su código es el siguiente:

    // Validación de la modificación o alta de una persona
    private void doDeletePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // se recupera el ID de la persona
        int id = Integer.parseInt(request.getParameter("id"));
        // se elimina la persona
        service.deleteOne(id);
        // redirección a la lista de personas
        response.sendRedirect("list");
    }
  • línea 5: la URL procesada tiene el formato [/do/delete?id=XX]. Se recupera el valor [XX] del parámetro [id].
  • línea 7: se solicita a la capa [service] la eliminación de la persona con el ID obtenido. No realizamos ninguna comprobación. Si la persona que se pretende eliminar no existe, la capa [dao] lanza una excepción que la capa [service] deja pasar. Tampoco la gestionamos aquí, en el controlador. Por lo tanto, se propagará hasta el servidor web, que, según la configuración, mostrará la página [exception.jsp], descrita en el apartado 14.8.2:

Image

  • línea 9: si se ha producido la eliminación (sin excepciones), se solicita al cliente que se redirija a la URL relativa [list]. Dado que la que se acaba de procesar es [/do/delete], la URL de redirección será [/do/list]. Por lo tanto, el navegador se verá obligado a realizar una [GET /do/list], lo que provocará que se muestre la lista de personas.

El método [doEditPersonne]


Este método gestiona la solicitud [GET /do/edit?id=XX], que solicita el formulario de actualización de la persona con id=XX. La URL [/do/edit?id=XX] es la de los enlaces [Modifier] y la del enlace [Ajout] de la vista [list.jsp]:

Image

cuyo código es el siguiente:

...
<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>

En la línea 11, se ve la URL [/do/edit?id=XX] del enlace [Modifier] y, en la línea 17, la URL [/do/edit?id=-1] del enlace [Ajout]. El método [doEditPersonne] debe mostrar el formulario de edición de la persona con id=XX o, si se trata de una nueva entrada, mostrar un formulario vacío.

El código del método [doEditPersonne] es el siguiente:

    // modificación o alta de una persona
    private void doEditPersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // se recupera el ID de la persona
        int id = Integer.parseInt(request.getParameter("id"));
        // ¿Añadir o modificar?
        Personne personne = null;
        if (id != -1) {
            // modificación: se recupera la persona que se va a modificar
            personne = service.getOne(id);
        } else {
            // adición: se crea una persona vacía
            personne = new Personne();
            personne.setId(-1);
        }
        // se inserta el objeto [Personne] en la plantilla de la 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());
        // visualización de la vista [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • el GET tiene como destino una URL del tipo [/do/edit?id=XX]. En la línea 5, recuperamos el valor de [id]. A continuación, hay dos casos:
  1. si id es distinto de -1, se trata de una modificación y hay que mostrar un formulario prellenado con los datos de la persona que se va a modificar. En la línea 10, se solicita esta persona a la capa [service].
  2. Si id es igual a -1, se trata de una alta y hay que mostrar un formulario vacío. Para ello, se crea una persona vacía en las líneas 13-14.
  • El objeto [Personne] obtenido se coloca en la plantilla de la página [edit.jsp] descrita en el apartado 14.8.2. Esta plantilla incluye los siguientes elementos: [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, dateNaissance, erreurDateNaissance, marie, nbEnfants, erreurNbEnfants]. Estos elementos se inicializan en las líneas 17-30, a excepción de aquellos cuyo valor es la cadena vacía [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants]. Se sabe que, en caso de que no estén presentes en la plantilla, la biblioteca JSTL mostrará una cadena vacía como su valor. Aunque el elemento [erreurEdit] también tiene como valor una cadena vacía, se inicializa de todos modos porque se realiza una comprobación de su valor en la página [edit.jsp].
  • Una vez que el modelo está listo, el control pasa a la página [edit.jsp], líneas 32-33, que generará la vista [edit].

El método [doValidatePersonne]


Este método procesa la solicitud [POST /do/validate] que valida el formulario de actualización. Esta solicitud POST se activa mediante el botón [Valider]:

Image

Recordemos los campos de entrada del formulario HTML de la vista anterior:

<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>

La consulta POST contiene los parámetros [prenom, nom, dateNaissance, marie, nbEnfants, id, version] y se envía a la URL [/do/validate] (línea 1). Se procesa mediante el siguiente método [doValidatePersonne]:

// Validación de la modificación o la incorporación de una persona
    public void doValidatePersonne(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // se recuperan los elementos enviados
        boolean formulaireErroné = false;
        boolean erreur;
        // el nombre
        String prenom = request.getParameter("prenom").trim();
        // ¿Es válido el nombre?
        if (prenom.length() == 0) {
            // se anota el error
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // el apellido
        String nom = request.getParameter("nom").trim();
        // ¿Es válido el nombre?
        if (nom.length() == 0) {
            // se anota el error
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // la fecha de nacimiento
        Date dateNaissance = null;
        try {
            dateNaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateNaissance").trim());
        } catch (ParseException e) {
            // se señala el error
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // estado civil
        boolean marie = Boolean.parseBoolean(request.getParameter("marie"));
        // Número de hijos
        int nbEnfants = 0;
        erreur = false;
        try {
            nbEnfants = Integer.parseInt(request.getParameter("nbEnfants")
                    .trim());
            if (nbEnfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // se señala el error
            erreur = true;
        }
        // ¿Número de hijos incorrecto?
        if (erreur) {
            // se señala el error
            request.setAttribute("erreurNbEnfants",
                    "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // identificación de la persona
        int id = Integer.parseInt(request.getParameter("id"));
        // versión
        long version = Long.parseLong(request.getParameter("version"));
        // ¿Hay algún error en el formulario?
        if (formulaireErroné) {
            // Se vuelve a mostrar el formulario con los mensajes de error
            showFormulaire(request, response, "");
            // fin
            return;
        }
        // El formulario es correcto: se registra a la persona
        Personne personne = new Personne(id, prenom, nom, dateNaissance, marie,
                nbEnfants);
        personne.setVersion(version);
        try {
            // registro
            service.saveOne(personne);
        } catch (DaoException ex) {
            // Se vuelve a mostrar el formulario con el mensaje del error producido
            showFormulaire(request, response, ex.getMessage());
            // fin
            return;
        }
        // se redirige a la lista de personas
        response.sendRedirect("list");
    }

    // se muestra el formulario ya rellenado
    private void showFormulaire(HttpServletRequest request,
            HttpServletResponse response, String erreurEdit)
            throws ServletException, IOException {
        // se prepara la plantilla de la 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());
        // Visualización de la vista [edit]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • líneas 8-14: se recupera el parámetro [prenom] de la solicitud POST y se comprueba su validez. Si resulta incorrecto, el elemento [erreurPrenom] se inicializa con un mensaje de error y se incluye en los atributos de la consulta.
  • líneas 16-22: se procede de forma similar con el parámetro [nom]
  • líneas 24-32: se procede de forma similar con el parámetro [dateNaissance]
  • línea 34: se recupera el parámetro [marie]. No se comprueba su validez porque, a priori, procede del valor de un botón de opción. Dicho esto, nada impide que un programa genere un [POST /personnes-01/do/validate] acompañado de un parámetro [marie] inventado. Por lo tanto, deberíamos comprobar la validez de este parámetro. En este caso, nos basamos en nuestra gestión de excepciones, que provoca que se muestre la página [exception.jsp] si el controlador no las gestiona por sí mismo. Así pues, si la conversión del parámetro [marie] a valor booleano falla en la línea 34, se generará una excepción que dará lugar al envío de la página [exception.jsp] al cliente. Este funcionamiento nos conviene.
  • líneas 34-54: se recupera el parámetro [nbEnfants] y se comprueba su valor.
  • línea 56: se recupera el parámetro [id] sin comprobar su valor
  • línea 58: se hace lo mismo con el parámetro [version]
  • líneas 60-65: si el formulario contiene errores, se vuelve a mostrar con los mensajes de error generados anteriormente
  • líneas 67-69: si es válido, se crea un nuevo objeto [Personne] con los elementos del formulario
  • líneas 70-78: se guarda el usuario. El proceso de guardado puede fallar. En un entorno multiusuario, es posible que el usuario que se va a modificar haya sido eliminado o que ya lo haya modificado otra persona. En ese caso, la capa [dao] lanzará una excepción que se gestiona aquí.
  • línea 80: si no se ha producido ninguna excepción, se redirige al cliente a la URL [/do/list] para mostrarle el nuevo estado del grupo.
  • línea 75: si se ha producido una excepción al guardar, se vuelve a solicitar que se vuelva a mostrar el formulario inicial, pasándole el mensaje de error de la excepción (tercer parámetro).

El método [showFormulaire] (líneas 84-101) genera la plantilla necesaria para la página [edit.jsp] con los valores introducidos (request.getParameter(" ... ")). Recordemos que los mensajes de error ya han sido insertados en la plantilla mediante el método [doValidatePersonne]. La página [edit.jsp] se muestra en las líneas 99-100.

14.9. Pruebas de la aplicación web

En el apartado 14.1 se presentaron varias pruebas. Invitamos al lector a volver a realizarlas. A continuación mostramos otras capturas de pantalla que ilustran los casos de conflictos de acceso a los datos en un entorno multiusuario:

[Firefox] será el navegador del usuario U1. Este solicita la URL [http://localhost:8080/personnes-01]:

Image

[IE] será el navegador del usuario U2. Este solicita la misma URL:

Image

El usuario U1 accede a la modificación del registro de la persona [Lemarchand]:

Image

El usuario U2 hace lo mismo:

Image

El usuario U1 realiza modificaciones y las valida:

El usuario U2 hace lo mismo:

El usuario U2 vuelve a la lista de personas mediante el enlace [Annuler] del formulario:

Image

Encuentra a la persona [Lemarchand] tal y como la modificó U1. Ahora, U2 elimina a [Lemarchand]:

U1 sigue teniendo su propia lista y quiere modificar [Lemarchand] de nuevo:

U1 utiliza el enlace [Retour à la liste] para ver de qué se trata:

Image

Descubre que, efectivamente, [Lemarchand] ya no forma parte de la lista...

14.10. Conclusión

Hemos implementado la arquitectura MVC en una arquitectura de tres capas [web, metier, dao] con un ejemplo básico de gestión de una lista de personas. Esto nos ha permitido utilizar los conceptos que se habían presentado en las secciones anteriores. En la versión estudiada, la lista de personas se mantenía en memoria. Próximamente estudiaremos versiones en las que esta lista se almacene en una tabla de una base de datos.

Pero antes, vamos a presentar una herramienta llamada Spring IoC, que facilita la integración de las diferentes capas de una aplicación ntier.