In the previous chapters, you learned how to retrieve data from an in-memory database using the Controller, but you didn't learn how to store the data in an in-memory database from the View. In Spring MVC, the process of putting an HTML form element's values into model data is called form binding.
In all the previous chapter's examples, you saw that the data transfer took place from the Model to the View via the Controller. The following line is a typical example of how we put data into the Model from the Controller:
model.addAttribute(greeting,"Welcome")
Similarly, the next line shows how we retrieve that data in the View using a JSTL expression:
<p> ${greeting} </p>
But what if we want to put data into the Model from the View? How do we retrieve that data in the Controller? For example, consider a scenario where an admin of our store wants to add new product information to our store by filling out and submitting an HTML form. How can we collect the values filled out in the HTML form elements and process them in the Controller? This is where Spring tag library tags help us to bind the HTML tag element's values to a form backing bean in the Model. Later, the Controller can retrieve the form backing bean from the Model using the @ModelAttribute
(org.springframework.web.bind.annotation.ModelAttribute
) annotation.
The form backing bean (sometimes called the form bean) is used to store form data. We can even use our domain objects as form beans; this works well when there's a close match between the fields in the form and the properties in our domain object. Another approach is creating separate classes for form beans, which is sometimes called Data Transfer Objects (DTO).
The Spring tag library provides some special <form>
and <input>
tags, which are more or less similar to HTML form and input tags, but have some special attributes to bind form elements' data with the form backed bean. Let's create a Spring web form in our application to add new products to our product list:
ProductRepository
interface and add one more method declaration to it as follows:void addProduct(Product product);
InMemoryProductRepository
class as follows:@Override public void addProduct(Product product) { String SQL = "INSERT INTO PRODUCTS (ID, " + "NAME," + "DESCRIPTION," + "UNIT_PRICE," + "MANUFACTURER," + "CATEGORY," + "CONDITION," + "UNITS_IN_STOCK," + "UNITS_IN_ORDER," + "DISCONTINUED) " + "VALUES (:id, :name, :desc, :price, :manufacturer, :category, :condition, :inStock, :inOrder, :discontinued)"; Map<String, Object> params = new HashMap<>(); params.put("id", product.getProductId()); params.put("name", product.getName()); params.put("desc", product.getDescription()); params.put("price", product.getUnitPrice()); params.put("manufacturer", product.getManufacturer()); params.put("category", product.getCategory()); params.put("condition", product.getCondition()); params.put("inStock", product.getUnitsInStock()); params.put("inOrder", product.getUnitsInOrder()); params.put("discontinued", product.isDiscontinued()); jdbcTemplate.update(SQL, params); }
ProductService
interface and add one more method declaration to it as follows:void addProduct(Product product);
ProductServiceImpl
class as follows:@Override public void addProduct(Product product) { productRepository.addProduct(product); }
ProductController
class and add two more request mapping methods as follows:@RequestMapping(value = "/products/add", method = RequestMethod.GET) public String getAddNewProductForm(Model model) { Product newProduct = new Product(); model.addAttribute("newProduct", newProduct); return "addProduct"; } @RequestMapping(value = "/products/add", method = RequestMethod.POST) public String processAddNewProductForm(@ModelAttribute("newProduct") Product newProduct) { productService.addProduct(newProduct); return "redirect:/market/products"; }
addProduct.jsp
under the src/main/webapp/WEB-INF/views/
directory and add the following tag reference declaration as the very first line in it:addProduct.jsp
. Note that I skipped some <form:input>
binding tags for some of the fields of the product domain object, but I strongly encourage you to add binding tags for the skipped fields while you are trying out this exercise:<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/ bootstrap.min.css"> <title>Products</title> </head> <body> <section> <div class="jumbotron"> <div class="container"> <h1>Products</h1> <p>Add products</p> </div> </div> </section> <section class="container"> <form:form method="POST" modelAttribute="newProduct" class="form-horizontal"> <fieldset> <legend>Add new product</legend> <div class="form-group"> <label class="control-label col-lg-2 col-lg-2" for="productId">Product Id</label> <div class="col-lg-10"> <form:input id="productId" path="productId" type="text" class="form:input-large"/> </div> </div> <!-- Similarly bind <form:input> tag for name,unitPrice,manufacturer,category,unitsInStock and unitsInOrder fields--> <div class="form-group"> <label class="control-label col-lg-2" for="description">Description</label> <div class="col-lg-10"> <form:textarea id="description" path="description" rows = "2"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="discontinued">Discontinued</label> <div class="col-lg-10"> <form:checkbox id="discontinued" path="discontinued"/> </div> </div> <div class="form-group"> <label class="control-label col-lg-2" for="condition">Condition</label> <div class="col-lg-10"> <form:radiobutton path="condition" value="New" />New <form:radiobutton path="condition" value="Old" />Old <form:radiobutton path="condition" value="Refurbished" />Refurbished </div> </div> <div class="form-group"> <div class="col-lg-offset-2 col-lg-10"> <input type="submit" id="btnAdd" class="btn btn-primary" value ="Add"/> </div> </div> </fieldset> </form:form> </section> </body> </html>
http://localhost:8080/webstore/market/products/add
. You will be able to see a web page showing a web form to add product information as shown in the following screenshot:http://localhost:8080/webstore/market/products
.In the whole sequence, steps 5 and 6 are very important steps and need to be observed carefully. Everything mentioned prior to step 5 was very familiar to you I guess. Anyhow, I will give you a brief note on what we did in steps 1 to 4.
In step 1, we just created an addProduct
method declaration in our ProductRepository
interface to add new products. And in step 2, we just implemented the addProduct
method in our InMemoryProductRepository
class. Steps 3 and 4 are just a Service layer extension for ProductRepository
. In step 3, we declared a similar addProduct
method in our ProductService
and implemented it in step 4 to add products to the repository via the productRepository
reference.
Okay, coming back to the important step; all we did in step 5 was add two request mapping methods, namely getAddNewProductForm
and processAddNewProductForm
:
@RequestMapping(value = "/products/add", method = RequestMethod.GET) public String getAddNewProductForm(Model model) { Product newProduct = new Product(); model.addAttribute("newProduct", newProduct); return "addProduct"; } @RequestMapping(value = "/products/add", method = RequestMethod.POST) public String processAddNewProductForm(@ModelAttribute("newProduct") Product productToBeAdded) { productService.addProduct(productToBeAdded); return "redirect:/market/products"; }
If you observe those methods carefully, you will notice a peculiar thing: both the methods have the same URL mapping value in their @RequestMapping
annotations (value = "/products/add"
). So if we enter the URL http://localhost:8080/webstore/market/products/add
in the browser, which method will Spring MVC map that request to?
The answer lies in the second attribute of the @RequestMapping
annotation (method = RequestMethod.GET
and method = RequestMethod.POST
). Yes if you look again, even though both methods have the same URL mapping, they differ in the request method.
So what is happening behind the scenes is that, when we enter the URL http://localhost:8080/webstore/market/products/add
in the browser, it is considered as a GET request, so Spring MVC will map that request to the getAddNewProductForm
method. Within that method, we simply attach a new empty Product
domain object with the model
, under the attribute name newProduct
. So in the addproduct.jsp
View, we can access that newProduct
Model object:
Product newProduct = new Product(); model.addAttribute("newProduct", newProduct);
Before jumping into the processAddNewProductForm
method, let's review the addproduct.jsp
View file in some detail, so that you understand the form processing flow without confusion. In addproduct.jsp
, we just added a <form:form>
tag from Spring's tag library:
<form:form modelAttribute="newProduct" class="form-horizontal">
Since this special <form:form>
tag is coming from a Spring tag library, we need to add a reference to that tag library in our JSP file; that's why we added the following line at the top of the addProducts.jsp
file in step 6:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
In the Spring <form:form>
tag, one of the important attributes is modelAttribute
. In our case, we assigned the value newProduct
as the value of modelAttribute
in the <form:form>
tag. If you remember correctly, you can see that this value of the modelAttribute
, and the attribute name we used to store the newProduct
object in the Model from our getAddNewProductForm
method, are the same. So the newProduct
object that we attached to the model
from the Controller method (getAddNewProductForm
) is now bound to the form. This object is called the form backing bean in Spring MVC.
Okay, now you should look at every <form:input>
tag inside the <form:form>
tag. You can observe a common attribute in every tag. That attribute is path
:
<form:input id="productId" path="productId" type="text" class="form:input-large"/>
The path
attribute just indicates the field name that is relative to the form backing bean. So the value that is entered in this input box at runtime will be bound to the corresponding field of the form bean.
Okay, now it's time to come back and review our processAddNewProductForm
method. When will this method be invoked? This method will be invoked once we press the submit button on our form. Yes, since every form submission is considered a POST request, this time the browser will send a POST request to the same URL http://localhost:8080/webstore/products/add
.
So this time the processAddNewProductForm
method will get invoked since it is a POST request. Inside the processAddNewProductForm
method, we are simply calling the addProduct
service method to add the new product to the repository:
productService.addProduct(productToBeAdded);
But the interesting question here is, how come the productToBeAdded
object is populated with the data that we entered in the form? The answer lies in the @ModelAttribute
(org.springframework.web.bind.annotation.ModelAttribute
) annotation. Notice the method signature of the processAddNewProductForm
method:
public String processAddNewProductForm(@ModelAttribute("newProduct") Product productToBeAdded)
Here if you look at the value attribute of the @ModelAttribute
annotation, you can observe a pattern. Yes, the @ModelAttribute
annotation's value and the value of the modelAttribute
from the <form:form>
tag are the same. So Spring MVC knows that it should assign the form bounded newProduct
object to the processAddNewProductForm
method's productToBeAdded
parameter.
The @ModelAttribute
annotation is not only used to retrieve an object from the Model, but if we want we can even use the @ModelAttribute
annotation to add objects to the Model. For instance, we can even rewrite our getAddNewProductForm
method to something like the following, using the @ModelAttribute
annotation:
@RequestMapping(value = "/products/add", method = RequestMethod.GET)
public String getAddNewProductForm(@ModelAttribute("newProduct") Product newProduct) {
return "addProduct";
}
You can see that we haven't created a new empty Product
domain object and attached it to the model
. All we did was add a parameter of the type Product
and annotated it with the @ModelAttribute
annotation, so Spring MVC will know that it should create an object of Product
and attach it to the model
under the name newProduct
.
One more thing that needs to be observed in the processAddNewProductForm
method is the logical View name it is returning: redirect:/market/products
. So what we are trying to tell Spring MVC by returning the string redirect:/market/products
? To get the answer, observe the logical View name string carefully; if we split this string with the ":
" (colon) symbol, we will get two parts. The first part is the prefix redirect
and the second part is something that looks like a request path: /market/products
. So, instead of returning a View name, we are simply instructing Spring to issue a redirect request to the request path /market/products
, which is the request path for the list
method of our ProductController
. So, after submitting the form, we list the products using the list
method of ProductController
.
As a matter of fact, when we return any request path with the redirect:
prefix from a request mapping method, Spring will use a special View object called RedirectView
(org.springframework.web.servlet.view.RedirectView
) to issue the redirect command behind the scenes. We will see more about RedirectView
in the next chapter.
Instead of landing on a web page after the successful submission of a web form, we are spawning a new request to the request path /market/products
with the help of RedirectView
. This pattern is called redirect-after-post; it is a commonly used pattern with web-based forms. We are using this pattern to avoid double submission of the same form.
It is great that we created a web form to add new products to our web application under the URL http://localhost:8080/webstore/market/products/add
. Why don't you create a customer registration form in our application to register a new customer in our application? Try to create a customer registration form under the URL http://localhost:8080/webstore/customers/add
.