Controllers play a crucial role in a web application: they execute the actual request, prepare the model, and select a view to render. In conjunction with the dispatcher servlet, controllers also play a crucial role in the request processing workflow. The controller is the glue between the core application and the web interface to the application. In this chapter, we will take a look at the two different controller approaches and cover the out-of-the-box implementations provided with the Spring Framework.
This chapter will also take a look at the supporting components for request processing. For example, we will cover form submission and how to apply internationalization (I18N).
The controller is the component that is responsible for responding to the action the user takes. This action could be a form submission, clicking a link, or simply accessing a page. The controller selects or updates the data needed for the view. It also select the name of the view to render or can render the view itself. With Spring MVC, we have two options when writing controllers. We can either implement an interface or put an annotation on the class. The interface is org.springframework.web.servlet.mvc.Controller
, and the annotation is org.springframework.stereotype.Controller
. The main focus of this book is the annotation-based approach (aka Spring @MVC) for writing controllers. However, we feel that we still need to mention the interface-based approach.
Although both approaches work for implementing a controller, there are two major differences between them. The first difference is about flexibility, and the second is about mapping URLs to controllers. Annotation-based controllers allow for very flexible method signatures, whereas the interface-based approach has a predefined method on the interface that we must implement. Getting access to other interesting collaborators is harder (but not impossible!).
For the interface-based approach, we must do explicit external mapping of URLs to these controllers; in general, this approach is combined with an org.springframework.web.servlet.handler
.SimpleUrlHandlerMapping
, so that all the URLs are in a single location. Having all of the URLs in a single location is one advantage the interface-based approach has over the annotation-based approach. The annotation-based approach has its mappings scattered throughout the codebase, which makes it harder to see which URL is mapped to which request-handling method. The advantage of annotation-based controllers is that, when you open a controller, you can see which URLs it is mapped to.
In this section, we will show how to write both types of controllers, as well as how to configure basic view controllers.
To write an interface-based controller, we need to create a class that implements the Controller
interface. Listing 5-1 shows the API for that interface. When implementing this interface, we must implement the handleRequest
method. This method needs to return an org.springframework.web.servlet.ModelAndView
object or null
when the controller handles the response itself.
package org.springframework.web.servlet.mvc;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.ModelAndView;
public interface Controller {
ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception;
}
Let’s take a look at a small sample. If we take the com.apress.prospringmvc.bookstore.web.IndexController
and create an interface-based controller out of it, it would look something like what you see in Listing 5-2. We implement the handleRequest
method and return an instance of ModelAndView
with a view name.
package com.apress.prospringmvc.bookstore.web;
// javax.servlet imports omitted
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;
public class IndexController implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response)
throws Exception {
return new ModelAndView("index");
}
}
In addition to writing this controller, we would need to configure an instance of org.springframework.web.servlet.HandlerMapping
to map /index.htm
to this controller (see Chapter 3 for more information). We would also need to make sure that there is an org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter
registered to execute the interface-based controllers (this is registered by default).
The sample given here is quite straightforward. Now image a controller that has some page flow. In that case, we would need to check whether the request is a GET
or POST
request; based on that, we would need to execute different controller logic. With large controllers, this can become quite cumbersome.
Table 5-1 shows the Controller
implementations that ship with the framework. Many of these are deprecated (as of Spring 3.0) or can be considered deprecated in favor of the newer annotation-based controllers. Check the descriptions of each controller for deprecation notes.
To write an annotation-based controller, we need to write a class and put the Controller
annotation on that class. Also, we need to add an org.springframework.web.bind.annotation.RequestMapping
annotation to the class, a method, or both. Listing 5-3 shows an annotation-based approach to our IndexController
.
package com.apress.prospringmvc.bookstore.web;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class IndexController {
@RequestMapping(value = "/index.htm")
public ModelAndView indexPage() {
return new ModelAndView(“index");
}
}
The controller contains a method with the RequestMapping
annotation, and it specifies that it should be mapped to the /index.htm
URL, which is the request-handling method. The method has no required parameters, and we can return anything we want; for now, we want to return a ModelAndView
.
The mapping is in the controller definition, and we need an instance of a HandlerMapping
to interpret these mappings. There are two implementations that can help us out here: the org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
and the org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
. The first is one of the defaults registered by the org.springframework.web.servlet.DispatcherServlet.
The second is one of the defaults registered by Spring @MVC (which we enabled with the org.springframework.web.servlet.config.EnableWebMvc
annotation). We are going to use the Spring @MVC default because it is both more powerful and flexible than the DefaultAnnotationHandlerMapping
. We will see that power and flexibility throughout the book.
The two controller samples we have written so far are called view controllers. They don’t select data; rather, they only select the view name to render. If we had a large application with more of these views, it would become quite cumbersome to maintain and write these. Spring MVC can help us out here, enabling us simply to add an org.springframework.web.servlet.mvc.ParameterizableViewController
to our configuration and to configure it accordingly. We would need to configure an instance to return index
as a view name and map it to the /index.htm
URL. Listing 5-4 shows what needs to be added to make this work.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
// Other imports ommitted
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
// Other methods ommitted
@Bean(name = "/index.htm")
public Controller index() {
ParameterizableViewController index;
index = new ParameterizableViewController();
index.setViewName("index");
return index;
}
}
So how does it work? We create the controller, set the view name to return, and then explicitly give it the name of /index.htm
(see the highlighted parts). The explicit naming makes it possible for the org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
to pick up our controller and map it to the URL. However, if this were to grow significantly larger, then we would need to create quite a few of these methods. Again, Spring MVC is here to help us. Because we have enabled the new Spring MVC configuration, we can utilize it to our advantage. We can override the addViewControllers
method (one of the methods of the org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
) and simply register our view names to certain URLs. In Listing 5-5 shows how to do this.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
// Other imports ommitted
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
// Other methods ommitted
@Override
public void addViewControllers(final ViewControllerRegistry registry) {
registry.addViewController("/index.htm").setViewName("index");
}
}
The result is the same. A ParameterizableViewController
is created and mapped to the /index.htm
URL (see Figure 5-1). However, the second approach is easier and less cumbersome to use than the first one.
Writing request-handling methods can be a challenge. For example, how should you map a method to an incoming request? Several things could be a factor here, including the URL, the method used (e.g., GET
or POST
1), the availability of parameters or HTTP headers2, or even the request content type or the content type (e.g., XML, JSON, or HTML) to be produced. All these can influence which method is selected to handle the request.
The first step in writing a request-handling method is to put an org.springframework.web.bind.annotation.RequestMapping
annotation on the method. This mapping is detected by the org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
to create the mapping of incoming URLs to the correct method (see the “Spring MVC Components” section in Chapter 4 for more information on handler mapping). Next, we need to specify which web request we want to execute the specified handler.
The annotation can be put on both the type (the controller) and the method level. We can use the one on the type level to do some coarse-grained mapping (e.g., the URL), and then use the annotation on the method level to further specify when to execute the method (e.g., a GET
or POST
request).
Table 5-2shows which attributes we can set on the RequestMapping
annotation and how they influence the mapping.
________________
In Table 5-3, there are a couple of sample mappings that also show the effect of class- and method-level matching. As already mentioned, the RequestMapping
annotation on the class applies to all methods in the controller. This mechanism can be used to do coarse-grained mapping on the class level and finer-grained mapping on the method level.
________________
3 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
A request-handling method can have various method arguments and return values. Most arguments mentioned in Table 5-4can be used in arbitrary order. However, there is a single exception to that rule: the org.springframework.validation.BindingResult
argument. That argument has to follow a model object that we use to bind request parameters to.
The org.springframework.web.servlet.mvc.support.RedirectAttributes
deserve a little more explanation than what is shown in Table 5-4. With RedirectAttributes
, it is possible to declare exactly which attributes are needed for the redirect. By default, all model attributes are exposed when doing a redirect. Because a redirect always leads to a GET
request, all primitive model attributes (or collections/arrays of primitives) will be encoded as request parameters. However, with the annotated controllers, there are also objects in the model (like the path variables and other implicit values) that don’t need to be exposed and that are outside of our control.
The RedirectAttributes
can help us out here. When this is used as a method argument and a redirect is being issued, only the attributes added to the RedirectAttributes
instance are going to be added to the URL.
In addition to specifying attributes encoded in the URL, it is also possible to specify so called flash attributes. Flash attributes are attributes that are stored before the redirect and retrieved and made available as model attributes after the redirect. This is done by using the configured org.springframework.web.servlet.FlashMapManager
. The use of flash attributes is useful for objects that cannot be encoded (non-primitive objects) or to keep URLs clean.
The UriComponentsBuilder
provides a mechanism for building and encoding URIs. It can take a URL pattern and replace or extend variables. This can be done for relative or absolute URLs. This mechanism is particularly useful when creating URLs, as opposed to cases where we need to think about encoding parameters or doing string concatenation ourselves. This component handles all these things in a consistent manner for us. The code in Listing 5-6 creates the /book/detail/42
URL.
UriComponentsBuilder.fromPath("/book/detail/{bookId}");
.build();
.expand("42")
.encode()
The sample given is quite simple; however, it is possible to specify more variables (e.g., bookId
) and replace them (e.g., specify the port
or host
). There is also the ServletUriComponentsBuilder
subclass, which we can use to operate on the current request. For example, we might use it to replace, not only path variables, but also request parameters.
In addition to explicitly supported types (as mentioned in the previous section), there are also a couple of annotations that we can use to annotate our method arguments (see Table 5-5). Some of these can also be used with the method argument types mentioned in Table 5-4. In that case, they are used to specify what the name of the attribute in the request, cookie, header, or response must be, as well as whether the parameter is required.
All the parameter values are converted to the argument type by using type conversion. The type-conversion system uses an org.springframework.core.convert.converter.Converter
or PropertyEditor
to convert from a String
type to the actual type.
All these different method argument types and annotations allow us to write very flexible request-handling methods. However, we could extend this mechanism by extending the framework. Resolving those method argument types is done by various implementations of the org.springframework.web.method.support.HandlerMethodArgumentResolver
. Listing 5-7 shows that interface. If we want, we can create our own implementation of this interface and register it with the framework. You can find more information on this in Chapter 7.
package org.springframework.web.method.support;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory)
throws Exception;
}
________________
4 http://en.wikipedia.org/wiki/List_of_HTTP_header_fields
Let’s take a closer look at all the different annotation types we can use. All these annotations have a few attributes that we can set and that have default values or may be required.
All of the annotations mentioned in Table 5-5 have a value attribute. This value attribute refers to the name of the object to use (what it applies to depends on the annotation). If this value isn’t filled, then the fallback is to use the name of the method argument. This fallback is only usable if the classes are compiled with debug information.5 An exception to this rule occurs when using the ModelAttribute
annotation. Instead of the name of the method argument, it infers the name from the type of argument, using the simple classname as the argument name. If the type is an array or collection, it makes this plural by adding List
. If we were to use our com.apress.prospringmvc.bookstore.domain.Book
as an argument, then the name would be book
; if it were an array or collection, then it would become bookList
.
The RequestParam
annotation can be placed on any argument in a request-handling method. When present, it is used to retrieve a parameter from the request. When put on a Map
, there is some special handling, depending on whether the name
attribute is set. If the name is set, the value is retrieved and converted into a Map
. For conversion (see the “Data Binding” section in this chapter for more information), if no name is given, all request parameters are added to the map as key/value pairs.
________________
The RequestHeader
annotation can be placed on any method argument. It is used to bind a method argument to a request header. When placed on a Map
, all available request headers are put in the map as key/value pairs. If it is placed on another type of argument, then the value is converted into the type by using a org.springframework.core.convert.converter.Converter
or PropertyEditor
(see the “Data Binding” section for more information).
The RequestBody
annotation is used to mark a method parameter we want to bind to the body of the web request. The body is converted into the method parameter type by locating and calling an org.springframework.http.converter.HttpMessageConverter
. This converter is selected based on the requests content-type. If no converter is found, an org.springframework.web.HttpMediaTypeNotSupportedException
is thrown. By default, this leads to a response with code 415 (SC_UNSUPPORTED_MEDIA_TYPE) being send to the client.
Optionally, method parameters can also be annotated with javax.validation.Valid
or org.springframework.validation.annotation.Validated
to enforce validation for the created object. You can find more information on validation in the “Validation of Model Attributes” section later in this chapter.
When the RequestPart
annotation is put on a method argument of the type javax.servlet.http.Part
, org.springframework.web.multipart.MultipartFile
(or on a collection or array of the latter,) then we will get the content of that file (or group of files) injected. If it is put on any other argument type, the content is passed through an org.springframework.http.converter.HttpMessageConverter
for the content type detected on the file. If no suitable converter is found, then an org.springframework.web.HttpMediaTypeNotSupportedException
is thrown.
The ModelAttribute
annotation can be placed on method arguments, as well as on methods. When placed on a method argument, it is used to bind this argument to a model object. When placed on a method, that method is used to construct a model object, and this method will be called before request-handling methods are called. These kinds of methods can be used to create an object to be edited in a form or to supply data needed by a form to render itself. (For more information, see the “Data Binding” section.)
The PathVariable
annotation can be used in conjunction with path variables. Path variables can be used in a URL pattern to bind the URL to a variable. Path variables are denoted as {name}
in our URL mapping. If we were to use a URL mapping of /book/{isbn}/image
, then isbn
would be available as a path variable.
This CookieValue
annotation can be placed on any argument in the request-handling method. When present, it is used to retrieve a cookie. When placed on an argument of type javax.servlet.http.Cookie
, we get the complete cookie. Otherwise, the value of the cookie is converted into the argument type.
In addition to all the different method argument types, a request handling method can also have one of several different return values. Table 5-12 lists the default supported and handling of method return values for request handling methods.
When an arbitrary object is returned and there is no ModelAttribute
annotation present, the framework tries to determine a name to use as the name for the object in the model. It basically takes the simple name of the class (the classname without the package) and lowercases the first letter. For example, the name of our com.apress.prospringmvc.bookstore.domain.Book
becomes book
. When the return type is a collection or array, it becomes the simple name of the class, suffixed with List
. Thus a collection of Book
objects becomes bookList
.
This same logic is applied when we use a Model
or ModelMap
to add objects without an explicit name. This also has the advantage of using the specific objects, instead of a plain Map
to gain access to the underlying implicit model.
Although the list of supported return values is already quite extensive, we can use the flexibility and extensibility of the framework to create our own handler. The method’s return values are handled by an implementation of the org.springframework.web.method.support.HandlerMethodReturnValueHandler
interface (see Listing 5-8).
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;
}
Let’s take some of the theory we’ve developed thus far and apply it to our controllers. For example, all the menu options we have on our page lead to a 404 error, which indicates that the page cannot be found.
In this section, we are going to add some controllers and views to our application. We will start by creating a simple login controller operating with the request and request parameters. Next, we will add a book search page that uses an object. And finally, we will conclude by building a controller that retrieves and shows the details of a book.
Before we can start writing our controller, we need to have a login page. In the WEB-INF/views
directory, we create a file named login.jsp
. The resulting structure should look like the one shown in Figure 5-2.
The login page needs some content, as shown in Listing 5-9.
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<c:if test="${exception ne null}">
<div class="error">${exception.message}</div>
</c:if>
<form action="<c:url value="/login"/>" method="post">
<fieldset>
<legend>Login</legend>
<table>
<tr>
<td>Username</td>
<td>
<input type="text" id="username" name="username"
placeholder="Usename"/></td>
</tr>
<tr>
<td>Password</td>
<td>
<input type="password" id="password" name="password"
placeholder="Password"/></td>
</tr>
<tr><td colspan="2" align="center">
<button id="login">Login</button>
</td></tr>
</table>
</fieldset>
</form>
In addition to the page, we need to have a controller and map it to /login
. Let’s create the com.apress.prospringmvc.bookstore.web.controller.LoginController
and start by having it render our page (see Listing 5-10).
package com.apress.prospringmvc.bookstore.web.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
@RequestMapping(value = "/login")
public class LoginController {
@RequestMapping(method = RequestMethod.GET)
public String login() {
return "login";
}
}
After the application has been redeployed and we click the Login button, we should see a page like the one shown in Figure 5-3.
If we now enter the username and password (jd/secret) and press the Login button, we are greeted with an error page (error code 405) that indicates that the method (POST
) is not supported. This is correct because our controller doesn’t yet have a method that handles a POST
request. So, let’s add a method to our controller that actually handles our login. Listing 5-11 shows the modified controller.
package com.apress.prospringmvc.bookstore.web.controller;
// Other imports omitted, see Listing 5-10
import org.springframework.beans.factory.annotation.Autowired;
import com.apress.prospringmvc.bookstore.domain.Account;
import com.apress.prospringmvc.bookstore.service.AccountService;
import com.apress.prospringmvc.bookstore.service.AuthenticationException;
@Controller
@RequestMapping(value = "/login")
public class LoginController {
public static final String ACCOUNT_ATTRIBUTE = "account";
@Autowired
private AccountService accountService;
@RequestMapping(method = RequestMethod.GET)
public String login() {
return "login";
}
@RequestMapping(method = RequestMethod.POST)
public String handleLogin(HttpServletRequest request, HttpSession session)
throws AuthenticationException {
try {
String username = request.getParameter("username");
String password = request.getParameter("password");
Account account = this.accountService.login(username, password);
session.setAttribute(ACCOUNT_ATTRIBUTE, account);
return "redirect:/index.htm";
} catch (AuthenticationException ae) {
request.setAttribute("exception", ae);
return "login";
}
}
}
Before we move on, let’s drill down on how the handleLogin
method works. The username
and password
parameters are retrieved from the request, and these are used to call the login
method on the AccountService
. If the correct credentials are supplied, we get an Account instance for the user (which we store in the session), and then we redirect to the index page. If the credentials are not correct, the service throws an AuthenticationException
, which, for now, is handled by the controller. The exception is stored as a request
attribute, and we return the user to the login page.
Although the current controller does its work, we are still operating directly on the HttpServletRequest
. This is a quite cumbersome (but sometimes necessary) approach; however, we would generally want to avoid this and use the flexible method signatures to make our controllers simpler. With that in mind, let’s modify the controller and limit our use of directly accessing the request (see Listing 5-12).
package com.apress.prospringmvc.bookstore.web.controller;
import org.springframework.web.bind.annotation.RequestParam;
// Other imports omitted, see Listing 5-11
@Controller
@RequestMapping(value = "/login")
public class LoginController {
// Other methods omitted
@RequestMapping(method = RequestMethod.POST)
public String handleLogin(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request,
HttpSession session)
throws AuthenticationException {
try {
Account account = this.accountService.login(username, password);
session.setAttribute(ACCOUNT_ATTRIBUTE, account);
return "redirect:/index.htm";
} catch (AuthenticationException ae) {
request.setAttribute("exception", ae);
return "login";
}
}
}
Using the RequestParam
annotation simplified our controller. However, our exception handling dictates that we still need access to the request. This will change in the next chapter when we implement exception handling.
There is still one drawback with this approach, and that is our lack of support for the Back button in a browser. If we go back a page, we will get a nice popup asking if we want to resubmit the form. It is a common approach to do a redirect after a POST
6 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
@RequestMapping(method = RequestMethod.POST)
public String handleLogin(@RequestParam String username,
@RequestParam String password,
RedirectAttributes redirect,
HttpSession session)
throws AuthenticationException {
try {
Account account = this.accountService.login(username, password);
session.setAttribute(ACCOUNT_ATTRIBUTE, account);
return "redirect:/index.htm";
} catch (AuthenticationException ae) {
redirect.addFlashAttribute("exception", ae);
return "redirect:/login";
}
}
}
________________
When the application is redeployed and we log in, typing in the wrong username/password combination will still raise an error message; however, when we press the Back button, the popup request for a form submission is gone.
Until now, everything we have done is quite low level. Our solutions include working with the request and/or response directly or through a bit of abstraction with the org.springframework.web.bind.annotation.RequestParam
. However, we work in an object-oriented programming language, and where possible, we want to work with objects. We will explore this in the next section.
We have a bookstore, and we want to sell books. At the moment, however, there is nothing in our web application that allows the user to search for or even see a list of books. Let’s address this by creating a book search page, so that the users of our application can search for books.
First, we create a directory book
in the /WEB-INF/views
directory. In that directory, we create a file called search.jsp
. This file is our search form, and it will also display the results of the search. The code for this can be seen in Listing 5-14.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<form method="GET" action="<c:url value="/book/search"/>">
<fieldset>
<legend>Search Criteria</legend>
<table>
<tr>
<td><label for="title">Title</label></td>
<td><input id="title" name="title" /></td>
</tr>
</table>
</fieldset>
<button id="search">Search</button>
</form>
<c:if test="${not empty bookList}">
<table>
<tr><th>Title</th><th>Description</th><th>Price</th></tr>
<c:forEach items="${bookList}" var="book">
<tr>
<td>${book.title}</td>
<td>${book.description}</td>
<td>${book.price}</td>
</tr>
</c:forEach>
</table>
</c:if>
The page consists of a form with a field to fill in a (partial) title that will be used to search for books. When there are results, we will show a table to the user containing the results. Now that we have a page, we also need a controller that can handle the requests. Listing 5-15 shows the initial com.apress.prospringmvc.bookstore.web.controller.BookSearchController
.
package com.apress.prospringmvc.bookstore.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.apress.prospringmvc.bookstore.domain.BookSearchCriteria;
import com.apress.prospringmvc.bookstore.service.BookstoreService;
import javax.servlet.http.HttpServletRequest
@Controller
public class BookSearchController {
@Autowired
private BookstoreService bookstoreService;
@RequestMapping(value = "/book/search", method = RequestMethod.GET)
public String list(Model model, HttpServletRequest request) {
BookSearchCriteria criteria = new BookSearchCriteria();
criteria.setTitle(request.getParameter("title");
model.addAttribute(this.bookstoreService.findBooks(criteria));
return "book/search";
}
}
The controller will react on the URL; retrieve the title parameter from the request (this is the name of the field in our page, as shown in Listing 5-13); and finally, proceed with a search. The results of the search are put in the model. Initially it will display all the books; however, as soon as a title is entered, it will limit the results based on that title (see Figure 5-4).
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 {
@Autowired
private BookstoreService bookstoreService;
@RequestMapping(value = "/book/search", method = RequestMethod.GET)
public String list(Model model, BookSearchCriteria criteria) {
model.addAttribute(this.bookstoreService.findBooks(criteria));
return "book/search";
}
}
With Spring MVC, this is what we call data binding. To enable data binding, we needed to modify our com.apress.prospring.bookstore.web.controller.BookSearchController
so it uses a method argument, instead of working with the request
directly (see Listing 5-14). Alternatively, it could use RequestParam
to retrieve the parameters and set them on the object. This will force Spring to use data binding on the criteria
method argument. Doing so will map all request parameters with the same name as one of our object’s properties to that object (i.e., the request parameter title
will be mapped to the property title
). Using data binding will greatly simplify our controller (you can find more in-depth information on this in the “Data Binding” section of this chapter).
We can do even better! Instead of returning a String
, we could return something else. For example, let’s modify our controller to return a collection of books. This collection is added to the model with the name bookList
, as explained earlier in this chapter. Listing 5-16 shows this controller, but where do we select the view to render? It isn’t explicitly specified. In Chapter 4, we mentioned that the org.springframework.web.servlet.RequestToViewNameTranslator
kicks in if there is no explicitly mentioned view to render. We see that mechanism working here. It takes the URL (http://[server]:[port]/chapter5-bookstore/book/search
); strips the server, port, and application name; removes the suffix (if any); and then uses the remaining book/search
as the name of the view to render (exactly what we have been returning).
package com.apress.prospringmvc.bookstore.web.controller;
// Other imports omitted, see Listing 5-15
@Controller
public class BookSearchController {
@Autowired
private BookstoreService bookstoreService;
@RequestMapping(value = "/book/search", method = RequestMethod.GET)
public Collection<Book> list(BookSearchCriteria criteria ) {
return this.bookstoreService.findBooks(criteria);
}
}
Now let’s put some more functionality into our search page. For example, let’s make the title of a book a link that navigates to a book’s details page that shows an image of and some information about the book. We’ll start by modifying our search.jsp
and adding links (see Listing 5-18).
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<form method="POST" action="<c:url value="/book/search"/>">
<fieldset>
<legend>Search Criteria</legend>
<table>
<tr>
<td><label for="title">Title</label></td>
<td><input id="title" name="title"/></td>
</tr>
</table>
</fieldset>
<button id="search">Search</button>
</form>
<c:if test="${not empty bookList}">
<table>
<tr><th>Title</th><th>Description</th><th>Price</th></tr>
<c:forEach items="${bookList}" var="book">
<tr>
<td><a href="<c:url value="/book/detail/${book.id}"/>">${book.title}</a></td>
<td>${book.description}</td>
<td>${book.price}</td>
</tr>
</c:forEach>
</table>
</c:if>
The highlighted line is the only change we need to make to this page. At this point, we have generated a URL based on the id
of the book, so we should get a URL like /book/detail/4
that shows us the details of the book with id 4. Let’s create a controller to react to this URL and extract the id from the URL (see Listing 5-19).
package com.apress.prospringmvc.bookstore.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import com.apress.prospringmvc.bookstore.domain.Book;
import com.apress.prospringmvc.bookstore.service.BookstoreService;
@Controller
public class BookDetailController {
@Autowired
private BookstoreService bookstoreService;
@RequestMapping(value = "/book/detail/{bookId}")
public String details(@PathVariable("bookId") long bookId, Model model) {
Book book = this.bookstoreService.findBook(bookId);
model.addAttribute(book);
return "book/detail";
}
}
The highlighted code is what makes the extraction of the id possible. This is the org.springframework.web.bind.annotation.PathVariable
in action. The URL mapping contains the {bookId}
part, which tells Spring MVC to bind that part of the URL to a path variable called bookId
. We can then use the annotation to retrieve the path variable again. In addition to the controller, we also need a JSP to show the details. The code in Listing 5-20 creates a detail.jsp
in the book
directory.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:url value="/resources/images/books/${book.isbn}/book_front_cover.png" var="bookImage"/>
<img src="${bookImage}" align="left" alt="${book.title}" width="250"/>
<table>
<tr><td>Title</td><td>${book.title}</td></tr>
<tr><td>Description</td><td>${book.description}</td></tr>
<tr><td>Author</td><td>${book.author}</td></tr>
<tr><td>Year</td><td>${book.year}</td></tr>
<tr><td>ISBN</td><td>${book.isbn}</td></tr>
<tr><td>Price</td><td>${book.price}</td></tr>
</table>
If we click one of the links from the search page after redeployment, we should be greeted with a details page that shows an image of and some information about the book (see Figure 5-5).
In this section, we will explore the benefits and possibilities of using data binding, including how we can configure and extend it. However, we’ll begin by explaining the basics of data binding. Listing 5-21 shows our com.apress.prospringmvc.bookstore.domain.BookSearchCriteria
JavaBean. It is a simple object with two properties: title
and category
.
package com.apress.prospringmvc.bookstore.domain;
public class BookSearchCriteria {
private String title;
private Category category;
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public void setCategory(Category category) {
this.category = category;
}
public Category getCategory() {
return this.category;
}
}
Assume we receive the following request: http://localhost:8080/chapter5-bookstore/book/search?title=Agile
. In this case, the title
property receives the value of Agile
. Behind the scenes, Spring calls the setTitle
method on our JavaBean, which we specified as a method argument in the list
method on our controller. If there were a parameter named category
in the request, then Spring would call the setCategory
method; however, it would first try to convert the parameter (which is always a String
) into a com.apress.prospring.bookstore.domain.Category
JavaBean.
However, data binding isn’t limited to simple setter methods. We can also bind to nested properties and even to indexed collections like maps, arrays, and lists. Nested binding happens when the parameter name contains a dot (.); for instance, address.street=Somewhere
leads to getAddress().setStreet("Somewhere")
.
To bind to indexed collections, we must use a notation with square brackets in which we enclose the index. When using a map, this index doesn’t have to be a numeric. For instance, list[2].name
would bind a name property on the third element in the list. Similarly, map['foo'].name
would bind the name property to the value under the key foo
in the map.
We have two options for customizing the behavior of data binding: globally or per controller. Of course, we can mix both strategies together by performing a global setup, and then fine-tuning it per controller.
To customize data binding globally, we need to create a class that implements the org.springframework.web.bind.support.WebBindingInitializer
interface. Spring MVC provides a configurable implementation of this interface, the org.springframework.web.bind.support.ConfigurableWebBindingInitializer
. An instance of the interface must be registered with the handler mapping implementation, so that it can be used. After an instance of org.springframework.web.bind.WebDataBinder
is created, the initBinder
method of the org.springframework.web.bind.support.WebBindingInitializer
is called.
The provided implementation allows us to set a couple of properties. When a property is not set, it uses the defaults as specified by the org.springframework.web.bind.WebDataBinder
. If we were to want to specify more properties, it would be quite easy to extend the default implementation and add the desired behavior. It is possible to set the same properties here as in the controller (see Table 5-13).
When we want to extend the configuration provided by Spring @MVC, we need to do some extending and overriding. This is because the default configuration already configures a org.springframework.web.bind.support.ConfigurableWebBindingInitializer
; however, it doesn’t expose it as a bean. Instead, we need to extend org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
and override the requestMappingHandlerAdapter
method (see Listing 5-22).
public class WebMvcContextConfiguration extends WebMvcConfigurationSupport {
@Override
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
ConfigurableWebBindingInitializer webBindingInitializer;
webBindingInitializer = (ConfigurableWebBindingInitializer)
adapter.getWebBindingInitializer();
webBindingInitializer.setDirectFieldAccess(true);
//Do other re-configuration
return adapter;
}
}
For the per controller option, we must implement a method in the controller and put the org.springframework.web.bind.annotation.InitBinder
annotation on that method. The method must have no return value (void) and at least an org.springframework.web.bind.WebDataBinder
as a method argument. The method can have the same arguments as a request-handling method. However, it cannot have a method argument with the org.springframework.web.bind.annotation.ModelAttribute
annotation. This is because the model is available after binding; and in this method, we are going to configure how we bind.
The org.springframework.web.bind.annotation.InitBinder
annotation has a single attribute named value
that can take the model attribute names or request parameter names this init-binder method is going to apply to. The default is to apply to all model attributes and request parameters.
To customize binding, we need to configure our org.springframework.web.bind.WebDataBinder
. This object has several configuration options (setter methods) that we can use, as shown in Table 5-14.
In addition to setting these properties, we can also tell the org.springframework.web.bind.WebDataBinder
to use bean property access (the default) or direct field access. This can be done by calling the initBeanPropertyAccess
or initDirectFieldAccess
method to set property access or direct field access, respectively. The advantage of direct field access is that we don’t have to write getter/setters for each field we want to use for binding. Listing 5-23 shows an example init-binder method.
package com.apress.prospringmvc.bookstore.web.controller;
//Imports omitted
@Controller
@RequestMapping(value = "/customer")
public class RegistrationController {
// Other methods omitted
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.initDirectFieldAccess();
binder.setDisallowedFields("id");
binder.setRequiredFields("username", "password", "emailAddress");
}
}
To fully utilize data binding, we have to use model attributes. Furthermore, we should use one of these model attributes as the object our form fields are bound to. In our com.apress.prospringmvc.bookstore.web.controller.BookSearchController
, we added an object as a method argument, and Spring used that as the object to bind the request parameters to. However, it is possible to have more control over our objects and how we create objects. For this, we can use the org.springframework.web.bind.annotation.ModelAttribute
annotation. This annotation can be put both on a method and on method arguments.
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).
Caution When a ModelAttribute
annotation is put on a method, this method will be 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 {
@Autowired
private BookstoreService bookstoreService;
@ModelAttribute
public BookSearchCriteria criteria() {
return new BookSearchCriteria();
}
@ModelAttribute("categories")
public List<Category> getCategories() {
return this.bookstoreService.findAllCategories();
}
@RequestMapping(value = "/book/search", method = { RequestMethod.GET })
public Collection<Book> list(BookSearchCriteria criteria) {
return this.bookstoreService.findBooks(criteria);
}
}
Methods annotated with ModelAttribute
have the same flexibility in method argument types as the request-handling methods. Of course, they shouldn’t operate on the response and cannot have ModelAttribute
annotation method arguments. We could also have the method return void; however, we would then need to include an org.springframework.ui.Model
, org.springframework.ui.ModelMap
or Map
as a method argument and explicitly add its value to the model.
The annotation can also be placed on request-handling methods, indicating that the return value of the method is to be used as a model attribute. The name of the view is then derived from the request that uses the configured org.springframework.web.servlet.RequestToViewNameTranslator
.
When using the annotation on a method argument, the argument is looked up from the model. If it isn’t found, an instance of the argument type is created using the default constructor. Listing 5-25 shows our com.apress.prospring.bookstore.web.controller.BookSearchController
with the annotation.
package com.apress.prospringmvc.bookstore.web.controller;
// Imports omitted see Listing 5-22
@Controller
public class BookSearchController {
// Methods omitted see Listing 5-22
@RequestMapping(value = "/book/search", method = { RequestMethod.GET })
public Collection<Book> list(@ModelAttribute("bookSearchCriteria") BookSearchCriteria
criteria) {
return this.bookstoreService.findBooks(criteria);
}
}
It can be beneficial to store a model attribute in the session between requests. For example, imagine we need to edit a customer record. The first request gets the customer from the database. It is then edited in the application, and the changes are submitted back and applied to the customer. If we don’t store the customer in the session, then the customer record must be retrieved again from the database. This can be inconvenient.
In Spring @MVC, you can tell the framework to store certain model attributes in the session. For this, you can use the org.springframework.web.bind.annotation.SessionAttributes
annotation (see Table 5-15). You should use this annotation to store model attributes in the session, so they survive multiple HTTP requests. However, you should not use this annotation to store something in the session, and then use the javax.servlet.http.HttpSession
to retrieve it. The session attributes are also only usable from within the same controller, so you should not use them as a transport to move objects between controllers. If you need something like that, we suggest that you use Spring Web Flow (see Chapters 10-12).
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();
}
To be able to use all the data binding features provided by the framework, we also need to use the tag library to write forms. Spring MVC ships with two tag libraries. The first is a general-purpose library (see Table 5-16), and the second is a library used to simplify writing forms in our pages (see Table 5-17). We can use the general-purpose library to write our forms (this was how it worked with the Spring Framework before 2.0). This is a very powerful approach, but it is also quite cumbersome (albeit it can still be used as a fallback in those corner cases where the form tag library isn’t sufficient).
When using the form tag library, we need to specify which model attribute property we want to bind the form element to. We do this by specifying the path
attribute on our form element. There are several properties we can set on the various form tags, but Table 5-18 shows the main properties. For the form tags that use a collection of items (e.g., select, checkboxes, and radiobuttons), there are a few additional shared attributes (see Table 5-19).
We can apply this logic to our order JSP page. The page would still have the same functionality, but it would fully utilize the data binding functionality from Spring MVC (see Listing 5-27). In this case, we simply need to replace the normal input element tags with the form element tags, and then supply the path to bind to. Of course, we also need to add the taglib
declaration to the top of the JSP.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="GET" modelAttribute="bookSearchCriteria">
<fieldset>
<legend>Search Criteria</legend>
<table>
<tr>
<td><form:label path="title">Title</form:label></td>
<td><form:input path="title"/></td>
</tr>
</table>
</fieldset>
<button id="search">Search</button>
</form:form>
<c:if test="${not empty bookList}">
<table>
<tr><th>Title</th><th>Description</th><th>Price</th></tr>
<c:forEach items="${bookList}" var="book">
<tr>
<td><a href="<c:url value="/book/detail/${book.id}"/>">${book.title}</a></td>
<td>${book.description}</td>
<td>${book.price}</td>
</tr>
</c:forEach>
</table>
</c:if>
If we redeploy and issue a new search at this point, we see that our title field keeps the previously entered value (see Figure 5-6). This is due to our use of data binding in combination with the form tags.
Now it’s time to make things a bit more interesting by adding a dropdown box (a HTML select) to select a category to search for in addition to the title. We already have the categories in our model (see Listing 5-23). We simply want to add a dropdown and bind it to the id
field of the category (see Listing 5-28). We add a select
tag and tell it which model attribute contains the items
to render. We also specify the value
and label
to show for the each of the items. The value
is bound to the model attribute used for the form.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="GET" modelAttribute="bookSearchCriteria">
<fieldset>
<legend>Search Criteria</legend>
<table>
<tr>
<td><form:label path="title">Title</form:label></td>
<td><form:input path="title"/></td>
</tr>
<tr>
<td><form:label path="category.id">Category</form:label></td>
<td>
<form:select path="category.id" items="${categories}" itemValue="id"
itemLabel="name"/>
</td>
</tr>
</table>
</fieldset>
<button id="search">Search</button>
</form:form>
// Result table omitted
An important part of data binding is type conversion. When we receive a request, the only thing we have are String
instances. However, in the real world we use a lot of different object types, not just text representations. Therefore, we want to convert those String
instances into something we can use, which is where type conversion comes in. In Spring MVC, there are three ways to do type conversion:
Property editors are the old-style of doing type conversion, whereas converters and formatters are the new way of doing type conversion. Converters and formatters are more flexible; as such, they are also more powerful than property editors. In addition, relying on property editors also pulls in the whole java.beans
package, including all its support classes, which we just don’t need in a web environment.
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).
The converter API in Spring 3 is a general purpose type-conversion system. Within a Spring container, this system is used as an alternative to property editors to convert bean property value strings into the required property type. We can also use this API to our advantage in our application whenever we need to do type conversion. The converter system is a strongly typed conversion system and uses generics to enforce this.
There are four different interfaces that can be used to implement a converter, all of which can be found in the org.springframework.core.convert.converter
package:
Converter
ConverterFactory
GenericConverter
ConditionalGenericConverter
Let’s explore the four different APIs.
Listing 5-29 shows the Converter API, which is very straightforward. It has a single convert
method that takes a source
argument and transforms it into a target. The source and target types are expressed by the S
and T
generic type arguments.
package org.springframework.core.convert.converter;
public interface Converter<S, T> {
T convert(S source);
}
Listing 5-30 shows the ConverterFactory
API that is useful when you need to have conversion logic for an entire class hierarchy. For this, we can parameterize S
to be type we are converting from (the source), and we parameterize R
as the base type we want to convert to. We can then create the appropriate converter inside the implementation of this factory.
package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}
When we require more sophisticated conversion logic, we can use the org.springframework.core.convert.converter.GenericConverter
(see Listing 5-31). It is more flexible, but less strongly typed than the previous converter types. It supports converting between multiple source and target types. During a conversion, we have access to the source and target type descriptions, which can be useful for complex conversion logic. This also allows for type conversion to be driven by annotation (i.e., we can parse the annotation at runtime to determine what needs to be done).
package org.springframework.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import java.util.Set;
public interface GenericConverter {
Set<ConvertiblePair> getConvertibleTypes();
Object convert(Object source,
TypeDescriptor sourceType,
TypeDescriptor targetType);
}
An example of this type of conversion logic would be a converter that converts from an array to a collection. A converter would first inspect the type of element being converted, so that we could apply additional conversion logic to different elements.
Listing 5-32 shows a specialized version of the GenericConverter
that allows us to specify a condition for when it should execute. For example, we could create a converter that uses one of the BigDecimal
s valueOf
methods to convert a value, but this would only be useful if we could actually invoke that method with the given sourceType
.
package org.springframework.core.convert.converter;
import org.springframework.core.convert.TypeDescriptor;
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}
The converters are executed behind the org.springframework.core.convert.ConversionService
interface (see Listing 5-33); typical implementations of this interface also implement the org.springframework.core.convert.converter.ConverterRegistry
interface, which enables the easy registration of additional converters. When using Spring @MVC, there is a preconfigured instance of the org.springframework.format.support.DefaultFormattingConversionService
(which also allows for executing and registering formatters).
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);
}
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;
}
To create a formatter, we need to implement the org.springframework.format.Formatter
interface and specify the type T
as the type we want to convert. For example, imagine we had a formatter that could convert Date
instances to text, and vice-versa. We would specify T
as Date
and use the Locale
to determine the specific date format to use for performing the conversion (see Listing 5-35).
package com.apress.prospringmvc.bookstore.formatter;
// java.text and java.util imports omitted
import org.springframework.format.Formatter;
import org.springframework.util.StringUtils;
public class DateFormatter implements Formatter<Date> {
private String format;
@Override
public String print(Date object, Locale locale) {
return getDateFormat(locale).format(object);
}
@Override
public Date parse(String text, Locale locale) throws ParseException {
return getDateFormat(locale).parse(text);
}
private DateFormat getDateFormat(Locale locale) {
if (StringUtils.hasText(this.format)) {
return new SimpleDateFormat(this.format, locale);
} else {
return SimpleDateFormat.getDateInstance(SimpleDateFormat.MEDIUM, locale);
}
}
public void setFormat(String format) {
this.format = format;
}
}
Formatters can also be driven by annotations instead of by field type. If we want to bind a formatter to an annotation, we have to implement the org.springframework.format.AnnotationFormatterFactory
(see Listing 5-36).
package org.springframework.format;
public interface AnnotationFormatterFactory<A extends Annotation> {
Set<Class<?>> getFieldTypes();
Printer<?> getPrinter(A annotation, Class<?> fieldType);
Parser<?> getParser(A annotation, Class<?> fieldType);
}
We need to parameterize A
with the annotation type we want to associate with it. The getPrinter
and getParser
methods should return an org.springframework.format.Printer
and org.springframework.format.Parser
, respectively. We can then use these to convert from or to the annotation type. Let’s imagine we have a com.apress.prospringmvc.bookstore.formatter.DateFormat
annotation that we can use to set the format for a date
field. We could then implement the factory shown in Listing 5-37.
package com.apress.prospringmvc.bookstore.formatter;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import org.springframework.format.AnnotationFormatterFactory;
import org.springframework.format.Parser;
import org.springframework.format.Printer;
public class DateFormatAnnotationFormatterFactory implements
AnnotationFormatterFactory<DateFormat> {
@Override
public Set<Class<?>> getFieldTypes() {
Set<Class<?>> types = new HashSet<Class<?>>(1);
types.add(Date.class);
return types;
}
@Override
public Printer<?> getPrinter(DateFormat annotation, Class<?> fieldType) {
return createFormatter(annotation);
}
@Override
public Parser<?> getParser(DateFormat annotation, Class<?> fieldType) {
return createFormatter(annotation);
}
private DateFormatter createFormatter(DateFormat annotation) {
DateFormatter formatter = new DateFormatter();
formatter.setFormat(annotation.format());
return formatter;
}
}
If we want to use an org.springframework.core.convert.converter.Converter
or an org.springframework.format.Formatter
in Spring MVC, then we need to add some configuration. The org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter
has a method for this. The addFormatters
method can be overridden to register additional converters and/or formatters. This method has an org.springframework.format.FormatterRegistry
(see Listing 5-38) as an argument, and it can be used to register the additional converters and/or formatters (the FormatterRegistry
extends the org.springframework.core.convert.converter.ConverterRegistry
, which offers the same functionality for Converter
implementations).
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);
}
To convert from a String
to a com.apress.prospringmvc.bookstore.domain.Category
, we will implement an org.springframework.core.convert.converter.GenericConverter
(see Listing 5-39) and register it in our configuration (see Listing 5-40). The com.apress.prospringmvc.bookstore.converter.StringToEntityConverter
takes a String
as its source and transforms it into a configurable entity type. It then uses a javax.persistence.EntityManager
to load the record from the database.
package com.apress.prospringmvc.bookstore.converter;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
public class StringToEntityConverter implements GenericConverter {
private static final String ID_FIELD = "id";
private final Class<?> clazz;
@PersistenceContext
private EntityManager em;
public StringToEntityConverter(Class<?> clazz) {
super();
this.clazz = clazz;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> types = new HashSet<GenericConverter.ConvertiblePair>();
types.add(new ConvertiblePair(String.class, this.clazz));
types.add(new ConvertiblePair(this.clazz, String.class));
return types;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType,
TypeDescriptor targetType) {
if (String.class.equals(sourceType.getType())) {
if (StringUtils.isBlank((String) source)) {
return null;
}
Long id = Long.parseLong((String) source);
return this.em.find(this.clazz, id);
} else if (this.clazz.equals(sourceType.getType())) {
try {
if (source == null) {
return "";
} else {
return FieldUtils.readField(source, ID_FIELD, true).toString();
}
} catch (IllegalAccessException e) {
}
}
throw new IllegalArgumentException("Cannot convert " + source + " into a suitable
type!");
}
}
package com.apress.prospringmvc.bookstore.web.config;
... [import ommitted]
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
…
@Bean
public StringToEntityConverter categoryConverter() {
return new StringToEntityConverter(Category.class);
}
@Override
public void addFormatters(final FormatterRegistry registry) {
registry.addConverter(categoryConverter());
registry.addFormatter(new DateFormatter("dd-MM-yyyy"));
}
...
}
In addition to the category conversion, we also need to do date conversions. Therefore, Listing 5-38 also includes an org.springframework.format.datetime.DateFormatter
with a pattern for converting dates.
Now that we have covered type conversion, let’s see it in action. We will create the user registration page that allows us to enter the details for the com.apress.prospringmvc.bookstore.domain.Account
object. First, we need a web page under WEB-INF/views
. Next, we need to create a customer
directory and place a register.jsp
file in it. The content is included only in part of Listing 5-41 because there is a lot of repetition in this page for all the different fields.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="POST" modelAttribute="account">
<fieldset>
<legend>Personal</legend>
<table>
<tr>
<td>
<form:label path="firstName" cssErrorClass="error">
Firstname
</form:label>
</td>
<td><form:input path="firstName" /></td>
<td><form:errors path="firstName"/></td>
</tr>
// Other Account fields omitted
</table>
<fieldset>
<legend>Userinfo</legend>
<table>
<tr>
<td><form:label path="username" cssErrorClass="error">
Username
</form:label></td>
<td><form:input path="username"/></td>
<td><form:errors path="username"/></td>
</tr>
// Password and emailAddress field omitted.
</table>
</fieldset>
<button id="save”>Save</button>
</form:form>
We also need a controller for this, so we will create the com.apress.prospringmvc.bookstore.web.controller.RegistrationController
. In this controller, we will use a couple of data binding features. First, we will disallow the submission of an id
field (to prevent someone from editing another user). Second, we will preselect the user’s country based on the current Locale
. Listing 5-42 shows our controller.
package com.apress.prospringmvc.bookstore.web.controller;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.apress.prospringmvc.bookstore.domain.Account;
import com.apress.prospringmvc.bookstore.service.AccountService;
@Controller
@RequestMapping("/customer/register")
public class RegistrationController {
@Autowired
private AccountService accountService;
@ModelAttribute("countries")
public Map<String, String> countries(Locale currentLocale) {
Map<String, String> countries = new TreeMap<String, String>();
for (Locale locale : Locale.getAvailableLocales()) {
countries.put(locale.getCountry(), locale.getDisplayCountry(currentLocale));
}
return countries;
}
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields("id");
binder.setRequiredFields("username","password","emailAddress");
}
@RequestMapping(method = RequestMethod.GET)
@ModelAttribute
public Account register(Locale currentLocale) {
Account account = new Account();
account.getAddress().setCountry(currentLocale.getCountry());
return account;
}
@RequestMapping(method = { RequestMethod.POST, RequestMethod.PUT })
public String handleRegistration(@ModelAttribute Account account, BindingResult result) {
if (result.hasErrors()) {
return "customer/register";
}
this.accountService.save(account);
return "redirect:/customer/account/" + account.getId();
}
}
The controller has a lot going on. For example, the initBinder
method configures our binding. It disallows the setting of the id
property and sets some required fields. We also have a method that prepares our model by adding all the available countries in the JDK to the model. Finally, we have two request-handling methods, one for a GET
request (the initial request when we enter our page) and one for POST
/PUT
requests when we submit our form. Notice the org.springframework.validation.BindingResult
attribute next to the model attribute. This is what we can use to detect errors; and based on that, we can redisplay the original page. Also remember that the error
tags in the JSP those are used to display error messages for fields or objects (we’ll cover this in more depth in the upcoming sections). When the application is redeployed and we click the Register
link, we should see the page shown in Figure 5-7.
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).
The error messages are created by the data binding facilities in Spring MVC. Later in this chapter, we will see how we can influence the messages displayed. For now, let’s leave them intact. If we fill in proper information and click Save, we are redirected to an account page (for which we already have provided the basic controller and implementation).
We’ve already mentioned validation a couple of times. We’ve also referred to the org.springframework.validation
package a couple of times. Validating our model attributes is quite easy to accomplish with the validation abstraction from the Spring Framework. Validation isn’t bound to the web; it is about validating objects. Therefore, validation can also be used outside the web layer; in fact, it can be used anywhere.
The main abstraction for validation is the org.springframework.validation.Validator
interface. This interface has two callback methods. The supports
method is used to determine if the validator instance can validate the object. The validate
method is used to actually validate the object (see Listing 5-43).
package org.springframework.validation;
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
The supports
method is called to see if a validator can validate the current object type. If that returns true
, the framework will call the validate
method with the object to validate and an instance of an implementation of the org.springframework.validation.Errors
interface. When doing binding, this will be an implementation of the org.springframework.validation.BindingResult
. When doing validation, it is a good idea to include an Errors
or BindingResult
(the latter extends Errors
) method attribute. This way, we can handle situations where there is a bind or validation error. If this is not the case, an org.springframework.validation.BindException
will be thrown.
When using Spring @MVC, we have two options for triggering validation. The first is to inject the validator into our controller and to call the validate
method on the validator ourselves. The second is to add the javax.validation.Valid
(JSR-303) or org.springframework.validation.annotation.Validated
annotation to our method attribute. The annotation from the Spring Framework is more powerful than the one from the javax.validation
package. The Spring annotation enables us to specify hints; when combined with a JSR-303 validator (e.g., hibernate-validation
), can be used to specify validation groups.
Validation and bind errors lead to message codes that are registered with the Errors
instance. In general, simply showing an error code to the user isn’t very informative, so the code has to be resolved to a message. This is where the org.springframework.context.MessageSource
comes into play. The error codes are passed as message codes to the configured message source and used to retrieve the message. If we don’t configure a message source, we will be greeted with a nice stacktrace indicating that a message for code x cannot be found. So, before we proceed, let’s configure the MessageSource
shown in Listing 5-44.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.context.support.ResourceBundleMessageSource;
// Other imports omitted
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource;
messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("messages");
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
// Other methods omitted
}
We configure a message source, and then configure it to load a resource bundle with basename messages (we’ll learn more about this in the “Internationalization” section later in this chapter). When a message is not found, we return the code as the message. This is especially useful during development because we can quickly see which message codes are missing from our resource bundles.
Let’s implement validation for our com.apress.prospringmvc.bookstore.domain.Account
class. We want to validate whether an account is valid; and for that, we need a username, password and a valid e-mail address. To be able to handle shipping, we also need an address, city, and country. Without this information, the account isn’t valid. Now let’s see how we can use the validation framework to our advantage.
We’ll begin by implementing our own validator. In this case, we will create a com.apress.prospringmvc.bookstore.validation.AccountValidator
(see Listing 5-45) and use an init-binder method to configure it.
package com.apress.prospringmvc.bookstore.validation;
import java.util.regex.Pattern;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.apress.prospringmvc.bookstore.domain.Account;
public class AccountValidator implements Validator {
private static final String EMAIL_PATTERN =
"^[_A-Za-z0-9-]+(\.[_A-Za-z0-9-]+)*@"
+"[A-Za-z0-9]+(\.[A-Za-z0-9]+)*(\.[A-Za-z]{2,})$";
@Override
public boolean supports(Class<?> clazz) {
return (Account.class).isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmpty(errors, "username",
"required", new Object[] {"Username"});
ValidationUtils.rejectIfEmpty(errors, "password",
"required", new Object[] {"Password"});
ValidationUtils.rejectIfEmpty(errors, "emailAddress",
"required", new Object[] {"Email Address"});
ValidationUtils.rejectIfEmpty(errors, "address.street",
"required", new Object[] {"Street"});
ValidationUtils.rejectIfEmpty(errors, "address.city",
"required", new Object[] {"City"});
ValidationUtils.rejectIfEmpty(errors, "address.country",
"required", new Object[] {"Country"});
if (!errors.hasFieldErrors("emailAddress")) {
Account account = (Account) target;
String email = account.getEmailAddress();
if (!emai.matches(EMAIL_PATTERN)) {
errors.rejectValue("emailAddress", "invalid");
}
}
}
}
Note Specifying requiredFields
on the org.springframework.web.bind.WebDataBinder
would result in the same validation logic as with the ValidationUtils.rejectIfEmptyOrWhiteSpace
. In our case, however, we have all the validation logic in one place, rather than having it spread over two places.
This validator implementation will check if the fields are not null
and non-empty. If the field is empty, it will register an error for the given field. The error is a collection of message codes, and this collection of message codes is determined by an org.springframework.validation.MessageCodesResolver
implementation. The default implementation, org.springframework.validation.DefaultMessageCodesResolver
, will resolve to four different codes (see Table 5-21). The order in the table is also the order in which the error codes are resolved to a proper message.
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
}
If we submit illegal values after redeployment, we will be greeted with some error codes, as shown in Figure 5-9.
Instead of implementing our own validator, we could also the JSR-303 annotations to add validation. For this, we would only need to annotate our com.apress.prospringmvc.bookstore.domain.Account
object with JSR-303 annotations (see Listing 5-47) and then leave the javax.validation.Valid
annotation in place. When using these annotations, the error code used is slightly different than the one used in our custom validator (see Table 5-22). However, the registration page doesn’t need to change, so it remains the same as before. In our init-binder method, we do not need to set the validator because a JSR-303 capable validator is automatically detected (the sample project uses the one from Hibernate).
package com.apress.prospringmvc.bookstore.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.validation.Valid;
import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty;
@Entity
public class Account implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private Date dateOfBirth;
@Embedded
@Valid
private Address address = new Address();
@NotEmpty
@Email
private String emailAddress;
@NotEmpty
private String username;
@NotEmpty
private String password;
// getters and setters omitted
}
Note The NotEmpty
and Email
annotations come from the org.hibernate.validator.constraints
package; thus this package is not a standard JSR-303 annotation, but an extension to it.
When using JSR-303 annotations, if we submit the form with invalid values, we get a result like the one shown in Figure 5-10. As we can see, there are messages displayed instead of codes. How is that possible? There are some default messages shipped with the validator implementation we use. We can override these if we want by specifying one of the codes from Table 5-22 in our resource bundle (see the next section).
For internationalization to work, we need to configure different components to be able to resolve messages based on the language (locale) of the user. For example, there is the org.springframework.context.MessageSource
, which lets us resolve messages based on message codes and locale. To be able to resolve the locale, we also need an org.springframework.web.servlet.LocaleResolver
. Finally, to be able to change the locale, we also need to configure an org.springframework.web.servlet.i18n.LocaleChangeInterceptor
(the next chapter will cover interceptors in more depth).
The message source is the component that actually resolves our message based on a code and the locale. Spring provides a couple of implementations of the org.springframework.context.MessageSource
interface. Two of those implementations are implementations that we can use, while the other implementations simply delegate to another message source.
The two implementations provided by the Spring Framework are in the org.springframework.context.support
package. Table 5-23 briefly describes both of them.
We configure both beans more or less the same way. One thing we need is a bean named messageSource
. Which implementation we choose doesn’t really matter. For example, we could even create our own implementation that uses a database to load the messages.
The configuration in Listing 5-48 configures an org.springframework.context.support.ReloadableResourceBundleMessageSource
that loads a file named messages.properties
from the classpath. It will also try to load the messages_[locale].properties
for the Locale
we are currently using to resolve the messages.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
// Other imports omitted
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource;
messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
}
The following snippets show two properties files (actually resource bundles) that are loaded. The messages in the messages.properties
(see Listing 5-49) file are treated as the defaults, and they can be overridden in the language-specific messages_nl.properties
file (see Listing 5-50).
home.title=Welcome
invalid.account.emailaddress=Invalid email address.
required=Field {0} is required.
home.title=Welkom
invalid.account.emailaddress=Ongeldig emailadres.
required=Veld {0} is verplicht.
For the message source to do its work correctly, we also need to configure an org.springframework.web.servlet.LocaleResolver
(this can be found this in the org.springframework.web.servlet.i18n
package). Several different implementations ship with Spring that can make our lives easier. The locale resolver is a strategy that is used to detect which Locale
to use. The different implementations each use a different way of resolving the locale (see Table 5-24).
If we want our users to be able to change the locale, we need to configure an org.springframework.web.servlet.i18n.LocaleChangeInterceptor
(see Listing 5-51). This interceptor inspects the current incoming requests and checks whether there is a parameter named locale
on the request. If this is present, the interceptor uses the earlier configured locale resolver to change the current user’s Locale
. The parameter name can be configured.
package com.apress.prospringmvc.bookstore.web.config;
import org.springframework.context.MessageSource;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
// Other imports omitted
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.apress.prospringmvc.bookstore.web" })
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
@Bean
public HandlerInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor localeChangeInterceptor;
localeChangeInterceptor = new LocaleChangeInterceptor();
localeChangeInterceptor.setParamName("lang");
return localeChangeInterceptor;
}
@Bean
public LocaleResolver localeResolver() {
return new CookieLocaleResolver();
}
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource;
messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(true);
return messageSource;
}
}
Note In general, it is a good idea to have the LocaleChangeInterceptor
as one of the first interceptors. If something goes wrong, we want to inform the user in the correct language.
If we redeploy our application, we should get localized error messages if we switch the language (of course, this works only if we add the appropriate error codes to the resource bundles). However, using the MessageSource
for error messages isn’t its only use; we can also use MessageSource
to retrieve our labels, titles, error messages, and so on from our resource bundles. We can use the message
tag for that. Listing 5-52 shows a modified book search page, which uses the message
tag to fill the labels, titles, and headers. If we switch the language, we should get localized messages (see Figures 5-11 and 5-12).
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form method="GET" modelAttribute="bookSearchCriteria">
<fieldset>
<legend><spring:message code="book.searchcriteria"/></legend>
<table>
<tr>
<td><form:label path="title">
<spring:message code="book.title" />
</form:label></td>
<td><form:input path="title" /></td>
</tr>
<tr>
<td><form:label path="category">
<spring:message code="book.category" />
</form:label></td>
<td>
<form:select path="category" items="${categories}"
itemValue="id" itemLabel="name"/>
</td>
</tr>
</table>
</fieldset>
<button id="search"><spring:message code="button.search"/></button>
</form:form>
<c:if test="${not empty bookList}">
<table>
<tr>
<th><spring:message code="book.title"/></th>
<th><spring:message code="book.description"/></th>
<th><spring:message code="book.price" /></th>
</tr>
<c:forEach items="${bookList}" var="book">
<tr>
<td>
<a href="<c:url value="/book/detail/${book.id}"/>">${book.title}</a>
</td>
<td>${book.description}</td>
<td>${book.price}</td>
</tr>
</c:forEach>
</table>
</c:if>
This chapter covered all things we need to write controllers and handle forms. We began by exploring the RequestMapping
annotation and how that can be used to map requests to a method to handle a request. We also explored flexible method signatures and covered which method argument types and return values are supported out of the box.
Next, we dove into the deep end and started writing controllers and modifying our existing code. We also introduced form objects and covered how to bind the properties to fields. And we explained data binding and explored Spring’s type-conversion system and how that is used to convert from and to certain objects. We also wrote our own implementation of a Converter
to convert from text to a Category
object.
In addition to type conversion, we also explored validation. There are two ways of doing validation: we can create our own implementation of a Validator
interface, or we can use the JSR-303 annotations on the objects we want to validate. Enabling validation is done with either the Valid
or the Validated
annotation.
To make it easier to bind certain fields to attributes of a form object, there is the Spring Form Tag library, which helps us to write HTML forms. This library also helps us to display bind and validation errors to the user.
Finally we covered how to implement internationalization on our web pages and how to convert the validation and error codes to proper messages to show to the end user.
In the next chapter, we are going to explore some more advanced features of Spring MVC. Along the way, we will see how we can further extend and customize the existing infrastructure.