C H A P T E R  5

Implementing Controllers

Controllers play a crucial role in a web application: they execute the actual request, prepare the model, and select a view to render. In conjunction with the dispatcher servlet, controllers also play a crucial role in the request processing workflow. The controller is the glue between the core application and the web interface to the application. In this chapter, we will take a look at the two different controller approaches and cover the out-of-the-box implementations provided with the Spring Framework.

This chapter will also take a look at the supporting components for request processing. For example, we will cover form submission and how to apply internationalization (I18N).

Introducing Controllers

The controller is the component that is responsible for responding to the action the user takes. This action could be a form submission, clicking a link, or simply accessing a page. The controller selects or updates the data needed for the view. It also select the name of the view to render or can render the view itself. With Spring MVC, we have two options when writing controllers. We can either implement an interface or put an annotation on the class. The interface is org.springframework.web.servlet.mvc.Controller, and the annotation is org.springframework.stereotype.Controller. The main focus of this book is the annotation-based approach (aka Spring @MVC) for writing controllers. However, we feel that we still need to mention the interface-based approach.

Although both approaches work for implementing a controller, there are two major differences between them. The first difference is about flexibility, and the second is about mapping URLs to controllers. Annotation-based controllers allow for very flexible method signatures, whereas the interface-based approach has a predefined method on the interface that we must implement. Getting access to other interesting collaborators is harder (but not impossible!).

For the interface-based approach, we must do explicit external mapping of URLs to these controllers; in general, this approach is combined with an org.springframework.web.servlet.handler .SimpleUrlHandlerMapping, so that all the URLs are in a single location. Having all of the URLs in a single location is one advantage the interface-based approach has over the annotation-based approach. The annotation-based approach has its mappings scattered throughout the codebase, which makes it harder to see which URL is mapped to which request-handling method. The advantage of annotation-based controllers is that, when you open a controller, you can see which URLs it is mapped to.

In this section, we will show how to write both types of controllers, as well as how to configure basic view controllers.

Interface-based Controllers

To write an interface-based controller, we need to create a class that implements the Controller interface. Listing 5-1 shows the API for that interface. When implementing this interface, we must implement the handleRequest method. This method needs to return an org.springframework.web.servlet.ModelAndView object or null when the controller handles the response itself.

Listing 5-1. The Controller Interface

package org.springframework.web.servlet.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;

public interface Controller {
    ModelAndView handleRequest(HttpServletRequest request,
                               HttpServletResponse response) throws Exception;
}

Let’s take a look at a small sample. If we take the com.apress.prospringmvc.bookstore.web.IndexController and create an interface-based controller out of it, it would look something like what you see in Listing 5-2. We implement the handleRequest method and return an instance of ModelAndView with a view name.

Listing 5-2. An Interface-based IndexController

package com.apress.prospringmvc.bookstore.web;
// javax.servlet imports omitted
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

public class IndexController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request,
                                      HttpServletResponse response)
    throws Exception {
        return new ModelAndView("index");
    }
}

In addition to writing this controller, we would need to configure an instance of org.springframework.web.servlet.HandlerMapping to map /index.htm to this controller (see Chapter 3 for more information). We would also need to make sure that there is an org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter registered to execute the interface-based controllers (this is registered by default).

The sample given here is quite straightforward. Now image a controller that has some page flow. In that case, we would need to check whether the request is a GET or POST request; based on that, we would need to execute different controller logic. With large controllers, this can become quite cumbersome.

Table 5-1 shows the Controller implementations that ship with the framework. Many of these are deprecated (as of Spring 3.0) or can be considered deprecated in favor of the newer annotation-based controllers. Check the descriptions of each controller for deprecation notes.

Image

Image

Annotation-based Controllers

To write an annotation-based controller, we need to write a class and put the Controller annotation on that class. Also, we need to add an org.springframework.web.bind.annotation.RequestMapping annotation to the class, a method, or both. Listing 5-3 shows an annotation-based approach to our IndexController.

Listing 5-3. An Annotation-based IndexController

package com.apress.prospringmvc.bookstore.web;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class IndexController {

    @RequestMapping(value = "/index.htm")
    public ModelAndView indexPage() {
        return new ModelAndView(“index");
    }
}

The controller contains a method with the RequestMapping annotation, and it specifies that it should be mapped to the /index.htm URL, which is the request-handling method. The method has no required parameters, and we can return anything we want; for now, we want to return a ModelAndView.

The mapping is in the controller definition, and we need an instance of a HandlerMapping to interpret these mappings. There are two implementations that can help us out here: the org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping and the org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping. The first is one of the defaults registered by the org.springframework.web.servlet.DispatcherServlet. The second is one of the defaults registered by Spring @MVC (which we enabled with the org.springframework.web.servlet.config.EnableWebMvc annotation). We are going to use the Spring @MVC default because it is both more powerful and flexible than the DefaultAnnotationHandlerMapping. We will see that power and flexibility throughout the book.

Configuring View Controllers

The two controller samples we have written so far are called view controllers. They don’t select data; rather, they only select the view name to render. If we had a large application with more of these views, it would become quite cumbersome to maintain and write these. Spring MVC can help us out here, enabling us simply to add an org.springframework.web.servlet.mvc.ParameterizableViewController to our configuration and to configure it accordingly. We would need to configure an instance to return index as a view name and map it to the /index.htm URL. Listing 5-4 shows what needs to be added to make this work.

Listing 5-4. A ParameterizableViewController Configuration

package com.apress.prospringmvc.bookstore.web.config;

import org.springframework.web.servlet.mvc.ParameterizableViewController;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

// Other imports ommitted

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

    // Other methods ommitted

    @Bean(name = "/index.htm")
    public Controller index() {
        ParameterizableViewController index;
        index = new ParameterizableViewController();
        index.setViewName("index");
        return index;
    }
}

So how does it work? We create the controller, set the view name to return, and then explicitly give it the name of /index.htm (see the highlighted parts). The explicit naming makes it possible for the org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping to pick up our controller and map it to the URL. However, if this were to grow significantly larger, then we would need to create quite a few of these methods. Again, Spring MVC is here to help us. Because we have enabled the new Spring MVC configuration, we can utilize it to our advantage. We can override the addViewControllers method (one of the methods of the org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter) and simply register our view names to certain URLs. In Listing 5-5 shows how to do this.

Listing 5-5. A ViewController Configuration

package com.apress.prospringmvc.bookstore.web.config;

import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

// Other imports ommitted

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

  // Other methods ommitted
    @Override
    public void addViewControllers(final ViewControllerRegistry registry) {
        registry.addViewController("/index.htm").setViewName("index");
    }
}

The result is the same. A ParameterizableViewController is created and mapped to the /index.htm URL (see Figure 5-1). However, the second approach is easier and less cumbersome to use than the first one.

Image

Figure 5-1. The index page

Request-Handling Methods

Writing request-handling methods can be a challenge. For example, how should you map a method to an incoming request? Several things could be a factor here, including the URL, the method used (e.g., GET or POST1), the availability of parameters or HTTP headers2, or even the request content type or the content type (e.g., XML, JSON, or HTML) to be produced. All these can influence which method is selected to handle the request.

The first step in writing a request-handling method is to put an org.springframework.web.bind.annotation.RequestMapping annotation on the method. This mapping is detected by the org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping to create the mapping of incoming URLs to the correct method (see the “Spring MVC Components” section in Chapter 4 for more information on handler mapping). Next, we need to specify which web request we want to execute the specified handler.

The annotation can be put on both the type (the controller) and the method level. We can use the one on the type level to do some coarse-grained mapping (e.g., the URL), and then use the annotation on the method level to further specify when to execute the method (e.g., a GET or POST request).

Table 5-2shows which attributes we can set on the RequestMapping annotation and how they influence the mapping.

Image

Image

________________

In Table 5-3, there are a couple of sample mappings that also show the effect of class- and method-level matching. As already mentioned, the RequestMapping annotation on the class applies to all methods in the controller. This mechanism can be used to do coarse-grained mapping on the class level and finer-grained mapping on the method level.

Image

Image

________________

3 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html

Supported Method Argument Types

A request-handling method can have various method arguments and return values. Most arguments mentioned in Table 5-4can be used in arbitrary order. However, there is a single exception to that rule: the org.springframework.validation.BindingResult argument. That argument has to follow a model object that we use to bind request parameters to.

Image

Image

Image

RedirectAttributes

The org.springframework.web.servlet.mvc.support.RedirectAttributes deserve a little more explanation than what is shown in Table 5-4. With RedirectAttributes, it is possible to declare exactly which attributes are needed for the redirect. By default, all model attributes are exposed when doing a redirect. Because a redirect always leads to a GET request, all primitive model attributes (or collections/arrays of primitives) will be encoded as request parameters. However, with the annotated controllers, there are also objects in the model (like the path variables and other implicit values) that don’t need to be exposed and that are outside of our control.

The RedirectAttributes can help us out here. When this is used as a method argument and a redirect is being issued, only the attributes added to the RedirectAttributes instance are going to be added to the URL.

In addition to specifying attributes encoded in the URL, it is also possible to specify so called flash attributes. Flash attributes are attributes that are stored before the redirect and retrieved and made available as model attributes after the redirect. This is done by using the configured org.springframework.web.servlet.FlashMapManager. The use of flash attributes is useful for objects that cannot be encoded (non-primitive objects) or to keep URLs clean.

UriComponentsBuilder

The UriComponentsBuilder provides a mechanism for building and encoding URIs. It can take a URL pattern and replace or extend variables. This can be done for relative or absolute URLs. This mechanism is particularly useful when creating URLs, as opposed to cases where we need to think about encoding parameters or doing string concatenation ourselves. This component handles all these things in a consistent manner for us. The code in Listing 5-6 creates the /book/detail/42 URL.

Listing 5-6. The UriComponentsBuilder Sample Code

UriComponentsBuilder.fromPath("/book/detail/{bookId}");
.build();
.expand("42")
.encode()

The sample given is quite simple; however, it is possible to specify more variables (e.g., bookId) and replace them (e.g., specify the port or host). There is also the ServletUriComponentsBuilder subclass, which we can use to operate on the current request. For example, we might use it to replace, not only path variables, but also request parameters.

Supported Method Argument Annotations

In addition to explicitly supported types (as mentioned in the previous section), there are also a couple of annotations that we can use to annotate our method arguments (see Table 5-5). Some of these can also be used with the method argument types mentioned in Table 5-4. In that case, they are used to specify what the name of the attribute in the request, cookie, header, or response must be, as well as whether the parameter is required.

All the parameter values are converted to the argument type by using type conversion. The type-conversion system uses an org.springframework.core.convert.converter.Converter or PropertyEditor to convert from a String type to the actual type.

Image

All these different method argument types and annotations allow us to write very flexible request-handling methods. However, we could extend this mechanism by extending the framework. Resolving those method argument types is done by various implementations of the org.springframework.web.method.support.HandlerMethodArgumentResolver. Listing 5-7 shows that interface. If we want, we can create our own implementation of this interface and register it with the framework. You can find more information on this in Chapter 7.

Listing 5-7. The HandlerMethodArgumentResolver Interface

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodArgumentResolver {

    boolean supportsParameter(MethodParameter parameter);

    Object resolveArgument(MethodParameter parameter,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest,
                           WebDataBinderFactory binderFactory)
                               throws Exception;
}

________________

4 http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

Let’s take a closer look at all the different annotation types we can use. All these annotations have a few attributes that we can set and that have default values or may be required.

All of the annotations mentioned in Table 5-5 have a value attribute. This value attribute refers to the name of the object to use (what it applies to depends on the annotation). If this value isn’t filled, then the fallback is to use the name of the method argument. This fallback is only usable if the classes are compiled with debug information.5 An exception to this rule occurs when using the ModelAttribute annotation. Instead of the name of the method argument, it infers the name from the type of argument, using the simple classname as the argument name. If the type is an array or collection, it makes this plural by adding List. If we were to use our com.apress.prospringmvc.bookstore.domain.Book as an argument, then the name would be book; if it were an array or collection, then it would become bookList.

RequestParam

The RequestParam annotation can be placed on any argument in a request-handling method. When present, it is used to retrieve a parameter from the request. When put on a Map, there is some special handling, depending on whether the name attribute is set. If the name is set, the value is retrieved and converted into a Map. For conversion (see the “Data Binding” section in this chapter for more information), if no name is given, all request parameters are added to the map as key/value pairs.

Image

________________

RequestHeader

The RequestHeader annotation can be placed on any method argument. It is used to bind a method argument to a request header. When placed on a Map, all available request headers are put in the map as key/value pairs. If it is placed on another type of argument, then the value is converted into the type by using a org.springframework.core.convert.converter.Converter or PropertyEditor (see the “Data Binding” section for more information).

Image

RequestBody

The RequestBody annotation is used to mark a method parameter we want to bind to the body of the web request. The body is converted into the method parameter type by locating and calling an org.springframework.http.converter.HttpMessageConverter. This converter is selected based on the requests content-type. If no converter is found, an org.springframework.web.HttpMediaTypeNotSupportedException is thrown. By default, this leads to a response with code 415 (SC_UNSUPPORTED_MEDIA_TYPE) being send to the client.

Optionally, method parameters can also be annotated with javax.validation.Valid or org.springframework.validation.annotation.Validated to enforce validation for the created object. You can find more information on validation in the “Validation of Model Attributes” section later in this chapter.

RequestPart

When the RequestPart annotation is put on a method argument of the type javax.servlet.http.Part, org.springframework.web.multipart.MultipartFile (or on a collection or array of the latter,) then we will get the content of that file (or group of files) injected. If it is put on any other argument type, the content is passed through an org.springframework.http.converter.HttpMessageConverter for the content type detected on the file. If no suitable converter is found, then an org.springframework.web.HttpMediaTypeNotSupportedException is thrown.

Image

ModelAttribute

The ModelAttribute annotation can be placed on method arguments, as well as on methods. When placed on a method argument, it is used to bind this argument to a model object. When placed on a method, that method is used to construct a model object, and this method will be called before request-handling methods are called. These kinds of methods can be used to create an object to be edited in a form or to supply data needed by a form to render itself. (For more information, see the “Data Binding” section.)

Image

PathVariable

The PathVariable annotation can be used in conjunction with path variables. Path variables can be used in a URL pattern to bind the URL to a variable. Path variables are denoted as {name}in our URL mapping. If we were to use a URL mapping of /book/{isbn}/image, then isbn would be available as a path variable.

Image

CookieValue

This CookieValue annotation can be placed on any argument in the request-handling method. When present, it is used to retrieve a cookie. When placed on an argument of type javax.servlet.http.Cookie, we get the complete cookie. Otherwise, the value of the cookie is converted into the argument type.

Image

Supported Method Return Values

In addition to all the different method argument types, a request handling method can also have one of several different return values. Table 5-12 lists the default supported and handling of method return values for request handling methods.

Image

Image

When an arbitrary object is returned and there is no ModelAttribute annotation present, the framework tries to determine a name to use as the name for the object in the model. It basically takes the simple name of the class (the classname without the package) and lowercases the first letter. For example, the name of our com.apress.prospringmvc.bookstore.domain.Book becomes book. When the return type is a collection or array, it becomes the simple name of the class, suffixed with List. Thus a collection of Book objects becomes bookList.

This same logic is applied when we use a Model or ModelMap to add objects without an explicit name. This also has the advantage of using the specific objects, instead of a plain Map to gain access to the underlying implicit model.

Although the list of supported return values is already quite extensive, we can use the flexibility and extensibility of the framework to create our own handler. The method’s return values are handled by an implementation of the org.springframework.web.method.support.HandlerMethodReturnValueHandler interface (see Listing 5-8).

Listing 5-8. The HandlerMethodReturnValueHandler Interface

package org.springframework.web.method.support;

import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;

public interface HandlerMethodReturnValueHandler {

    boolean supportsReturnType(MethodParameter returnType);

    void handleReturnValue(Object returnValue,
                           MethodParameter returnType,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest) throws Exception;
}

Writing Annotation-based Controllers

Let’s take some of the theory we’ve developed thus far and apply it to our controllers. For example, all the menu options we have on our page lead to a 404 error, which indicates that the page cannot be found.

In this section, we are going to add some controllers and views to our application. We will start by creating a simple login controller operating with the request and request parameters. Next, we will add a book search page that uses an object. And finally, we will conclude by building a controller that retrieves and shows the details of a book.

A Simple Login Controller

Before we can start writing our controller, we need to have a login page. In the WEB-INF/views directory, we create a file named login.jsp. The resulting structure should look like the one shown in Figure 5-2.

Image

Figure 5-2. The directory structure after adding login.jsp

The login page needs some content, as shown in Listing 5-9.

Listing 5-9. The login page, Login.jsp

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<c:if test="${exception ne null}">
    <div class="error">${exception.message}</div>
</c:if>
<form action="<c:url value="/login"/>" method="post">
    <fieldset>
        <legend>Login</legend>
        <table>
        <tr>
            <td>Username</td>
            <td>
                <input type="text" id="username" name="username"
                        placeholder="Usename"/></td>
        </tr>
        <tr>
            <td>Password</td>
            <td>
                <input type="password" id="password" name="password"
                       placeholder="Password"/></td>
        </tr>
        <tr><td colspan="2" align="center">
            <button id="login">Login</button>
        </td></tr>
        </table>
    </fieldset>
</form>

In addition to the page, we need to have a controller and map it to /login. Let’s create the com.apress.prospringmvc.bookstore.web.controller.LoginController and start by having it render our page (see Listing 5-10).

Listing 5-10. The Initial LoginController

package com.apress.prospringmvc.bookstore.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value = "/login")
public class LoginController {

    @RequestMapping(method = RequestMethod.GET)
    public String login() {
        return "login";
    }
}

After the application has been redeployed and we click the Login button, we should see a page like the one shown in Figure 5-3.

Image

Figure 5-3. The login page

If we now enter the username and password (jd/secret) and press the Login button, we are greeted with an error page (error code 405) that indicates that the method (POST) is not supported. This is correct because our controller doesn’t yet have a method that handles a POST request. So, let’s add a method to our controller that actually handles our login. Listing 5-11 shows the modified controller.

Listing 5-11. The Modified LoginController

package com.apress.prospringmvc.bookstore.web.controller;

// Other imports omitted, see Listing 5-10

import org.springframework.beans.factory.annotation.Autowired;

import com.apress.prospringmvc.bookstore.domain.Account;
import com.apress.prospringmvc.bookstore.service.AccountService;
import com.apress.prospringmvc.bookstore.service.AuthenticationException;

@Controller
@RequestMapping(value = "/login")
public class LoginController {

    public static final String ACCOUNT_ATTRIBUTE = "account";

    @Autowired
    private AccountService accountService;

    @RequestMapping(method = RequestMethod.GET)
    public String login() {
        return "login";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String handleLogin(HttpServletRequest request, HttpSession session)
    throws AuthenticationException {
        try {
            String username = request.getParameter("username");
            String password = request.getParameter("password");
            Account account = this.accountService.login(username, password);
            session.setAttribute(ACCOUNT_ATTRIBUTE, account);
            return "redirect:/index.htm";
        } catch (AuthenticationException ae) {
            request.setAttribute("exception", ae);
            return "login";
        }
    }
}

Before we move on, let’s drill down on how the handleLogin method works. The username and password parameters are retrieved from the request, and these are used to call the login method on the AccountService. If the correct credentials are supplied, we get an Account instance for the user (which we store in the session), and then we redirect to the index page. If the credentials are not correct, the service throws an AuthenticationException, which, for now, is handled by the controller. The exception is stored as a request attribute, and we return the user to the login page.

Although the current controller does its work, we are still operating directly on the HttpServletRequest. This is a quite cumbersome (but sometimes necessary) approach; however, we would generally want to avoid this and use the flexible method signatures to make our controllers simpler. With that in mind, let’s modify the controller and limit our use of directly accessing the request (see Listing 5-12).

Listing 5-12. The LoginController with RequestParam

package com.apress.prospringmvc.bookstore.web.controller;

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

// Other imports omitted, see Listing 5-11

@Controller
@RequestMapping(value = "/login")
public class LoginController {

    // Other methods omitted

    @RequestMapping(method = RequestMethod.POST)
    public String handleLogin(@RequestParam String username,
                              @RequestParam String password,
                              HttpServletRequest request,
                              HttpSession session)
            throws AuthenticationException {
        try {
            Account account = this.accountService.login(username, password);
            session.setAttribute(ACCOUNT_ATTRIBUTE, account);
            return "redirect:/index.htm";
        } catch (AuthenticationException ae) {
            request.setAttribute("exception", ae);
            return "login";
        }
    }
}

Using the RequestParam annotation simplified our controller. However, our exception handling dictates that we still need access to the request. This will change in the next chapter when we implement exception handling.

There is still one drawback with this approach, and that is our lack of support for the Back button in a browser. If we go back a page, we will get a nice popup asking if we want to resubmit the form. It is a common approach to do a redirect after a POST6 request; that way, we can work around the double submission problem. In Spring, we can address this by using RedirectAttributes. Listing 5-13 highlights the final modifications to our controller in bold.

Listing 5-13. The LoginController with RedirectAttributes

package com.apress.prospringmvc.bookstore.web.controller;

// Other imports omitted, see Listing 5-11

import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping(value = "/login")
public class LoginController {

    // Other methods omitted

    @RequestMapping(method = RequestMethod.POST)
    public String handleLogin(@RequestParam String username,Image
                              @RequestParam String password,Image
                              RedirectAttributes redirect,Image
                              HttpSession session) Image
    throws AuthenticationException {
        try {
            Account account = this.accountService.login(username, password);
            session.setAttribute(ACCOUNT_ATTRIBUTE, account);
            return "redirect:/index.htm";
        } catch (AuthenticationException ae) {
            redirect.addFlashAttribute("exception", ae);
            return "redirect:/login";
        }
    }
}

________________

When the application is redeployed and we log in, typing in the wrong username/password combination will still raise an error message; however, when we press the Back button, the popup request for a form submission is gone.

Until now, everything we have done is quite low level. Our solutions include working with the request and/or response directly or through a bit of abstraction with the org.springframework.web.bind.annotation.RequestParam. However, we work in an object-oriented programming language, and where possible, we want to work with objects. We will explore this in the next section.

Book Search Page

We have a bookstore, and we want to sell books. At the moment, however, there is nothing in our web application that allows the user to search for or even see a list of books. Let’s address this by creating a book search page, so that the users of our application can search for books.

First, we create a directory book in the /WEB-INF/views directory. In that directory, we create a file called search.jsp. This file is our search form, and it will also display the results of the search. The code for this can be seen in Listing 5-14.

Listing 5-14. The Search Page Form

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<form method="GET" action="<c:url value="/book/search"/>">
    <fieldset>
        <legend>Search Criteria</legend>
        <table>
            <tr>
                <td><label for="title">Title</label></td>
                <td><input id="title" name="title" /></td>
            </tr>
        </table>
    </fieldset>
    <button id="search">Search</button>
</form>

<c:if test="${not empty bookList}">
    <table>
        <tr><th>Title</th><th>Description</th><th>Price</th></tr>
        <c:forEach items="${bookList}" var="book">
            <tr>
                <td>${book.title}</td>
                <td>${book.description}</td>
                <td>${book.price}</td>
            </tr>
        </c:forEach>
    </table>
</c:if>

The page consists of a form with a field to fill in a (partial) title that will be used to search for books. When there are results, we will show a table to the user containing the results. Now that we have a page, we also need a controller that can handle the requests. Listing 5-15 shows the initial com.apress.prospringmvc.bookstore.web.controller.BookSearchController.

Listing 5-15. The BookSearchController with Search

package com.apress.prospringmvc.bookstore.web.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.apress.prospringmvc.bookstore.domain.BookSearchCriteria;
import com.apress.prospringmvc.bookstore.service.BookstoreService;

import javax.servlet.http.HttpServletRequest

@Controller
public class BookSearchController {

    @Autowired
    private BookstoreService bookstoreService;

    @RequestMapping(value = "/book/search", method = RequestMethod.GET)
    public String list(Model model, HttpServletRequest request) {
        BookSearchCriteria criteria = new BookSearchCriteria();
        criteria.setTitle(request.getParameter("title");
        model.addAttribute(this.bookstoreService.findBooks(criteria));
        return "book/search";
    }
}

The controller will react on the URL; retrieve the title parameter from the request (this is the name of the field in our page, as shown in Listing 5-13); and finally, proceed with a search. The results of the search are put in the model. Initially it will display all the books; however, as soon as a title is entered, it will limit the results based on that title (see Figure 5-4).

Image

Figure 5-4. The book search page showing the results

As mentioned earlier, working with the HttpServletRequest directly isn’t necessary in most cases. Let’s make our search method a little simpler by putting the com.apress.prospringmvc.bookstore.domain.BookSearchCriteria in the list of method arguments (see Listing 5-16).

Listing 5-16. The BookSearchController with BookSearchCriteria as a Method Argument

package com.apress.prospringmvc.bookstore.web.controller;

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

// Other imports omitted, see Listing 5-15

@Controller
public class BookSearchController {

    @Autowired
    private BookstoreService bookstoreService;

    @RequestMapping(value = "/book/search", method = RequestMethod.GET)
    public String list(Model model, BookSearchCriteria criteria) {
        model.addAttribute(this.bookstoreService.findBooks(criteria));
        return "book/search";
    }
}

With Spring MVC, this is what we call data binding. To enable data binding, we needed to modify our com.apress.prospring.bookstore.web.controller.BookSearchController so it uses a method argument, instead of working with the request directly (see Listing 5-14). Alternatively, it could use RequestParam to retrieve the parameters and set them on the object. This will force Spring to use data binding on the criteria method argument. Doing so will map all request parameters with the same name as one of our object’s properties to that object (i.e., the request parameter title will be mapped to the property title). Using data binding will greatly simplify our controller (you can find more in-depth information on this in the “Data Binding” section of this chapter).

We can do even better! Instead of returning a String, we could return something else. For example, let’s modify our controller to return a collection of books. This collection is added to the model with the name bookList, as explained earlier in this chapter. Listing 5-16 shows this controller, but where do we select the view to render? It isn’t explicitly specified. In Chapter 4, we mentioned that the org.springframework.web.servlet.RequestToViewNameTranslator kicks in if there is no explicitly mentioned view to render. We see that mechanism working here. It takes the URL (http://[server]:[port]/chapter5-bookstore/book/search); strips the server, port, and application name; removes the suffix (if any); and then uses the remaining book/search as the name of the view to render (exactly what we have been returning).

Listing 5-17. The BookSearchController Alternate Version

package com.apress.prospringmvc.bookstore.web.controller;

// Other imports omitted, see Listing 5-15

@Controller
public class BookSearchController {

    @Autowired
    private BookstoreService bookstoreService;

    @RequestMapping(value = "/book/search", method = RequestMethod.GET)
    public Collection<Book> list(BookSearchCriteria criteria ) {
        return this.bookstoreService.findBooks(criteria);
    }
}

Book Detail Page

Now let’s put some more functionality into our search page. For example, let’s make the title of a book a link that navigates to a book’s details page that shows an image of and some information about the book. We’ll start by modifying our search.jsp and adding links (see Listing 5-18).

Listing 5-18. The Modified Search Page

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<form method="POST" action="<c:url value="/book/search"/>">
    <fieldset>
        <legend>Search Criteria</legend>
        <table>
            <tr>
                <td><label for="title">Title</label></td>
                <td><input id="title" name="title"/></td>
            </tr>
        </table>
    </fieldset>
    <button id="search">Search</button>
</form>

<c:if test="${not empty bookList}">
    <table>
        <tr><th>Title</th><th>Description</th><th>Price</th></tr>
        <c:forEach items="${bookList}" var="book">
            <tr>
                <td><a href="<c:url value="/book/detail/${book.id}"/>">${book.title}</a></td>
                <td>${book.description}</td>
                <td>${book.price}</td>
            </tr>
        </c:forEach>
    </table>
</c:if>

The highlighted line is the only change we need to make to this page. At this point, we have generated a URL based on the id of the book, so we should get a URL like /book/detail/4 that shows us the details of the book with id 4. Let’s create a controller to react to this URL and extract the id from the URL (see Listing 5-19).

Listing 5-19. The BookDetailController

package com.apress.prospringmvc.bookstore.web.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import com.apress.prospringmvc.bookstore.domain.Book;
import com.apress.prospringmvc.bookstore.service.BookstoreService;

@Controller
public class BookDetailController {

    @Autowired
    private BookstoreService bookstoreService;

    @RequestMapping(value = "/book/detail/{bookId}")
    public String details(@PathVariable("bookId") long bookId, Model model) {
        Book book = this.bookstoreService.findBook(bookId);
        model.addAttribute(book);
        return "book/detail";
    }
}

The highlighted code is what makes the extraction of the id possible. This is the org.springframework.web.bind.annotation.PathVariable in action. The URL mapping contains the {bookId} part, which tells Spring MVC to bind that part of the URL to a path variable called bookId. We can then use the annotation to retrieve the path variable again. In addition to the controller, we also need a JSP to show the details. The code in Listing 5-20 creates a detail.jsp in the book directory.

Listing 5-20. The Book’s Details Page

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<c:url value="/resources/images/books/${book.isbn}/book_front_cover.png" var="bookImage"/>
<img src="${bookImage}" align="left" alt="${book.title}" width="250"/>

<table>
    <tr><td>Title</td><td>${book.title}</td></tr>
    <tr><td>Description</td><td>${book.description}</td></tr>
    <tr><td>Author</td><td>${book.author}</td></tr>
    <tr><td>Year</td><td>${book.year}</td></tr>
    <tr><td>ISBN</td><td>${book.isbn}</td></tr>
    <tr><td>Price</td><td>${book.price}</td></tr>
</table>

If we click one of the links from the search page after redeployment, we should be greeted with a details page that shows an image of and some information about the book (see Figure 5-5).

Image

Figure 5-5. The book’s details page

Data Binding

In this section, we will explore the benefits and possibilities of using data binding, including how we can configure and extend it. However, we’ll begin by explaining the basics of data binding. Listing 5-21 shows our com.apress.prospringmvc.bookstore.domain.BookSearchCriteria JavaBean. It is a simple object with two properties: title and category.

Listing 5-21. The BookSearchCriteria JavaBean

package com.apress.prospringmvc.bookstore.domain;

public class BookSearchCriteria {

    private String title;
    private Category category;

    public String getTitle() {
        return this.title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
    public void setCategory(Category category) {
        this.category = category;
    }

    public Category getCategory() {
        return this.category;
    }
}

Assume we receive the following request: http://localhost:8080/chapter5-bookstore/book/search?title=Agile. In this case, the title property receives the value of Agile. Behind the scenes, Spring calls the setTitle method on our JavaBean, which we specified as a method argument in the list method on our controller. If there were a parameter named category in the request, then Spring would call the setCategory method; however, it would first try to convert the parameter (which is always a String) into a com.apress.prospring.bookstore.domain.Category JavaBean.

However, data binding isn’t limited to simple setter methods. We can also bind to nested properties and even to indexed collections like maps, arrays, and lists. Nested binding happens when the parameter name contains a dot (.); for instance, address.street=Somewhere leads to getAddress().setStreet("Somewhere").

To bind to indexed collections, we must use a notation with square brackets in which we enclose the index. When using a map, this index doesn’t have to be a numeric. For instance, list[2].name would bind a name property on the third element in the list. Similarly, map['foo'].name would bind the name property to the value under the key foo in the map.

Customizing Data Binding

We have two options for customizing the behavior of data binding: globally or per controller. Of course, we can mix both strategies together by performing a global setup, and then fine-tuning it per controller.

Global Customization

To customize data binding globally, we need to create a class that implements the org.springframework.web.bind.support.WebBindingInitializer interface. Spring MVC provides a configurable implementation of this interface, the org.springframework.web.bind.support.ConfigurableWebBindingInitializer. An instance of the interface must be registered with the handler mapping implementation, so that it can be used. After an instance of org.springframework.web.bind.WebDataBinder is created, the initBinder method of the org.springframework.web.bind.support.WebBindingInitializer is called.

The provided implementation allows us to set a couple of properties. When a property is not set, it uses the defaults as specified by the org.springframework.web.bind.WebDataBinder. If we were to want to specify more properties, it would be quite easy to extend the default implementation and add the desired behavior. It is possible to set the same properties here as in the controller (see Table 5-13).

Image

When we want to extend the configuration provided by Spring @MVC, we need to do some extending and overriding. This is because the default configuration already configures a org.springframework.web.bind.support.ConfigurableWebBindingInitializer; however, it doesn’t expose it as a bean. Instead, we need to extend org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport and override the requestMappingHandlerAdapter method (see Listing 5-22).

Listing 5-22. Reusing the ConfigurableWebBindingInitializer

public class WebMvcContextConfiguration extends WebMvcConfigurationSupport {
    @Override
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
        ConfigurableWebBindingInitializer webBindingInitializer;Image
        webBindingInitializer = (ConfigurableWebBindingInitializer) Image
adapter.getWebBindingInitializer();
        webBindingInitializer.setDirectFieldAccess(true);
        //Do other re-configuration
        return adapter;
    }
}

Per Controller Customization

For the per controller option, we must implement a method in the controller and put the org.springframework.web.bind.annotation.InitBinder annotation on that method. The method must have no return value (void) and at least an org.springframework.web.bind.WebDataBinder as a method argument. The method can have the same arguments as a request-handling method. However, it cannot have a method argument with the org.springframework.web.bind.annotation.ModelAttribute annotation. This is because the model is available after binding; and in this method, we are going to configure how we bind.

The org.springframework.web.bind.annotation.InitBinder annotation has a single attribute named value that can take the model attribute names or request parameter names this init-binder method is going to apply to. The default is to apply to all model attributes and request parameters.

To customize binding, we need to configure our org.springframework.web.bind.WebDataBinder. This object has several configuration options (setter methods) that we can use, as shown in Table 5-14.

Image

Image

In addition to setting these properties, we can also tell the org.springframework.web.bind.WebDataBinder to use bean property access (the default) or direct field access. This can be done by calling the initBeanPropertyAccess or initDirectFieldAccess method to set property access or direct field access, respectively. The advantage of direct field access is that we don’t have to write getter/setters for each field we want to use for binding. Listing 5-23 shows an example init-binder method.

Listing 5-23. An Example init-binder Method

package com.apress.prospringmvc.bookstore.web.controller;

//Imports omitted

@Controller
@RequestMapping(value = "/customer")
public class RegistrationController {

    // Other methods omitted

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.initDirectFieldAccess();
        binder.setDisallowedFields("id");
        binder.setRequiredFields("username", "password", "emailAddress");
    }

}

ModelAttributes

To fully utilize data binding, we have to use model attributes. Furthermore, we should use one of these model attributes as the object our form fields are bound to. In our com.apress.prospringmvc.bookstore.web.controller.BookSearchController, we added an object as a method argument, and Spring used that as the object to bind the request parameters to. However, it is possible to have more control over our objects and how we create objects. For this, we can use the org.springframework.web.bind.annotation.ModelAttribute annotation. This annotation can be put both on a method and on method arguments.

Using ModelAttribute on Methods

We can use the ModelAttribute annotation on methods to create an object to be used in our form (e.g., when editing or updating) or to get reference data (i.e., data that is needed to render the form like a list of categories). Let’s modify our controller to add a list of categories to the model and an instance of a com.apress.prospring.bookstore.domain.BookSearchCriteria object (see Listing 5-24).

Image Caution When a ModelAttribute annotation is put on a method, this method will be called before the request-handling method is called!

Listing 5-24. The BookSearchController with ModelAttribute Methods

package com.apress.prospringmvc.bookstore.web.controller;

// Other imports omitted.

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

@Controller
public class BookSearchController {

    @Autowired
    private BookstoreService bookstoreService;

    @ModelAttribute
    public BookSearchCriteria criteria() {
        return new BookSearchCriteria();
    }

    @ModelAttribute("categories")
    public List<Category> getCategories() {
        return this.bookstoreService.findAllCategories();
    }

    @RequestMapping(value = "/book/search", method = { RequestMethod.GET })
    public Collection<Book> list(BookSearchCriteria criteria) {
        return this.bookstoreService.findBooks(criteria);
    }
}

Methods annotated with ModelAttribute have the same flexibility in method argument types as the request-handling methods. Of course, they shouldn’t operate on the response and cannot have ModelAttribute annotation method arguments. We could also have the method return void; however, we would then need to include an org.springframework.ui.Model, org.springframework.ui.ModelMap or Map as a method argument and explicitly add its value to the model.

The annotation can also be placed on request-handling methods, indicating that the return value of the method is to be used as a model attribute. The name of the view is then derived from the request that uses the configured org.springframework.web.servlet.RequestToViewNameTranslator.

Using ModelAttribute on Method Arguments

When using the annotation on a method argument, the argument is looked up from the model. If it isn’t found, an instance of the argument type is created using the default constructor. Listing 5-25 shows our com.apress.prospring.bookstore.web.controller.BookSearchController with the annotation.

Listing 5-25. The BookSearchController with ModelAttribute Annotation on a Method Argument

package com.apress.prospringmvc.bookstore.web.controller;

// Imports omitted see Listing 5-22

@Controller
public class BookSearchController {

    // Methods omitted see Listing 5-22

    @RequestMapping(value = "/book/search", method = { RequestMethod.GET })
    public Collection<Book> list(@ModelAttribute("bookSearchCriteria") BookSearchCriteriaImage
 criteria) {
        return this.bookstoreService.findBooks(criteria);
    }
}
Using SessionAttributes

It can be beneficial to store a model attribute in the session between requests. For example, imagine we need to edit a customer record. The first request gets the customer from the database. It is then edited in the application, and the changes are submitted back and applied to the customer. If we don’t store the customer in the session, then the customer record must be retrieved again from the database. This can be inconvenient.

In Spring @MVC, you can tell the framework to store certain model attributes in the session. For this, you can use the org.springframework.web.bind.annotation.SessionAttributes annotation (see Table 5-15). You should use this annotation to store model attributes in the session, so they survive multiple HTTP requests. However, you should not use this annotation to store something in the session, and then use the javax.servlet.http.HttpSession to retrieve it. The session attributes are also only usable from within the same controller, so you should not use them as a transport to move objects between controllers. If you need something like that, we suggest that you use Spring Web Flow (see Chapters 10-12).

Image

When using the org.springframework.web.bind.annotation.SessionAttributes annotation to store model attributes in the session, we also need to tell the framework when to remove those attributes. For this, we need to use the org.springframework.web.bind.support.SessionStatus interface (see Listing 5-26). When we finish using the attributes, we need to call the setComplete method on the interface. To access that interface, we can simply include it as a method argument (see Table 5-4).

Listing 5-26. The SessionStatus Interface

package org.springframework.web.bind.support;

public interface SessionStatus {

    void setComplete();

    boolean isComplete();

}
Form Tag Library

To be able to use all the data binding features provided by the framework, we also need to use the tag library to write forms. Spring MVC ships with two tag libraries. The first is a general-purpose library (see Table 5-16), and the second is a library used to simplify writing forms in our pages (see Table 5-17). We can use the general-purpose library to write our forms (this was how it worked with the Spring Framework before 2.0). This is a very powerful approach, but it is also quite cumbersome (albeit it can still be used as a fallback in those corner cases where the form tag library isn’t sufficient).

Image

Image

Image

Image

When using the form tag library, we need to specify which model attribute property we want to bind the form element to. We do this by specifying the path attribute on our form element. There are several properties we can set on the various form tags, but Table 5-18 shows the main properties. For the form tags that use a collection of items (e.g., select, checkboxes, and radiobuttons), there are a few additional shared attributes (see Table 5-19).

Image

Image

We can apply this logic to our order JSP page. The page would still have the same functionality, but it would fully utilize the data binding functionality from Spring MVC (see Listing 5-27). In this case, we simply need to replace the normal input element tags with the form element tags, and then supply the path to bind to. Of course, we also need to add the taglib declaration to the top of the JSP.

Listing 5-27. The Order JSP with Form Tags

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="GET" modelAttribute="bookSearchCriteria">
    <fieldset>
        <legend>Search Criteria</legend>
        <table>
            <tr>
                <td><form:label path="title">Title</form:label></td>
                <td><form:input path="title"/></td>
            </tr>
        </table>
    </fieldset>
    <button id="search">Search</button>
</form:form>

<c:if test="${not empty bookList}">
    <table>
        <tr><th>Title</th><th>Description</th><th>Price</th></tr>
        <c:forEach items="${bookList}" var="book">
            <tr>
                <td><a href="<c:url value="/book/detail/${book.id}"/>">${book.title}</a></td>
                <td>${book.description}</td>
                <td>${book.price}</td>
            </tr>
        </c:forEach>
    </table>
</c:if>

If we redeploy and issue a new search at this point, we see that our title field keeps the previously entered value (see Figure 5-6). This is due to our use of data binding in combination with the form tags.

Image

Figure 5-6. The Title field remains filled

Now it’s time to make things a bit more interesting by adding a dropdown box (a HTML select) to select a category to search for in addition to the title. We already have the categories in our model (see Listing 5-23). We simply want to add a dropdown and bind it to the id field of the category (see Listing 5-28). We add a select tag and tell it which model attribute contains the items to render. We also specify the value and label to show for the each of the items. The value is bound to the model attribute used for the form.

Listing 5-28. The Search Page with a Category

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="GET" modelAttribute="bookSearchCriteria">
  <fieldset>
    <legend>Search Criteria</legend>
    <table>
      <tr>
        <td><form:label path="title">Title</form:label></td>
        <td><form:input path="title"/></td>
      </tr>
      <tr>
        <td><form:label path="category.id">Category</form:label></td>
        <td>
          <form:select path="category.id" items="${categories}" itemValue="id"Image
 itemLabel="name"/>
        </td>
            </tr>
        </table>
    </fieldset>
    <button id="search">Search</button>
</form:form>

// Result table omitted

Type Conversion

An important part of data binding is type conversion. When we receive a request, the only thing we have are String instances. However, in the real world we use a lot of different object types, not just text representations. Therefore, we want to convert those String instances into something we can use, which is where type conversion comes in. In Spring MVC, there are three ways to do type conversion:

  • Property Editors
  • Converters
  • Formatters

Property editors are the old-style of doing type conversion, whereas converters and formatters are the new way of doing type conversion. Converters and formatters are more flexible; as such, they are also more powerful than property editors. In addition, relying on property editors also pulls in the whole java.beans package, including all its support classes, which we just don’t need in a web environment.

Property Editors

Support for property editors has been part of the Spring Framework since its inception. To use this kind of type conversion, we create a PropertyEditor implementation (typically by subclassing PropertyEditorSupport). Property editors take a String and convert it into a strongly typed object—and vice versa. Spring provides several implementations for accomplishing this out of the box (see Table 5-20).

Image

Image

Converters

The converter API in Spring 3 is a general purpose type-conversion system. Within a Spring container, this system is used as an alternative to property editors to convert bean property value strings into the required property type. We can also use this API to our advantage in our application whenever we need to do type conversion. The converter system is a strongly typed conversion system and uses generics to enforce this.

There are four different interfaces that can be used to implement a converter, all of which can be found in the org.springframework.core.convert.converter package:

  • Converter
  • ConverterFactory
  • GenericConverter
  • ConditionalGenericConverter

Let’s explore the four different APIs.

Listing 5-29 shows the Converter API, which is very straightforward. It has a single convert method that takes a source argument and transforms it into a target. The source and target types are expressed by the S and T generic type arguments.

Listing 5-29. The Converter API

package org.springframework.core.convert.converter;

public interface Converter<S, T> {

    T convert(S source);
}

Listing 5-30 shows the ConverterFactory API that is useful when you need to have conversion logic for an entire class hierarchy. For this, we can parameterize S to be type we are converting from (the source), and we parameterize R as the base type we want to convert to. We can then create the appropriate converter inside the implementation of this factory.

Listing 5-30. The ConverterFactory API

package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

When we require more sophisticated conversion logic, we can use the org.springframework.core.convert.converter.GenericConverter (see Listing 5-31). It is more flexible, but less strongly typed than the previous converter types. It supports converting between multiple source and target types. During a conversion, we have access to the source and target type descriptions, which can be useful for complex conversion logic. This also allows for type conversion to be driven by annotation (i.e., we can parse the annotation at runtime to determine what needs to be done).

Listing 5-31. The GenericConverter API

package org.springframework.core.convert.converter;

import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;

import java.util.Set;

public interface GenericConverter {

    Set<ConvertiblePair> getConvertibleTypes();

    Object convert(Object source,
                   TypeDescriptor sourceType,
                   TypeDescriptor targetType);

}

An example of this type of conversion logic would be a converter that converts from an array to a collection. A converter would first inspect the type of element being converted, so that we could apply additional conversion logic to different elements.

Listing 5-32 shows a specialized version of the GenericConverter that allows us to specify a condition for when it should execute. For example, we could create a converter that uses one of the BigDecimals valueOf methods to convert a value, but this would only be useful if we could actually invoke that method with the given sourceType.

Listing 5-32. The ConditionalGenericConverter API

package org.springframework.core.convert.converter;

import org.springframework.core.convert.TypeDescriptor;

    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);

}

The converters are executed behind the org.springframework.core.convert.ConversionService interface (see Listing 5-33); typical implementations of this interface also implement the org.springframework.core.convert.converter.ConverterRegistry interface, which enables the easy registration of additional converters. When using Spring @MVC, there is a preconfigured instance of the org.springframework.format.support.DefaultFormattingConversionService (which also allows for executing and registering formatters).

Listing 5-33. The ConversionService API

package org.springframework.core.convert;

public interface ConversionService {

    boolean canConvert(Class<?> sourceType, Class<?> targetType);

    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);

    <T> T convert(Object source, Class<T> targetType);

    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
Formatters

The Converter API is a general purpose type-conversion system. It is strongly typed and can convert from any object type to another object type (if there is a converter available). However, this is not something we need in our web environment because we only deal with String objects there. On the other hand, we probably want to represent our objects as String to the client, and we might even want to do so in a localized way. This is where the Formatter API comes in (see Listing 5-34). It provides a simple and robust mechanism to convert from a String to a strongly typed object. It is an alternative to property editors, but it is also lighter (e.g., it doesn’t depend on the java.beans package) and more flexible (e.g, it has access to the Locale for localized content).

Listing 5-34. The Formatter API

package org.springframework.format;

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

import java.util.Locale

public interface Printer<T> {
    String print(T object, Locale locale);
}

import java.util.Locale
import java.text.ParseException;

public interface Parser<T> {
    T parse(String text, Locale locale) throws ParseException;
}

To create a formatter, we need to implement the org.springframework.format.Formatter interface and specify the type T as the type we want to convert. For example, imagine we had a formatter that could convert Date instances to text, and vice-versa. We would specify T as Date and use the Locale to determine the specific date format to use for performing the conversion (see Listing 5-35).

Listing 5-35. The Sample DateFormatter

package com.apress.prospringmvc.bookstore.formatter;

// java.text and java.util imports omitted

import org.springframework.format.Formatter;
import org.springframework.util.StringUtils;

public class DateFormatter implements Formatter<Date> {

    private String format;

    @Override
    public String print(Date object, Locale locale) {
        return getDateFormat(locale).format(object);
    }

    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        return getDateFormat(locale).parse(text);
    }

    private DateFormat getDateFormat(Locale locale) {
        if (StringUtils.hasText(this.format)) {
            return new SimpleDateFormat(this.format, locale);
        } else {
            return SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM, locale);
        }
    }

    public void setFormat(String format) {
        this.format = format;
    }
}

Formatters can also be driven by annotations instead of by field type. If we want to bind a formatter to an annotation, we have to implement the org.springframework.format.AnnotationFormatterFactory (see Listing 5-36).

Listing 5-36. The AnnotationFormatterFactory

package org.springframework.format;

public interface AnnotationFormatterFactory<A extends Annotation> {

    Set<Class<?>> getFieldTypes();

    Printer<?> getPrinter(A annotation, Class<?> fieldType);

    Parser<?> getParser(A annotation, Class<?> fieldType);
}

We need to parameterize A with the annotation type we want to associate with it. The getPrinter and getParser methods should return an org.springframework.format.Printer and org.springframework.format.Parser, respectively. We can then use these to convert from or to the annotation type. Let’s imagine we have a com.apress.prospringmvc.bookstore.formatter.DateFormat annotation that we can use to set the format for a date field. We could then implement the factory shown in Listing 5-37.

Listing 5-37. The DateFormatAnnotationFormatterFactory

package com.apress.prospringmvc.bookstore.formatter;

import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Parser;
import org.springframework.format.Printer;

public class DateFormatAnnotationFormatterFactory implementsImage
 AnnotationFormatterFactory<DateFormat> {

    @Override
    public Set<Class<?>> getFieldTypes() {
        Set<Class<?>> types = new HashSet<Class<?>>(1);
        types.add(Date.class);
        return types;
    }

    @Override
    public Printer<?> getPrinter(DateFormat annotation, Class<?> fieldType) {
        return createFormatter(annotation);
    }

    @Override
    public Parser<?> getParser(DateFormat annotation, Class<?> fieldType) {
        return createFormatter(annotation);
    }

    private DateFormatter createFormatter(DateFormat annotation) {
        DateFormatter formatter = new DateFormatter();
        formatter.setFormat(annotation.format());
        return formatter;
    }
}
Configuring Type Conversion

If we want to use an org.springframework.core.convert.converter.Converter or an org.springframework.format.Formatter in Spring MVC, then we need to add some configuration. The org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter has a method for this. The addFormatters method can be overridden to register additional converters and/or formatters. This method has an org.springframework.format.FormatterRegistry (see Listing 5-38) as an argument, and it can be used to register the additional converters and/or formatters (the FormatterRegistry extends the org.springframework.core.convert.converter.ConverterRegistry, which offers the same functionality for Converter implementations).

Listing 5-38. The FormatterRegistry Interface

package org.springframework.format;

import java.lang.annotation.Annotation;

import org.springframework.core.convert.converter.ConverterRegistry;

public interface FormatterRegistry extends ConverterRegistry {

    void addFormatter(Formatter<?> formatter);
    void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
    void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
    void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation>Image
 annotationFormatterFactory);

}

To convert from a String to a com.apress.prospringmvc.bookstore.domain.Category, we will implement an org.springframework.core.convert.converter.GenericConverter (see Listing 5-39) and register it in our configuration (see Listing 5-40). The com.apress.prospringmvc.bookstore.converter.StringToEntityConverter takes a String as its source and transforms it into a configurable entity type. It then uses a javax.persistence.EntityManager to load the record from the database.

Listing 5-39. The StringToEntityConverter

package com.apress.prospringmvc.bookstore.converter;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;

public class StringToEntityConverter implements GenericConverter {

    private static final String ID_FIELD = "id";

    private final Class<?> clazz;

    @PersistenceContext
    private EntityManager em;

    public StringToEntityConverter(Class<?> clazz) {
        super();
        this.clazz = clazz;
    }

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        Set<ConvertiblePair> types = new HashSet<GenericConverter.ConvertiblePair>();
        types.add(new ConvertiblePair(String.class, this.clazz));
        types.add(new ConvertiblePair(this.clazz, String.class));
        return types;
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType,
                          TypeDescriptor targetType) {
        if (String.class.equals(sourceType.getType())) {
            if (StringUtils.isBlank((String) source)) {
                return null;
            }
            Long id = Long.parseLong((String) source);
            return this.em.find(this.clazz, id);
        } else if (this.clazz.equals(sourceType.getType())) {
            try {
                if (source == null) {
                    return "";
                } else {
                    return FieldUtils.readField(source, ID_FIELD, true).toString();
                }
            } catch (IllegalAccessException e) {
            }
        }
        throw new IllegalArgumentException("Cannot convert " + source + " into a suitableImage
 type!");
    }
}

Listing 5-40. The CategoryConverter Configuration

package com.apress.prospringmvc.bookstore.web.config;

... [import ommitted]

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

    …

    @Bean
    public StringToEntityConverter categoryConverter() {
        return new StringToEntityConverter(Category.class);
    }

    @Override
    public void addFormatters(final FormatterRegistry registry) {
        registry.addConverter(categoryConverter());
        registry.addFormatter(new DateFormatter("dd-MM-yyyy"));
    }
    ...
}

In addition to the category conversion, we also need to do date conversions. Therefore, Listing 5-38 also includes an org.springframework.format.datetime.DateFormatter with a pattern for converting dates.

Using Type Conversion

Now that we have covered type conversion, let’s see it in action. We will create the user registration page that allows us to enter the details for the com.apress.prospringmvc.bookstore.domain.Account object. First, we need a web page under WEB-INF/views. Next, we need to create a customer directory and place a register.jsp file in it. The content is included only in part of Listing 5-41 because there is a lot of repetition in this page for all the different fields.

Listing 5-41. The Registration Page

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<form:form method="POST" modelAttribute="account">
    <fieldset>
        <legend>Personal</legend>
        <table>
            <tr>
                <td>
                     <form:label path="firstName" cssErrorClass="error">
                          Firstname
                     </form:label>
                  </td>
                  <td><form:input path="firstName" /></td>
                  <td><form:errors path="firstName"/></td>
             </tr>
             // Other Account fields omitted
        </table>
    <fieldset>
        <legend>Userinfo</legend>
        <table>
            <tr>
                <td><form:label path="username" cssErrorClass="error">
                         Username
                    </form:label></td>
                <td><form:input path="username"/></td>
                <td><form:errors path="username"/></td>
            </tr>
            // Password and emailAddress field omitted.  
         </table>
    </fieldset>
    <button id="save”>Save</button>
</form:form>

We also need a controller for this, so we will create the com.apress.prospringmvc.bookstore.web.controller.RegistrationController. In this controller, we will use a couple of data binding features. First, we will disallow the submission of an id field (to prevent someone from editing another user). Second, we will preselect the user’s country based on the current Locale. Listing 5-42 shows our controller.

Listing 5-42. The RegistrationController

package com.apress.prospringmvc.bookstore.web.controller;

import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.apress.prospringmvc.bookstore.domain.Account;
import com.apress.prospringmvc.bookstore.service.AccountService;

@Controller
@RequestMapping("/customer/register")
public class RegistrationController {

    @Autowired
    private AccountService accountService;

    @ModelAttribute("countries")
    public Map<String, String> countries(Locale currentLocale) {
        Map<String, String> countries = new TreeMap<String, String>();
        for (Locale locale : Locale.getAvailableLocales()) {
            countries.put(locale.getCountry(), locale.getDisplayCountry(currentLocale));
        }
        return countries;
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setDisallowedFields("id");
        binder.setRequiredFields("username","password","emailAddress");
    }

    @RequestMapping(method = RequestMethod.GET)
    @ModelAttribute
    public Account register(Locale currentLocale) {
        Account account = new Account();
        account.getAddress().setCountry(currentLocale.getCountry());
        return account;
    }

    @RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
    public String handleRegistration(@ModelAttribute Account account, BindingResult result) {
        if (result.hasErrors()) {
            return "customer/register";
        }
        this.accountService.save(account);
        return "redirect:/customer/account/" + account.getId();
    }
}

The controller has a lot going on. For example, the initBinder method configures our binding. It disallows the setting of the id property and sets some required fields. We also have a method that prepares our model by adding all the available countries in the JDK to the model. Finally, we have two request-handling methods, one for a GET request (the initial request when we enter our page) and one for POST/PUT requests when we submit our form. Notice the org.springframework.validation.BindingResult attribute next to the model attribute. This is what we can use to detect errors; and based on that, we can redisplay the original page. Also remember that the error tags in the JSP those are used to display error messages for fields or objects (we’ll cover this in more depth  in the upcoming sections). When the application is redeployed and we click the Register link, we should see the page shown in Figure 5-7.

Image

Figure 5-7. The account registration page

If we now enter an invalid date; leave the username, password and e-mail address fields blank; and then submit the form; the same page redisplays with some error messages (see Figure 5-8).

Image

Figure 5-8. The account registration page showing some errors

The error messages are created by the data binding facilities in Spring MVC. Later in this chapter, we will see how we can influence the messages displayed. For now, let’s leave them intact. If we fill in proper information and click Save, we are redirected to an account page (for which we already have provided the basic controller and implementation).

Validating Model Attributes

We’ve already mentioned validation a couple of times. We’ve also referred to the org.springframework.validation package a couple of times. Validating our model attributes is quite easy to accomplish with the validation abstraction from the Spring Framework. Validation isn’t bound to the web; it is about validating objects. Therefore, validation can also be used outside the web layer; in fact, it can be used anywhere.

The main abstraction for validation is the org.springframework.validation.Validator interface. This interface has two callback methods. The supports method is used to determine if the validator instance can validate the object. The validate method is used to actually validate the object (see Listing 5-43).

Listing 5-43. The Validator Interface

package org.springframework.validation;

public interface Validator {

    boolean supports(Class<?> clazz);

    void validate(Object target, Errors errors);
}

The supports method is called to see if a validator can validate the current object type. If that returns true, the framework will call the validate method with the object to validate and an instance of an implementation of the org.springframework.validation.Errors interface. When doing binding, this will be an implementation of the org.springframework.validation.BindingResult. When doing validation, it is a good idea to include an Errors or BindingResult (the latter extends Errors) method attribute. This way, we  can handle situations where there is a bind or validation error. If this is not the case, an org.springframework.validation.BindException will be thrown.

When using Spring @MVC, we have two options for triggering validation. The first is to inject the validator into our controller and to call the validate method on the validator ourselves. The second is to add the javax.validation.Valid (JSR-303) or org.springframework.validation.annotation.Validated annotation to our method attribute. The annotation from the Spring Framework is more powerful than the one from the javax.validation package. The Spring annotation enables us to specify hints; when combined with a JSR-303 validator (e.g., hibernate-validation), can be used to specify validation groups.

Validation and bind errors lead to message codes that are registered with the Errors instance. In general, simply showing an error code to the user isn’t very informative, so the code has to be resolved to a message. This is where the org.springframework.context.MessageSource comes into play. The error codes are passed as message codes to the configured message source and used to retrieve the message. If we don’t configure a message source, we will be greeted with a nice stacktrace indicating that a message for code x cannot be found. So, before we proceed, let’s configure the MessageSource shown in Listing 5-44.

Listing 5-44. The MessageSource Configuration

package com.apress.prospringmvc.bookstore.web.config;

import org.springframework.context.support.ResourceBundleMessageSource;
// Other imports omitted

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

@Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource;
        messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setUseCodeAsDefaultMessage(true);
        return messageSource;
    }
    // Other methods omitted
}

We configure a message source, and then configure it to load a resource bundle with basename messages (we’ll learn more about this in the “Internationalization” section later in this chapter). When a message is not found, we return the code as the message. This is especially useful during development because we can quickly see which message codes are missing from our resource bundles.

Let’s implement validation for our com.apress.prospringmvc.bookstore.domain.Account class. We want to validate whether an account is valid; and for that, we need a username, password and a valid e-mail address. To be able to handle shipping, we also need an address, city, and country. Without this information, the account isn’t valid. Now let’s see how we can use the validation framework to our advantage.

Implementing Our Own Validator

We’ll begin by implementing our own validator. In this case, we will create a com.apress.prospringmvc.bookstore.validation.AccountValidator (see Listing 5-45) and use an init-binder method to configure it.

Listing 5-45. The AccountValidator Implementation

package com.apress.prospringmvc.bookstore.validation;

import java.util.regex.Pattern;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import com.apress.prospringmvc.bookstore.domain.Account;

public class AccountValidator implements Validator {

    private static final String EMAIL_PATTERN =
    "^[_A-Za-z0-9-]+(\.[_A-Za-z0-9-]+)*@"
    +"[A-Za-z0-9]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$";

    @Override
    public boolean supports(Class<?> clazz) {
        return (Account.class).isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "username",
                                  "required", new Object[] {"Username"});
        ValidationUtils.rejectIfEmpty(errors, "password",
                                  "required", new Object[] {"Password"});
        ValidationUtils.rejectIfEmpty(errors, "emailAddress",
                                  "required", new Object[] {"Email Address"});
        ValidationUtils.rejectIfEmpty(errors, "address.street",
                                  "required", new Object[] {"Street"});
        ValidationUtils.rejectIfEmpty(errors, "address.city",
                                  "required", new Object[] {"City"});
        ValidationUtils.rejectIfEmpty(errors, "address.country",
                                  "required", new Object[] {"Country"});

        if (!errors.hasFieldErrors("emailAddress")) {
            Account account = (Account) target;
            String email = account.getEmailAddress();
            if (!emai.matches(EMAIL_PATTERN)) {
                errors.rejectValue("emailAddress", "invalid");
            }
        }
    }
}

Image Note Specifying requiredFields on the org.springframework.web.bind.WebDataBinder would result in the same validation logic as with the ValidationUtils.rejectIfEmptyOrWhiteSpace. In our case, however, we have all the validation logic in one place, rather than having it spread over two places.

This validator implementation will check if the fields are not null and non-empty. If the field is empty, it will register an error for the given field. The error is a collection of message codes, and this collection of message codes is determined by an org.springframework.validation.MessageCodesResolver implementation. The default implementation, org.springframework.validation.DefaultMessageCodesResolver, will resolve to four different codes (see Table 5-21). The order in the table is also the order in which the error codes are resolved to a proper message.

Image

The final part of this validation is that we need to configure our validator and tell the controller to validate our model attribute on submission. In Listing 5-46, we show the modified order controller. We only want to trigger validation on the final submission of our form.

Listing 5-46. The RegistrationController with Validation

package com.apress.prospringmvc.bookstore.web.controller;

import com.apress.prospringmvc.bookstore.domain.AccountValidator;

import javax.validation.Valid;
// Other imports omitted

@Controller
@RequestMapping("/customer/register")
public class RegistrationController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setDisallowedFields("id");
        binder.setValidator(new AccountValidator());
    }

    @RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
    public String handleRegistration(@Valid @ModelAttribute Account account, BindingResultImage
 result) {
        if (result.hasErrors()) {
            return "customer/register";
        }
        this.accountService.save(account);
        return "redirect:/customer/account/" + account.getId();
    }

    // Other methods omitted
}

If we submit illegal values after redeployment, we will be greeted with some error codes, as shown in Figure 5-9.

Image

Figure 5-9. The registration page with error codes

Using JSR-303 Validation

Instead of implementing our own validator, we could also the JSR-303 annotations to add validation. For this, we would only need to annotate our com.apress.prospringmvc.bookstore.domain.Account object with JSR-303 annotations (see Listing 5-47) and then leave the javax.validation.Valid annotation in place. When using these annotations, the error code used is slightly different than the one used in our custom validator (see Table 5-22). However, the registration page doesn’t need to change, so it remains the same as before. In our init-binder method, we do not need to set the validator because a JSR-303 capable validator is automatically detected (the sample project uses the one from Hibernate).

Listing 5-47. An Account with JSR-303 Annotations

package com.apress.prospringmvc.bookstore.domain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.validation.Valid;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;

@Entity
public class Account implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String firstName;
    private String lastName;

    private Date dateOfBirth;

    @Embedded
    @Valid
    private Address address = new Address();

    @NotEmpty
    @Email
    private String emailAddress;
    @NotEmpty
    private String username;
    @NotEmpty
    private String password;

    // getters and setters omitted
}

Image Note The NotEmpty and Email annotations come from the org.hibernate.validator.constraints package; thus this package is not a standard JSR-303 annotation, but an extension to it.

Image

When using JSR-303 annotations, if we submit the form with invalid values, we get a result like the one shown in Figure 5-10. As we can see, there are messages displayed instead of codes. How is that possible? There are some default messages shipped with the validator implementation we use. We can override these if we want by specifying one of the codes from Table 5-22 in our resource bundle (see the next section).

Image

Figure 5-10. The registration page with error messages

Internationalization

For internationalization to work, we need to configure different components to be able to resolve messages based on the language (locale) of the user. For example, there is the org.springframework.context.MessageSource, which lets us resolve messages based on message codes and locale. To be able to resolve the locale, we also need an org.springframework.web.servlet.LocaleResolver. Finally, to be able to change the locale, we also need to configure an org.springframework.web.servlet.i18n.LocaleChangeInterceptor (the next chapter will cover interceptors in more depth).

Message Source

The message source is the component that actually resolves our message based on a code and the locale. Spring provides a couple of implementations of the org.springframework.context.MessageSource interface. Two of those implementations are implementations that we can use, while the other implementations simply delegate to another message source.

The two implementations provided by the Spring Framework are in the org.springframework.context.support package. Table 5-23 briefly describes both of them.

Image

We configure both beans more or less the same way. One thing we need is a bean named messageSource. Which implementation we choose doesn’t really matter. For example, we could even create our own implementation that uses a database to load the messages.

The configuration in Listing 5-48 configures an org.springframework.context.support.ReloadableResourceBundleMessageSource that loads a file named messages.properties from the classpath. It will also try to load the messages_[locale].properties for the Locale we are currently using to resolve the messages.

Listing 5-48. The MessageSource Configuration in WebMvcContext

package com.apress.prospringmvc.bookstore.web.config;

import org.springframework.context.support.ReloadableResourceBundleMessageSource;

// Other imports omitted

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource;
        messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:/messages");
        messageSource.setUseCodeAsDefaultMessage(true);
        return messageSource;
    }
}

The following snippets show two properties files (actually resource bundles) that are loaded. The messages in the messages.properties (see Listing 5-49) file are treated as the defaults, and they can be overridden in the language-specific messages_nl.properties file (see Listing 5-50).

Listing 5-49. The messages.properties Snippet

home.title=Welcome

invalid.account.emailaddress=Invalid email address.
required=Field {0} is required.

Listing 5-50. The messages_nl.properties

home.title=Welkom

invalid.account.emailaddress=Ongeldig emailadres.
required=Veld {0} is verplicht.

LocaleResolver

For the message source to do its work correctly, we also need to configure an org.springframework.web.servlet.LocaleResolver (this can be found this in the org.springframework.web.servlet.i18n package). Several different implementations ship with Spring that can make our lives easier. The locale resolver is a strategy that is used to detect which Locale to use. The different implementations each use a different way of resolving the locale (see Table 5-24).

Image

LocaleChangeInterceptor

If we want our users to be able to change the locale, we need to configure an org.springframework.web.servlet.i18n.LocaleChangeInterceptor (see Listing 5-51). This interceptor inspects the current incoming requests and checks whether there is a parameter named locale on the request. If this is present, the interceptor uses the earlier configured locale resolver to change the current user’s Locale. The parameter name can be configured.

Listing 5-51. The Full Internationalization Configuration

package com.apress.prospringmvc.bookstore.web.config;

import org.springframework.context.MessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;

// Other imports omitted

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {

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

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

    @Bean
    public LocaleResolver localeResolver() {
        return new CookieLocaleResolver();
    }

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource;
        messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasename("classpath:/messages");
        messageSource.setUseCodeAsDefaultMessage(true);
        return messageSource;
    }
}

Image Note In general, it is a good idea to have the LocaleChangeInterceptor as one of the first interceptors. If something goes wrong, we want to inform the user in the correct language.

If we redeploy our application, we should get localized error messages if we switch the language (of course, this works only if we add the appropriate error codes to the resource bundles). However, using the MessageSource for error messages isn’t its only use; we can also use MessageSource to retrieve our labels, titles, error messages, and so on from our resource bundles. We can use the message tag for that. Listing 5-52 shows a modified book search page, which uses the message tag to fill the labels, titles, and headers. If we switch the language, we should get localized messages (see Figures 5-11 and 5-12).

Listing 5-52. The Book Search Page with the Message Tag

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<form:form method="GET" modelAttribute="bookSearchCriteria">
    <fieldset>
        <legend><spring:message code="book.searchcriteria"/></legend>
        <table>
            <tr>
                <td><form:label path="title">
                    <spring:message code="book.title" />
                </form:label></td>
                <td><form:input path="title" /></td>
            </tr>
            <tr>
                <td><form:label path="category">
                    <spring:message code="book.category" />
                </form:label></td>
                <td>
                <form:select path="category" items="${categories}"
                             itemValue="id" itemLabel="name"/>
                </td>
            </tr>
        </table>
    </fieldset>
    <button id="search"><spring:message code="button.search"/></button>
</form:form>

<c:if test="${not empty bookList}">
    <table>
        <tr>
            <th><spring:message code="book.title"/></th>
            <th><spring:message code="book.description"/></th>
            <th><spring:message code="book.price" /></th>
        </tr>
        <c:forEach items="${bookList}" var="book">
            <tr>
                <td>
         <a href="<c:url value="/book/detail/${book.id}"/>">${book.title}</a>
                </td>
                <td>${book.description}</td>
                <td>${book.price}</td>
            </tr>
        </c:forEach>
    </table>
</c:if>
Image

Figure 5-11. The book search page in English

Image

Figure 5-12. The book search page in Dutch

Summary

This chapter covered all things we need to write controllers and handle forms. We began by exploring the RequestMapping annotation and how that can be used to map requests to a method to handle a request. We also explored flexible method signatures and covered which method argument types and return values are supported out of the box.

Next, we dove into the deep end and started writing controllers and modifying our existing code. We also introduced form objects and covered how to bind the properties to fields. And we explained data binding and explored Spring’s type-conversion system and how that is used to convert from and to certain objects. We also wrote our own implementation of a Converter to convert from text to a Category object.

In addition to type conversion, we also explored validation. There are two ways of doing validation: we can create our own implementation of a Validator interface, or we can use the JSR-303 annotations on the objects we want to validate. Enabling validation is done with either the Valid or the Validated annotation.

To make it easier to bind certain fields to attributes of a form object, there is the Spring Form Tag library, which helps us to write HTML forms. This library also helps us to display bind and validation errors to the user.

Finally we covered how to implement internationalization on our web pages and how to convert the validation and error codes to proper messages to show to the end user.

In the next chapter, we are going to explore some more advanced features of Spring MVC. Along the way, we will see how we can further extend and customize the existing infrastructure.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset