Spring preprocess request in another controller method - spring

In one of my controllers I have:
#RequestMapping(value = "search", method = RequestMethod.GET)
public ModelAndView searchUsers(HttpSession session, HttpServletRequest request) {
UiUserSearchCriteria userSearchCriteria = (UiUserSearchCriteria) session
.getAttribute("UsersController_userSearchCriteria");
if (null == userSearchCriteria) {
userSearchCriteria= defaultUserSearchCriteria;
}
// Here be dragons
return searchUsers(userSearchCriteria, new BeanPropertyBindingResult(userSearchCriteria,
"userSearchCriteria"), session, request);
}
#RequestMapping(value = "search", method = RequestMethod.POST)
public ModelAndView searchUsers(
#ModelAttribute("userSearchCriteria") UiUserSearchCriteria userSearchCriteria,
BindingResult bindingResult, HttpSession session, HttpServletRequest request) {
userSearchCriteriaValidator.validate(userSearchCriteria, bindingResult);
if (bindingResult.hasErrors()) {
// Here be dragons
return new ModelAndView("searchUsers");
}
ModelAndView result = new ModelAndView("redirect:listUsers");
PagedListHolder<UiUser> userList = new PagedListHolder<UiUser>(
usersService.searchUsers(userSearchCriteria));
userList.setPageSize(10);
userList.setSort(defaultSort);
userList.resort();
session.setAttribute("UsersController_userList", userList);
session.setAttribute("UsersController_userSearchCriteria", userSearchCriteria);
return result;
}
The logic is simple: when the user navigates to search page I actually perform a search with default criteria and return him a list (this is the result of requirements changing, huh).
I found a problem in this code, accidentally. When default search criteria is not valid the behavior is: navigate to search -> populate search criteria with invalid criteria -> call another method (the second one, with POST) -> perform validation -> errors are not empty, so return searchUsers view. But the BindingResult bindingResult is actually syntethic, from previous method (new BeanPropertyBindingResult(userSearchCriteria, "userSearchCriteria")). So I got an error No binding result is bound to session (I agree with this).
I cannot have #ModelAttribute and BindingResult parameters (that, which are bound by Spring) pair in GET method to call POST with them.
So what is the best solutions for this?

I think you can simply associate your new BeanPropertyBindingResult(userSearchCriteria,
"userSearchCriteria") with an appropriate Spring model attribute name, this way:
BindingResult bindingResult = new BeanPropertyBindingResult(userSearchCriteria, "userSearchCriteria")
model.addAttribute(BindingResult.MODEL_KEY_PREFIX + "userSearchCriteria", bindingResult);
This is the default Spring MVC behavior of binding the validation results of a specific model attribute and should help you avoid the No binding result.. error

Related

Using RedirectView to redirect and then accessing redirect attributes in the Controller method

I am trying to create user form in my application using Spring MVC and then validating the ModelAttribute. If my validations fail, I use the ModelAndView to return the view showing validation errors on form. My Controller code to render blank form is:
#GetMapping("/home")
public ModelAndView getUserHome(){
ModelAndView mv = new ModelAndView();
//In actual application, I have more code to put attributes in ModelAndView that will be needed
//on view form, like select box options
mv.addObject("user", new User());
mv.setViewName("user/home/userHome");
return mv;
}
Once the user form on view is filled in, and submit button clicked, I POST the request to the following Controller method:
#PostMapping("/persistUser")
public ModelAndView saveUser(#ModelAttribute("user") #Validated User user, BindingResult bindingResult, ModelMap modelMap){
ModelAndView mv = new ModelAndView();
List<ObjectError> errors = bindingResult.getAllErrors();
if(bindingResult.hasErrors() && !(errors.size() == 1 && errors.get(0).getCode().equals("user.system.mismatch"))){
mv.addObject("user", user);
//more code to put more attributes like select box options on view
mv.setViewName("user/home/userHome");
return mv;
}
//if no errors, I have code to persist user object here. At this point
//I sure can have userId and pass this as request attribute to redirect URL.
//But, I need to also pass some messages(based on business logic) about what selections were
//made on user form. I do not have the option to persist these selections on backend,
//as our database does not have supporting columns for that. Therefore, I need to send them as
//redirect attributes. And these messages would be displayed only once, right after the user
//object was created. Upon subsequent visits to this userId view, these messages would no longer
//be required. So we don't really need to persist them at backend.
RedirectView rv = new RedirectView("/user/home?userId="+user.getId(), true, true, false);
//Here user.getId() is the userId newly persisted.
modelMap.addAttribute("message", messageSource.getMessage("user.system.mismatch", null, Locale.US));
return new ModelAndView(rv, modelMap);
}
I get the "user" model object here, perform validations, and if errors, I send user back to the form via ModelAndView. If no errors, I have code to persist object and redirect to another view that shows the user object that was just created. On this view, I need to display some messages which I am trying to pass as redirect attributes to the redirect receiving controller method. Please read the commented lines that explain why I need to pass the messages. I wrote my Controller method as follows:
#GetMapping("/user/home")
public ModelAndView getUser(
#RequestParam(name="userId", required = true) String userId,
#RequestParam(name="message", required = false) String message,
HttpSession session){
ModelAndView mv = new ModelAndView();
User user = (User)session.getAttribute("user");
mv.addObject("message", message);
mv.setViewName("user/home/view");
return mv;
}
I have not been able to retrieve the "message" attribute here. I have tried using RedirectAttributes too, but either way I am not able to retrieve the values stored in "message". Of course I can send "message" as another request parameter, but my messages tend to be long and I don't want to see super long URLs.
Can somebody please advice how do I tackle this problem? Thanks.
I finally figured a way to add "message" as RedirectAttributes and later retrieve it as ModelAttribute. Below are the modified version of controller methods:
#PostMapping("/persistUser")
public ModelAndView saveUser(#ModelAttribute("user") #Validated User user, BindingResult bindingResult, RedirectAttributes ra){
ModelAndView mv = new ModelAndView();
List<ObjectError> errors = bindingResult.getAllErrors();
if(bindingResult.hasErrors() && !(errors.size() == 1 && errors.get(0).getCode().equals("user.system.mismatch"))){
mv.addObject("user", user);
//more code to put more attributes like select box options on view
mv.setViewName("user/home/userHome");
return mv;
}
RedirectView rv = new RedirectView("/user/home?userId="+user.getId(), true, true, false);
//Here user.getId() is the userId newly persisted.
ra.addFlashAttribute("message", messageSource.getMessage("user.system.mismatch", null, Locale.US));
return new ModelAndView(rv);
}
And then the redirect receiving method is as follows:
#GetMapping("/user/home")
public ModelAndView getUser(
#RequestParam(name="userId", required = true) String userId,
#ModelAttribute(name="message") String message,
HttpSession session){
ModelAndView mv = new ModelAndView();
User user = (User)session.getAttribute("user");
mv.addObject("message", message);
mv.setViewName("user/home/view");
return mv;
}
This works as expected for me.

Neither BindingResult nor plain target object for bean name AFTER redirect

I have a form like this (in a page called add.jsp):
<form:form action="${pageContext.request.contextPath}/add" method="post" modelAttribute="addForm">
</form:form>
On GET request, i populate modelAttribute:
#RequestMapping(value ="add", method = RequestMethod.GET)
public ModelAndView add(Map<String, Object> model) {
model.put("addForm", new AddUserForm());
return new ModelAndView("add");
}
When i perform the form submitting (a POST request), i have the follow method:
#RequestMapping(value ="add", method = RequestMethod.POST)
public ModelAndView add(Map<String, Object> model, #Valid AddUserForm form, Errors errors) {
if (errors.hasErrors()) {
//model.put("addForm", new AddUserForm());
return new ModelAndView("add");
}
....
}
But i get this error: Neither BindingResult nor plain target object for bean name 'addForm' available as request attribute
My workaround is to add model.put("addForm", new AddUserForm());, the command that i have commented on POST request.... but... where is my error ?
In both case, you are returning the same view (i.e. "add") and this view contains a form with a modelAttribute="addForm" therefore the model MUST contains an object named "addForm".
If you don't wan't to populate your model with a new AddUserForm after a POST with errors, you probably should :
return another view (without the "addForm" model attribute)
or
reuse the same "addForm": model.put("addForm", form);

RedirectView vs redirect: in spring mvc

which is better to use to redirect url in spring mvc:
return new ModelAndView(new RedirectView("../abc/list.vm"));
or
return new ModelAndView("redirect:DummyRedirectPage.htm");
The ModelAndView object holds an instance variable that references its view object and which may be either:
A concrete View implementation such as the case when you use:
new ModelAndView(new RedirectView("../abc/list.vm"))
A String object holding the symbolic name of the view prefixed by redirect: or forward:, such is the case when using:
new ModelAndView("redirect:DummyRedirectPage.htm")
Now when the Spring base web entry, DispatcherServlet, is called to render a View, it will try to resolve the requested view against the given ModelAndView object, either getting the reference to its View sub-implementation it its afforded or resolving the View and creating it out of the ModelAndView view string representation:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// ...
View view;
if (mv.isReference()) {
// Resolve the view and instantiate it...
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
// ...
}
// ...
}
Note that org.springframework.web.servlet.ModelAndView#isReference returns true if the view is a String object.
Given below details, I would assume that the first ModelAndView would involve less computation thus may be prefered to the later.

#Valid and Binding Result for Data from DB

I am using Spring validation(JSR 303) in one of the web apps.I have no issues when a user submits data and spring validation works pretty neat.But I have a scenario where I have to fetch data from a service and validate it and then bind them to my view.(something non-form validation).How can I use #Valid in this case or does it have to be done differently?
Here is a sample code,i started with
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ModelAndView getView(
#PathVariable("id") final String id, #User user,
HttpSession session) {
User user= getUser();
BindingResult result = new BeanPropertyBindingResult(user, "user");
validator.validate(user, result);
if(result.hasErrors()){
logger.log(Level.ERROR, "Errors");
}
ModelAndView view = new ModelAndView ("home");
view.addObject("user",user );
view.addAllObject(result.getModel());
return view;
As far as I understand you need to inject default org.springframework.validation.Validator into your controller (if #Valid works you should be able to do it)
#Autowired
Validator validator;
run validation manually as follows
User user = ...;
BindingResult result = BeanPropertyBindingResult(user, "user");
validator.validate(user, result);
and merge results into ModelMap (declare it as argument of your method) as follows
model.addAllAttributes(result.getModel());

Spring Framework 3 and session attributes

I have form object that I set to request in GET request handler in my Spring controller. First time user enters to page, a new form object should be made and set to request. If user sends form, then form object is populated from request and now form object has all user givern attributes. Then form is validated and if validation is ok, then form is saved to database. If form is not validated, I want to save form object to session and then redirect to GET request handling page. When request is redirected to GET handler, then it should check if session contains form object.
I have figured out that there is #SessionAttributes("form") annotation in Spring, but for some reason following doesnt work, because at first time, session attribute form is null and it gives error:
org.springframework.web.HttpSessionRequiredException: Session attribute 'form' required - not found in session
Here is my controller:
#RequestMapping(value="form", method=RequestMethod.GET)
public ModelAndView viewForm(#ModelAttribute("form") Form form) {
ModelAndView mav = new ModelAndView("form");
if(form == null) form = new Form();
mav.addObject("form", form);
return mav;
}
#RequestMapping(value="form", method=RequestMethod.POST)
#Transactional(readOnly = true)
public ModelAndView saveForm(#ModelAttribute("form") Form form) {
FormUtils.populate(form, request);
if(form.validate())
{
formDao.save();
}
else
{
return viewForm(form);
}
return null;
}
It throws Exception if controller called first time even though added #SessionAttributes({"form"}) to class. So add following populateForm method will fix this.
#SessionAttributes({"form"})
#Controller
public class MyController {
#ModelAttribute("form")
public Form populateForm() {
return new Form(); // populates form for the first time if its null
}
#RequestMapping(value="form", method=RequestMethod.GET)
public ModelAndView viewForm(#ModelAttribute("form") Form form) {
ModelAndView mav = new ModelAndView("form");
if(form == null) form = new Form();
mav.addObject("form", form);
return mav;
}
#RequestMapping(value="form", method=RequestMethod.POST)
#Transactional(readOnly = true)
public ModelAndView saveForm(#ModelAttribute("form") Form form) {
// ..etc etc
}
}
The job of #SessionAttribute is to bind an existing model object to the session. If it doesn't yet exist, you need to define it. It's unnecessarily confusing, in my opinion, but try something like this:
#SessionAttributes({"form"})
#Controller
public class MyController {
#RequestMapping(value="form", method=RequestMethod.GET)
public ModelAndView viewForm(#ModelAttribute("form") Form form) {
ModelAndView mav = new ModelAndView("form");
if(form == null) form = new Form();
mav.addObject("form", form);
return mav;
}
#RequestMapping(value="form", method=RequestMethod.POST)
#Transactional(readOnly = true)
public ModelAndView saveForm(#ModelAttribute("form") Form form) {
// ..etc etc
}
}
Note that the #SessionAttributes is declared on the class, rather than the method. You can put wherever you like, really, but I think it makes more sense on the class.
The documentation on this could be much clearer, in my opinion.
if there is no defined session object so I think it's gonna be like this:
#SessionAttributes({"form"})
#Controller
public class MyController {
#RequestMapping(value="form", method=RequestMethod.GET)
public ModelAndView viewForm() {
ModelAndView mav = new ModelAndView("form");
if(form == null) form = new Form();
mav.addObject("form", form);
return mav;
}
#RequestMapping(value="form", method=RequestMethod.POST)
#Transactional(readOnly = true)
public ModelAndView saveForm(#ModelAttribute("form") Form form) {
// ..etc etc
}
}
#Controller
#SessionAttributes("goal")
public class GoalController {
#RequestMapping(value = "/addGoal", method = RequestMethod.GET)
public String addGoal(Model model) {
model.addAttribute("goal", new Goal(11));
return "addGoal";
}
#RequestMapping(value = "/addGoal", method = RequestMethod.POST)
public String addGoalMinutes(#ModelAttribute("goal") Goal goal) {
System.out.println("goal minutes " + goal.getMinutes());
return "addMinutes";
}
}
On page addGoal.jsp user enters any amount and submits page. Posted amount is stored in HTTP Session because of
#ModelAttribute("goal") Goal goal
and
#SessionAttributes("goal")
Without #ModelAttribute("goal") amount entered by user on addGoal page would be lost
I'm struggling with this as well. I read this post and it made some things clearer:
Set session variable spring mvc 3
As far as I understood it this basically says:
that Spring puts the objects specified by #SessionAttributes into the session only for the duration between the first GET request and the POST request that comes after it. After that the object is removed from the session. I tried it in a small application and it approved the statement.
So if you want to have objects that last longer throughout multiple GET and POST requests you will have to add them manually to the HttpSession, as usual.

Resources