Configuring the Spring container
Before we create controllers for our PWP project, the Spring container must be ready for bean injections and component declarations. First, our Spring MVC container must be annotation-driven so that we can utilize the annotation stereotypes used by the current Spring Framework specification in configuring containers.
To enable the use of annotations inside classes, the following tag must be appearing in the pwp-servlet.xml
:
<mvc:annotation-driven />
Second, when the <annotation-driven>
tag is enabled, the container must automatically scan all component classes that are part of the Spring MVC web project. This will be enabled through inserting the following tag into the pwp-servlet.xml
.
<context:component-scan base-package="org.packt.personal.web.portal" />
The base-package attribute indicates the base folder of the development directory structure (src
) where all components and beans are located. Declaring this tag enables auto-detection of the bean components in the project, which includes the controllers.
Drop all static resources like the CSS files into the ch01/webapp
folder. Declare the default servlet handler in order to allow the access of those static resources from the root of the web application even though the DispatcherServlet
is registered at the context root ch01
.
<mvc:default-servlet-handler />
Then, use the <mvc:resources>
element to point to the location of the static resources with a specific public URL pattern.
<mvc:resources mapping="/css/**" location="/css/" />
The complete XML-based Spring container of PWP is shown as follows:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:oxm="http://www.springframework.org/schema/oxm" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd http://www.springframework.org/schema/oxm http://www.springframework.org/schema/oxm/spring-oxm-4.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/ spring-beans-4.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/ spring-context-4.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd"> <mvc:annotation-driven /> <mvc:default-servlet-handler /> <mvc:resources mapping="/css/**" location="/css/" /> </beans>
The JavaConfig equivalent of our XML-based container will have this equivalent code:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; // Rest of the imports in sources @Configuration @ComponentScan(basePackages="org.packt.personal.web.portal") @EnableWebMvc public class PWPConfiguration extends WebMvcConfigurerAdapter { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/css/**"). addResourceLocations("/css/"); } }
The @EnableWebMvc
annotation is equivalent to <mvc:annotation-driven />
in the XML-based version . Lastly, the @ComponentScan
annotation is equivalent to the <context:component-scan base-package=" org.packt.personal.web.portal "/>
we have in our pwp-servlet.xml
. To allow recognition to static resources, the application must override the resource handler method.
Creating the controllers
The PWP project will consist of four content pages each having its own use request transaction. Users can update three of the four contents using session handling. In order to implement services to each page, controller
class must be created with the stereotype @Controller
instead of extending controller base classes or Spring-specific APIs. The home page of PWP has the following controller declaration:
@Controller public class IndexController { }
Each controller
class has more than service or handler methods. Each service method is being mapped to the user's request by the DispatcherServlet
. A valid service or handler method must have a @RequestMapping
stereotype written on top of the method or function signature. A sample controller with a service method is shown as follows:
@Controller public class IndexController { @RequestMapping(value="/index", method=RequestMethod.GET) public String getIndex(Model model) { model.addAttribute("greetings", "Welcome Page"); return "index"; } }
A service method is mapped to at least one URL or path, and has an HTTP method declared. The preceding sample getIndex()
is mapped to the /index.html
and processes a GET
request transaction.
There are actually four general classifications of a controller and these are the following:
- A controller that calls a page only: These controllers simply provide services that call view names like the following sample:
@Controller public class IndexController { @RequestMapping(value="/index", method=RequestMethod.GET) public String getIndex() { return "index"; } }
- A controller that brings model(s) to views: This group of controllers transport models to the view for presentation purposes. A controller can pass an
org.springframework.web.ModelAndView
to successful view pages with an object containing all the necessary objects, like in the following snippet:
@Controller public class IndexController { @RequestMapping(value="/index", method=RequestMethod.GET) public ModelAndView getIndex() { Map<String, Object> model = new HashMap<>(); model.put("greetings", "Welcome Page"); return new ModelAndView("index", "model", model); } }
- On the other hand, the most modern and simple way of transporting model data is through the use of the
org.springframework.ui.Model
object, wherein all objects transported to the views are either@ModelAttribute
or@SessionAttribute
. A simple sample on this approach is given as follows:
@Controller public class IndexController { @RequestMapping(value="/index", method=RequestMethod.GET) public String getIndex(Model model) { model.addAttribute("greetings", "Welcome Page"); return "index"; } }
- A controller that has multi-services or multi-actions: Controllers that have more than one service or action:
@Controller public class IndexController { @RequestMapping(value="/index", method=RequestMethod.GET) public String getIndex(Model model){ model.addAttribute("greetings", "Welcome Page"); return "index"; } @RequestMapping(value="/index_post", method=RequestMethod.POST) public String postIndex(Model model) { model.addAttribute("greetings", "Welcome Page"); return "index"; } }
- A controller that accepts request parameters (form controllers): These controllers have a form view and a success view. The form view accepts request parameters from the client. Then the request will be mapped to the service method, which will process all the request data. The result will be transported as a model, or as an attribute, to the corresponding view. Following is a sample snippet:
@Controller @SessionAttributes(value={"statusSess", "homeSess"}) @RequestMapping("/pwp/index_update") public class IndexUpdateController { @Autowired private Validator indexValidator; @InitBinder("homeForm") public void initBinder(WebDataBinder binder) { binder.setValidator(indexValidator); } @RequestMapping(method=RequestMethod.GET) public String initForm(Model model) { Home homeForm = new Home(); model.addAttribute("homeForm", homeForm); return "index_update"; } @RequestMapping(method=RequestMethod.POST) public String submitForm(Model model, @ModelAttribute("homeForm") @Validated Home homeForm, BindingResult binding) { model.addAttribute("homeForm", homeForm); String returnVal = "index"; if(binding.hasErrors()) { returnVal = "index_update"; } else { model.addAttribute("homeSess", homeForm); model.addAttribute("statusSess", "undefault"); } return returnVal; } }
The PWP controllers
This project utilizes all the preceding controller classification except for multi-action controllers. Following is the finished product of the PWP that highlights the home page. All content pages have an equivalent update transaction for updating their content. The home page, in particular, is invoked through the URL http://localhost:8080/ch01/pwp/index.html
where ch01
is the context root. With this URL, DispatcherServlet
will look for the controller that has the service method bearing the path /pwp/index
. Once the match is found, the service method calls the appropriate view with the data model, if there is one. The final step will be loading the content desired by the user.
A complete home page controller class with the default values of the cone is shown as follows:
package org.packt.personal.web.portal.controller; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import org.packt.personal.web.portal.model.form.Home; import org.packt.personal.web.portal.model.form.PostMessage; // Rest of the code in the sources @Controller @SessionAttributes("posts") @RequestMapping("pwp") public class IndexController { @ModelAttribute("posts") public List<PostMessage> newPosts() { return initPost(); } @RequestMapping(value="/index", method=RequestMethod.GET) public String getIndex(Model model, @ModelAttribute("posts") List<PostMessage> posts, @ModelAttribute("postForm") PostMessage postForm) { Home home = new Home(); home.setMessage(initMessage()); home.setQuote(initQuote()); model.addAttribute("home", home); if(posts == null) posts = newPosts(); if(validatePost(postForm)) { postForm.setDatePosted(new Date()); posts.add(postForm); } model.addAttribute("posts", posts); return "index"; } @RequestMapping(value="/index_redirect", method=RequestMethod.GET) public RedirectView updateIndex() { return new RedirectView("/ch01/pwp/index_update.html"); } public String initQuote() { String message = "Twenty years from now you will be more disappointed by the things .......... -Mark Twain"; return message; } public String initMessage() { String message = "Having a positive outlook on life is a crucial part of finding inspiration. In the paragraph above, ......"; return message; } public List<PostMessage> initPost() { List<PostMessage> posts = new ArrayList<>(); PostMessage post = new PostMessage(); post.setSubject("Welcome!"); post.setDatePosted(new Date()); post.setPostedMsg("Hello visitors!Feelfree to post on my portal!"); posts.add(post); return posts; } public boolean validatePost(PostMessage post) { try { if(post.getSubject().trim().length() == 0 || post.getPostedMsg().trim().length() == 0) { return false; } } catch(Exception e) { return false; } return true; } }
The method getIndex()
is responsible for loading the home page initially. The default content for message and content are given by two methods, namely initMessage()
and initPost()
. The initPost()
method initially populates the right navigation panel of the home page where all the posts will be listed. This posting feature will be elaborated on further in the next topics.
Generally, all these default methods are essential to generate the home page when no session data are available to replace this default content.
The update page on the home page will be invoked by clicking the Update button. Each portal has an Update button except for the Reach Out page. After clicking the Update button on the home page, DispatcherServlet
will look for the service that has the URL path /pwp/index_redirect
. There are two general ways to implement redirection and these are the following:
- Using the RedirectView API: This process of redirection is done through implementation of the handler method that returns
org.springframework.web.servlet.view.RedirectView
. Following are some snippets that show how to implement redirection.
@RequestMapping(value="/index_redirect", method=RequestMethod.GET) public RedirectView updateIndex() { return new RedirectView("/ch01/pwp/index_update.html"); } @RequestMapping(value="/index_redirect", method=RequestMethod.GET) public String updateIndex() { return (redirect:"/ch01/pwp/index_update.html"); }
- Using the <c:url> JSTL tag: This process is a direct process of redirection and being configured on the view page.
<c:url var=indexUpdate" value="/pwp/index_update.html"/> <a href="<c:out value="${indexUpdate }"/>">Update Home</a>
After redirection to the update page, the home page has two important pieces of content information that need to be updated, and these are the message and quote portions of the page:
All update pages have validators to check the attributes of the form data. On the home page, users cannot input quotes or messages that are less than 50 and more than 500 alphanumeric characters. Validation and object type checking occurs only if our controllers have form views. A controller that has form and success views is implemented in a different way. Following is a template of a form controller that is used by the PWP project:
package org.packt.personal.web.portal.controller; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.packt.personal.web.portal.converter.AgeConverter; import org.packt.personal.web.portal.converter.BirthDateConverter; // Rest of the codes in the sources @Controller @SessionAttributes(value = {"pStatusSess", "personSess"}) @RequestMapping("/pwp/personal_update") public class PersonalUpdateController { @Autowired Validator personalValidator; @InitBinder("personForm") public void initBinder(WebDataBinder binder) { binder.setValidator(personalValidator); binder.registerCustomEditor(Integer.class, "biography.age", new AgeConverter()); binder.registerCustomEditor(Integer.class, "education.year", new YearConverter()); binder.registerCustomEditor(Date.class, "biography.birthDate", new BirthDateConverter()); } @RequestMapping(method=RequestMethod.GET) public String initForm(Model model) { Biography bio = new Biography(); Education educ = new Education(); Personal personForm = new Personal(); personForm.setBiography(bio); personForm.setEducation(educ); references(model); model.addAttribute("personForm", personForm); return "personal_update"; } @RequestMapping(method=RequestMethod.POST) public String submitForm(Model model, @ModelAttribute("personForm") @Validated Personal personForm, BindingResult binding) { model.addAttribute("personForm", personForm); String returnVal = "personal"; if(binding.hasErrors()) { references(model); returnVal = "personal_update"; } else { model.addAttribute("personSess", personForm); model.addAttribute("pStatusSess", "undefault"); } return returnVal; } public void references(Model model) { List<String> hobbiesList = new ArrayList<String>(); hobbiesList.add("Not Applicable"); hobbiesList.add("Singing"); hobbiesList.add("Painting"); hobbiesList.add("Traveling"); hobbiesList.add("Writing"); hobbiesList.add("Swimming"); model.addAttribute("hobbiesList", hobbiesList); // Rest of the code in sources } }
Although there is no restriction as to what, and how many, handler methods are needed in a typical controller class, a form controller has to maintain having only one @RequestMapping
stereotype declared at the class level. The path indicated must be the controller's form view.
The preceding controller has two service operations namely the initForm()
and submitForm()
methods. The initForm()
service is always a GET
method, which always initializes the form view. It also binds the form domain object that contains the request data from the user. The submitForm()
is a POST service that is executed after form submission. This automatically extracts the validated domain object from the form and transports any model to the result views.
The references()
method is used to populate the form components found in the form view. Java Collection Framework (JCF) is used to contain all the data which can be hardcoded or which is from a data store.
The usual exception during form-handling happens when the form-backing object, or the model, is not set to bind with the form view. Whenever the browser calls the initForm()
, the following exception will be encountered.
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'home' available as request attribute at o.s.w.s.s.BindStatus.<init>(BindStatus.java:141)
In order for submitForm()
to access the form-backing object, the @ModelAttribute
annotation is needed to be declared in its parameters. An @ModelAttribute
local parameter indicates that the argument will be retrieved from the form view. If not present in the form view, the argument must be instantiated first in the initForm()
, and bound to the form view. Moreover, Spring MVC throws an exception when errors occur during request binding, by default. That is why in submitForm()
, one of the arguments is for BindIngResult
where we can extract all errors during the request transaction. The BindingResult
argument needs to be positioned right after our form-backing object, which is a rare case when it comes to the @RequestMapping
order of arguments.
If this rule is violated, the following exception will be thrown:
java.lang.IllegalStateException: Errors/BindingResult argument declared without preceding model attribute
Using @ModelAttribute and @SessionAttributes
The Personal Web Portal (PWP) application does not have database connectivity since the main highlight in this chapter are the raw Spring MVC 4 components. Also, it has no Spring security attached to it from the core, so there are no login/logout transaction for the user. All controllers in the PWP use a lot of @ModelAttribute
and @SessionAttributes
data.
The @ModelAttribute
data are actually the same with request attributes. These are objects created from one single request dispatching. Moreover, these data are also called request-scoped data, which means that these data values are only valid within one request-response transaction.
One use of @ModelAttribute
is to bind the form domain object to the form view page. On an incoming request transaction, any models with @ModelAttributes
are called before any controller handler method. The following initForm()
method is one example of this scenario.
@RequestMapping(method=RequestMethod.GET) public String initForm(Model model, @ModelAttribute("profesionalForm") Professional professionalForm) { return "professional_update"; } @RequestMapping(method=RequestMethod.GET) public String initForm(Model model) { Professional professionalForm = new Professional(); model.addAttribute("professionalForm", professionalForm); references(model); return "professional_update"; } public void references(Model model) { List<String> hobbiesList = new ArrayList<String>(); hobbiesList.add("Not Applicable"); hobbiesList.add("Singing"); hobbiesList.add("Painting"); hobbiesList.add("Traveling"); hobbiesList.add("Writing"); hobbiesList.add("Swimming"); model.addAttribute("hobbiesList", hobbiesList); List<String> readingsList = new ArrayList<String>(); readingsList.add("Not Applicable"); readingsList.add("Novel"); readingsList.add("Magazine"); readingsList.add("Newspaper"); readingsList.add("Diaries"); model.addAttribute("readingsList", readingsList); List<String> educLevelList = new ArrayList<String>(); educLevelList.add("Not Applicable"); educLevelList.add("Doctoral"); educLevelList.add("Masters"); educLevelList.add("College"); educLevelList.add("Vocational"); educLevelList.add("High School"); model.addAttribute("educLevelList", educLevelList); }
To initialize the @ModelAttribute
, it must be injected with an object value just like we have in the PWP project:
@ModelAttribute("greetings") public List<String> getGreetings() { return Greetings.allGreets(); }
The @SessionAttributes
, on the other hand, are used by the controller to declare session objects during session handling. Equally, these attributes are the session attributes in a typical JEE web component. As long as the session is not terminated, session data are always accessible for update and retrieval. Sessions were used in the PWP project to store values for all the dynamic content transactions. Here is a code snippet that shows how to declare, initialize and update values that are @SessionAttributes
.
@Controller @SessionAttributes("posts") @RequestMapping("pwp") public class IndexController { @ModelAttribute("posts") public List<PostMessage> newPosts() { return initPost(); } @RequestMapping(value="/index", method=RequestMethod.GET) public String getIndex(Model model, @ModelAttribute("posts") List<PostMessage> posts, @ModelAttribute("postForm") PostMessage postForm) { Home home = new Home(); home.setMessage(initMessage()); home.setQuote(initQuote()); model.addAttribute("home", home); if(posts == null) posts = newPosts(); if(validatePost(postForm)) { postForm.setDatePosted(new Date()); posts.add(postForm); } model.addAttribute("posts", posts); return "index"; } // Other codes refer to sources }
The @SessionAttributes
are declared on the controller level and are initialized as with any @ModelAttribute
. Always initialize the session data to avoid the following error:
Servlet.service() for servlet [pwp] in context with path [/ch01] threw exception [Expected session attribute 'posts'] with root cause org.springframework.web.HttpSessionRequiredException: Expected session attribute 'posts'
In order for the service operations to access the session attribute argument, be sure to have a @ModelAttribute
parameter on the local parameter of the service depicting the attribute data:
public String getIndex(Model model, @ModelAttribute("posts") List<PostMessage> posts, @ModelAttribute("postForm") PostMessage postForm) { }
These two attributes trigger the information workflow of the PWP project, even without the help of any web services or data store.
Form domain objects
Binding to the form view using @ModelAttribute
, forms domain objects that are plain POJOs, and are used to save request parameter values from the user. Sometimes these objects are also called form data models. See the following domain:
package org.packt.personal.web.portal.model.form; public class Home { private String message; private String quote; public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getQuote() { return quote; } public void setQuote(String quote) { this.quote = quote; } }
Instantiation of this model will be found in the initForm()
method and the binding to the form view page will be implemented using the Spring Form Taglib directive:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
The Spring MVC <form>
tag has its own form components that can be readily mapped to the setters of the form domain objects. These topics will be highlighted in Chapter 3, Student Management Portal (SMP), of this book. The following JSP code snippet shows how the binding is done through the <form>
tag:
<form:form commandName="homeForm" method="post"> <div class="form_settings"> <!-- insert the page content here --> <h2>Update Inspirational Message</h2> <p><form:textarea path="message" rows="8" cols="50"/></p> <span><form:errors path="message" cssStyle="color: #ff0000; "/></span> <br/> <br/> <br/> <h2>Update Best Quote (Required)</h2> <p><form:textarea path="quote" rows="8" cols="50"/></p> <span><form:errors path="quote" cssStyle="color: #ff0000; "/></span> <br/> <br/> <br/> <p style="padding-top: 15px; padding-left: 20px"><input class="submit" type="submit" value="Update Home Page"></p> </div> </form:form>
There are instances when a form view page contains several request parameters which need to be subdivided into groups.
Given this situation, the best strategy is to decompose the huge flat domain POJO into several specific form domain models, each containing related data. Then, create a main POJO component class, which will hold all smaller domain models. This class will be represented as the form-backing object for the form view. A sample implementation of the preceding content page is shown as follows:
package org.packt.personal.web.portal.model.form; public class Personal { private Biography biography; private Education education; public Biography getBiography() { return biography; } public void setBiography(Biography biography) { this.biography = biography; } public Education getEducation() { return education; } public void setEducation(Education education) { this.education = education; } } package org.packt.personal.web.portal.model.form; import java.util.Date; import java.util.List; public class Biography { private String firstName; private String lastName; private Integer age; private Date birthDate; private String location; private String country; private List<String> hobbies; private List<String> readings; // The getters and the setters } package org.packt.personal.web.portal.model.form; public class Education { private String educLevel; private String institution; private String degree; private String specialization; private Integer year; // The getters and the setters }
The binding of the component will be quite different compared to the usual domain model. To bind all biography attributes to the form components, it is important to access first the getter
method of the biography object, then through this we can now access all the setters of its attributes. The following example is a clear picture of the complicated binding:
<form:form commandName="personForm" method="post"> <div class="form_settings"> <!-- insert the page content here --> <h2>Update Personal Information</h2> <table style="width:100%; border-spacing:0;"> <tr><td>First Name (*)</td> <td><form:input path="biography.firstName"/></td> <td><form:errors path="biography.firstName" cssStyle="color: #ff0000"/></td> </tr> <tr><td>Last Name (*)</td> <td><form:input path="biography.lastName"/></td> <td><form:errors path="biography.lastName" cssStyle="color: #ff0000"/> </tr> <!-- Rest of the script in the sources --> </table> <!-- Rest of the code in sources --> </form:form>
The ViewResolver and view configuration
The interface ViewResolver
is responsible for the mapping of the view names to the actual view pages. It also provides the view interface, which addresses the request of a view to the view technology. In this PWP project, all of our pages are written in JSP-JSTL so the view interface used is org.springframework.web.servlet.view.JstlView
. The project's view resolver is implemented inside the Spring container in this way:
<bean id="viewResolver" class="org.springframework.web.servlet.view. ResourceBundleViewResolver"> <property name="basename"> <value>config.views</value> </property> </bean>
The views are also declared in the custom property file ./src/config/views.properties
:
hello.(class)=org.springframework.web.servlet.view.JstlView hello.url=/jsp/hello_world.jsp index.(class)=org.springframework.web.servlet.view.JstlView index.url=/jsp/index.jsp index_update.(class)=org.springframework.web.servlet.view.JstlView index_update.url=/jsp/index_update.jsp // See the sources
There are three popular generic ViewResolver
implementations that developers always use and these are:
InternalResourceViewResolver
: This is implemented whenever all the actual view pages are stored inside/WEB-INF/jsp
. It has two sets of properties, namely a prefix or suffix that needs to be configured to generate the final view page URL.<bean class="org.springframework.web.servlet.view. InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix"> <value>/WEB-INF/</value> </property> <property name="suffix"> <value>.jsp</value> </property> </bean>
- The prefix indicates the location of the actual views, while the suffix tells the controller the allowed extension of all the actual pages. By default,
InternalResourceViewResolver
resolves the view names into view objects of typeJstlView
if the JSTL library is present in the classpath. If the view template is not the default, theviewClass
property must be explicitly declared and mapped to other view templates likeorg.springframework.web.servlet.view.tiles2.TilesView
if tiles are to be used.
XmlViewResolver
: If you want to declare each individual view-mapping to the actual page in an XML format, then this implementation best fits the project. A sample implementation is shown as follows:
<bean id="helloWorld" class="org.springframework.web.servlet.view.JstlView"> <property name="url" value="/WEB-INF/helloWorld.jsp" /> </bean>
ResourceBundleViewResolver
: This implementation is the most flexible among the three, because the only thing needed here is a property containing all the view mappings. Moreover, one has the capability to combine different view technology in just one project, for the purpose of mixing together presentation layers.
A Spring MVC project can have more than resolvers, given that the order property in all definitions is defined to set order levels of 0 having the highest priority. The following code shows the ordering technique:
<bean class="org.springframework.web.servlet.view. InternalResourceViewResolver"> <property name="prefix"> <value>/WEB-INF/</value> </property> <property name="suffix"> <value>.jsp</value> </property> <property name="order" value="2" /> </bean> <bean class= "org.springframework.web.servlet.view.XmlViewResolver"> <property name="location"> <value>/WEB-INF/views.xml</value> </property> <property name="order" value="1" /> </bean> <bean class= "org.springframework.web.servlet.view.ResourceBundleViewResolver"> <property name="basename" value="views" /> <property name="order" value="0" /> </bean>
By convention, the InternalResourceViewResolver
is always given the least priority because it takes a little time to map the view name directly to the actual pages before all the remaining resolvers. This might give conflict to other mappings if other resolvers are not fast enough in mapping views.
Actual view pages
The view template used in this project is JSP-JSTL, since the view interface used to map to JSP pages is org.springframework.web.servlet.view.JstlView
. It is no longer recommended to use scriptlets.
Obviously, the actual view pages use some EL language components like the implicit object sessionScope
. EL language is part of the jsp-api.jar
libraries, so it is still acceptable to use its components. Moreover, we also used some JSTL tags like <c:out/>
and <c:url/>
. The tag <c:out/>
is always recommended to output values of EL expression ${}
, especially in generating reports wherein lots of the data are handled by EL expressions. The attribute escapeXml
means that all words that are HTML tags will be captured, and thus they will be rendered on the page as HTML components. But most importantly, redirection implemented inside the view page must use URL rewriting so that when cookies are cut-off, the session data will still be shared by all controllers and views.
Validating Form Data
All form domain objects must be validated using the org.springframework.validation.Validator
interface and annotations supported by JSR 303. The validation interface is implemented to create a set of validation rules as per the form view. A sample implementation used in PWP's home content page is shown as follows:
package org.packt.personal.web.portal.validator; import org.packt.personal.web.portal.model.form.Home; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; public class IndexValidator implements Validator { @Override public boolean supports(Class<?> clazz) { return Home.class.equals(clazz); } @Override public void validate(Object obj, Errors errors) { Home homeForm = (Home) obj; ValidationUtils.rejectIfEmptyOrWhitespace(errors, "message", "message.empty"); ValidationUtils.rejectIfEmptyOrWhitespace(errors, "quote", "quote.empty"); if(homeForm.getMessage().length() > 500) { errors.rejectValue("message", "message.maxlength"); } if(homeForm.getMessage().length() < 50) { errors.rejectValue("message", "message.minlength"); } if(homeForm.getQuote().length() > 500) { errors.rejectValue("quote", "quote.maxlength"); } if(homeForm.getQuote().length() < 50) { errors.rejectValue("quote", "quote.minlength"); } } }
It validates checks if the user entered a content message and quotes greater than 50, but not greater than 500 alphanumeric. The validator interface has two abstract methods to implement, namely:
public boolean supports()
: This method checks what type of@ModelAttribute
object is being validated.@SessionAttributes
are also included in this Boolean method.public void validate()
: If the preceding method confirms correctly the@ModelAttribute
to be validated, this method will be executed next, dealing with all data values of the domain object.
Validators are components of the Spring MVC project. Spring MVC 4 uses the @Autowired
stereotype to call the instance of the validator in the controller class. To enable the validator, the setValidator()
method of org.springframework.web.bind .WebDataBinder
has to be invoked inside the initBinder()
method. To avoid complications, it is recommended to set one validator per initBinder()
since we can create more than one initBinder()
inside a form controller.
For situations like in PWP where both the @SessionAttributes
and @ModelAttribute
are utilized by the operations, initBinder()
is recommended to be explicitly mapped to the specific attribute for the validator. Otherwise, the following exception shown will be encountered:
SEVERE: Servlet.service() for servlet [pwp] in context with path [/ch01] threw exception [Request processing failed; nested exception is java.lang.IllegalStateException: Invalid target for Validator [org.packt.personal.web.portal.validator.IndexValidator@5b4ca52d]: undefault] with root cause java.lang.IllegalStateException: Invalid target for Validator [org.packt.personal.web.portal.validator.IndexValidator@5b4ca52d]: undefault
To retrieve the result of the validation on a @ModelAttribute
as per initBinder()
, be sure to use the @Validated
stereotype with the model argument declared in the submitForm()
method. Following is a code that shows how to declare and enable validation in a form controller:
@Autowired private Validator indexValidator; @InitBinder("homeForm") public void initBinder(WebDataBinder binder) { binder.setValidator(indexValidator); } @RequestMapping(method=RequestMethod.POST) public String submitForm(Model model, @ModelAttribute("homeForm") @Validated Home homeForm, BindingResult binding) { model.addAttribute("homeForm", homeForm); String returnVal = "index"; if(binding.hasErrors()) { returnVal = "index_update"; } else { model.addAttribute("homeSess", homeForm); model.addAttribute("statusSess", "undefault"); } return returnVal; }
Validation using JSR 303
Aside from custom validation using the validator interface, Spring 4.x container supports annotations under the JSR 303 specifications that are applied directly to Java beans, used by the classes to impose specific validation rules. The following EmailController
uses @NotNull
to check if the two objects are not null
, otherwise an error will be detected by the BindingResult
.
@Controller @RequestMapping("/pwp/contact") public class EmailController { @NotNull @Autowired private SimpleMailMessage emailTemplate; @NotNull @Autowired private JavaMailSender emailSender; // See the sources }
Aside from @NotNull
, annotations like @Pattern
, and @Size
are also widely used in string matching and collection size restrictions, respectively.
Domain data type conversion and filtering
The main purpose of the initBinder()
is not purely to validate, but to bind request
parameter data to the form domain model. It checks if the request
parameter data matches the type of variables in the form-backing object. It provides conversion processes to data in order to avoid type mismatch and other related exceptions. The method has built-in property editors that you can use to check types. Some are custom editors of the type java.beans.PropertyEditorSupport
that check complicated matches with added custom transactions.
In the Personal portal page, there are data that needs to be converted into proper object types like age and birth date.
Remember that request
parameter data are all, by default, String
objects. The custom property editor can be helpful in converting String
data to other types, just to fit in the form-backing object's setter
methods. Following is a custom editor for this page:
package org.packt.personal.web.portal.converter; import java.beans.PropertyEditorSupport; public class AgeConverter extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { Integer age = 0; try { age = Integer.parseInt(text.trim()); } catch(Exception e) { age = 18; } setValue(age); } } package org.packt.personal.web.portal.converter; import java.beans.PropertyEditorSupport; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class BirthDateConverter extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { SimpleDateFormat format = new SimpleDateFormat("mm/yy/dd"); Date birthDate; try { birthDate = format.parse(text.trim()); } catch (ParseException e) { // TODO Auto-generated catch block birthDate = new Date(); } setValue(birthDate); } }
E-mail configuration
The Reach Out page opens an electronic communication between the portal owner and the portal reader or user. The Spring Framework supports e-mail operations with the org.springframework.mail
package as the root level package with the following API classes:
MailSender
: The central interface for sending e-mails is theMailSender
interface.SimpleMailMessage
: The simple value object encapsulating the properties of a simple mail such as from and to (plus many others) is the class.MailException
: The root exception of all e-mail checked exceptions which provide a higher level of abstraction over the lower level mail system exceptions.JavaMailSender
: The interface that adds specializedJavaMail
features such as MIME message support to its superclassMailSender
interface.MimeMessageHelper
: A class that comes in pretty handy when dealing withJavaMail
messages without using verbose JavaMail APIs.MimeMessagePreparator
: A callback interface for the preparation ofJavaMail
MIME messages.
This project sends three forms of e-mail template, namely the basic text-based e-mail, HTML-based e-mail and template-based e-mail. Following is PWP's way of sending an HTML-based e-mail.
public void sendMailHTML(Email emailForm) { String fromEmail = emailForm.getSendTo(); String toEmail = emailForm.getSendTo(); String emailSubject = emailForm.getSubject(); String emailBody = emailForm.getMessage(); MimeMessage mimeMessage = emailSender.createMimeMessage(); try { MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8"); mimeMessage.setContent("<i><b>"+emailBody +"</b></i>", "text/html"); helper.setFrom(fromEmail); helper.setTo(toEmail); helper.setSubject(emailSubject); } catch (MessagingException e) {} /* uncomment the following lines for attachment FileSystemResource file = new FileSystemResource("sample.jpg"); helper.addAttachment(file.getFilename(), file); */ emailSender.send(mimeMessage); System.out.println("Mail sent successfully."); }