© Marten Deinum and Iuliana Cosmina 2021
M. Deinum, I. CosminaPro Spring MVC with WebFluxhttps://doi.org/10.1007/978-1-4842-5666-4_5

5. Implementing Controllers

Marten Deinum1   and Iuliana Cosmina2
(1)
MEPPEL, Drenthe, The Netherlands
(2)
EDINBURGH, UK
 

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. This chapter looks at the two different controller approaches and covers the out-of-the-box implementations provided with the Spring Framework and as configured by Spring Boot.

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

In this chapter, we work with Thymeleaf HTML templates. JSP views are supported with Spring MVC using Apache Tiles, but we do not recommend that you use JSP views when using Spring Boot and an embedded container.1

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 selects 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 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 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 org.springframework.web.servlet.handler.SimpleUrlHandlerMapping, so that all the URLs are in a single location. Having all 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 you can see which URLs it is mapped to when you open a controller.

This section shows how to write both types of controllers and how to configure basic view controllers.

Interface-based Controllers

To write an interface-based controller , we need to create a class that implements the org.springframework.web.servlet.mvc.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.
package org.springframework.web.servlet.mvc;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
@FunctionalInterface
public interface Controller {
  ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
Listing 5-1

The Controller Interface

Let’s look at a small sample. If we take 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.
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");
  }
}
Listing 5-2

An Interface-based IndexController

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 org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter is registered to execute the interface-based controllers (this is registered by default).

The sample given here is straightforward. Now imagine 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 cumbersome.

Table 5-1 shows the Controller implementations that ship with the framework.
Table 5-1

A List of Existing Controller Implementations

Controller implementation

Description

UrlFilenameViewController

A controller implementation that takes the path of a URL and transforms that into a view name. It can be configured to append a prefix and/or suffix to the view name.

ParameterizableViewController

A controller that returns a configured view name.

ServletForwardingController

A controller implementation that forwards the request to a named servlet, which can be a servlet without any mapping. It is useful if you want to use the Spring MVC infrastructure to dispatch requests and apply interceptors.

ServletWrappingController

A controller implementation that wraps and manages a servlet implementation. It is useful if you want to use the Spring MVC infrastructure to dispatch requests and apply interceptors.

../images/300017_2_En_5_Chapter/300017_2_En_5_Figa_HTML.jpg All controllers listed in Table 5-1 reside in the org.springframework.web.servlet.mvc package.

Annotation-based Controllers

To write an annotation-based controller , we need to write a class and put the org.springframework.stereotype.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.
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");
  }
}
Listing 5-3

An Annotation-based IndexController

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 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping to interpret these mappings (registered by default).

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 cumbersome to maintain and write these. Spring MVC can help us here. Enabling us simply to add 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.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
// Other imports omitted
@Configuration
public class WebMvcContextConfiguration implements WebMvcConfigurer {
// Other methods omitted
  @Bean(name = "/index.htm")
  public Controller index() {
    ParameterizableViewController index = new ParameterizableViewController();
    index.setViewName("index");
    return index;
  }
}
Listing 5-4

A ParameterizableViewController Configuration

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 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 a few of these methods. Again, Spring MVC is here to help us. We can override the addViewControllers method (one of the methods of org.springframework.web.servlet.config.annotation.WebMvcConfigurer) and simply register our view names to certain URLs. Listing 5-5 shows how to do this.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// Other imports omitted
@Configuration
public class WebMvcContextConfiguration implements WebMvcConfigurer {
// Other methods omitted
  @Override
  public void addViewControllers(final ViewControllerRegistry registry) {
    registry.addViewController("/index.htm").setViewName("index");
  }
}
Listing 5-5

A ViewController Configuration

The result is the same. 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.
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig1_HTML.jpg
Figure 5-1

The index page

Request-Handling Methods

Writing request-handling methods can be a challenge. For example, how should a method be mapped to an incoming request? Several things could be a factor here, including the URL, the method used (e.g., GET or POST),2 the availability of parameters or HTTP headers,3 or even the request content type or the content type (e.g., XML, JSON, or HTML) to be produced. These and more 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-2 shows the attributes that can be set on the RequestMapping annotation and how they influence mapping.
Table 5-2

The RequestMapping Attributes

Attribute

Description

 

name

The name to use for this mapping. The name can be used with the MvcUriComponentsBuilder to generate dynamic links.

 

value or path

Specifies to which URL or URLs this controller reacts, such as /order.htm. We can also use ant-style expressions to specify the URLs.

 

method

Binds the method on specific HTTP methods. Supported methods include GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE. By default, OPTIONS and TRACE are handled by org.springframework.web.servlet.DispatcherServlet. To have those methods passed onto controllers, we need to set dispatchOptionsRequest and/or dispatchTraceRequest to true respectively on the servlet.

 

params

Narrows on the existence or absence of request parameters. Supported expressions include the following:

 
 

param-name=param-value

The specified param must have a certain value

 

param-name!=param-value

The specified param must not have a certain value.

 

!param-name

The specified param must be absent from the request.

headers

Narrows on the existence or absence of HTTP request headers.4 Supported expressions include the following:

 
 

header-name=header-value

The specified header must have a certain value.

 

header-name!=header-value

The specified header must not have a certain value.

 

!header-name

The specified header must be absent from the request header.

 

The value in the expression can also contain a wildcard (*) in Content-Type or Accept headers (i.e., content-type="text/*" will match all text-based content types).

 

consumes

Specifies the consumable media types of the mapped request. We use this to narrow the primary mapping. For instance, text/xml maps all requests for the content-type of XML, but we could also specify text/* to match all textual content types. We can also negate it: !text/xml matches all content types except this one. This parameter is preferred over using the headers parameter to specify a Content-Type header because it is more explicit.

 

produces

Specifies the producible media types this request-handling method accepts. It narrows the primary mapping. The same rules that apply to the consumes parameter also apply to this parameter. This parameter is preferred over using the headers parameter to specify an Accept header because it is more explicit.

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

Sample Mappings

Class

Method

Description

 

@RequestMapping(value="/order.htm")

Maps to all requests on the order.htm URL

@RequestMapping("/order.htm")

@RequestMapping(method=RequestMethod.GET)

Maps to all GET requests to the order.html URL

@RequestMapping("/order.*")

@RequestMapping(method={RequestMethod.PUT, RequestMethod.POST})

Maps to all PUT and POST requests to the order.* URL. * means any suffix or extension such as .htm, .doc, .xls, and so on

@RequestMapping(value="/customer.htm", consumes="application/json")

@RequestMapping(produces="application/xml")

Maps to all requests that post JSON and accept XML as a response

@RequestMapping(value="/order.htm")

@RequestMapping(params="add-line", method=RequestMethod.POST)

Maps to all POST requests to the order.htm URL that include an add-line parameter

@RequestMapping(value="/order.htm")

@RequestMapping(headers="!VIA")

Maps to all requests to the order.htm URL that don’t include a VIA HTTP header

Supported Method Argument Types

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

The Supported Method Argument Types

Argument Type

Description

javax.servlet.ServletRequest

The request object that triggered this method.

javax.servlet.http.HttpServletRequest

The HTTP request object that triggered this method.

org.springframework.web.multipart.MultipartRequest

The request object that triggered this method only works for multipart requests. This wrapper allows easy access to the uploaded files(s). Only exposes methods for multipart file access.

org.springframework.web.multipart.MultipartHttpServletRequest

The MultipartHttpServletRequest exposes both the HttpServletRequest and MultipartRequest methods.

javax.servlet.ServletResponse

The response associated with the request. It is useful if we need to write the response ourselves.

javax.servlet.http.HttpServletResponse

The response associated with the request. It is useful if we need to write the response ourselves.

javax.servlet.http.HttpSession

The underlying http session. If no session exists, one is initiated. This argument is therefore never null.

org.springframework.web.context.request.WebRequest

Allows more generic access to request and session attributes without ties to an underlying native API(e.g., Servlet or JSF).

org.springframework.web.context.request.NativeWebRequest

WebRequest extension that has accessor methods for the underlying request and response.

java.util.Locale

The currently selected locale as determined by the configured org.springframework.web.servlet.LocaleResolver.

java.io.InputStream

The stream as exposed by the getInputStream method on the ServletRequest

java.io.Reader

The reader exposed by the getReader method on the ServletRequest.

java.io.OutputStream

The responses stream as exposed by the getOutputStream method on the ServletResponse. It can write a response directly to the user.

java.io.Writer

The responses writer exposed by the getWriter method on the ServletResponse. It can write a response directly to the user.

javax.security.Principal

The currently authenticated user (can be null).

java.util.Map

The implicit model belonging to this controller/request.

org.springframework.ui.Model

The implicit model belonging to this controller/request. Model implementations have methods to add objects to the model for added convenience. When adding objects allows method chaining as each method returns the model.

org.springframework.ui.ModelMap

The implicit model belonging to this controller/request. The ModelMap is a map implementation that includes some methods to add objects to the model for added convenience.

org.springframework.web.multipart.MultipartFile

Binds the uploaded file(s) to a method parameter (multiple files are only supported by the multipart support of Spring). It works only when the request is a multipart form submission. The name of the request attribute to use is either taken from an optional org.springframework.web.bind.annotation.RequestPart annotation or derived from the name of the argument (the latter works only if that information is available in the class).

javax.servlet.http.Part

Binds the uploaded file(s) to a method parameter (multiple files are only supported by the multipart support of Spring). It works only when the request is a multipart form submission. The name of the request attribute to use is either taken from an optional org.springframework.web.bind.annotation.RequestPart annotation or derived from the name of the argument (the latter works only if that information is available in the class).

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

Enables specification of the exact list of attributes in case you want to issue a redirect. It can also add flash attributes. This argument is used instead of the implicit model in a redirect.

org.springframework.validation.Errors

The binding and validation results for a preceding model object.

org.springframework.validation.BindingResult

The binding and validation results for a preceding model object. Has accessor methods for the model and underlying infrastructure for type conversion. (For most use cases this isn’t needed, use Errors instead).

org.springframework.web.bind.support.SessionStatus

A handler used to mark handling as complete, which triggers the cleanup of session attributes indicated by org.springframework.web.bind.annotation.SessionAttributes. See the “Using SessionAttributes” section later in this chapter for more information.

org.springframework.web.util.UriComponentsBuilder

A URI builder for preparing a URL relative to the current request URL.

org.springframework.http.HttpEntity<?>

Represents an HTTP request or response entity. It consists of headers and a body of the request or response.

Form objects

Binds request parameters to bean properties using type conversion. These objects are exposed as model attributes. Can optionally be annotated with org.springframework.web.bind.annotation.ModelAttribute.

Request body object

Binds the request body to bean properties using message conversion. These objects need to be annotated with org.springframework.web.bind.annotation.RequestBody.

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) are encoded as request parameters. However, with annotated controllers, there are objects in the model (like path variables and other implicit values) that don’t need to be exposed and are outside of our control.

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

In addition to specifying attributes encoded in the URL, it is also possible to specify flash attributes , which 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. Flash attributes are 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 instead of cases where we need to think about encoding parameters or doing string concatenation ourselves. This component consistently handles these things for us. The code in Listing 5-6 creates the /book/detail/42 URL.
UriComponentsBuilder
  .fromPath("/book/detail/{bookId}")
  .buildAndExpand("42")
  .encode();
Listing 5-6

The UriComponentsBuilder Sample Code

The sample given is 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 specify the name of the attribute in the request, cookie, header, or response, as well as whether the parameter is required.
Table 5-5

The Supported Method Argument Annotations

Argument Type

Description

RequestParam

Binds the argument to a single request parameter or all request parameters.

RequestHeader

Binds the argument to a single request header or all request headers.5

RequestBody

Gets the request body for arguments with this annotation. The value is converted using org.springframework.http.converter.HttpMessageConverter.

RequestPart

Binds the argument to the part of a multipart form submission.

ModelAttribute

Binds and validates arguments with this annotation. The parameters from the incoming request are bound to the given object.

PathVariable

Binds the method parameter to a path variable specified in the URL mapping (the value attribute of the RequestMapping annotation).

CookieValue

Binds the method parameter to a javax.servlet.http.Cookie.

SessionAttribute

Binds the method parameter to a session attribute.

RequestAttribute

Binds the method parameter to a request attribute (not to be confused with a request parameter).

MatrixVariable

Binds the method parameter to a name-value pair within a path-segment.

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

../images/300017_2_En_5_Chapter/300017_2_En_5_Figb_HTML.jpg All annotations live in the org.springframework.web.bind.annotation package.

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 org.springframework.web.method.support.HandlerMethodArgumentResolver implementations. Listing 5-7 shows that interface. We can create our own implementation of this interface and register it with the framework if we want. You can find more information on this in Chapter 7.
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;
}
Listing 5-7

The HandlerMethodArgumentResolver Interface

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 the annotations 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 uses the name of the method argument. This fallback is only usable if the classes are compiled with parameter information.6 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, the name would be book; if it were an array or collection, it would become bookList.

RequestParam

The @RequestParam annotation can be placed on any argument in a request-handling method. When present, it retrieves 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 and Table 5-6 for more information), if no name is given, all request parameters are added to the map as key/value pairs.
Table 5-6

The RequestParam Attributes

Attribute

Default Value

Description

required

true

Indicates whether the parameter is required. If it is required and the parameter is missing, then org.springframework.web.bind.MissingServletRequestParameterException is thrown.

defaultValue

null

Indicates the default value to use when the parameter is missing from the request. Setting a default value is implicitly setting required to false. The value can either be a hardcoded value or a SpEL expression.

value or name

Empty string

Indicates the name of the parameter to look up from the request. If no name is specified, then the name is derived from the method argument name. If no name can be found, java.lang.IllegalArgumentException is thrown.

RequestHeader

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

The RequestHeader Attributes

Attribute

Default Value

Description

required

True

Indicates whether the parameter is required. If it is required and the parameter is missing, org.springframework.web.bind.ServletRequestBindingException is thrown. When set to false, null is used as the value; alternatively, the defaultValue is used when specified.

defaultValue

Null

Indicates the default value to use when the parameter is missing from the request. Setting a default value is implicitly setting required to false. The value can either be a hardcoded value or a SpEL expression.

value or name

Empty string

Indicates the name of the request header to bind to. If no name is specified, then the name is derived from the method argument name. If no name can be found, java.lang.IllegalArgumentException is thrown.

RequestBody

The @RequestBody annotation marks 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 org.springframework.http.converter.HttpMessageConverter. This converter is selected based on the requests content-type. If no converter is found, org.springframework.web.HttpMediaTypeNotSupportedException is thrown. By default, this leads to a response with code 415 (SC_UNSUPPORTED_MEDIA_TYPE) sent 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 type javax.servlet.http.Part, org.springframework.web.multipart.MultipartFile (or on a collection or array of the latter), we 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 org.springframework.http.converter.HttpMessageConverter for the content type detected on the file. If no suitable converter is found, then org.springframework.web.HttpMediaTypeNotSupportedException is thrown. (see the “Data Binding” section and Table 5-8 for more information).
Table 5-8

The RequestPart Attributes

Attribute

Default Value

Description

required

true

Indicates whether the parameter is required. If it is required and the parameter is missing, org.springframework.web.bind.ServletRequestBindingException is thrown. When set to false, null is used as a value; alternatively, defaultValue is used when specified.

value or name

Empty string

The name of the request header to bind to. If no name is specified, the name is derived from the method argument name. If no name is found, java.lang.IllegalArgumentException is thrown.

ModelAttribute

The @ModelAttribute annotation can be placed on method arguments, as well as on methods. When placed on a method argument, it binds this argument to a model object. When placed on a method, that method constructs a model object, and this method is called before any request-handling methods are called. These methods can create an object to be edited in a form or to supply data needed by a form to render itself. (see the “Data Binding” section and Table 5-9 for more information).tab9
Table 5-9

The ModelAttribute Attributes

Attribute

Default Value

Description

value or name

Empty string

The name of the model attribute to bind to. If no name is specified, the name is derived from the method argument type.

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, ISBN would be available as a path variable. (see the “Data Binding” section and Table 5-10 for more information).

CookieValue

This @CookieValue annotation can be placed on any argument in the request-handling method. When present, it retrieves 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. (see the “Data Binding” section and Table 5-11 for more information).

SessionAttribute

This @SessionAttribute annotation can be placed on any argument in the request-handling method. When present, it retrieves an attribute from the HttpSession. (see the “Data Binding” section and Table 5-12 for more information).

RequestAttribtue

This @RequestAttribtue annotation can be placed on any argument in the request-handling method. When present, it retrieves an attribute from the HttpServletRequest. Attributes are obtained using the getAttribute method of the request and shouldn’t be confused with parameters. For the latter, use the @RequestParam annotation. (see the “Data Binding” section and Table 5-13 for more information).

Supported Method Return Values

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

When an arbitrary object is returned, and there is no ModelAttribute annotation present, the framework tries to determine a name for the object in the model. It 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 extensive, we can use the framework’s flexibility and extensibility 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).
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;
}
Listing 5-8

The HandlerMethodReturnValueHandler Interface

Table 5-10

The PathVariable Attribute

Attribute

Default Value

Description

value or name

Empty string

The name of the path variable to bind to. If no name is specified, then the name is derived from the method argument name. If no name is found, java.lang.IllegalArgumentException is thrown.

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 add some controllers and views to our application. We start by creating a simple login controller operating with the request and request parameters. Next, we add a book search page that uses an object. And finally, we 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.html. The resulting structure should look like the one shown in Figure 5-2.
Table 5-11

The CookieValue Attributes

Attribute

Default Value

Description

required

true

Indicates whether the parameter is required. If it is required and the parameter is missing, org.springframework.web.bind.ServletRequestBindingException is thrown. When set to false, null is used as a value; alternatively, defaultValue is used when specified.

defaultValue

null

Indicates the default value to use when the parameter is missing from the request. Setting a default value is implicitly setting required to false. The value can either be a hardcoded value or a SpEL expression.

value or name

Empty string

Indicates the name of the cookie to bind to. If no name is specified, the name is derived from the method argument name. If no name is found, java.lang.IllegalArgumentException is thrown.

../images/300017_2_En_5_Chapter/300017_2_En_5_Fig2_HTML.jpg
Figure 5-2

The directory structure after adding login.html

The login page needs some content. The content common to all pages of the site is declared in the template/layout.html template. The login.html is declared to inherit some of that content and replace some of it using special Thymeleaf constructs like th:fragment and th:replace. The most important part of the login.html page is the login form shown in Listing 5-9.
<form action="#" th:action="@{/login}" method="POST" id="loginForm">
    <fieldset>
        <legend>Login</legend>
        <table>
            <tr>
                <td>Username</td>
                <td>
                    <input type="text" id="username" name="username"
                      placeholder="Username"/>
                </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>
Listing 5-9

The Login Page, login.html

Table 5-12

The SessionAttribute Attributes

Attribute

Default Value

Description

required

true

Indicates whether the parameter is required. If it is required and the parameter is missing, org.springframework.web.bind.ServletRequestBindingException is thrown. When set to false, null is used as a value.

value or name

Empty string

Indicates the name of the cookie to bind to. If no name is specified, the name is derived from the method argument name. If no name is found, java.lang.IllegalArgumentException is thrown.

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).
package com.apress.prospringmvc.bookstore.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping("/login")
public class LoginController {
  @GetMapping
  public String login() {
    return "login";
  }
}
Listing 5-10

The Initial LoginController

Table 5-13

The RequestAttribtue Attributes

Attribute

Default Value

Description

required

true

Indicates whether the parameter is required. If it is required and the parameter is missing, org.springframework.web.bind.ServletRequestBindingException is thrown. When set to false, null is used as a value.

value or name

Empty string

Indicates the name of the cookie to bind to. If no name is specified, the name is derived from the method argument name. If no name is found, java.lang.IllegalArgumentException is thrown.

Table 5-14

The Supported Method Return Values

Argument Type

Description

org.springframework.web.servlet.ModelAndView

When a ModelAndView is returned, this is used as-is. It should contain the full model to use and the name of a view (or the View) to render (the latter is optional).

org.springframework.ui.Model

Indicates that this method returned a model. Objects in this model are added to the controller’s implicit model and made available for view rendering. The name of the view is determined by org.springframework.web.servlet.RequestToViewNameTranslator.

java.util.Maporg.springframework.ui.ModelMap

The map elements are added to the controller’s implicit model and made available for view rendering. The name of the view is determined by org.springframework.web.servlet.RequestToViewNameTranslator.

org.springframework.web.servlet.View

The view to render.

java.lang.CharSequence (generally java.lang.String)

The name of the view to render. If annotated with @ModelAttribute, it is added to the model.

java.lang.Void

The model is already prepared by the controller, and the name of the view is determined by org.springframework.web.servlet.RequestToViewNameTranslator.

org.springframework.http.HttpEntity<?>org.springframework.http.ResponseEntity<?>

Specifies the headers and entity body to return to the user. The entity body is converted and sent to the response stream through org.springframework.http.converter.HttpMessageConverter. Optionally, the HttpEntity can also set a status code to send to the user.

org.springframework.http.HttpHeaders

Specifies the headers to return to the user.

org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody

Asynchronously write a result to the client. Requires async processing to be enabled (on by default).

org.springframework.web.context.request.async.DeferredResultorg.springframework.util.concurrent.ListenableFuturejava.util.concurrent.CompletionStagejava.util.concurrent.Callable

Async result types for use in a async processing environment. Requires async processing to be enabled (on by default).

Any other return type

All other return types are used as model attributes. The name is either derived from the return type or the name specified in org.springframework.web.bind.annotation.ModelAttribute.

Unless the method return value is annotated with @ResponseBody, it is written to client using a HttpMessageConverter instead of being used as a model attribute. For more insights, see Chapter 7 on REST controllers.

Table 5-15

The ConfigurableWebBindingInitializer Properties

Attribute

Description

autoGrowNestedPaths

If set to true, a path containing a null value is populated with a default object value instead of resulting in an exception. Moreover, this default value is used for further traversal of the expression. This property also controls auto growing of collections when an out-of-bounds index is accessed. The default value is true.

bindingErrorProcessor

Sets the org.springframework.validation.BindingErrorProcessor implementation.

conversionService

Sets the instance of org.springframework.core.convert.ConversionService.

directFieldAccess

When set to true, we don’t need to write getters/setters to access the fields. The default is false.

messageCodesResolver

Sets org.springframework.validation.MessageCodesResolver.

propertyEditorRegistrar propertyEditorRegistrars

Registers one or more org.springframework.beans.PropertyEditorRegistrars. It is useful when we have old-style PropertyEditors that we want to use for type conversion.

Validator

Sets the org.springframework.validation.Validator implementation.

Table 5-16

WebDataBinder properties .

Attribute

Description

allowedFields

Specifies the fields that are allowed for binding. It is a white list; only fields included in this list are used for binding. Field names can also contain an asterisk (*) for matching field names with a certain pattern. By default, all fields are allowed. See the disallowedFields attribute for information on excluding fields from binding.

autoGrowCollectionLimit

Sets the maximum size to auto grow a collection when binding. This setting can prevent out of memory errors when binding on large collections. By default, it is set to 256.

autoGrowNestedPaths

If set to true, populates a path containing a null value with a default object value instead of raising an exception. This default object value is used for further traversal of the expression. This property also controls auto growing of collections when an out-of-bounds index is being accessed. By default, it is set to true.

bindEmptyMultipartFiles

By default, replaces the already bound multipart files with an empty multipart file holder if the user resubmits a multipart form without choosing a different file. If this isn’t desired and you want null instead, then turn this property off.

bindingErrorProcessor

Sets the org.springframework.validation.BindingErrorProcessor implementation. Spring provides org.springframework.validation.DefaultBindingErrorProcessor as a default implementation.

conversionService

Sets org.springframework.core.convert.ConversionService.

disallowedFields

Specifies the fields that aren’t allowed for binding. It is a blacklist of request parameter names to ignore during binding. In general, it is wise to put fields like the ID and version fields in there. Like allowedFields, this property can contain an * for matching field names with a certain pattern.

extractOldValueForEditor

Specifies whether to extract the old values for editors and converters. By default, the old values are kept in the binding result. If this isn’t desired, set this property to false. It is also useful to set this to false if you have getters with side effects (e.g., they set other properties or default values).

fieldDefaultPrefix

Specifies the prefix to identify parameters that contain default values for empty fields. The default value is !.

fieldMarkerPrefix

Specifies the prefix to identify parameters that mark fields that aren’t submitted. Generally, this is useful with checkboxes. A checkbox that isn’t checked isn’t submitted as part of the request. Note that this mechanism still lets us receive a value. The default marker is _ (underscore).

ignoreInvalidFields

If set to true, ignores invalid fields. Should we ignore bind parameters with corresponding fields in our model object, but which aren’t accessible? Generally this happens with a nested path, when part of that path resolves to null. The default is false (i.e., it does not ignore these fields).

ignoreUnknownFields

Indicates whether to ignore parameters that aren’t represented as parameters on our model objects. When set to false, all the parameters that are submitted must be represented on our model objects. The default is true.

messageCodesResolver

Sets org.springframework.validation.MessageCodesResolver. Spring provides org.springframework.validation.DefaultMessageCodesResolver as the default implementation.

requiredFields

Sets the fields that are required. When a required field is not set, this leads to a bind error.

validator

Sets the org.springframework.validation.Validator implementation.

Table 5-17

The SessionAttributes Attributes

Argument Name

Description

value

The names of the model attributes that should be stored in the session.

types

The fully qualified classnames (types) of model attributes should be stored in the session. All attributes in a model of this type are stored in the session, regardless of their names.

Table 5-18

The Thymeleaf standard expression types .

Construct

Expression Type

Comment

${...}

Variable expressions.

OGNL expressions or Spring EL executed on the model attributes.

*{...}

Selection expressions.

OGNL expressions or Spring EL executed on an object previously selected with a variable expression.

#{...}

Message (i18n) expressions.

Also called internationalization expressions to allow retrieval of locale-specific messages.

@{...}

Link (URL) expressions.

Builds URLs and adds useful context and session info.

~{...}

Fragment expressions.

An easy way to represent fragments of markup and move them around templates. Can specify inheritance or overriding of template elements.

Table 5-19

Thymeleaf expressions used in the Bookstore project.

Expression

Description

<head th:replace="~{template/layout :: head('Search books')}"></head>

Parametrizable fragment expression: the <head../> element in the current template is replaced with the one from template/layout.html, and the argument value is injected wherever required in this <head../> element declaration.

<div id="header" th:replace="~{template/layout :: header}" ></div>

Fragment expression: this div element is replaced with the one with the same ID from template template/layout.html.

<h1 id="pagetitle" th:text="#{book.search.title}">SEARCH TITLE</h1>

Message expression: the value of the <h1> element is replaced with the message value matching the book.search.title message key. The resulting HTML element, for and EN locale is <h1 id="pagetitle">Search Books</h1>.

th:action="@{/book/search}"

Link expression: this generates a full URL by adding the protocol, domain name, and application context in front of the value provided as argument (e.g., http://localhost:8080/chapter5-bookstore/book/search). It provides a POST endpoint for a form.

th:object="${bookSearchCriteria}"

Variable expression: this declares the model object to use for collecting the form data.

<th:block th:each="book : ${bookList}"> </th:block>

Variable expression – iteration: the content within the block is repeated for each element in the bookList

<td><input type="text" th:field="{title}"/><span th:if="${#fields.hasErrors('title')}"   class="error" th:errors="{title}"></span></td>

Variable expression – validation: within a bean-backed form, secondary elements can display validation errors. In general, if the user enters a title that violates the @Valid constraints, it bounces back to this page with the error message displayed.

Table 5-20

Spring’s Default Property Editors in use by Spring MVC

Class

Explanation

ByteArrayPropertyEditor

An editor for byte arrays that converts Strings to their corresponding byte representations.

CharterEditor

An editor for Character or char fields. It converts a (Unicode) String into a character. Throws java.lang.IllegalArgumentException if more than one character is being parsed. Registered by default.

CharsetEditor

An editor for java.nio.charset.Charset that expects the same name syntax as the name method of java.nio.charset.Charset.

ClassEditorClassArrayEditor

An editor that parses Strings representing classes into actual classes and vice versa. When a class is not found, java.lang.IllegalArgumentException is thrown.

CurrencyEditor

An editor for Currency that translates currency codes into Currency objects. It also exposes the currency code as the text representation of a Currency object.

CustomBooleanEditor

A customizable property editor for Boolean properties.

CustomCollectionEditor

A property editor for collections that converts any source Collection into a given target Collection type.

CustomMapEditor

Property editor for Map that converts any source Map into a given target Map type.

CustomDateEditor

A customizable property editor for java.util.Date that supports a custom DateFormat. It is not registered by default, and it must be user registered as needed with the appropriate format.

CustomNumberEditor

A customizable property editor for any Number subclass like Integer, Long, Float, and so on. It is registered by default, but can be overridden by registering a custom instance of it as a custom editor.

FileEditor

An editor capable of resolving Strings to java.io.File objects. It is registered by default.

InputStreamEditor

A one-way property editor capable of taking a text string and producing java.io.InputStream, so that InputStream properties may be directly set as Strings. Note that the default usage does not close the InputStream for you! It is registered by default.

LocaleEditor

An editor capable of resolving to Locale objects and vice versa (the String format is [country][variant], which is the same behavior the toString() method of Locale provides). It is registered by default.

PatternEditor

An editor capable of resolving Strings to JDK 1.5 Pattern objects and vice versa.

PropertiesEditor

An editor capable of converting Strings (formatted using the format as defined in the Javadoc for the Properties class) to Properties objects. It is registered by default.

StringTrimmerEditor

A property editor that trims Strings. Optionally, it allows transforming an empty String into a null value. It is not registered by default; it must be registered by the user as needed.

TimeZoneEditor

An editor for Timezone that translates timezone IDs into TimeZone objects. Note that it does not expose a text representation for TimeZone objects.

URIEditor

An editor for java.net.URI that directly populates a URI property instead of using a String property as a bridge. By default, this editor encodes String into URIs.

URLEditor

An editor capable of resolving a String representation of a URL into an actual java.net.URL object. It is registered by default.

UUIDEditor

Converts a String into java.util.UUID and vice versa, registered by default.

ZoneId

Converts a String into java.time.ZoneId and vice versa, registered by default.

Table 5-21

The Error Codes for Field Errors

Pattern

Example

code + object name + field

required.newOrder.name

code + field

required.name

code + field type

required.java.lang.String

Code

required

Table 5-22

The Error Codes for Field Errors with JSR-303 Annotations

Pattern

Example

annotation name + object name + field

NotEmpty.newOrder.name

annotation name + field

NotEmpty.name

annotation name + field type

NotEmpty.java.lang.String

annotation name

NotEmpty

Table 5-23

A MessageSource Overview

Class

Description

ResourceBundleMessageSource

Uses the ResourceBundle facility available on the JVM. It can only load resources from the classpath.

ReloadableResourceBundleMessageSource

Works similarly to the ResourceBundleMessageSource but adds reloading and caching capabilities. It allows the resources to be anywhere on the file system; it uses the resource loading mechanism in Spring.

Table 5-24

The LocaleResolver Overview

Class

Description

FixedLocaleResolver

Always resolves to a fixed locale. All the users of our website use the same locale, so changing the locale isn’t supported.

SessionLocaleResolver

Resolves (and stores) the locale in the user’s HttpSession. The attribute stores the locale can be configured and the default locale to use if no locale is present. The drawback to this is that the locale isn’t stored between visits, so it must be set at least once on the user’s session.

AcceptHeaderLocaleResolver

Uses the HTTP accept header to resolve the locale. In general, this is the locale of the user’s operating system, so changing the locale isn’t supported. It is also the default LocalResolver used by the DispatcherServlet.

CookieLocaleResolver

Uses a cookie to store the user’s locale. The advantage of this resolver is that the locale is kept on the client’s machine, so it is available on subsequent visits to the website. The cookie name and timeout can be configured, as well as the default locale.

After the application has been restarted, and we click the Login button, we should see a page like the one shown in Figure 5-3.
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig3_HTML.jpg
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 handles our login. Listing 5-11 shows the modified controller.
package com.apress.prospringmvc.bookstore.web.controller;
import com.apress.prospringmvc.bookstore.domain.Account;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.servlet.http.HttpSession;
@Controller
@RequestMapping("/login")
public class LoginController {
  private static final String ACCOUNT_ATTRIBUTE = "account";
  private final AccountService accountService;
  public LoginController(AccountService accountService) {
    this.accountService = accountService;
  }
  @GetMapping
  public String login() {
    return "login";
  }
  @PostMapping
  public String handleLogin(HttpServletRequest request, HttpSession session) {
    try {
      var username = request.getParameter("username");
      var password = request.getParameter("password");
      var account = this.accountService.login(username, password);
      session.setAttribute(ACCOUNT_ATTRIBUTE, account);
      return "redirect:/index.htm";
    } catch (AuthenticationException ae) {
      request.setAttribute("exception", ae);
      return "login";
    }
  }
}
Listing 5-11

The Modified LoginController

Before we move on, let’s drill down on how the handleLogin method works. The username and password parameters are retrieved from the request; they 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 authentication exception, which is handled by the controller for now. 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 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).
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
  @PostMapping
  public String handleLogin(@RequestParam String username, @RequestParam String password,
 HttpServletRequest request, HttpSession session)
                            throws AuthenticationException {
    try {
      var account = this.accountService.login(username, password);
      session.setAttribute(_ACCOUNT_ATTRIBUTE_, account);
      return "redirect:/index.htm";
    } catch (AuthenticationException ae) {
      request.setAttribute("exception", ae);
      return "login";
    }
  }
}
Listing 5-12

The LoginController with RequestParam

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

There is one drawback with this approach: the lack of support for the Back button in a browser. If we go back a page, we get a nice popup asking if we want to resubmit the form. It is a common approach to redirect after a POST7 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.
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
  @PostMapping
  public String handleLogin(@RequestParam String username,
                            @RequestParam String password,
                            RedirectAttributes redirect,
                            HttpSession session)
                             throws AuthenticationException {
    try {
      var 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";
    }
  }
}
Listing 5-13

The LoginController with RedirectAttributes

When the application is redeployed, and we log in, typing in the wrong username/password combination raises 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 low level. Our solutions include working with the request and/or response directly or through a bit of abstraction with 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 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.html. This file is our search form, and it also displays the results of the search. The code for the search form and the result table can be seen in Listing 5-14.
<form action="#" th:action="@{/book/search}"
            method="GET" id="bookSearchForm">
    <fieldset>
        <legend>Search Criteria</legend>
        <table>
            <tr>
                <td><label>Title</label></td>
                <td><input type="text"/></td>
            </tr>
        </table>
    </fieldset>
    <button id="search"">Search</button>
</form>
<!-- other HTML and JavaScript code omitted -->
<table id="bookSearchResults">
    <thead>
        <tr>
            <th>Title</th>
            <th>Description</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
    <th:block th:each="book : ${bookList}">
        <tr>
            <td th:text="${book.title}"></td>
            <td th:text="${book.description}"></td>
            <td th:text="${book.price}"></td>
        </tr>
    </th:block>
    </tbody>
</table>
Listing 5-14

The Search Page Form

The page consists of a form with a field to fill in a (partial) title that searches for books. When there are results, we 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.
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 {
  private final BookstoreService bookstoreService;
  public BookSearchController(BookstoreService bookstoreService) {
    this.bookstoreService=bookstoreService;
  }
  @GetMapping("/book/search")
  public String list(Model model, HttpServletRequest request) {
    var criteria = new BookSearchCriteria();
    criteria.setTitle(request.getParameter("title"));
    model.addAttribute(this.bookstoreService.findBooks(criteria));
    return "book/search";
  }
}
Listing 5-15

The BookSearchController with Search

The controller reacts to the URL, retrieves the title parameter from the request (this is the name of the field in our page, as shown in Listing 5-13), and finally, proceeds with a search. The results of the search are put in the model. Initially, it displays all the books; however, as soon as a title is entered, it limits the results based on that title (see Figure 5-4).
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig4_HTML.jpg
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).
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 {
  @GetMapping("/book/search")
  public String list(Model model, BookSearchCriteria criteria) {
    model.addAttribute(this.bookstoreService.findBooks(criteria));
    return "book/search";
  }
}
Listing 5-16

The BookSearchController with BookSearchCriteria as a Method Argument

In Spring MVC, this is called data binding. To enable data binding, we needed to modify 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 forces Spring to use data binding on the criteria method argument. Doing so maps all request parameters with the same name as one of our object’s properties to that object (i.e., the request parameter title is mapped to the property title). Using data binding simplifies 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 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).
package com.apress.prospringmvc.bookstore.web.controller;
// Other imports omitted, see listing 5-15
@Controller
public class BookSearchController {
  @GetMapping(value = "/book/search")
  public Collection<Book> list(BookSearchCriteria criteria ) {
    return this.bookstoreService.findBooks(criteria);
  }
}
Listing 5-17

The BookSearchController Alternate Version

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 and some information about the book. We start by modifying our search.html and adding links (see Listing 5-18).
<form action="#" th:action="@{/book/search}"
            method="GET" id="bookSearchForm">
    <fieldset>
        <legend >Search Criteria</legend>
        <table>
            <tr>
                <td><label for="title">Title</label></td>
                <td><input type="text" th:field="*{title}"/></td>
            </tr>
        </table>
    </fieldset>
    <button id="search">Search</button>
</form>
<!-- other HTML and JavaScript code omitted -->
<table id="bookSearchResults">
    <thead>
        <tr>
            <th>Title</th>
            <th>Description</th>
            <th>Price</th>
        </tr>
    </thead>
    <tbody>
    <th:block th:each="book : ${bookList}">
        <tr>
            <td><a th:href="@{/book/detail/} + ${book.id}"
                th:text="${book.title}"></a></td>
            <td th:text="${book.description}"></td>
            <td th:text="${book.price}"></td>
        </tr>
    </th:block>
    </tbody>
</table>
Listing 5-18

The Modified Search Page

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).
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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.apress.prospringmvc.bookstore.domain.Book;
import com.apress.prospringmvc.bookstore.service.BookstoreService;
@Controller
public class BookDetailController {
  @GetMapping(value = "/book/detail/{bookId}")
  public String details(@PathVariable("bookId") long bookId, Model model) {
    var book = this.bookstoreService.findBook(bookId);
    model.addAttribute(book);
    return "book/detail";
  }
}
Listing 5-19

The BookDetailController

The highlighted code is what makes the extraction of the ID possible. This is 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 an HTML page to show the details. The code in Listing 5-20 creates a detail.html in the book directory.
<img th:src="@{'/resources/images/books/' + ${book.isbn} + '/book_front_cover.png'}"
    align="left" alt="${book.title}" width="250"/>
<table>
    <tr>
        <td>Title</td>
        <td th:text="${book.title}"></td>
    </tr>
    <tr>
        <td >Description</td>
        <td th:text="${book.description}"></td>
    </tr>
    <tr>
        <td>Author</td>
        <td th:text="${book.author}"></td>
    </tr>
    <tr>
        <td>Year</td>
        <td th:text="${book.year}"></td>
    </tr>
    <tr>
        <td>ISBN</td>
        <td th:text="${book.isbn}"></td>
    </tr>
    <tr>
        <td >Price</td>
        <td th:text="${book.price}"></td>
    </tr>
</table>
Listing 5-20

The Book’s detail.html Page

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).
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig5_HTML.jpg
Figure 5-5

The book’s details page

Data Binding

This section explores the benefits of using data binding , including how to configure and extend it. However, we begin by explaining the basics of data binding. Listing 5-21 shows our com.apress.prospringmvc.bookstore.domain.BookSearchCriteria class. It is a simple object with two properties: title and category.
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;
  }
}
Listing 5-21

The BookSearchCriteria JavaBean

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, Spring would call the setCategory method; however, it would first try to convert the parameter (which is always a String) to 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 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 to enclose the index. When using a map, this index doesn’t have to be 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. We can mix both strategies 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, org.springframework.web.bind.support.ConfigurableWebBindingInitializer. An instance of the interface must be registered with the handler mapping implementation to be used. After an instance of org.springframework.web.bind.WebDataBinder is created, the initBinder method of 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 org.springframework.web.bind.WebDataBinder. If we were to want to specify more properties, it would be 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-15).

Spring Boot makes configuring and reusing the ConfigurableWebBindingInitializer easier. It detects a bean of the specific type and uses that for further configuring the handler adapters.
@Bean
public ConfigurableWebBindingInitializer configurableWebBindingInitializer(
          Validator mvcValidator,
          FormattingConversionService conversionService) {
  var initializer = new ConfigurableWebBindingInitializer();
  initializer.setDirectFieldAccess(true);
  initializer.setValidator(mvcValidator);
  initializer.setConversionService(conversionService);
  return initializer;
}
Listing 5-22

Configure the ConfigurableWebBindingInitializer

../images/300017_2_En_5_Chapter/300017_2_En_5_Figc_HTML.jpg Notice the Validator and FormattingConversionService dependencies in the method signature. These are needed to enable validation and type conversion from the incoming String parameter to the actual type needed.

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 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 configure the way 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 that this init-binder method applies 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-16.

In addition to setting these properties, we can also tell 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.
package com.apress.prospringmvc.bookstore.web.controller;
//Imports omitted
@Controller
@RequestMapping("/customer")
public class RegistrationController {
// Other methods omitted
  @InitBinder
  public void initBinder(WebDataBinder binder) {
    binder.initDirectFieldAccess();
    binder.setDisallowedFields("id");
    binder.setRequiredFields("username", "password", "emailAddress");
  }
}
Listing 5-23

An Example init-binder Method

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

When an @ModelAttribute annotation is put on a method, this method is called before the request-handling method is called!
package com.apress.prospringmvc.bookstore.web.controller;
// Other imports omitted.
import org.springframework.web.bind.annotation.ModelAttribute;
@Controller
public class BookSearchController {
  private final BookstoreService bookstoreService;
  @ModelAttribute
  public BookSearchCriteria criteria() {
    return new BookSearchCriteria();
  }
  @ModelAttribute("categories")
  public List<Category> getCategories() {
    return this.bookstoreService.findAllCategories();
  }
  @GetMapping("/book/search")
  public Collection<Book> list(BookSearchCriteria criteria) {
    return this.bookstoreService.findBooks(criteria);
  }
}
Listing 5-24

The BookSearchController with ModelAttribute Methods

Methods annotated with @ModelAttribute have the same flexibility in method argument types as 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 need to include org.springframework.ui.Model, org.springframework.ui.ModelMap, or java.util.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 method’s return value is 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 com.apress.prospring.bookstore.web.controller.BookSearchController with the annotation.
package com.apress.prospringmvc.bookstore.web.controller;
// Imports omitted see listing 5-22
@Controller
public class BookSearchController {
// Methods omitted see listing 5-22
  @GetMapping("/book/search")
  public Collection<Book> list(@ModelAttribute("bookSearchCriteria")
    BookSearchCriteria criteria) {
    return this.bookstoreService.findBooks(criteria);
  }
}
Listing 5-25

The BookSearchController with ModelAttribute Annotation on a Method Argument

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 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-17). You should use this annotation to store model attributes in the session to 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 that, we suggest using plain access to the HttpSession.

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).
package org.springframework.web.bind.support;
public interface SessionStatus {
  void setComplete();
  boolean isComplete();
}
Listing 5-26

The SessionStatus Interface

Thymeleaf expressions

To use all the data binding features provided by the framework, we rely on the view technology, in this case, Thymeleaf. Thymeleaf parses the search.html template and evaluates the various template expressions to render the form. Spring Framework can be used with JSP views, and in this case, the Spring Tag Library can write forms and bind form elements to Spring objects.

When using Thymeleaf views, regardless of template types (HTML, text, etc.), you can use five types of standard expressions (or constructs). They are listed in Table 5-18.

A number of these expressions have been used in the examples in this chapter. Table 5-19 lists a few of these with a short explanation for each.

Given all the information introduced about the Thymeleaf expressions, if we load the search page and issue a search, 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 Thymeleaf expressions.
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig6_HTML.jpg
Figure 5-6

The Title field remains filled

Now it’s time to make things a bit more interesting by adding a drop-down box (an HTML select) to select a category to search for in addition to the title. First, we already have the categories in our model (see Listing 5-27).
package com.apress.prospringmvc.bookstore.web.controller;
// other import statements omitted
@Controller
public class BookSearchController {
    @ModelAttribute("categories")
    public Iterable<Category> getCategories() {
        return this.bookstoreService.findAllCategories();
    }
    // other code omitted
}
Listing 5-27

Adding Categories to the search.html Model

We simply want to add a drop-down and bind it to the category’s ID field (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 each of the items. The value is bound to the model attribute used for the form.
<form action="#" th:action="@{/book/search}"
        th:object="${bookSearchCriteria}"
        method="GET" id="bookSearchForm">
    <fieldset>
        <legend>Search Criteria</legend>
        <table>
            <tr>
                <td>
                    <label for="title">Title</label>
                </td>
                <td><input type="text" th:field="*{title}"/></td>
            </tr>
            <tr>
                <td>
                    <label for="category">Category</label>
                </td>
                <td>
                    <select th:field="*{category}">
                        <option th:each="c : ${categories}"
                                th:value="${c.id}" th:text="${c.name}"
                                th:selected="${c.id==1}">
                        </option>
                    </select>
                </td>
            </tr>
        </table>
    </fieldset>
    <button id="search">Search</button>
</form>
// Result table omitted
Listing 5-28

The Search Page with a Category Drop-down Element

Type Conversion

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

  • Converters

  • Formatters

Property editors are old-style type conversion, whereas converters and formatters are the new way. Converters and formatters are more flexible; as such, they are also more powerful than property editors. Also, relying on property editors pulls in the whole java.beans package, including all its support classes, which you 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).

../images/300017_2_En_5_Chapter/300017_2_En_5_Figd_HTML.jpg All property editors are in the org.springframework.beans.propertyeditors package.

Converters

The converter API in Spring 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.

Four different interfaces can implement a converter, all of which are 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.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
  T convert(S source);
}
Listing 5-29

The Converter API

Listing 5-30 shows the ConverterFactory API, which is useful when you need to have conversion logic for an entire class hierarchy. For this, we can parameterize S to be the 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.
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
  <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
Listing 5-30

The ConverterFactory API

When we require more sophisticated conversion logic, we can use 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 sources and target types. During a conversion, we have access to the source and target type descriptions, which is useful for complex conversion logic. This also allows type conversion to be driven by annotation (i.e., we can parse the annotation at runtime to determine what needs to be done).
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);
}
Listing 5-31

The GenericConverter API

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

Listing 5-32 shows a specialized version of 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 invoke that method with the given source type.
package org.springframework.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
  boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
Listing 5-32

The ConditionalGenericConverter API

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 org.springframework.format.support.DefaultFormattingConversionService (which also allows executing and registering formatters).
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);
}
Listing 5-33

The ConversionService API

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).
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;
}
Listing 5-34

The Formatter API

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 java.util.Date instances to text, and vice versa. We would specify T as Date and use the Locale to determine the specific date format for performing the conversion (see Listing 5-35).
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;
  }
}
Listing 5-35

The Sample DateFormatter

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 org.springframework.format.AnnotationFormatterFactory (see Listing 5-36).
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);
}
Listing 5-36

The AnnotationFormatterFactory Interface

We need to parameterize A with the annotation type we want to associate with it. The getPrinter and getParser methods should return 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.
package com.apress.prospringmvc.bookstore.formatter;
import java.util.Date;
import java.util.Set;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
public class DateFormatAnnotationFormatterFactory
    implements AnnotationFormatterFactory<DateFormat> {
  @Override
  public Set<Class<?>> getFieldTypes() {
    return Set.of(Date.class);
  }
  @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) {
    var formatter = new DateFormatter();
    formatter.setFormat(annotation.format());
    return formatter;
  }
}
Listing 5-37

The DateFormatAnnotationFormatterFactory Class

Configuring Type Conversion

If we want to use org.springframework.core.convert.converter.Converter or org.springframework.format.Formatter in Spring MVC, then we need to add some configuration.

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 org.springframework.format.FormatterRegistry (see Listing 5-38) as an argument, and it can register the additional converters and/or formatters. (The FormatterRegistry extends org.springframework.core.convert.converter.ConverterRegistry, which offers the same functionality for Converter implementations).
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> annotationFormatterFactory);
}
Listing 5-38

The FormatterRegistry Interface

To convert from a String to a com.apress.prospringmvc.bookstore.domain.Category, we implement 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.
package com.apress.prospringmvc.bookstore.converter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Set;
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() {
        return Set.of(
                new ConvertiblePair(String.class, this.clazz),
                    new ConvertiblePair(this.clazz, String.class));
    }
    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (String.class.equals(sourceType.getType())) {
            if (!StringUtils.hasText((String) source)) {
                return null;
            }
            var id = Long.parseLong((String) source);
            return this.em.find(this.clazz, id);
        } else if (this.clazz.equals(sourceType.getType())) {
                if (source == null) {
                        return "";
                } else {
                        var field = ReflectionUtils.findField(source.getClass(), ID_FIELD);
                        if (field != null) {
                            ReflectionUtils.makeAccessible(field);
                            return ReflectionUtils.getField(field, source);
                        }
                }
        }
        throw new IllegalArgumentException("Cannot convert " + source + " into a suitable type!");
    }
}
Listing 5-39

The StringToEntityConverter

package com.apress.prospringmvc.bookstore.web.config;
// other imports omitted
@Configuration
public class WebMvcContextConfiguration implements WebMvcConfigurer {
// other code omitted
  @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"));
  }
}
Listing 5-40

The CategoryConverter Configuration

In addition to the category conversion, we also need to do date conversions. Therefore, Listing 5-38 also includes 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 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.html file in it. The content included in Listing 5-41 has been simplified, because there is a lot of repetition on this page for all the different fields.
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{template/layout :: head('Register')}"></head>
<!-- other HTML elements omitted -->
<form action="#" th:action="@{/customer/register}"
  th:object="${account}" method="POST" id="registerForm">
<fieldset>
<legend >Personal</legend>
<table>
    <tr>
        <td><label for="firstName">Firstname</label></td>
        <td><input type="text" th:field="*{firstName}"/>
            <span th:if="${#fields.hasErrors('firstName')}"
                class="error" th:errors="*{firstName}"></span>
        </td>
    </tr>
    <tr>
        <td><label for="lastName" >Lastname</label></td>
        <td><input type="text" th:field="*{lastName}"/>
            <span th:if="${#fields.hasErrors('lastName')}"
                class="error" th:errors="*{lastName}"></span>
        </td>
    </tr>
<tr>
        <td><label for="title" >date of Birth</label></td>
        <td><input type="date" th:field="*{dateOfBirth}"/>
            <span th:if="${#fields.hasErrors('dateOfBirth')}"
                class="error" th:errors="*{dateOfBirth}"></span>
        </td>
    </tr>
</table>
 <button id="search" >Save</button>
<!-- other form elements omitted -->
</fieldset>
</form>
Listing 5-41

The Registration Page

We also need a controller for this, so we create the com.apress.prospringmvc.bookstore.web.controller.RegistrationController. In this controller, we use a couple of data binding features. First, we disallow the submission of an ID field (to prevent someone from editing another user). Then, we preselect the user’s country based on the current Locale. Listing 5-42 shows our controller.
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;
@Controller
@RequestMapping("/customer/register")
public class RegistrationController {
    private final AccountService accountService;
    public RegistrationController(AccountService accountService) {
        this.accountService = accountService;
    }
  @ModelAttribute("countries")
  public Map<String, String> countries(Locale currentLocale) {
    var countries = new TreeMap<String,String>();
    for (var 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");
  }
  @GetMapping
  @ModelAttribute
  public Account register(Locale currentLocale) {
    var 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();
  }
}
Listing 5-42

The RegistrationController

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 expressions in the Thymeleaf template display error messages for fields or objects (this is covered in the upcoming sections). When the application is redeployed, and you click the Register link, you should see the page shown in Figure 5-7.
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig7_HTML.jpg
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).
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig8_HTML.jpg
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, you 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 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 determines if the validator instance can validate the object. The validate method validates the object (see Listing 5-43).
package org.springframework.validation;
public interface Validator {
  boolean supports(Class<?> clazz);
  void validate(Object target, Errors errors);
}
Listing 5-43

The Validator Interface

The supports method is called to see if a validator can validate the current object type. If that returns true, the framework calls the validate method with the object to validate and an instance of an implementation of the org.springframework.validation.Errors interface. When binding, this is an implementation of org.springframework.validation.BindingResult. When doing validation, it is good 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, org.springframework.validation.BindException is thrown.

When using Spring MVC, we have two options for triggering validation. The first is to inject the validator into our controller and call the validate method on the validator. 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), we can 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 must be resolved to a message. This is where org.springframework.context.MessageSource comes into play. The error codes are passed as message codes to the configured message source and retrieve the message. If we don’t configure a message source, we are greeted with a nice stack trace, indicating that a message for error code cannot be found. So, before we proceed, let’s configure the MessageSource shown in Listing 5-44.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.context.support.ResourceBundleMessageSource;
// Other imports omitted
@Configuration
public class WebMvcContextConfiguration extends WebMvcConfigurer {
  @Bean
  public MessageSource messageSource() {
    var messageSource = new ResourceBundleMessageSource();
    messageSource.setBasename("messages");
    messageSource.setUseCodeAsDefaultMessage(true);
    return messageSource;
  }
// Other methods omitted
}
Listing 5-44

The MessageSource Configuration Bean

We configure a message source and then configure it to load a resource bundle with basename messages (you 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 handle shipping, we also need an address, city, and country. Without this information, the account isn’t valid. Now let’s see how to use the validation framework to our advantage.

Implementing Our Validator

We begin by implementing our own validator. In this case, we create a com.apress.prospringmvc.bookstore.validation.AccountValidator (see Listing 5-45) and use an init-binder method to configure it.
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[] {"Emailaddress"});
    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")) {
      var account = (Account) target;
      var email = account.getEmailAddress();
      if (!emai.matches(EMAIL_PATTERN)) {
        errors.rejectValue("emailAddress", "invalid");
      }
    }
  }
}
Listing 5-45

The AccountValidator Implementation

../images/300017_2_En_5_Chapter/300017_2_En_5_Fige_HTML.jpg Specifying requiredFields on 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 checks if the fields are not null and non-empty. If the field is empty, it registers an error for the given field. The error is a collection of message codes determined by an org.springframework.validation.MessageCodesResolver implementation. The default implementation, org.springframework.validation.DefaultMessageCodesResolver, resolves to four different codes (see Table 5-21). The order in the table is also how the error codes are resolved to a proper message.

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.
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, BindingResult result) {
    if (result.hasErrors()) {
      return "customer/register";
    }
    this.accountService.save(account);
    return "redirect:/customer/account/" + account.getId();
  }
// Other methods omitted
}
Listing 5-46

The RegistrationController with Validation

If we submit illegal values after redeployment, we are greeted with some error codes, as shown in Figure 5-9.
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig9_HTML.jpg
Figure 5-9

The registration page with error codes

Using JSR-303 Validation

Instead of implementing our own validator, we could also use 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. Our init-binder method does not need to set the validator because a JSR-303 capable validator is automatically detected (the sample project uses the one from Hibernate).
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.*;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
@Entity
public class Account {
  @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
}
Listing 5-47

An Account with JSR-303 Annotations

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 you 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).
../images/300017_2_En_5_Chapter/300017_2_En_5_Fig10_HTML.jpg
Figure 5-10

The registration page with error messages

Internationalization

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

Message Source

The message source is the component that 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.

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 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 org.springframework.context.support.ReloadableResourceBundleMessageSource, which loads a file named messages.properties from the classpath. It also tries to load the messages_[locale].properties for the locale we are currently using to resolve the messages.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
// Other imports omitted
@Configuration
public class WebMvcContextConfiguration extends WebMvcConfigurer {
  @Bean
  public MessageSource messageSource() {
    var messageSource = new ReloadableResourceBundleMessageSource();
    messageSource.setBasename("classpath:/messages");
    messageSource.setUseCodeAsDefaultMessage(true);
    return messageSource;
  }
}
Listing 5-48

The MessageSource Configuration in WebMvcContext

The following snippets show two properties files (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).
home.title=Welcome
invalid.account.emailaddress=Invalid email address.
required=Field {0} is required.
Listing 5-49

The messages.properties Snippet

home.title=Welkom
invalid.account.emailaddress=Ongeldig emailadres.
required=Veld {0} is verplicht.
Listing 5-50

messages_nl.properties

LocaleResolver

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

LocaleChangeInterceptor

If we want our users to change the locale, we need to configure org.springframework.web.servlet.i18n.LocaleChangeInterceptor (see Listing 5-51). This interceptor inspects the current incoming requests and checks whether a parameter named locale is 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.
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
public class WebMvcContextConfiguration implements WebMvcConfigurer {
  @Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(localeChangeInterceptor());
  }
  @Bean
  public HandlerInterceptor localeChangeInterceptor() {
    var localeChangeInterceptor = new LocaleChangeInterceptor();
    localeChangeInterceptor.setParamName("lang");
    return localeChangeInterceptor;
  }
  @Bean
  public LocaleResolver localeResolver() {
    return new CookieLocaleResolver();
  }
}
Listing 5-51

The Full Internationalization Configuration

../images/300017_2_En_5_Chapter/300017_2_En_5_Figf_HTML.jpg In general, it is a good idea to have 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 Thymeleaf’s message expressions for that. Listing 5-52 shows a modified book search page , which uses message expressions to fill the labels, titles, and headers. If we switch the language, we should get localized messages (see Figures 5-11 and 5-12).
<form action="#" th:action="@{/book/search}"
    th:object="${bookSearchCriteria}"
    method="GET" id="bookSearchForm">
    <fieldset>
        <legend th:text="#{book.searchcriteria}">SEARCH CRITERIA</legend>
        <table>
            <tr>
                <td><label for="title" th:text="#{book.title}">TITLE</label></td>
                <td><input type="text" th:field="*{title}"/></td>
            </tr>
            <tr>
                <td><label for="category" th:text="#{book.category}">CATEGORY</label></td>
                <td>
                    <select th:field="*{category}">
                        <option th:each="c : ${categories}" th:value="${c.id}"
                                th:text="${c.name}" th:selected="${c.id==1}">
                        </option>
                    </select>
                </td>
            </tr>
        </table>
    </fieldset>
    <button id="search" th:text="#{button.search}">SEARCH</button>
</form>
 <table id="bookSearchResults">
    <thead>
    <tr>
        <th th:text="#{book.title}">TITLE</th>
        <th th:text="#{book.description}">DESCRIPTION</th>
        <th th:text="#{book.price}">PRICE</th>
        <th></th>
    </tr>
    </thead>
    <tbody>
    <th:block th:each="book : ${bookList}">
        <tr>
            <td><a th:href="@{/book/detail/} + ${book.id}"
                th:text="${book.title}">TITLE</a></td>
            <td th:text="${book.description}">DESC</td>
            <td th:text="${book.price}">PRICE</td>
            <td><a th:href="@{/cart/add/} + ${book.id}"
                th:text="#{book.addtocart}">CART</a></td>
        </tr>
    </th:block>
    </tbody>
</table>
Listing 5-52

The Book Search Page with the Message Tag

../images/300017_2_En_5_Chapter/300017_2_En_5_Fig11_HTML.jpg
Figure 5-11

The book search page in English

../images/300017_2_En_5_Chapter/300017_2_En_5_Fig12_HTML.jpg
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 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 converts 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 to validate: we can create our own implementation of a Validator interface or 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 a form object’s attributes, 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 explore some more advanced features of Spring MVC. Along the way, you see how to 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
3.144.161.116