Skip to content

4. Actions: the model

Let’s return to the architecture of a Spring MVC application:

In the previous chapter, we looked at the process that routes the request [1] to the controller and the action [2a] that will handle it, a mechanism known as routing. We also presented the various responses an action can send back to the browser. So far, we have presented actions that did not process the request presented to them. A request [1] carries various pieces of information that Spring MVC presents [2a] to the action in the form of a model. This term should not be confused with the M model of a V view [2c] that is produced by the action:

  • the client’s HTTP request arrives at [1];
  • in [2], the information contained in the request is transformed into an action model [3], often but not necessarily a class, which serves as input to the action [4];
  • at [4], the action, based on this model, generates a response. This response has two components: a view V [6] and the model M of this view [5];
  • the view V [6] will use its model M [5] to generate the HTTP response intended for the client.

In the MVC model, the action [4] is part of the C (controller), the view model [5] is the M, and the view [6] is the V.

This chapter examines the mechanisms for linking the information carried by the request—which is inherently strings—to the action model, which can be a class with properties of various types.

Note: the term [Action Model] is not a recognized term.

We create a new controller for these new actions:

  

The [ActionModelController] will be as follows for now:


package istia.st.springmvc.controllers;

import org.springframework.web.bind.annotation.RestController;

@RestController
public class ActionModelController {

}
  • Line 5: Note that the [@RestController] annotation causes the response sent to the client to be the string serialization of the controller's action results;

4.1. [/m01]: GET parameters

We add the following [/m01] action:



    // ----------------------- retrieve parameters with GET------------------------
    @RequestMapping(value = "/m01", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m01(String name, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", name, age);
}
  • Line 4: The action accepts two parameters named [name] and [age]. They will be initialized with parameters bearing the same names in the HTTP GET request;

The results are as follows in Chrome [1-3]:

  • in [1], the GET request with the parameters [name] and [age];
  • in [3], we see that the [/m01] action has successfully retrieved these parameters;

4.2. [/m02]: POST parameters

We add the following [/m02] action:



    // ----------------------- retrieve parameters with POST------------------------
    @RequestMapping(value = "/m02", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m02(String name, String age) {
        return String.format("Hello [%s-%s]!, Greetings from Spring Boot!", name, age);
}
  • Line 4: The action accepts two parameters named [name] and [age]. They will be initialized with parameters bearing the same names in the HTTP POST request;

The results with [Advanced REST Client] are as follows:

  • In [1-3], the POST request with the parameters [name] and [age];
  • In [4-5], we set the HTTP header [Content-Type] for the POST request. It must be [Content-Type: application/x-www-form-urlencoded];
  • in [6], [Form Data] provides the list of parameters for a POST operation. Here we see the parameters [name] and [age];
  • in [7], the server response showing that the [/m02] action successfully retrieved the [name] and [age] parameters;

4.3. [/m03]: Parameters with the Same Names

We saw in section 2.5.2.8 that the multi-select list could send parameters with the same names to the server. Let’s see how an action can retrieve them. We add the following [/m03] action:


    // ----------------------- retrieving parameters with the same names-----------------
    @RequestMapping(value = "/m03", method = RequestMethod.POST, produces = "text/plain;charset=UTF-8")
    public String m03(String name[]) {
        return String.format("Hello [%s]!, Greetings from Spring Boot!", String.join("-", names));
}
  • Line 2: The action accepts a parameter named [name[]]. It will be initialized here with all parameters bearing this name, whether in a GET or POST request, since the request type has not been specified here;

The results are as follows:

  • With a POST [1], we send the parameters [2];
  • parameters are also included in the URL [3];
  • in [4], the four parameters with the same name [name]: [Query String parameters] are the URL parameters, [Form Data] are the posted parameters;
  • in [5], we see that the action [/m03] retrieved the four parameters named [name];

4.4. [/m04]: mapping the action's parameters to a Java object

Consider the following new action [/m04]:


    // ------ map the parameters to an object (Command Object) ---------------
    @RequestMapping(value = "/m04", method = RequestMethod.POST)
    public Person m04(Person person) {
        return person;
}
  • Line 3: The action takes a Person of the following type as a parameter:

public class Person {

    // identifier
    private Integer id;
    // name
    private String name;
    // age
    private int age;
....
    // getters and setters
...
}
  • To create the [Person] parameter, Spring MVC calls [new Person()];
  • then, if there are parameters named after the fields [id, name, age] of the created object, it instantiates them using their setters;
  • line 4: the action returns a [Person] type, which will therefore be serialized into a string before being sent to the client. We saw that by default, the serialization performed is JSON serialization. The client should therefore receive the JSON string of a person;

Here is an example:

  • in [1], the parameters [id, name, age] to construct a [Person] object;
  • in [2], the JSON string for this person;

What happens if we don’t send all the fields for a person? Let’s try:

  • in [2], only the [id] parameter has been initialized;

4.5. [/m05]: retrieve elements from a URL

Consider the following new action [/m05]:


    // ----------------------- retrieve elements from the URL ------------------------
    @RequestMapping(value = "/m05/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, String> m05(@PathVariable("a") String a, @PathVariable("b") String b) {
        Map<String, String> map = new HashMap<String, String>();
        map.put("a", a);
        map.put("b", b);
        return map;
}
  • Line 2: The URL being processed is in the form [/m05/{a}/x/{b}], where {param} is a URL parameter;
  • Line 3: The URL parameter elements are retrieved using the [@PathVariable] annotation;
  • lines 4–6: the retrieved elements [a] and [b] are placed in a dictionary;
  • line 7: the response will be the JSON string of this dictionary;

The results are as follows:

 

4.6. [/m06]: Retrieving URL elements and parameters

Consider the following new action [/m06]:


    // -------- retrieve URL elements and parameters---------------
    @RequestMapping(value = "/m06/{a}/x/{b}", method = RequestMethod.GET)
    public Map<String, Object> m06(@PathVariable("a") Integer a, @PathVariable("b") Double b, Double c) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("a", a);
        map.put("b", b);
        map.put("c", c);
        return map;
}
  • line 3: we retrieve both URL elements [Integer a, Double b] and a parameter (GET or POST) [Double c];
  • lines 4–7: these elements are placed in a dictionary;
  • line 8: which forms the client's response, which will therefore receive the JSON string from this dictionary;

Here are the results:

 

Note the / at the end of the path [http://localhost:8080/m06/100/x/200.43/]. Without it, we get the following incorrect result:

 

4.7. [/m07]: access the entire request

Here is the new [/m07] action:


    // ------ access the HttpServletRequest ------------------------
    @RequestMapping(value = "/m07", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m07(HttpServletRequest request) {
        // HTTP headers
        Enumeration<String> headerNames = request.getHeaderNames();
        StringBuffer buffer = new StringBuffer();
        while (headerNames.hasMoreElements()) {
            String name = headerNames.nextElement();
            buffer.append(String.format("%s : %s\n", name, request.getHeader(name)));
        }
        return buffer.toString();
}
  • line 3: we ask Spring MVC to inject the [HttpServletRequest request] object, which encapsulates all the information available about the request;
  • lines 5–10: we retrieve all the HTTP headers from the request to assemble them into a string that we send to the client (line 11);

The results are as follows:

  • in [1], the HTTP headers of the request;
  • in [2], the response. All the HTTP headers from the request are indeed present there.

4.8. [/m08]: access to the [Writer] object

Consider the following action:


    // ----------------------- writer injection ------------------------
    @RequestMapping(value = "/m08", method = RequestMethod.GET)
    public void m08(Writer writer) throws IOException {
        writer.write("Hello, world!");
}
  • Line 3: Spring MVC injects the [Writer writer] object, which allows writing to the response stream to the client;
  • line 3: the action returns a [void] type, indicating that it must construct the response to the client itself;
  • line 4: Adding text to the response stream to the client;

The results are as follows:

  • in [2], we see that the HTTP header [Content-Type] was not sent;
  • in [3], the response;

4.9. [/m09]: accessing an HTTP header

Consider the following action:


    // ----------------------- RequestHeader injection ------------------------
    @RequestMapping(value = "/m09", method = RequestMethod.GET)
    public String m09(@RequestHeader("User-Agent") String userAgent) {
        return userAgent;
}
  • line 3: the [@RequestHeader("User-Agent")] annotation retrieves the [User-Agent] HTTP header;
  • line 4: the text of this header is returned;

The results are as follows:

  • in [2], the HTTP header [User-Agent];
  • in [3], the action [/m08] correctly retrieved this header;

A cookie is generally an HTTP header that the:

  • server sends to the client for the first time;
  • the client then systematically sends back to the server;

First, let’s create an action that creates the cookie:


    // ----------------------- Cookie creation ------------------------
    @RequestMapping(value = "/m10", method = RequestMethod.GET)
    public void m10(HttpServletResponse response) {
        response.addCookie(new Cookie("cookie1", "remember me"));
}
  • Line 3: We inject the [HttpServletResponse response] object to have full control over the response;
  • line 4: we create a cookie with a key [cookie1] and a value [remember me] (Note: accented characters in a cookie’s value cause errors);
  • line 3: the action returns nothing. Furthermore, it writes nothing to the response body. The client will therefore receive an empty document. The response is used only to add the HTTP header for a cookie;

Let’s look at the results:

  • in [1]: the request;
  • in [2]: the response is empty;
  • in [3]: the cookie created by the action;

Now let’s create an action to retrieve this cookie, which the browser will now send with every request:


    // ----------------------- Cookie injection ------------------------
    @RequestMapping(value = "/m11", method = RequestMethod.GET)
    public String m10(@CookieValue("cookie1") String cookie1) {
        return cookie1;
}
  • Line 3: The [@CookieValue("cookie1")] annotation retrieves the cookie with the key [cookie1];
  • line 4: this value will be the response sent to the client;

Let's look at the results:

  • in [2], we see that the browser returns the cookie;
  • in [3], the action has successfully retrieved it;

4.11. [/m12]: accessing the body of a POST

POST parameters are usually accompanied by the HTTP header [Content-Type: application/x-www-form-urlencoded]. The entire posted string can be accessed. We create the following action:


    // ----------- retrieve the body of a POST request of type String------------------------
    @RequestMapping(value = "/m12", method = RequestMethod.POST)
    public String m12(@RequestBody String requestBody) {
        return requestBody;
}
  • Line 3: The [@RequestBody] annotation allows you to retrieve the POST body. Here, we assume that it is of type [String];
  • line 4: we return this body to the client;

Here is a first example:

  • in [2], the posted values;
  • in [3], the request’s [Content-Type] HTTP header;
  • in [4], the server's response;

POSTed parameters do not always have the simple form [p1=v1&p2=v2] that we have often used so far. Let’s consider a more complex case:

  • in [2-3]: we enter the posted values in the form [key:value];
  • in [5], the string that was posted;

With the type [Content-Type: application/x-www-form-urlencoded], the posted string must be in the form [p1=v1&p2=v2]. If we want to post anything, we’ll use the type [Content-Type: text/plain]. Here’s an example:

  • in [2-3], we create the HTTP header [Content-Type]. By default [5], this is the one that will be used instead of the one defined in [6]. The attribute [charset=utf-8] is important. Without it, we lose the accented characters in the posted string;
  • in [4], the posted string that we correctly retrieve in [7];

4.12. [/m13, /m14]: retrieving values posted in JSON

It is possible to post parameters with the HTTP header [Content-Type: application/json]. We create the following action:


    // ----------------------- retrieve the JSON body of a POST
    @RequestMapping(value = "/m13", method = RequestMethod.POST, consumes = "application/json")
    public String m13(@RequestBody Person person) {
        return person.toString();
}
  • Line 2: [consumes = "application/json"] specifies that the action expects a JSON body;
  • line 3: [@RequestBody] represents this body. This annotation has been associated with an object of type [Person]. The JSON body will be automatically deserialized into this object;
  • line 4: we use the [Person].toString() method to return something other than the JSON string sent;

Here is an example:

  • in [2], the posted JSON string;
  • in [3], the [Content-Type] of the request;
  • in [4], the server's response;

You can do the same thing differently:


    // ----------------------- retrieve the JSON body of a POST 2 -------------------
    @RequestMapping(value = "/m14", method = RequestMethod.POST, consumes = "text/plain")
    public String m14(@RequestBody String requestBody) throws JsonParseException, JsonMappingException, IOException {
        Person person = new ObjectMapper().readValue(requestBody, Person.class);
        return person.toString();
}
  • line 2: we specified that the method expects a stream of type [text/plain]. Spring MVC will then treat the request body as a [String] type (line 3);
  • line 4: the JSON string is deserialized into a [Person] object (see section 9.7, page 542);

The results are as follows:

  • in [3], make sure to use [text/plain];

4.13. [/m15]: retrieve the session

Let’s revisit the execution architecture of an action:

The controller class is instantiated at the start of the client request and destroyed at the end of it. Therefore, it cannot be used to store data between requests, even if it is called repeatedly. You may want to store two types of data:

  • data shared by all users of the web application. This is generally read-only data;
  • data shared among requests from the same client. This data is stored in an object called a Session. We refer to this as the client session to denote the client's memory. All requests from a client have access to this session. They can store and read information from it.

Above, we show the types of memory that an action has access to:

  • the application’s memory, which mostly contains read-only data and is accessible to all users;
  • a specific user’s memory, or session, which contains read/write data and is accessible to successive requests from the same user;
  • not shown above, there is a request memory, or request context. A user’s request may be processed by several successive actions. The request context allows Action 1 to pass information to Action 2.

Let’s look at a first example illustrating these different types of memory:


    // ----------------------- retrieve the session ------------------------
    @RequestMapping(value = "/m15", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m15(HttpSession session) {
        // retrieve the object with key [counter] from the session
        Object objCounter = session.getAttribute("counter");
        // convert it to an integer to increment it
        int iCounter = objCounter == null ? 0 : (Integer) objCounter;
        iCounter++;
        // Put it back into the session
        session.setAttribute("counter", iCounter);
        // return it as the result of the action
        return String.valueOf(iCounter);
}

Spring MVC maintains the user's session in an object of type [HttpSession].

  • line 3: we ask Spring MVC to inject the [HttpSession] object into the action parameters;
  • line 5: we retrieve an attribute named [counter] from it. A session behaves like a dictionary, a set of [key, value] pairs. If the key [counter] does not exist in the session, we get a null pointer;
  • Line 7: The value associated with the key [counter] will be of type [Integer];
  • line 8: increment the counter;
  • line 10: Update the counter in the session;
  • line 12: the counter value is sent to the client;

When [/m15] is executed for the:

  • first time, line 12, the counter will have the value 1;
  • the second time, line 5 will retrieve this value 1 and set it to 2;
  • ...

Here is an example of execution:

  • in [1], we indeed get the first value of the counter;
  • in [2], the server has sent a session cookie. It has the key [JSESSIONID] and a value that is a unique string of characters for each user. Remember that the browser always sends back the cookies it receives. So when we request the action [/m15] a second time, the client will send this cookie back, which will allow the server to recognize it and link it to its session. This is how the user’s session is maintained;

Let’s look at the second request:

  • in [3], we see that the client sends the session cookie. Note that in the server’s response, this session cookie is no longer present. It is now the client that sends it to be recognized;
  • In [4], the second value of the counter. It has indeed been incremented;

4.14. [/m16]: retrieving a [session] scope object

We may want to put all the data from a user’s session into a single object and place only that object in the session. We’ll take this approach. We’ll put the counter into the following [SessionModel] object:

  

package istia.st.springmvc.models;

import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionModel {

    private int counter;

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }

}
  • line 7: the [@Component] annotation is a Spring annotation (line 5) that makes the [SessionModel] class a component whose lifecycle is managed by Spring;
  • line 8: the [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] annotation is also a Spring annotation (lines 3–4). When Spring MVC encounters it, the corresponding class is created and placed in the user’s session. The attribute [proxyMode = ScopedProxyMode.TARGET_CLASS] is important. It is thanks to this that Spring MVC creates one instance per user rather than a single instance for all users (singleton);
  • line 11: the counter;

For this new Spring component to be recognized, the application configuration must be checked in the [Application] class:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • Line 9: Spring components are searched for in the [istia.st.springmvc.controllers] package. This is no longer sufficient. We update this line as follows:

@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })

We have added the package containing the [SessionModel] class.

Now, we add the following action:


    @Autowired
    private SessionModel session;
    
    // ------ manage a session scope object [Autowired] -----------
    @RequestMapping(value = "/m16", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m16() {
        session.setCounter(session.getCounter() + 1);
        return String.valueOf(session.getCounter());
}
  • Lines 1-2: The Spring [SessionModel] component is injected [@Autowired] into the controller. Recall that a Spring controller is a singleton. It is therefore paradoxical to inject a component with a narrower scope—in this case, [Session] scope—into it. This is where the [@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)] annotation on the [SessionModel] component comes into play. Every time the controller code accesses the [session] field in line 2, a proxy method is executed to return the session of the request currently being processed by the controller;
  • line 6: the [HttpSession] object is no longer needed in the action parameters;
  • line 7: the counter is retrieved and incremented;
  • line 8: its value is returned;

Here is an example of execution:

The first time

The second time

Now, let’s take another browser to represent a second user. Here, we’ll use the Opera browser:

Above in [1], this second user retrieves a counter value of 1. This shows that their session and that of the first user are different. If we look at the client/server exchanges (Ctrl-Shift-I for Opera as well), we see in [2] that this second user has a session cookie different from that of the first user. This is what ensures the independence of the sessions.

4.15. [/m17]: Retrieving an [application] scope object

Let’s revisit the execution architecture of an action:

We know how to create the user session. We will now create an [application]-scope object whose content will be read-only and accessible to all users. We introduce the [ApplicationModel] class, which will serve as the [application]-scope object:

 

package istia.st.springmvc.models;

import java.util.concurrent.atomic.AtomicLong;

import org.springframework.stereotype.Component;

@Component
public class ApplicationModel {

    // counter
    private AtomicLong counter = new AtomicLong(0);

    // getters and setters
    public AtomicLong getCounter() {
        return counter;
    }

    public void setCounter(AtomicLong counter) {
        this.counter = counter;
    }

}
  • Line 5: The [@Component] annotation ensures that the [ApplicationModel] class will be a component managed by Spring. The default nature of Spring components is the [singleton] type: the component is created as a single instance when the Spring container is instantiated, i.e., generally when the application starts. We can use this lifecycle to store configuration information in the singleton that will be accessible to all users;
  • line 11: a counter of type [AtomicLong]. This type has an atomic method called [incrementAndGet]. This means that a thread executing this method is guaranteed that another thread will not read the counter’s value (Get) between its own read (Get) and its increment (increment) by the first thread, which would cause errors since two threads would read the same counter value, and the counter, instead of being incremented by two, would be incremented by one;

We create the following new action [/m17]:


@Autowired
    private ApplicationModel application;

    // ----- manage an application-scoped object [Autowired] ------------------------
    @RequestMapping(value = "/m17", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
    public String m17() {
        return String.valueOf(application.getCounter().incrementAndGet());
    }
  • lines 1-2: we inject the [ApplicationModel] component into the controller. It is a singleton. Therefore, each user will have a reference to the same object;
  • Line 7: We return the [application] scope counter after incrementing it;

Here are two examples, one with Chrome and the other with Opera:

Above, we see that both browsers worked with the same counter, which was not the case with the session. These two browsers represent two different users who both have access to the [application] scope data. Generally speaking, we should avoid placing read/write information in [application] scope objects, as was done above with the counter. Indeed, the execution threads of different users access [application] scope data simultaneously. If there is writable information, write access must be synchronized, as was done above with the [AtomicLong] type. Concurrent access is a source of programming errors. Therefore, it is preferable to place only read-only information in [application] scope objects.

4.16. [/m18]: Retrieving a [session] scope object with [@SessionAttributes]

There is another way to retrieve [session] scope information. We will place the following object in the session:


package istia.st.springmvc.models;

public class Container {
    // the counter
    public int counter = 10;

    // the getters and setters
    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }
}

We will use this object with the following two actions:


    // using [@SessionAttribute] ----------------------
    @RequestMapping(value = "/m18", method = RequestMethod.GET)
    public void m18(HttpSession session) {
        // Here we set the [container] key in the session
        session.setAttribute("container", new Container());
    }

    // Using [@ModelAttribute] ----------------------
    // The [container] key from the session will be injected here
    @RequestMapping(value = "/m19", method = RequestMethod.GET)
    public String m19(@ModelAttribute("container") Container container) {
        container.setCounter(1 + container.getCounter());
        return String.valueOf(container.getCounter());
    }
  • lines 3–6: the [/m18] action returns no result. It is used solely to create an object in the session with the key [container];
  • line 11: In the [/m19] action, the [@ModelAttribute] annotation is used. The behavior of this annotation is quite complex. The [container] parameter of this annotation can refer to various things, and in particular to a session object. For this to work, the object must have been declared with a [@SessionAttributes] annotation on the class itself:

@RestController
@SessionAttributes({"container"})
public class ActionModelController {
  • Line 2 above designates the [container] key as part of the session attributes;

To summarize:

  • in [/m18], the [container] key is placed in the session;
  • the [@SessionAttributes({"container"})] annotation ensures that this key can be injected into a parameter annotated with [@ModelAttribute("container")];
  • not visible in the following execution example, but information annotated with [@ModelAttribute] is automatically part of the M model passed to the V view;

Here is an example of execution. First, we put the [container] key into the session with action [/m18] [1]. Next, we call action [/m19] twice to see the counter increment.

4.17. [/m20-/m23]: injecting data with [@ModelAttribute]

Consider the following new action:


    // the p attribute will be part of all view [Model] models ----------------
    @ModelAttribute("p")
    public Person getPerson() {
        return new Person(7, "abcd", 14);
    }

    // ---------------instantiation of @ModelAttribute --------------------------
    // will be injected if it is in the session
    // will be injected if the controller has defined a method for this attribute
    // may come from the URL fields if a String-to-attribute-type converter exists
    // otherwise, it is constructed using the default constructor
    // then the model attributes are initialized with the GET or POST parameters
    // the final result will be part of the model produced by the action
    
    // the p attribute is injected into the arguments------------------------
    @RequestMapping(value = "/m20", method = RequestMethod.GET)
    public Person m20(@ModelAttribute("p") Person person) {
        return person;
}
  • Lines 2–5: define a model attribute named [p]. This is the model M of a view V, represented by a [Model] type in Spring MVC. A model behaves like a dictionary of [key, value] pairs. Here, the key [p] is associated with the [Person] object constructed by the [getPerson] method. The method name can be anything;
  • line 17: the model attribute with key [p] is injected into the action’s parameters. This injection follows the rules in lines 8–12. Here, we are in the case defined on line 9. Therefore, on line 17, the parameter [Person person] will be the object [Person(7, 'abcd', 14)];
  • Line 18: The [person] object is returned for validation. It will be serialized into JSON before being sent to the client.

Here is an example:

 

Now, let's examine the following action:


    // --------- The p attribute is automatically part of the M model of the V view
    @RequestMapping(value = "/m21", method = RequestMethod.GET)
    public String m21(Model model) {
        return model.toString();
}

An action that wants to display a view V must construct its model M. Spring MVC manages this using a [Model] type that can be injected into the action’s parameters. Initially, this model is empty or contains information tagged with the [@ModelAttribute] annotation. The action may or may not enrich this model before passing it to a view.

  • Line 3: injection of the model M;
  • line 4: we want to see what’s inside. We serialize it into a string to send it to the client. Here, the [Person.toString] method will be used. It must therefore exist;

Here is an execution:

 

Above, we see that the instructions:


    @ModelAttribute("p")
    public Person getPerson() {
        return new Person(7, "abcd", 14);
}

have created an entry [p, Person(7, "abcd", 14)] in the model. This is always the case.

Now consider the following case:


    // otherwise, it is constructed using the default constructor
    // then the model attributes are initialized with the GET or POST parameters

with the following action:


    // --------- the model attribute [param1] is part of the model but is not initialized
    @RequestMapping(value = "/m22", method = RequestMethod.GET)
    public String m22(@ModelAttribute("param1") String p1, Model model) {
        return model.toString();
}
  • line 3: the key model attribute [param1] does not exist. In this case, the associated type must have a default constructor. This is the case here for the [String] type, but we cannot write [@ModelAttribute("param1") Integer p1] because the [Integer] class does not have a default constructor;
  • Line 4: We return the model to see if the key model attribute [param1] is part of it;

Here is an example of execution:

 

The model attribute [param1] is indeed present in the model, but the [toString] method of the associated value does not provide any information about this value.

Now consider the following action, where we explicitly place information in the model:


    // --------- the model attribute [param2] is explicitly placed in the model
    @RequestMapping(value = "/m23", method = RequestMethod.GET)
    public String m23(String p2, Model model) {
        model.addAttribute("param2", p2);
        return model.toString();
}
  • Line 4: The value [p2] retrieved on line 3 is added to the model under the key [param2]:

Here is an example of execution:

 

The rules change if the action parameter is an object. Here is a first example:


    // ------ the model attribute [unePersonne] is automatically placed in the model
    @RequestMapping(value = "/m23b", method = RequestMethod.GET)
    public String m23b(@ModelAttribute("unePersonne") Personne p1, Model model) {
        return model.toString();
}

The action does not modify the model provided to it. The result is as follows:

We can see that the annotation [@ModelAttribute("unePersonne") Personne p1] has added the person [p1] to the model, associated with the key [unePersonne].

Now let's consider the following action:


    // --------- the person p1 is automatically added to the model
    // -------- with the key being the name of its class with the first character lowercase
    @RequestMapping(value = "/m23c", method = RequestMethod.GET)
    public String m23c(Person p1, Model model) {
        return model.toString();
}
  • line 4: we did not include the [@ModelAttribute] annotation;

The result is as follows:

We can see that the presence of the [Person p1] parameter has placed the person [p1] in the model, associated with the key [person], which is the name of the [Person] class with the first character lowercase.

4.18. [/m24]: validating the action model

Consider the following action model [ActionModel01]:

 

package istia.st.springmvc.models;

import javax.validation.constraints.NotNull;

public class ActionModel01 {

    // data
    @NotNull
    private Integer a;
    @NotNull
    private Double b;

    // getters and setters
...
    }
  • Lines 8 and 9: The [@NotNull] annotation is a validation constraint that specifies that the annotated data cannot be null;

Let's now examine the following action:


    // ----------------------- model validation ------------------------
    @RequestMapping(value = "/m24", method = RequestMethod.GET)
    public Map<String, Object> m24(@Valid ActionModel01 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // any errors?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            // iterate through the list of errors
            for (FieldError error : result.getFieldErrors()) {
                buffer.append(String.format("[%s:%s:%s:%s:%s]", error.getField(), error.getRejectedValue(),
                        String.join(" - ", error.getCodes()), error.getCode(), error.getDefaultMessage()));
            }
            map.put("errors", buffer.toString());
        } else {
            // no errors
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
}
  • Line 3: An [ActionModel01] object will be instantiated and its fields [a, b] initialized with parameters of the same names. The [@Valid] annotation indicates that validation constraints must be checked. The results of this validation will be placed in the [BindingResult] parameter (second parameter). The following validations will take place:
    • due to the [@NotNull] annotations, the parameters [a] and [b] must be present;
    • because of the [Integer a] type, the [a] parameter, which is inherently of type [String], must be convertible to type [Integer];
    • Because of the [Double b] type, the [b] parameter, which is inherently of type [String], must be convertible to a [Double] type;

With the [@Valid] annotation, validation errors will be reported in the [BindingResult result] parameter. Without the [@Valid] annotation, validation errors cause the action to crash, and the server sends the client an HTTP response with a 500 status (Internal Server Error).

  • Line 3: The action’s result is of type [Map]. The JSON string of this result will be sent to the client. We construct two types of dictionaries:
    • in case of failure, a dictionary with an entry ['errors', value] where [value] is a string describing all the errors (line 13);
    • in case of success, a dictionary with an entry ['data', value] where [value] is itself a dictionary with two entries: ['a', value], ['b', value] (line 19);
  • lines 9–12: for each detected error [error], the string [error.getField(), error.getRejectedValue(), error.Codes, error.getDefaultMessage()] is constructed:
    • the first element is the erroneous field, [a] or [b],
    • the second element is the rejected value, [x] for example,
    • the third element is a list of error codes. We will look at their roles shortly;
    • the fourth element is the error code. It is part of the previous list;
    • the last element is the default error message. In fact, there can be multiple error messages;

Here are some examples of execution:

Above, we see that:

  • the assignment of 'x' to the field [ActionModel01.a] failed, and the error message explains why;
  • the assignment of 'y' to the field [ActionModel01.b] failed, and the error message explains why;

Note the error codes for field [a]: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. We will revisit these error codes when it comes time to customize the error message. Note that the error code is [typeMismatch].

Another example:

Here, the parameters [a] and [b] were not passed. The [@NotNull] validators in the action model [ActionModel01] then did their job;

Finally, correct values:

4.19. [m/24]: customizing error messages

Let’s return to a screenshot from the previous example:

Above, we see the default error messages. Clearly, we cannot keep these in a real application. It is possible to customize these error messages. To do this, we will use the error codes. Above, we see that the error for the [a] field has the following codes: [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch]. These error codes range from the most specific to the least specific:

  • [typeMismatch.actionModel01.a]: type error on the [a] field of type [ActionModel01];
  • [typeMismatch.a]: type error on a field named [a];
  • [typeMismatch.java.lang.Integer]: type error on an Integer type;
  • [typeMismatch]: type error;

We also note that the error code for the [a] field obtained via [error.getCode()] is [typeMismatch] (see screenshot above).

We will place the error messages in a properties file:

  

The [messages.properties] file above will be as follows:


NotNull=The field cannot be empty
typeMismatch=Invalid format
typeMismatch.model01.a=The [a] parameter must be an integer

Each line has the following format:

    key=message

Here, the key will be an error code and the message will be the error message associated with that code.

Let’s review the error codes for the two fields:

  • [typeMismatch.actionModel01.a - typeMismatch.a - typeMismatch.java.lang.Integer - typeMismatch], when parameter [a] is invalid;
  • [typeMismatch.actionModel01.b - typeMismatch.b - typeMismatch.java.lang.Double - typeMismatch:typeMismatch ] when the [b] parameter is invalid;
  • [NotNull.actionModel01.a - NotNull.a - NotNull.java.lang.Integer - NotNull] when the parameter [a] is missing;
  • [NotNull.actionModel01.b - NotNull.b - NotNull.java.lang.Double - NotNull] when parameter [b] is missing;

The [messages.properties] file must contain an error message for all possible error cases. In the case where

  • parameters [a] and [b] are missing, the code [NotNull] will be used;
  • if parameter [a] is incorrect, we have included messages for two codes [typeMismatch.actionModel01.a, typeMismatch]. We will see which one is used;
  • if parameter [b] is incorrect, the code [typeMismatch] will be used;

To use the [messages.properties] file, you must configure Spring:

  

We remove the configuration annotations from the [Application] class:


package istia.st.springmvc.main;

import org.springframework.boot.SpringApplication;

public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Config.class, args);
    }
}
  • Line 8: The Spring Boot application is launched. The first parameter of the static method [SpringApplication.run] is the class that now configures the application;

The [Config] class is as follows:


package istia.st.springmvc.main;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }
}
  • lines 11–13: the configuration annotations that were previously in the [Application] class are now here;
  • line 14: to configure a Spring MVC application, you must extend the [WebMvcConfigurerAdapter] class;
  • line 15: the [@Bean] annotation introduces a Spring component, a singleton;
  • line 16: we define a bean named [messageSource] (the method name). This bean is used to define the application’s message files and must have this name;
  • Lines 17–19: Tell Spring that the message file:
    • is located in the [i18n] folder within the project’s classpath (line 18),
    • is named [messages.properties] (line 18). In fact, the term [messages] is the root of the message file names rather than the name itself. We will see that in the context of internationalization, there can be multiple message files, one per supported locale. Thus, , we might have [messages_fr.properties] for French and [messages_en.properties] for English. The suffixes added to the [messages] root are standardized. You cannot use just anything;

In the STS project, the [i18n] folder must be placed in the resources folder because it is added to the project’s classpath:

  

To use this file, we create the following new action:


// Form validation, error message handling ------------------------
    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
        // the results dictionary
        Map<String, Object> map = new HashMap<String, Object>();
        // the Spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // locale
        Locale locale = RequestContextUtils.getLocale(request);
        // Any errors?
        if (result.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (FieldError error : result.getFieldErrors()) {
                // Look up the error message based on the error codes
                // the message is searched for in the message files
                // error codes as an array
                String[] codes = error.getCodes();
                // as a string
                String listCodes = String.join(" - ", codes);
                // search
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // Did we find it?
                if (msg == null) {
                    throw new Exception(String.format("Please enter a message for one of the codes [%s]", listCodes));
                }
                // Found - add the error message to the error message string
                buffer.append(String.format("[%s:%s:%s:%s]", locale.toString(), error.getField(), error.getRejectedValue(),
                        String.join(" - ", msg)));
            }
            map.put("errors", buffer.toString());
        } else {
            // ok
            Map<String, Object> mapData = new HashMap<String, Object>();
            mapData.put("a", data.getA());
            mapData.put("b", data.getB());
            map.put("data", mapData);
        }
        return map;
    }

This code is similar to that of the [/m24] action. Here are the differences:

  • line 3: we inject the request [HttpServletRequest request] into the action’s parameters. We’ll need it;
  • lines 7–8: we retrieve the Spring context. This context contains all the Spring beans in the application. It also provides access to the message files;
  • line 10: we retrieve the application locale. This term is explained in more detail below;
  • lines 15–31: For each error, we search for a message corresponding to one of these error codes. They are searched for in the order of the codes found in [error.getCodes()]. As soon as a message is found, we stop;
  • line 26: how to retrieve a message from [messages.properties]:
    • the first parameter is the code searched for in [messages.properties],
    • the second is an array of parameters, as messages are sometimes parameterized. This is not the case here,
    • the third is the locale used (obtained on line 10). The locale specifies the language used, [fr_FR] for French (France), [en_US] for English (US). The message is looked for in messages_[locale].properties, so for example [messages_fr_FR.properties]. If this file does not exist, the message is looked for in [messages_fr.properties]. If this file does not exist, the message is looked for in [messages.properties]. It is this last case that will work for us;
  • lines 25–29: somewhat unexpectedly, when searching for a non-existent code in a message file, an exception is thrown rather than a null pointer;
  • lines 33–35: We handle the case where there is no error message;
  • lines 37–38: we construct the error string. In it, we include the locale and the error message found;

Here are some examples of execution:

 

We see that:

  • the application locale is [fr_FR]. This is a default value since we haven’t done anything to initialize it;
  • the message used for both fields is as follows:
NotNull=The field cannot be empty

Another example:

 

We see that:

  • the error message used for the parameter [a] is as follows:

typeMismatch.actionModel01.a=Parameter [a] must be an integer
  • the error message used for parameter [b] is as follows:
typeMismatch=Invalid format

Why are there two different messages? For parameter [a], there were two possible messages:


typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer

The error codes were examined in the order specified by the [error.getCodes()] array. It turns out that this order goes from the most specific code to the most general one. That is why the code [typeMismatch.model01.a] was found first.

4.20. [/m25]: Internationalizing a Spring MVC Application

Now that we know how to customize error messages in French, we would also like to have them in English, which brings us to the internationalization of a Spring MVC application. To handle this, we will expand the configuration class [Config] to look like this:


package istia.st.springmvc.main;

import java.util.Locale;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

@Configuration
@ComponentScan({ "istia.st.springmvc.controllers", "istia.st.springmvc.models" })
@EnableAutoConfiguration
public class Config extends WebMvcConfigurerAdapter {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("i18n/messages");
        return messageSource;
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
    }

    @Bean
    public CookieLocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("lang");
        localeResolver.setDefaultLocale(new Locale("fr"));
        return localeResolver;
    }
}
  • Lines 28–32: We create a request interceptor. A request interceptor extends the [HandlerInterceptor] interface. Such a class inspects the incoming request before it is processed by an action. Here, the [localeChangeInterceptor] will look for a parameter named [lang] in the incoming GET or POST request and will change the application’s locale based on that parameter. Thus, if the parameter is [lang=en_US], the application’s locale will become US English;
  • lines 34–37: we override the [WebMvcConfigurerAdapter.addInterceptors] method to add the previous interceptor;
  • lines 39–45: are used to configure how the locale will be encapsulated in a cookie. We know that a cookie can serve as user memory, since the client browser systematically sends it back to the server. The previous [localeChangeInterceptor] interceptor creates a cookie encapsulating the locale. Line 42 gives this cookie the name [lang]. The cookie is also used to change the locale;
  • line 43: specifies that if the [lang] cookie is absent, the locale will be [fr];

In summary, the locale for a request can be set in two ways:

  • by passing a parameter named [lang];
  • by sending a cookie named [lang]. This cookie is automatically created after the previous method is executed;

To use this locale, we will create message files for the [fr] and [en] locales:

 

The [messages_fr.properties] file is as follows:


NotNull=The field cannot be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=The [a] parameter must be an integer

The [messages_en.properties] file is as follows:


NotNull=The field cannot be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer

The [messages.properties] file is a copy of the [messages_en.properties] file. Note that the [messages.properties] file is used when no file matching the request's locale is found. In our case, if the user sends a [lang=en] parameter, since the [messages_en.properties] file does not exist, the [messages.properties] file will be used. The user will therefore see messages in English.

Let’s try it. First, in Chrome’s developer tools (Ctrl-Shift-I), check your cookies:

 

If you have a cookie named [lang], delete it. Then, in Chrome, navigate to the URL [http://localhost:8080/m25]:

 

The browser sent the following HTTP headers:

GET /m25 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36
Referer: http://localhost:8080/m25
Accept-Encoding: gzip, deflate, sdch
Accept-Language: fr-FR,fr;q=0.8,en-US;q=0.6,en;q=0.4

We can see that in these headers, there is no [lang] cookie. In this case, our code uses the [fr] locale. This is shown in the screenshot. Let’s try another case:

  • in [1], we passed the [lang=en] parameter to set the locale to [en];
  • in [2], we see the new locale;
  • in [3], the message is now in English;

Now let’s look at the HTTP exchanges:

 

We can see above that the server sent back a [lang] cookie. This has an important consequence: the locale for the next request will be [en] again because of the [lang] cookie that will be sent back by the browser. We should therefore keep the messages in English. Let’s verify this:

 

Above, we see that the locale has remained [en]. Because of the cookie that the browser systematically sends, it will remain that way until the user changes it by sending the [lang] parameter as follows:

 

4.21. [/m26]: injecting the locale into the action template

In the previous example, we saw one way to retrieve the locale from the request:


    @RequestMapping(value = "/m25", method = RequestMethod.GET)
    public Map<String, Object> m25(@Valid ActionModel01 data, BindingResult result, HttpServletRequest request)
            throws Exception {
...
        // locale
        Locale locale = RequestContextUtils.getLocale(request);
// any errors?

The locale can be directly injected into the action parameters. Here is an example:


    @RequestMapping(value = "/m26", method = RequestMethod.GET)
    public String m26(Locale locale) {
        return String.format("locale=%s", locale.toString());
}
 

As shown above, the validity of the requested locale is not checked. However, the browser's subsequent request triggers a server-side exception because the locale cookie it receives is incorrect.

4.22. [/m27]: Validating a model with Hibernate Validator

Consider the following new action:


    //validating a model with Hibernate Validator ------------------------
    @RequestMapping(value = "/m27", method = RequestMethod.POST)
    public Map<String, Object> m27(@Valid ActionModel02 data, BindingResult result) {
        Map<String, Object> map = new HashMap<String, Object>();
        // Any errors?
        if (result.hasErrors()) {
            // iterate through the list of errors
            for (FieldError error : result.getFieldErrors()) {
                map.put(error.getField(),
                        String.format("[message=%s, codes=%s]", error.getDefaultMessage(), String.join("|", error.getCodes())));
            }
        } else {
            // no errors
            map.put("data", data);
        }
        return map;
}

Here we have code we've seen several times now:

  • line 3: the action [/m27] is requested via a POST;
  • lines 8–11: each error will be identified by [field, message] with:
    • field: the field with the error,
    • message: the associated error message and the list of error codes;
  • line 14: if there are no errors, the JSON string of the posted values is returned;

Line 3: the following action model [ActionModel02] is used:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel02 {

    @NotNull(message = "This field is required")
    @AssertFalse(message = "Only the value [false] is accepted")
    private Boolean assertFalse;
    
    @NotNull(message = "This field is required")
    @AssertTrue(message = "Only the value [true] is accepted")
    private Boolean assertTrue;
    
    @NotNull(message = "This field is required")
    @Future(message = "The date must be after today")
    private Date dateInFuture;
    
    @NotNull(message = "This field is required")
    @Past(message = "The date must be earlier than today")
    private Date dateInPast;
    
    @NotNull(message = "This field is required")
    @Max(value = 100, message = "Maximum 100")
    private Integer intMax100;
    
    @NotNull(message = "This field is required")
    @Min(value = 10, message = "Minimum 10")
    private Integer intMin10;
    
    @NotNull(message = "This field is required")
    @NotBlank(message = "The string must not be empty")
    private String strNotBlank;
    
    @NotNull(message = "This field is required")
    @Size(min = 4, max = 6, message = "The string must be between 4 and 6 characters")
    private String strBetween4and6;
    
    @NotNull(message = "This field is required")
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$", message = "The format must be hh:mm:ss")
    private String hhmmss;
    
    @NotNull(message = "This field is required")
    @Email(message = "Invalid address")
    private String email;
    
    @NotNull(message = "This field is required")
    @Length(max = 4, min = 4, message = "The string must be exactly 4 characters long")
    private String str4;
    
    @Range(min = 10, max = 14, message = "The value must be in the range [10,14]")
    @NotNull(message = "This field is required")
    private Integer int1014;
    
    @URL(message = "Invalid URL")
    private String url;

    // getters and setters

...
}

The class uses validation constraints from two packages:

  • [javax.validation.constraints] on lines 5–13;
  • [org.hibernate.validator.constraints] on lines 15–19;

The Maven dependencies for these two packages are included in the project:

  

Here, we won't be using internationalized messages, but rather messages defined within the constraint using the [message] attribute. To test this action, we'll use [Advanced Rest Client]:

  • in [1-2], the POST request;
  • in [3], the HTTP header [Content-Type] to use;
  • in [4], the [Add new value] link allows you to add a [parameter, value] pair;
  • in [5], enter a field from [ActionModel02], here the [assertFalse] field:

    @NotNull(message = "This field is required")
    @AssertFalse(message = "Only the value [false] is accepted")
private Boolean assertFalse;
  • In [6], enter an incorrect value to see an error message. Above, the [@AssertFalse] constraint requires that the [assertFalse] field have the value [false];
  • in [7], the server's response: the [@NotNull] constraint for empty fields was triggered, and the associated error message was returned;
  • in [8], the message for the [assertFalse] field for which the [@AssertFalse] constraint was not satisfied, along with the error codes. Note that these codes can be associated with internationalized messages;

Here is another example:

 

Image

The reader is invited to test the various error cases until posting all valid data:

Note: the date format is the Anglo-Saxon format: mm/dd/yyyy.

4.23. [/m28]: Externalizing error messages

In the [ActionModel02] class, we hard-coded the messages. It is preferable to externalize them into message files. We follow the example of the [/m25] action. We create the following new action model [ActionModel03]:

  

package istia.st.springmvc.models;

import java.util.Date;

import javax.validation.constraints.AssertFalse;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Future;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.URL;

public class ActionModel03 {

    @NotNull
    @AssertFalse
    private Boolean assertFalse;
    
    @NotNull
    @AssertTrue
    private Boolean assertTrue;
    
    @NotNull
    @Future
    private Date dateInFuture;
    
    @NotNull
    @Past
    private Date dateInPast;
    
    @NotNull
    @Max(value = 100)
    private Integer intMax100;
    
    @NotNull
    @Min(value = 10)
    private Integer intMin10;
    
    @NotNull
    @NotBlank
    private String strNotBlank;
    
    @NotNull
    @Size(min = 4, max = 6)
    private String strBetween4and6;
    
    @NotNull
    @Pattern(regexp = "^\\d{2}:\\d{2}:\\d{2}$")
    private String hhmmss;
    
    @NotNull
    @Email
    private String email;
    
    @NotNull
    @Length(max = 4, min = 4)
    private String str4;
    
    @Range(min = 10, max = 14)
    @NotNull
    private Integer int1014;
    
    @URL
    private String url;

    // getters and setters
        ...
}

Error messages are externalized in the [messages.properties] files:

  

The [messages_fr.properties] file is as follows:


NotNull=The field cannot be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=The value must be in the range [10,14]
NotBlank.actionModel03.strNotBlank=The string must not be blank
AssertFalse.actionModel03.assertFalse=Only the value [false] is accepted
Pattern.actionModel03.hhmmss=The format must be hh:mm:ss
Past.actionModel03.dateInPast=The date must be on or before today
Future.actionModel03.dateInFuture=The date must be later than today
Length.actionModel03.str4=The string must be exactly 4 characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only the value [true] is accepted
Email.actionModel03.email=Invalid address
Size.actionModel03.strBetween4and6=The string must be between 4 and 6 characters long
URL.actionModel03.url=Invalid URL

Error messages have been added to lines 4–16. They are in the following format:

code=message

The codes cannot be arbitrary. They are the ones displayed in the previous [/m27] action. For example:

Image

In the message files, you must use one of the four codes above for the [int1014] field.

The [messages_en.properties] file is as follows:


NotNull=The field cannot be empty
typeMismatch=Invalid format
typeMismatch.actionModel01.a=Parameter [a] must be an integer
Range.actionModel03.int1014=Value must be in the [10,14] range
NotBlank.actionModel03.strNotBlank=String cannot be empty
AssertFalse.actionModel03.assertFalse=Only the boolean value [false] is allowed
Pattern.actionModel03.hhmmss=String format is hh:mm:ss
Past.actionModel03.dateInPast=Date must be on or before today's date
Future.actionModel03.dateInFuture=Date must be after today's date
Length.actionModel03.str4=String must be four characters long
Min.actionModel03.intMin10=Minimum 10
Max.actionModel03.intMax100=Maximum 100
AssertTrue.actionModel03.assertTrue=Only boolean [true] is allowed
Email.actionModel03.email=Invalid email
Size.actionModel03.strBetween4and6=String must be between four and six characters long
URL.actionModel03.url=Invalid URL

The action model [ActionModel03] is used by the following action:


// ----------------------- externalization of error messages ------------------------
    @RequestMapping(value = "/m28", method = RequestMethod.POST)
    public Map<String, Object> m28(@Valid ActionModel03 data, BindingResult result, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<String, Object>();
        // The Spring application context
        WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
        // locale
        Locale locale = RequestContextUtils.getLocale(request);
        // Any errors?
        if (result.hasErrors()) {
            for (FieldError error : result.getFieldErrors()) {
                // Look up the error message based on the error codes
                // Search for the message in the message files
                // error codes as an array
                String[] codes = error.getCodes();
                // as a string
                String listCodes = String.join(" - ", codes);
                // search
                String msg = null;
                int i = 0;
                while (msg == null && i < codes.length) {
                    try {
                        msg = ctx.getMessage(codes[i], null, locale);
                    } catch (Exception e) {

                    }
                    i++;
                }
                // Did we find it?
                if (msg == null) {
                    msg = String.format("Enter a message for one of the codes [%s]", listCodes);
                }
                // Found it - add the error to the dictionary
                map.put(error.getField(), msg);
            }
        } else {
            // no errors
            map.put("data", data);
        }
        return map;
    }

We have already discussed this type of code. The only thing that really matters is line 23: the error message returned depends on the locale of the request.

Here is an example in French:

and now in English: