Skip to content

14. MVC Web Application in a 3-Tier Architecture – Example 1

14.1. Introduction

Up to this point, we have limited ourselves to examples intended for educational purposes. For that reason, they had to be simple. We now present a basic application that is nonetheless more feature-rich than any of those presented so far. It will be unique in that it uses the three layers of a 3-tier architecture:

Image

Readers are encouraged to review the principles of an MVC web application in a 3-tier architecture in Section 4 if they have forgotten them.

The web application we are going to write will allow us to manage a group of people using four operations:

  • list of people in the group
  • add a person to the group
  • modifying a person in the group
  • removing a person from the group

These are the four basic operations on a database table. We will write two versions of this application:

  • In version 1, the [DAO] layer will not use a database. The group members will be stored in a simple [ArrayList] object managed internally by the [DAO] layer. This will allow the reader to test the application without the constraints of a database.
  • In Version 2, we will place the group of people in a database table. We will demonstrate that this can be done without affecting the web layer of Version 1, which will remain unchanged.

The following screenshots show the pages that the application exchanges with the user.

Image

Image

Image

 

14.2. The Eclipse Project

The application project is named [people-01]:

Image

This project covers the three layers of the application’s 3-tier architecture:

  • the [dao] layer is contained in the package [istia.st.mvc.personnes.dao]
  • the [business] or [service] layer is contained in the package [istia.st.mvc.personnes.service]
  • the [web] or [ui] layer is contained in the package [istia.st.mvc.personnes.web]
  • the package [istia.st.mvc.personnes.entities] contains objects shared between different layers
  • the package [istia.st.mvc.people.tests] contains the JUnit tests for the [DAO] and [service] layers

We will explore the three layers [dao], [service], and [web] in turn. Since it would take too long to write and might be too tedious to read, we may sometimes move through the explanations quickly, except when the material presented is new.

14.3. Representation of a person

The application manages a group of people. The screenshots in Section 14.1 showed some of the characteristics of a person. Formally, these are represented by a [Person] class:

Image

The [Person] class is as follows:

package istia.st.springmvc.people.entities;

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

public class Person {

    // unique identifier for the person
    private int id;
    // current version
    private long version;
    // name
    private String lastName;
    // first name
    private String firstName;
    // date of birth
    private Date birthDate;
    // marital status
    private boolean married = false;
    // number of children
    private int numberOfChildren;

    // getters - setters
...

    // default constructor
    public Person() {

    }

    // constructor with initialization of the person's fields
    public Person(int id, String firstName, String lastName, Date birthDate,
            boolean married, int numberOfChildren) {
        setId(id);
        setLastName(lastName);
        setFirstName(firstName);
        setBirthDate(birthDate);
        setSpouse(spouse);
        setNumberOfChildren(numberOfChildren);
    }

    // Constructor for a person created by copying another person
    public Person(Person p) {
        setId(p.getId());
        setVersion(p.getVersion());
        setLastName(p.getLastName());
        setFirstName(p.getFirstName());
        setDateOfBirth(p.getDateOfBirth());
        setLastName(p.getLastName());
        setNumberOfChildren(p.getNumberOfChildren());
    }


    // toString
    public String toString() {
        return "[" + id + "," + version + "," + firstName + "," + lastName + ","
                + new SimpleDateFormat("dd/MM/yyyy").format(dateOfBirth)
                + "," + married + "," + numberOfChildren + "]";
    }
}
  • A person is identified by the following information:
    • id: a unique identifier for a person
    • lastName: the person's last name
    • firstName: their first name
    • dateOfBirth: their date of birth
    • maritalStatus: whether they are married or not
    • nbChildren: the number of children
  • The [version] attribute is an attribute artificially added for the purposes of the application. From an object-oriented perspective, it would likely have been preferable to add this attribute to a class derived from [Person]. Its necessity becomes apparent when considering use cases for the web application. One such use case is as follows:

At time T1, user U1 begins editing a person P. At this point, the number of children is 0. U1 changes this number to 1, but before validating the change, user U2 begins editing the same person P. Since U1 has not yet validated their change, U2 sees the number of children as 0. U2 changes the name of person P to uppercase. Then U1 and U2 save their changes in that order. U2’s change will take precedence: the name will be in uppercase and the number of children will remain at zero, even though U1 believes they changed it to 1.

The concept of a person’s version helps us solve this problem. Let’s revisit the same use case:

At time T1, a user U1 begins editing a person P. At this point, the number of children is 0 and the version is V1. They change the number of children to 1, but before they commit their edit, a user U2 enters the edit mode for the same person P. Since U1 has not yet committed their edit, U2 sees the number of children as 0 and the version as V1. U2 changes the name of person P to uppercase. Then U1 and U2 commit their edits in that order. Before committing a change, we verify that the user modifying person P has the same version as the currently saved version of person P. This will be the case for user U1. Their change is therefore accepted, and we then change the version of the modified person from V1 to V2 to indicate that the person has undergone a change. When validating U2’s modification, we will notice that they have version V1 of person P, whereas the current version is V2. We can then inform user U2 that someone else acted before them and that they must start with the new version of person P. They will do so, retrieve a version V2 of person P who now has a child, capitalize the name, and validate. Their modification will be accepted if the registered person P still has version V2. Ultimately, the modifications made by U1 and U2 will be taken into account, whereas in the use case without versions, one of the modifications was lost.

  • lines 32–40: a constructor capable of initializing a person’s fields. The [version] field is omitted.
  • lines 43–51: a constructor that creates a copy of the person passed to it as a parameter. We now have two objects with identical content but referenced by two different pointers.
  • Line 55: The [toString] method is redefined to return a string representing the person’s state

14.4. The [DAO] layer

The [DAO] layer consists of the following classes and interfaces:

Image

  • [IDao] is the interface presented by the [dao] layer
  • [DaoImpl] is an implementation of this interface where the group of people is encapsulated in an [ArrayList] object
  • [DaoException] is a type of unchecked exception thrown by the [dao] layer

The [ IDao] interface is as follows:

package istia.st.springmvc.people.dao;

import istia.st.springmvc.people.entities.Person;

import java.util.Collection;

public interface IDao {
    // list of all people
    Collection getAll();
    // Get a specific person
    Person getOne(int id);
    // add/edit a person
    void saveOne(Person person);
    // delete a person
    void deleteOne(int id);
}
  • The interface has four methods for the four operations we want to perform on the group of people:
    • getAll: to retrieve a collection of people
    • getOne: to retrieve a person with a specific ID
    • saveOne: to add a person (id=-1) or modify an existing person (id ≠ -1)
    • deleteOne: to delete a person with a specific ID

The [DAO] layer may throw exceptions. These will be of type [ DaoException]:

package istia.st.springmvc.personnes.dao;

public class DaoException extends RuntimeException {

    // error code
    private int code;

    public int getCode() {
        return code;
    }

// constructor
    public DaoException(String message, int code) {
        super(message);
        this.code = code;
    }
}
  • Line 3: The [DaoException] class, which extends [RuntimeException], is an unhandled exception type: the compiler does not require us to:
    • handle this type of exception with a try/catch block when calling a method that might throw it
    • include the "throws DaoException" keyword in the signature of a method that might throw the exception

This technique prevents us from having to sign the methods of the [IDao] interface with exceptions of a specific type. Any implementation that throws unchecked exceptions will then be acceptable, thereby bringing flexibility to the architecture.

  • Line 6: an error code. The [dao] layer will throw various exceptions identified by different error codes. This will allow the layer responsible for handling the exception to determine the exact source of the error and take appropriate action. There are other ways to achieve the same result. One of them is to create an exception type for each possible error type, for example MissingLastNameException, MissingFirstNameException, IncorrectAgeException, ...
  • lines 13–16: the constructor that allows you to create an exception identified by an error code and an error message.
  • lines 8–10: the method that allows the exception handler to retrieve the error code.

The class [ DaoImpl] implements the [IDao] interface:

package istia.st.springmvc.people.dao;

import istia.st.springmvc.people.entities.Person;

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 people = new ArrayList();

    // ID of the next person
    private int id = 0;

    // initializations
    public void init() {
        try {
            Person p1 = new Person(-1, "Joachim", "Major",
                    new SimpleDateFormat("dd/MM/yyyy").parse("11/13/1984"),
                    true, 2);
            saveOne(p1);
            Person p2 = new Person(-1, "Mélanie", "Humbort",
                    new SimpleDateFormat("dd/MM/yyyy").parse("12/02/1985"),
                    false, 1);
            saveOne(p2);
            Person p3 = new Person(-1, "Charles", "Lemarchand",
                    new SimpleDateFormat("dd/MM/yyyy").parse("01/03/1986"),
                    false, 0);
            saveOne(p3);
        } catch (ParseException ex) {
            throw new DaoException(
                    "Error initializing the [dao] layer: "
                            + ex.toString(), 1);
        }
    }

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

    // get a specific person
    public Person getOne(int id) {
        // find the person
        int i = getPosition(id);
        // Did we find them?
        if (i != -1) {
            return new Person(((Person) people.get(i)));
        } else {
            throw new DaoException("Person with id [" + id + "] unknown", 2);
        }
    }

    // Add or modify a person
    public void saveOne(Person person) {
        // Is the person parameter valid?
        check(person);
        // Add or update?
        if (person.getId() == -1) {
            // Add
            person.setId(getNextId());
            person.setVersion(1);
            people.add(person);
            return;
        }
        // modification - find the person
        int i = getPosition(person.getId());
        // Did we find it?
        if (i == -1) {
            throw new DaoException("The person with Id [" + person.getId()
                    + "] that we want to modify does not exist", 2);
        }
        // Do we have the correct version of the original?
        OriginalPerson = (Person) people.get(i);
        if (original.getVersion() != person.getVersion()) {
            throw new DaoException("The original of the person [" + person
                    + "] has changed since it was first read", 3);
        }
        // wait 10 ms
        //wait(10);
        // OK—make the change
        original.setVersion(original.getVersion()+1);
        original.setLastName(person.getLastName());
        original.setFirstName(person.getFirstName());
        original.setDateOfBirth((person.getDateOfBirth()));
        original.setLastName(person.getLastName());
        original.setNumberOfChildren(person.getNumberOfChildren());
    }

    // Delete a person
    public void deleteOne(int id) {
        // find the person
        int i = getPosition(id);
        // Did we find them?
        if (i == -1) {
            throw new DaoException("Person with ID [" + id + "] unknown", 2);
        } else {
            // remove the person
            people.remove(i);
        }
    }

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

    // search for a person
    private int getPosition(int id) {
        int i = 0;
        boolean found = false;
        // iterate through the list of people
        while (i < people.size() && !found) {
            if (id == ((Person) people.get(i)).getId()) {
                found = true;
            } else {
                i++;
            }
        }
        // result?
        return found? i: -1;
    }

    // Check a person
    private void check(Person p) {
        // person p
        if (p == null) {
            throw new DaoException("Person null", 10);
        }
        // id
        if (p.getId() != -1 && p.getId() < 0) {
            throw new DaoException("Invalid ID [" + p.getId() + "]", 11);
        }
        // date of birth
        if (p.getDateOfBirth() == null) {
            throw new DaoException("Missing date of birth", 12);
        }
        // number of children
        if (p.getNumberOfChildren() < 0) {
            throw new DaoException("Invalid number of children [" + p.getNbEnfants()
                    + "] is invalid", 13);
        }
        // name
        if (p.getName() == null || p.getName().trim().length() == 0) {
            throw new DaoException("Name missing", 14);
        }
        // first name
        if (p.getFirstName() == null || p.getFirstName().trim().length() == 0) {
            throw new DaoException("First name missing", 15);
        }
    }

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

We will only outline this code. However, we will spend a little time on the trickiest parts.

  • Line 13: the [ArrayList] object that will hold the group of people
  • line 16: the ID of the last person added. Each time a new person is added, this ID will be incremented by 1.

The [DaoImpl] class will be instantiated as a single instance. This is known as a singleton. A web application serves its users simultaneously. At any given time, there are multiple threads running on the web server. These threads share the singletons:

  • the one from the [dao] layer
  • the one in the [service] layer
  • those of the various controllers, data validators, etc., in the web layer

If a singleton has private fields, you should immediately ask yourself why it has them. Are they justified? Indeed, they will be shared among different threads. If they are read-only, this is not a problem as long as they can be initialized at a time when you are sure there is only one active thread. We generally know how to identify this moment. It is when the web application starts up but has not yet begun serving clients. If they are read/write, then access synchronization to the fields must be implemented; otherwise, disaster is inevitable. We will illustrate this problem when we test the [dao] layer.

  • The [DaoImpl] class has no constructor. Therefore, its default constructor will be used.
  • Lines 19–38: The [init] method will be called when the singleton of the [dao] layer is instantiated. It creates a list of three people.
  • Lines 41–43: Implements the [getAll] method of the [IDao] interface. It returns a reference to the list of people.
  • Lines 46–55: Implements the [getOne] method of the [IDao] interface. Its parameter is the ID of the person being searched for.

To retrieve it, we call a private method [getPosition] in lines 113–126. This method returns the position in the list of the person being searched for, or -1 if the person was not found.

If the person is found, the [getOne] method returns a reference (line 51) to a copy of that person, not to the person themselves. In fact, when a user wants to edit a person, the information about that person is requested from the [dao] layer and passed up to the [web] layer for modification, in the form of a reference to a [Person] object. This reference serves as the input container in the edit form. When the user submits their changes in the web layer, the contents of the input container will be modified. If the container is a reference to the actual person in the [ArrayList] of the [dao] layer, then that person is modified even though the changes have not been presented to the [service] and [dao] layers. The latter is the only layer authorized to manage the list of people. Therefore, the web layer must work on a copy of the person to be modified. Here, the [dao] layer provides this copy.

If the person being searched for is not found, a [DaoException] is thrown with error code 2 (line 53).

  • lines 94–104: implements the [deleteOne] method of the [IDao] interface. Its parameter is the ID of the person to be deleted. If the person to be deleted does not exist, a [DaoException] is thrown with error code 2.
  • Lines 58–91: Implements the [saveOne] method of the [IDao] interface. Its parameter is a [Person] object. If this object has an id of -1, then it is a new person being added. Otherwise, it modifies the person in the list with that id using the values in the parameter.
    • Line 60: The validity of the [Person] parameter is checked by a private method [check] defined on lines 129–155. This method performs basic checks on the values of the various fields of [Person]. Whenever an anomaly is detected, a [DaoException] with a specific error code is thrown. Since the [saveOne] method does not handle this exception, it will be propagated to the calling method.
    • Line 62: If the [Person] parameter has an id equal to -1, then this is an addition. The [Person] object is added to the internal list of people (line 66), with the first available id (line 64), and a version number equal to 1 (line 65).
    • If the [Person] parameter has an [id] other than -1, this involves modifying the person in the internal list with that [id]. First, we check (lines 70–75) that the person to be modified exists. If this is not the case, we throw a [DaoException] with error code 2.
    • If the person does exist, we verify that its current version matches that of the [Person] parameter, which contains the changes to be applied to the original. If this is not the case, it means that the user attempting to modify the person does not have the latest version. We inform them of this by throwing a [DaoException] with error code 3 (lines 79–80).
    • If everything goes well, the changes are made to the original person record (lines 85–90)

It is clear that this method must be synchronized. For example, between the moment we verify that the person to be modified is indeed present and the moment the modification is made, the person could have been removed from the list by someone else. The method should therefore be declared [synchronized] to ensure that only one thread executes it at a time. The same applies to the other methods of the [IDao] interface. We do not do this, preferring to move this synchronization to the [service] layer. To highlight synchronization issues, during testing of the [dao] layer we will pause the execution of [saveOne] for 10 ms (line 83) between the moment we know we can make the modification and the moment we actually make it. The thread executing [saveOne] will then lose the CPU to another thread. This increases our chances of seeing access conflicts in the list of people.

14.5. [DAO] layer tests

A JUnit test is written for the [dao] layer:

[TestDao] is the JUnit test. To highlight concurrent access issues to the list of people, threads of type [ThreadDaoMajEnfants] are created. They are responsible for increasing the number of children for a given person by 1.

[TestDao] has five tests, [test1] through [test5]. We present only two of them here; readers are invited to explore the others in the source code associated with this article.

package istia.st.springmvc.personnes.tests;

import java.text.ParseException;
...

public class TestDao extends TestCase {

    // [dao] layer
    private DaoImpl dao;

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

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

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

    // Modifying or deleting a non-existent element
    public void test2() throws ParseException {
...
    }

    // managing person versions
    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 {
    ...
}
  • line 9: reference to the implementation of the [dao] layer being tested
  • lines 12–15: the JUnit test constructor. It creates an instance of type [DaoImpl] from the [dao] layer to be tested and initializes it.

The [test1] method tests the four methods of the [IDao] interface as follows:

    public void test1() throws ParseException {
        // current list
        Collection people = dao.getAll();
        int numberOfPeople = people.size();
        // display
        doList(people);
        // add a person
        Person p1 = new Person(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        dao.saveOne(p1);
        int id1 = p1.getId();
        // check - an exception will be thrown if the person is not found
        p1 = dao.getOne(id1);
        assertEquals("X", p1.getName());
        // update
        p1.setName("Y");
        dao.saveOne(p1);
        // verification - an exception will be thrown if the person is not found
        p1 = dao.getOne(id1);
        assertEquals("Y", p1.getName());
        // deletion
        dao.deleteOne(id1);
        // verification
        int errorCode = 0;
        boolean error = false;
        try {
            p1 = dao.getOne(id1);
        } catch (DaoException ex) {
            error = true;
            errorCode = ex.getCode();
        }
        // There must be a code 2 error
        assertTrue(error);
        assertEquals(2, errorCode);
        // list of people
        people = dao.getAll();
        assertEquals(numberOfPeople, people.size());
    }
  • Line 3: Request the list of people
  • line 6: display the list
[1,1,Joachim,Major,01/13/1984,true,2]
[2,1,Mélanie,Humbort,01/12/1985,false,1]
[3,1,Charles,Lemarchand,01/01/1986,false,0]

The test then adds a person, modifies them, and deletes them. Thus, the four methods of the [IDao] interface are used.

  • Lines 8–10: A new person is added (id=-1).
  • Line 11: We retrieve the ID of the added person because the addition assigned one to them. Before that, they did not have one.
  • Lines 13–14: We ask the [dao] layer for a copy of the person who was just added. Keep in mind that if the requested person is not found, the [dao] layer throws an exception. This will cause a crash on line 13. We could have handled this case more cleanly. On line 14, we check the name of the person retrieved.
  • Lines 16–17: We modify this name and ask the [DAO] layer to save the changes.
  • Lines 19–20: We ask the [DAO] layer for a copy of the person who was just added and verify their new name.
  • Line 22: Delete the person added at the beginning of the test.
  • lines 23-34: we ask the [dao] layer for a copy of the person who has just been deleted. We should get a [DaoException] with code 2.
  • Lines 36–37: The list of people is requested again. We should get the same list as at the beginning of the test.

The [test4] method aims to highlight issues with concurrent access to the [dao] layer’s methods. Recall that these methods have not been synchronized. The test code is as follows:

    public void test4() throws Exception {
        // add a person
        Person p1 = new Person(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
        // Create N threads to update the number of children
        final int N = 10;
        Thread[] tasks = new Thread[N];
        for (int i = 0; i < tasks.length; i++) {
            tasks[i] = new ThreadDaoMajEnfants("thread # " + i, dao, id1);
            tasks[i].start();
        }
        // wait for the threads to finish
        for (int i = 0; i < tasks.length; i++) {
            tasks[i].join();
        }
        // retrieve the person
        p1 = dao.getOne(id1);
        // they must have N children
        assertEquals(N, p1.getNumberOfChildren());
        // delete person p1
        dao.deleteOne(p1.getId());
        // verification
        boolean error = false;
        int errorCode = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            error = true;
            errorCode = ex.getCode();
        }
        // there should be an error with code 2
        assertTrue(error);
        assertEquals(2, errorCode);
    }
  • lines 3–6: we add a person P with no children to the list. We record their [id] (line 6).
  • lines 7–13: We launch N threads. Each of them will increment the number of children for person P by 1. Ultimately, person P should have N children.
  • lines 15–17: The [test4] method that launched the N threads waits for them to finish their work before checking the new number of children for person P.
  • lines 18–21: We retrieve person P and verify that their number of children is N.
  • Lines 22–35: Person P is removed, and we verify that they no longer exist in the list.

In line 11, we see that the threads are of type [ThreadDaoMajEnfants]. The constructor for this type has three parameters:

  1. the name given to the thread, used to track it via logs
  2. a reference to the [dao] layer so that the thread can access it
  3. the ID of the person the thread is supposed to work on

The [ThreadDaoMajEnfants] type is as follows:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.people.dao.IDao;
import istia.st.mvc.people.entities.Person;

public class ThreadDaoMajEnfants extends Thread {
    // thread name
    private String name;
    // reference to the [dao] layer
    private IDao dao;
    // ID of the person we will be working on
    private int personId;

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

    // thread body
    public void run() {
        // tracking
        tracking("started");
        // loop until we've successfully incremented by 1
        // the number of children of the person with idPersonne
        boolean finished = false;
        int numberOfChildren = 0;
        while (!finished) {
            // retrieve a copy of the person with idPerson
            Person person = dao.getOne(idPerson);
            nbChildren = person.getNbChildren();
            // tracking
            tracking("" + nbEnfants + " -> " + (nbEnfants + 1) + " for version "+person.getVersion());
            // wait 10 ms to release the CPU
            try {
                // tracking
                track("start waiting");
                // pause to let the processor
                Thread.sleep(10);
                // tracking
                follow("end of wait");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // wait completed - we try to validate the copy
            // in the meantime, other threads may have modified the original
            int errorCode = 0;
            try {
                // Increment the number of children for this copy by 1
                person.setNumberOfChildren(numberOfChildren + 1);
                // we try to modify the original
                dao.saveOne(person);
                // Success—the original has been modified
                finished = true;
            } catch (DaoException ex) {
                // retrieve the error code
                errorCode = ex.getCode();
                // must be a version 3 error - otherwise retry
                // the exception
                if (errorCode != 3) {
                    throw ex;
                } else {
                    // follow
                    log(ex.getMessage());
                }
                // the original has changed - start over
            }
        }
        // tracking
        tracking("finished and passed the number of children to " + (nbChildren + 1));
    }

    // tracking
    private void track(String message) {
        System.out
                .println(name + " [" + new Date().getTime()+ "] : " + message);
    }
}
  • line 9: [ThreadDaoMajEnfants] is indeed a thread
  • lines 18–22: the constructor that initializes the thread with three pieces of information
    1. the name [name] given to the thread
    2. a reference [dao] to the [dao] layer. Note that, once again, we are working with the interface type [IDao] and not the implementation type [DaoImpl].
    3. the identifier [id] of the person the thread is supposed to work on

When [test4] launches a thread [ThreadDaoMajEnfants] (line 12 of test4), its [run] method (line 25) is executed:

  • lines 78–81: the private method [suivi] allows for screen logging. The [run] method uses it to track the thread’s execution.
  • The thread attempts to increment the number of children for person P with identifier [id] by 1. This update may require multiple attempts. Let’s consider two threads [TH1] and [TH2]. [TH1] requests a copy of person P from the [dao] layer. It obtains it and notes that it has version V1. [TH1] is interrupted. [TH2], which was following it, does the same thing and obtains the same version V1 of person P. [TH2] is interrupted. [TH2] resumes control, increments the number of children for P, and saves its changes. We know that these changes are now saved and that P’s version will change to V2. [TH1] has finished its work. [TH2] resumes control and does the same. Its update to P will be rejected because it holds a copy of P in version V1, whereas the original P is now in version V2. [TH2] must then repeat the entire cycle [read -> update -> save]. This is why we find the loop in lines 32–72. In this loop, the thread:
  • requests a copy of person P to modify (line 34)
  • waits 10 ms (line 43). This is artificial and aims to interrupt the thread between reading person P and actually updating them in the list of people in order to increase the likelihood of conflicts.
  • increments the number of children of P (line 54) and saves P (line 56). If the thread does not have the correct version of P, an exception will be thrown by the [dao] layer. We then retrieve the exception code (line 61) to verify that it is indeed code 3 (incorrect version of P). If this is not the case, the exception is rethrown to the calling method, ultimately the [test4] test method. If we have the code 3 exception, then we restart the cycle [read -> update -> save]. If there is no exception, then the update has been made and the thread’s work is complete.

What do the tests show?

In the first configuration tested:

  • we comment out the wait statement in the [saveOne] method of [DaoImpl] (line 83, section 14.4).
        // wait 10 ms
        //wait(10);
  • the [test4] method creates 100 threads (line 8, section 14.5).
        // creation of N threads to update the number of children
        final int N = 100;

The following results are obtained:

Image

All five tests were successful.

In the second configuration tested:

  • the wait instruction in the [saveOne] method of [DaoImpl] is uncommented (line 83, section 14.4).
        // wait 10 ms
        wait(10);
  • the [test4] method creates 2 threads (line 8, section 14.5).
        // creation of N threads to update the number of children
        final int N = 2;

The following results are obtained:

The [test4] test failed. We created two threads, each tasked with incrementing by 1 the number of children of a person P who initially had 0. We therefore expected 2 children after the two threads ran, but we only have one.

Let’s examine the screen logs from [test4] to understand what happened:

thread #0 [1145536368171]: started
thread #0 [1145536368171]: 0 -> 1 for version 1
thread #0 [1145536368171]: waiting
thread #1 [1145536368171]: started
thread #1 [1145536368171]: 0 -> 1 for version 1
thread #1 [1145536368171]: start waiting
thread #0 [1145536368187]: end of wait
thread #1 [1145536368187]: end of wait
thread #0 [1145536368187]: finished and set the number of children to 1
thread #1 [1145536368187]: finished and set the number of children to 1
  • Line 1: Thread #0 begins its work
  • line 2: it has retrieved a copy of person P and finds that the number of children is 0
  • line 3: it encounters the [Thread.sleep(10)] in its [run] method and therefore pauses at time [1145536368171] (ms)
  • line 4: thread #1 then takes over the processor and begins its work
  • Line 5: It has retrieved a copy of person P and finds that the number of children is 0
  • Line 6: It encounters the [Thread.sleep(10)] in its [run] method and therefore pauses
  • Line 7: Thread 0 regains the CPU at time [1145536368187] (ms), i.e., 16 ms after losing it.
  • line 8: same for thread #1
  • line 9: thread #0 has updated itself and set the number of children to 1
  • line 10: thread #1 has done the same

The question is: why was thread #1 able to perform its update when, normally, it no longer held the correct version of person P, which had just been updated by thread #0?

First, we can observe an anomaly between lines 7 and 8: it appears that thread #0 lost the CPU between these two lines to thread #1. What was it doing at that moment? It was executing the [saveOne] method of the [dao] layer. This method has the following skeleton (see section 14.4):

    public void saveOne(Person person) {
...
        // modification - we look for the person
....
        // do we have the correct version of the original?
...
        // wait 10 ms
        wait(10);
        // OK—let's make the change
    ...
}
  • Thread #0 executed [saveOne] and proceeded to line 8, where it was forced to release the processor. In the meantime, it read person P’s version, which was 1 because person P had not yet been updated.
  • Since the CPU became free, thread #1 took it over. It, in turn, executed [saveOne] and reached line 8, where it was forced to release the CPU. In the meantime, it read person P’s version, which was 1 because person P still hadn’t been updated.
  • Since the processor became free, thread #0 acquired it. Starting at line 9, it performed its update and set the number of children to 1. Then the [run] method of thread #0 finished, and the thread displayed the log stating that it had set the number of children to 1 (line 9).
  • Since the processor became free, thread #1 inherited it. Starting at line 9, it performed its update and set the number of children to 1. Why 1? Because it holds a copy of P with the number of children set to 0. This is indicated by the log (line 5). Then the [run] method of thread #1 finished, and the thread displayed the log stating that it had set the number of children to 1 (line 10).

Where does the problem come from? It stems from the fact that thread #0 did not have time to commit its change and thus update the version of person P before thread #1 attempted to read that version to check if person P had changed. This scenario is unlikely but not impossible. We had to force thread #0 to lose the CPU to make it appear with just two threads. Without this workaround, the previous configuration had failed to reproduce this same scenario with 100 threads. The [test4] test had been successful.

What is the solution? There are undoubtedly several. One of them, which is simple to implement, is to synchronize the [saveOne] method:


    public synchronized void saveOne(Person person)

The [synchronized] keyword ensures that only one thread at a time can execute the method. Thus, thread #1 will only be allowed to execute [saveOne] once thread #0 has exited it. We can then be sure that the version of person P will have been changed by the time thread #1 enters [saveOne]. Its update will then be rejected because it will not have the correct version of P.

These are the four methods of the [dao] layer that would need to be synchronized. However, we decide to keep this layer as described and to move the synchronization to the [service] layer. There are several reasons for this:

  • We assume that access to the [dao] layer always occurs through a [service] layer. This is the case in our web application.
  • it may also be necessary to synchronize access to the methods of the [service] layer for reasons other than those that would cause us to synchronize those of the [dao] layer. In this case, there is no need to synchronize the methods of the [dao] layer. If we are certain that:
  • all access to the [DAO] layer goes through the [service] layer
  • only one thread at a time uses the [service] layer

then we can be sure that the methods of the [DAO] layer will not be executed by two threads at the same time.

We will now explore the [service] layer.

14.6. The [service] layer

The [service] layer consists of the following classes and interfaces:

Image

  • [IService] is the interface exposed by the [dao] layer
  • [ServiceImpl] is an implementation of this interface

The [IService] interface is as follows:

package istia.st.springmvc.people.service;

import istia.st.springmvc.people.entities.Person;

import java.util.Collection;

public interface IService {
    // list of all people
    Collection getAll();
    // get a specific person
    Person getOne(int id);
    // add/modify a person
    void saveOne(Person person);
    // delete a person
    void deleteOne(int id);
}

It is identical to the [IDao] interface.

The [ServiceImpl] implementation of the [IService] interface is as follows:

package istia.st.springmvc.people.service;

import istia.st.springmvc.people.dao.IDao;
import istia.st.springmvc.people.entities.Person;

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 people
    public synchronized Collection getAll() {
        return dao.getAll();
    }

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

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

    // delete a person
    public synchronized void deleteOne(int id) {
        dao.deleteOne(id);
    }
}
  • lines 10–19: The [IDao dao] attribute is a reference to the [dao] layer. It will be initialized by Spring IoC.
  • lines 22–24: implementation of the [getAll] method of the [IService] interface. The method simply delegates the request to the [dao] layer.
  • lines 27–29: implementation of the [getOne] method of the [IService] interface. The method simply delegates the request to the [dao] layer.
  • Lines 32–34: Implementation of the [saveOne] method of the [IService] interface. The method simply delegates the request to the [dao] layer.
  • Lines 37–39: Implementation of the [deleteOne] method of the [IService] interface. The method simply delegates the request to the [dao] layer.
  • All methods are synchronized (using the `synchronized` keyword), ensuring that only one thread at a time can use the [service] layer and, consequently, the [dao] layer.

14.7. Tests for the [service] layer

A JUnit test is written for the [service] layer:

[TestService] is the JUnit test. The tests performed are exactly the same as those performed for the [dao] layer. The skeleton of [TestService] is as follows:

package istia.st.springmvc.people.tests;

...

public class TestService extends TestCase {

    // [service] layer
    private ServiceImpl service;

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

    // list of people
    private void doList(Collection people) {
...
    }

    // test1
    public void test1() throws ParseException {
        // current list
        Collection people = service.getAll();
        int numberOfPeople = people.size();
        // display
        doList(people);
        // add a person
        Person p1 = new Person(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        service.saveOne(p1);
        int id1 = p1.getId();
        // check - an error will occur if the person is not found
        p1 = service.getOne(id1);
        assertEquals("X", p1.getName());
...
    }

    // Modify or delete a non-existent element
    public void test2() throws ParseException {
...
    }

    // managing person versions
    public void test3() throws ParseException, InterruptedException {
...
    }

    // optimistic locking - multi-threaded access
    public void test4() throws Exception {
        // add a person
        Person p1 = new Person(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p1);
        int id1 = p1.getId();
        // Create N threads to update the number of children
        final int N = 100;
        Thread[] tasks = new Thread[N];
        for (int i = 0; i < tasks.length; i++) {
            tasks[i] = new ThreadServiceUpdateChildren("thread # " + i, service,
                    id1);
            tasks[i].start();
        }
...
    }

    // validity tests for saveOne
    public void test5() throws ParseException {
    ...
    }
}
  • Lines 9: The [service] layer being tested is of type [ServiceImpl].
  • lines 11–15: the JUnit test constructor creates an instance of the [service] layer to be tested (line 12), creates an instance of the [dao] layer (line 13), and instructs the [service] layer to use this [dao] layer (line 14).

The [test1] method tests the four methods of the [IService] interface in the same way as the test method of the [dao] layer with the same name. The only difference is that we access the [service] layer (lines 25, 32, 35) rather than the [dao] layer.

The [test4] method aims to highlight issues with concurrent access to the methods of the [service] layer. It is, once again, identical to the [test4] test method of the [dao] layer. However, there are a few details that differ:

  • we address the [service] layer rather than the [dao] layer (line 55)
  • we pass a reference to the [service] layer to the threads rather than to the [dao] layer (line 61)

The [ThreadServiceMajEnfants] type is also nearly identical to the [ThreadDaoMajEnfants] type, with the exception that it works with the [service] layer rather than the [dao] layer:

package istia.st.springmvc.people.tests;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.people.entities.Person;
import istia.st.mvc.people.service.IService;

public class ThreadServiceMajEnfants extends Thread {

    // thread name
    private String name;
    // reference to the [service] layer
    private IService service;
    // ID of the person we will be working on
    private int personId;

    public ThreadServiceUpdateChildren(String name, IService service, int personId) {
        this.name = name;
        this.service = service;
        this.personId = personId;
    }

    public void run() {
...
    }

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

}
  • Line 12: The thread works with the [service] layer

We are running the tests with the configuration that caused issues at the [dao] layer:

  • we uncomment the wait statement in the [saveOne] method of [DaoImpl] (line 83, section 14.4).
        // wait 10 ms
        wait(10);
  • The [test4] method creates 100 threads (line 65, section 14.7).
        // create N threads to update the number of children
        final int N = 100;

The results obtained are as follows:

It was the synchronization of the methods in the [service] layer that enabled the success of the [test4] test.

14.8. The [web] layer

Let’s review the 3-tier architecture of our application:

The [web] layer will provide screens to the user to allow them to manage the group of people:

  • list of people in the group
  • add a person to the group
  • editing a person in the group
  • removing a person from the group

To do this, it will rely on the [service] layer, which in turn will call upon the [DAO] layer. We have already presented the screens managed by the [web] layer (section 14.1). To describe the web layer, we will present the following in turn:

  • its configuration
  • its views
  • its controller
  • some tests

14.8.1. Web Application Configuration

The Eclipse project for the application is as follows:

Image

  • In the [istia.st.mvc.personnes.web] package, you will find the [Application] controller.
  • The JSP/JSTL pages are in [WEB-INF/views].
  • The [lib] folder contains the third-party libraries required by the application. They are visible in the [Web App Libraries] folder.

[web.xml]


The [web.xml] file is the file used by the web server to load the application. Its content is as follows:


<?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-people-01</display-name>
    <!--  ServletPerson -->
    <servlet>
        <servlet-name>people</servlet-name>
        <servlet-class>
            istia.st.mvc.people.web.Application
        </servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/views/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErrors</param-name>
            <param-value>/WEB-INF/views/errors.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/views/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  ServletPersonne mapping -->
    <servlet-mapping>
        <servlet-name>people</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/views/exception.jsp</location>
    </error-page>
</web-app>
  • lines 27-30: URLs [/do/*] will be handled by the [people] servlet
  • lines 9-12: the [personnes] servlet is an instance of the [Application] class, a class we will build.
  • lines 13-24: define three parameters [urlList, urlEdit, urlErrors] identifying the URLs of the JSP pages for the [list, edit, errors] views.
  • lines 32–34: The application has a default entry page [index.jsp] located at the root of the web application folder.
  • lines 36–39: The application has a default error page that is displayed when the web server encounters an exception not handled by the application.
    • Line 37: The <exception-type> tag specifies the type of exception handled by the <error-page> directive; here, it is the [java.lang.Exception] type and its subtypes, meaning all exceptions.
    • Line 38: The <location> tag specifies the JSP page to display when an exception of the type defined by <exception-type> occurs. The exception that occurred is available on this page in an object named exception if the page has the directive:

<%@ page isErrorPage="true" %>
  • (continued)
    • If <exception-type> specifies type T1 and an exception of type T2 (not derived from T1) is propagated up to the web server, the server sends the client a proprietary exception page, which is generally not very user-friendly. Hence the importance of the <error-page> tag in the [web.xml] file.

[index.jsp]


This page is displayed if a user directly requests the application context without specifying a URL, i.e., here [/personnes-01]. Its content is as follows:


<%@ 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] redirects the client to the URL [/do/list]. This URL displays the list of people in the group.

14.8.2. The application's JSP/JSTL pages


The [ list.jsp] view


It is used to display the list of people:

Image

Its code is as follows:


<%@ 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 - People</title>
    </head>
    <body background="<c:url value="/resources/standard.jpg"/>">
        <h2>List of people</h2>
        <table border="1">
            <tr>
                <th>ID</th>
                <th>Version</th>
                <th>Last Name</th>
                <th>Last Name</th>
                <th>Date of birth</th>
                <th>Husband</th>
                <th>Number of children</th>
                <th></th>
            </tr>
            <c:forEach var="person" items="${people}">
                <tr>
                    <td><c:out value="${person.id}"/></td>
                    <td><c:out value="${person.version}"/></td>
                    <td><c:out value="${person.firstName}"/></td>
                    <td><c:out value="${person.lastName}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${person.dateOfBirth.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}"/>">Edit</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Delete</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Add</a>
    </body>
</html>

  • This view receives an element in its template:
  • the [people] element associated with an [ArrayList] of [Person] objects
  • lines 22–34: we iterate through the ${people} list to display an HTML table containing the people in the group.
  • line 31: the URL pointed to by the [Edit] link is set using the [id] field of the current person so that the controller associated with the URL [/do/edit] knows which person to edit.
  • line 32: the same is done for the [Delete] link.
  • line 28: To display the person’s date of birth in the format DD/MM/YYYY, we use the <dt> tag from the [DateTime] tag library of the Apache [Jakarta Taglibs] project:

Image

The description file for this tag library is defined on line 3.

  • Line 37: The [Add] link for adding a new person targets the URL [/do/edit], just like the [Edit] link on line 31. The value -1 for the [id] parameter indicates that this is an addition rather than an edit.

The [ edit.jsp] view


It is used to display the form for adding a new person or modifying an existing one:

The code for the [edit.jsp] view is as follows:


<%@ 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 - People</title>
    </head>
    <body background="../resources/standard.jpg">
        <h2>Add/Edit a Person</h2>
        <c:if test="${errorEdit != ''}">
            <h3>Update failed:</h3>
          The following error occurred: ${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>First Name</td>
                    <td>
                        <input type="text" value="${first_name}" name="first_name" size="20">
                    </td>
                    <td>${firstNameError}</td>
                </tr>
                <tr>
                    <td>Last Name</td>
                    <td>
                        <input type="text" value="${lastName}" name="lastName" size="20">
                    </td>
                    <td>${nameError}</td>
                </tr>
                <tr>
                <td>Date of birth (DD/MM/YYYY)</td>
                    <td>
                        <input type="text" value="${dateNaissance}" name="dateNaissance">
                    </td>
                    <td>${birthDateError}</td>
                </tr>
                <tr>
                    <td>Spouse</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Yes
                                <input type="radio" name="marie" value="false">No
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Yes
                                <input type="radio" name="marie" value="false" checked>No
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Number of children</td>
                    <td>
                        <input type="text" value="${nbEnfants}" name="nbEnfants">
                    </td>
                    <td>${nbEnfantsError}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Submit">
            <a href="<c:url value="/do/list"/>">Cancel</a>
        </form>
    </body>
</html>

This view displays a form for adding a new person or updating an existing one. From now on, to simplify the text, we will use the single term [update]. The [Submit] button (line 73) triggers a POST request to the URL [/do/validate] (line 16). If the POST fails, the [edit.jsp] view is re-displayed with the error(s) that occurred; otherwise, the [list.jsp] view is displayed.

  • The [edit.jsp] view, which is displayed both on a GET request and on a failed POST request, receives the following elements in its model:
attribute
GET
POST
id
ID of the person being updated
same
version
its version
same
first name
first name
First name entered
last name
his/her last name
last name entered
dateOfBirth
his/her date of birth
entered date of birth
married
marital status
Marital status entered
nbChildren
number of children
number of children entered
errorEdit
empty
An error message indicating that the addition or modification failed during the POST triggered by the [Submit] button. Empty if no error.
errorFirstName
empty
indicates an incorrect first name – empty otherwise
errorName
empty
indicates an incorrect name – empty otherwise
birthDateError
empty
indicates an incorrect date of birth – empty otherwise
errorNumberOfChildren
empty
indicates an incorrect number of children – empty otherwise
  • lines 11-15: if the form POST fails, [errorEdit!=''] will be returned and an error message will be displayed.
  • line 16: the form will be submitted to the URL [/do/validate]
  • line 20: the [id] element of the template is displayed
  • line 24: the [version] element of the template is displayed
  • lines 26-32: entering the person’s first name:
    • When the form is initially displayed (GET), ${firstName} displays the current value of the [firstName] field of the updated [Person] object, and ${firstNameError} is empty.
    • in case of an error after the POST, the entered value ${firstName} is displayed again, along with any error message ${firstNameError}
  • lines 33-39: entering the person's last name
  • lines 40–46: Entering the person’s date of birth
  • Lines 47–61: Entering the person’s marital status using a radio button. We use the value of the [married] field of the [Person] object to determine which of the two radio buttons should be selected.
  • lines 62-68: enter the person’s number of children
  • line 71: a hidden HTML field named [id] with a value equal to the [id] field of the person being updated, -1 for an addition, or another value for a modification.
  • line 72: a hidden HTML field named [version] with a value equal to the [id] field of the person being updated.
  • Line 73: The [Submit] button of the form
  • line 74: a link to return to the list of people. It is labeled [Cancel] because it allows the user to exit the form without submitting it.

The [ exception.jsp] view


It is used to display a page indicating that an exception not handled by the application has occurred and has been propagated to the web server.

For example, let’s delete a person who does not exist in the group:

The code for the [exception.jsp] view is as follows:


<%@ 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 - People</title>
    </head>
    <body background="<c:url value="/resources/standard.jpg"/>">
        <h2>MVC - People</h2>
        The following exception occurred:
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Back to list</a>
    </body>
</html>

  • This view receives a key in its template, the [exception] element, which is the exception that was intercepted by the web server. For this element to be included in the JSP page template by the web server, the page must have defined the tag on line 3.
  • Line 6: We set the HTTP status code of the response to 200. This is the first HTTP header of the response. The 200 status code indicates to the client that its request was successful. Typically, an HTML document has been included in the server’s response. This is the case here. If the HTTP status code of the response is not set to 200, it will have the value 500, which means an error occurred. In fact, when the web server intercepts an unhandled exception, it considers this situation abnormal and signals it with a 500 code. The response to an HTTP 500 code varies by browser: Firefox displays the HTML document that may accompany this response, while IE ignores this document and displays its own page. This is why we replaced the 500 code with the 200 code.
  • Line 16: The exception text is displayed
  • Line 18: The user is offered a link to return to the list of people

The [ erreurs .jsp] view


It is used to display a page reporting application initialization errors, i.e., errors detected during the execution of the [init] method of the controller servlet. This could be, for example, the absence of a parameter in the [web.xml] file, as shown in the example below:

Image

The code for the [errors.jsp] page is as follows:


<%@ 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 - People</title>
  </head>
  <body>
      <h2>The following errors occurred</h2>
    <ul>
            <c:forEach var="error" items="${errors}">
                <li>${error}</li>
            </c:forEach>
    </ul>
  </body>
</html>

The page receives an [errors] element in its template, which is an [ArrayList] of [String] objects; these are error messages. They are displayed by the loop in lines 13–15.

14.8.3. The application controller

The [Application] controller is defined in the [istia.st.mvc.personnes.web] package:

Image


Structure of the [ ] controller and initialization


The skeleton of the [Application] controller is as follows:

package istia.st.mvc.personnes.web;

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

@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // instance parameters
    private String errorUrl = null;
    private ArrayList initializationErrors = new ArrayList<String>();
    private String[] parameters = { "urlList", "urlEdit", "urlErrors" };
    private Map params = new HashMap<String, String>();

    // service
    ServiceImpl service = null;

    // init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // retrieve the servlet's initialization parameters
        ServletConfig config = getServletConfig();
        // process the other initialization parameters
        String value = null;
        for (int i = 0; i < parameters.length; i++) {
            // parameter value
            value = config.getInitParameter(parameters[i]);
            // Is the parameter present?
            if (value == null) {
                // log the error
                initializationErrors.add("The parameter [" + parameters[i]
                        + "] has not been initialized");
            } else {
                // store the parameter value
                params.put(parameters[i], value);
            }
        }
        // The URL for the [errors] view requires special handling
        errorsUrl = config.getInitParameter("errorsUrl");
        if (errorsUrl == null)
            throw new ServletException(
                    "The [urlErrors] parameter has not been initialized");
        // instantiation of the [dao] layer
        DaoImpl dao = new DaoImpl();
        dao.init();
        // instantiate 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 people
    private void doListPeople(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

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

    // validation of editing / adding a person
    private void doDeletePerson(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
...
    }

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

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

    // post
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        // pass control to GET
        doGet(request, response);    }
}
  • lines 20–36: retrieve the parameters specified in the [web.xml] file.
  • Lines 39–41: The [urlErrors] parameter must be present because it specifies the URL of the [errors] view, which displays any initialization errors. If it does not exist, the application is terminated by throwing a [ServletException] (line 40). This exception will be propagated to the web server and handled by the <error-page> tag in the [web.xml] file. The [exception.jsp] view is therefore displayed:

Image

The [Back to list] link above is inactive. Clicking it returns the same response as long as the application has not been modified and reloaded. It is useful for other types of exceptions, as we have already seen.

  • line 43: creates a [DaoImpl] instance implementing the [dao] layer
  • line 44: initializes this instance (creates an initial list of three people)
  • line 46: creates an instance of [ServiceImpl] implementing the [service] layer
  • line 47: initializes the [service] layer by providing it with a reference to the [dao] layer

After the controller is initialized, its methods have a [service] reference to the [service] layer (line 15) that they will use to execute the actions requested by the user. These will be intercepted by the [doGet] method, which will have them processed by a specific method of the controller:

Url
HTTP Method
Controller method
/do/list
GET
doListPeople
/do/edit
GET
doEditPerson
/do/validate
POST
doValidatePerson
/do/delete
GET
doDeletePerson

The [doGet] method


The purpose of this method is to route the processing of user-requested actions to the correct method. Its code is as follows:

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

        // Check how the servlet initialization went
        if (initializationErrors.size() != 0) {
            // redirect to the error page
            request.setAttribute("errors", initializationErrors);
            getServletContext().getRequestDispatcher(errorURL).forward(request, response);
            // end
            return;
        }
        // retrieve the request method
        String method = request.getMethod().toLowerCase();
        // retrieve the action to be executed
        String action = request.getPathInfo();
        // action?
        if (action == null) {
            action = "/list";
        }
        // execute action
        if (method.equals("get") && action.equals("/list")) {
            // list of people
            doListPeople(request, response);
            return;
        }
        if (method.equals("get") && action.equals("/delete")) {
            // delete a person
            doDeletePerson(request, response);
            return;
        }
        if (method.equals("get") && action.equals("/edit")) {
            // display form for adding/editing a person
            doEditPerson(request, response);
            return;
        }
        if (method.equals("post") && action.equals("/validate")) {
            // validation of the form for adding or editing a person
            doValidatePerson(request, response);
            return;
        }
        // other cases
        doListPeople(request, response);
    }
  • lines 7–13: we check that the list of initialization errors is empty. If it is not, we display the [errors(errors)] view, which will report the error(s).
  • line 15: We retrieve the [get] or [post] method that the client used to make the request.
  • line 17: retrieve the value of the [action] parameter from the request.
  • Lines 23–27: Process the [GET /do/list] request, which requests the list of people.
  • Lines 28–32: Process the [GET /do/delete] request, which requests the deletion of a person.
  • Lines 33–37: Process the [GET /do/edit] request, which requests the form to update a person.
  • lines 38–42: processing the [POST /do/validate] request, which requests validation of the updated person.
  • line 44: if the requested action is not one of the previous five, then we treat it as if it were [GET /do/list].

The [doListPersonnes] method


This method handles the [GET /do/list] request, which requests the list of people:

Image

Its code is as follows:

1
2
3
4
5
6
7
8
9
    // display list of people
    private void doListPeople(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // the [list] view template
        request.setAttribute("people", service.getAll());
        // display the view [list]
        getServletContext()
                .getRequestDispatcher((String) params.get("urlList")).forward(request, response);
    }
  • Line 5: We request the list of people in the group from the [service] layer and store it in the model under the key "people".
  • line 7: the [list.jsp] view described in section 14.8.2 is displayed.

The [doDeletePerson] method


This method handles the [GET /do/delete?id=XX] request, which requests the deletion of the person with id=XX. The URL [/do/delete?id=XX] is that of the [Delete] links in the [list.jsp] view:

Image

whose code is as follows:

...
<html>
    <head>
        <title>MVC - people</title>
    </head>
    <body background="<c:url value="/resources/standard.jpg"/>">
...
            <c:forEach var="person" items="${people}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Edit</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Delete</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Add</a>
    </body>
</html>

Line 12 shows the URL [/do/delete?id=XX] for the [Delete] link. The [doDeletePerson] method, which handles this URL, must delete the person with id=XX and then display the updated list of people in the group. Its code is as follows:

    // validation of modification / addition of a person
    private void doDeletePerson(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's ID
        int id = Integer.parseInt(request.getParameter("id"));
        // delete the person
        service.deleteOne(id);
        // redirect to the list of people
        response.sendRedirect("list");
    }
  • Line 5: The URL being processed is in the form [/do/delete?id=XX]. We retrieve the value [XX] from the [id] parameter.
  • line 7: we ask the [service] layer to delete the person with the obtained ID. We do not perform any validation. If the person we are trying to delete does not exist, the [dao] layer throws an exception that is propagated up to the [service] layer. We do not handle it here in the controller either. It will therefore propagate up to the web server, which, by configuration, will display the [exception.jsp] page, described in section 14.8.2:

Image

  • Line 9: If the deletion was successful (no exception), the client is redirected to the relative URL [list]. Since the URL just processed was [/do/delete], the redirect URL will be [/do/list]. The browser will therefore perform a [GET /do/list] request, which will display the list of people.

The [doEditPerson] method


This method handles the [GET /do/edit?id=XX] request, which requests the form to update the person with id=XX. The URL [/do/edit?id=XX] is the one used for the [Edit] and [Add] links in the [list.jsp] view:

Image

whose code is as follows:

...
<html>
    <head>
        <title>MVC - people</title>
    </head>
    <body background="<c:url value="/resources/standard.jpg"/>">
...
            <c:forEach var="person" items="${people}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${person.id}"/>">Edit</a></td>
                    <td><a href="<c:url value="/do/delete?id=${person.id}"/>">Delete</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Add</a>
    </body>
</html>

On line 11, we see the URL [/do/edit?id=XX] for the [Edit] link, and on line 17, the URL [/do/edit?id=-1] for the [Add] link. The [doEditPersonne] method must display the edit form for the person with id=XX, or if it is an addition, display an empty form.

The code for the [doEditPerson] method is as follows:

    // edit / add a person
    private void doEditPerson(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the person's ID
        int id = Integer.parseInt(request.getParameter("id"));
        // Add or edit?
        Person person = null;
        if (id != -1) {
            // Update - retrieve the person to be updated
            person = service.getOne(id);
        } else {
            // add - create an empty person
            person = new Person();
            person.setId(-1);
        }
        // we put the [Person] object into the [edit] view model
        request.setAttribute("errorEdit", "");
        request.setAttribute("id", person.getId());
        request.setAttribute("version", person.getVersion());
        request.setAttribute("firstName", person.getFirstName());
        request.setAttribute("lastName", person.getLastName());
        Date birthDate = person.getBirthDate();
        if (birthDate != null) {
            request.setAttribute("dateOfBirth", new SimpleDateFormat(
                    "dd/MM/yyyy").format(birthDate));
        } else {
            request.setAttribute("dateOfBirth", "");
        }
        request.setAttribute("spouse", person.getSpouse());
        request.setAttribute("nbChildren", person.getNbChildren());
        // display the [edit] view
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • The GET request targets a URL of the form [/do/edit?id=XX]. On line 5, we retrieve the value of [id]. Then there are two cases:
  1. If id is not equal to -1, this is an update, and we need to display a form pre-filled with the information of the person to be edited. On line 10, this person is requested from the [service] layer.
  2. If id is equal to -1, this is an addition, and an empty form must be displayed. To do this, an empty person is created on lines 13–14.
  • The [Person] object is placed in the [edit.jsp] page template described in Section 14.8.2. This template includes the following elements: [errorEdit, id, version, firstName, errorFirstName, lastName, errorLastName, dateOfBirth, errorDateOfBirth, spouse, numberOfChildren, errorNumberOfChildren]. These elements are initialized in lines 17–30, with the exception of those whose value is the empty string [firstNameError, lastNameError, birthDateError, childrenCountError]. We know that if they are missing from the template, the JSTL library will display an empty string for their value. Although the [errorEdit] element also has an empty string as its value, it is nevertheless initialized because a check is performed on its value in the [edit.jsp] page.
  • Once the model is ready, control is passed to the [edit.jsp] page, lines 32–33, which will generate the [edit] view.

The [doValidatePersonne] method


This method handles the [POST /do/validate] request, which validates the update form. This POST is triggered by the [Validate] button:

Image

Let’s review the input elements of the HTML form in the view above:

<form method="post" action="<c:url value="/do/validate"/>">
....
        <input type="text" value="${firstName}" name="firstName" size="20">
....
        <input type="text" value="${last_name}" name="last_name" size="20">
....
        <input type="text" value="${dateOfBirth}" name="dateOfBirth">
...
        <input type="radio" name="married" value="true" checked>Yes
....
        <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="Submit">
</form>

The POST request contains the parameters [firstName, lastName, dateOfBirth, spouse, numberOfChildren, id, version] and is sent to the URL [/do/validate] (line 1). It is processed by the following [doValidatePerson] method:

// validation of a person's modification or addition
    public void doValidatePerson(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        // retrieve the posted elements
        boolean formError = false;
        boolean error;
        // the first name
        String firstName = request.getParameter("firstName").trim();
        // Is the first name valid?
        if (firstName.length() == 0) {
            // log the error
            request.setAttribute("firstNameError", "First name is required");
            formIsInvalid = true;
        }
        // the last name
        String lastName = request.getParameter("lastName").trim();
        // Is the first name valid?
        if (name.length() == 0) {
            // log the error
            request.setAttribute("lastNameError", "Last name is required");
            formIsInvalid = true;
        }
        // date of birth
        Date birthDate = null;
        try {
            dateOfBirth = new SimpleDateFormat("dd/MM/yyyy").parse(request
                    .getParameter("dateOfBirth").trim());
        } catch (ParseException e) {
            // log the error
            request.setAttribute("birthDateError", "Invalid date");
            formIsInvalid = true;
        }
        // marital status
        boolean married = Boolean.parseBoolean(request.getParameter("married"));
        // number of children
        int numberOfChildren = 0;
        error = false;
        try {
            nbChildren = Integer.parseInt(request.getParameter("nbChildren")
                    .trim());
            if (nbChildren < 0) {
                error = true;
            }
        } catch (NumberFormatException ex) {
            // record the error
            error = true;
        }
        // Is the number of children incorrect?
        if (error) {
            // report the error
            request.setAttribute("incorrectNumberOfChildren",
                    "Incorrect number of children");
            formIsInvalid = true;
        }
        // Person ID
        int id = Integer.parseInt(request.getParameter("id"));
        // version
        long version = Long.parseLong(request.getParameter("version"));
        // Is the form invalid?
        if (formIsInvalid) {
            // Redisplay the form with error messages
            showForm(request, response, "");
            // done
            return;
        }
        // The form is valid - save the person
        Person person = new Person(id, firstName, lastName, dateOfBirth, marriedTo,
                nbChildren);
        person.setVersion(version);
        try {
            // save
            service.saveOne(person);
        } catch (DaoException ex) {
            // Redisplay the form with the error message
            showForm(request, response, ex.getMessage());
            // done
            return;
        }
        // redirect to the list of people
        response.sendRedirect("list");
    }

    // display pre-filled form
    private void showForm(HttpServletRequest request,
            HttpServletResponse response, String errorEdit)
            throws ServletException, IOException {
        // prepare the [edit] view template
        request.setAttribute("errorEdit", errorEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("firstName", request.getParameter("firstName").trim());
        request.setAttribute("lastName", request.getParameter("lastName").trim());
        request.setAttribute("dateOfBirth", request.getParameter(
                "dateOfBirth").trim());
        request.setAttribute("spouse", request.getParameter("spouse"));
        request.setAttribute("nbEnfants", request.getParameter("nbEnfants")
                .trim());
        // display the [edit] view
        getServletContext()
                .getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • lines 8-14: the [firstName] parameter from the POST request is retrieved and its validity is checked. If it is incorrect, the [firstNameError] element is initialized with an error message and placed in the request attributes.
  • lines 16–22: the same process is followed for the [lastName] parameter
  • lines 24–32: The same process is applied to the [dateOfBirth] parameter
  • Line 34: The [spouse] parameter is retrieved. We do not check its validity because, in principle, it comes from the value of a radio button. That said, nothing prevents a program from making a [POST /people-01/do/validate] request accompanied by a fictitious [spouse] parameter. We should therefore test the validity of this parameter. Here, we rely on our exception handling, which causes the [exception.jsp] page to be displayed if the controller does not handle the exception itself. So, if the conversion of the [marie] parameter to a boolean fails on line 34, an exception will be thrown, resulting in the [exception.jsp] page being sent to the client. This behavior works for us.
  • Lines 34–54: We retrieve the [nbEnfants] parameter and check its value.
  • Line 56: We retrieve the [id] parameter without checking its value
  • Line 58: We do the same for the [version] parameter
  • Lines 60–65: If the form is invalid, it is redisplayed with the error messages generated earlier
  • Lines 67–69: If it is valid, we create a new [Person] object using the form’s fields
  • lines 70–78: the person is saved. The save operation may fail. In a multi-user environment, the person to be modified may have been deleted or already modified by someone else. In this case, the [dao] layer will throw an exception, which we handle here.
  • Line 80: If no exception occurred, the client is redirected to the URL [/do/list] to display the group’s new status.
  • Line 75: If an exception occurred during saving, we request that the initial form be redisplayed, passing it the exception’s error message (3rd parameter).

The [showFormulaire] method (lines 84–101) constructs the template required for the [edit.jsp] page using the entered values (request.getParameter(" ... ")). Recall that the error messages have already been added to the template by the [doValidatePersonne] method. The [edit.jsp] page is displayed in lines 99–100.

14.9. Testing the Web Application

A number of tests were presented in Section 14.1. We invite the reader to run them again. Here we show additional screenshots illustrating cases of data access conflicts in a multi-user environment:

[Firefox] will be user U1’s browser. User U1 requests the URL [http://localhost:8080/personnes-01]:

Image

[IE] will be user U2’s browser. User U2 requests the same URL:

Image

User U1 begins editing the record for [Lemarchand]:

Image

User U2 does the same:

Image

User U1 makes changes and saves:

User U2 does the same:

User U2 returns to the list of people using the [Cancel] link on the form:

Image

They find the person [Lemarchand] as modified by U1. Now U2 deletes [Lemarchand]:

U1 still has their own list and wants to edit [Lemarchand] again:

U1 uses the [Back to list] link to see what’s going on:

Image

He discovers that [Lemarchand] is indeed no longer on the list...

14.10. Conclusion

We have implemented the MVC architecture within a 3-tier architecture [web, business logic, DAO] using a basic example of managing a list of people. This allowed us to apply the concepts presented in the previous sections. In the version we examined, the list of people was kept in memory. We will soon explore versions where this list is stored in a database table.

But first, we will introduce a tool called Spring IoC, which facilitates the integration of the different layers of a n-tier application.