I am writing a wizard-like controller that handles the management of a single bean across multiple views. I use #SessionAttributes to store the bean, and SessionStatus.setComplete() to terminate the session in the final call. However, if the user abandons the wizard and goes to another part of the application, I need to force Spring to re-create the #ModelAttribute when they return. For example:
#Controller
#SessionAttributes("commandBean")
#RequestMapping(value = "/order")
public class OrderController
{
#RequestMapping("/*", method=RequestMethod.GET)
public String getCustomerForm(#ModelAttribute("commandBean") Order commandBean)
{
return "customerForm";
}
#RequestMapping("/*", method=RequestMethod.GET)
public String saveCustomer(#ModelAttribute("commandBean") Order commandBean, BindingResult result)
{
[ Save the customer data ];
return "redirect:payment";
}
#RequestMapping("/payment", method=RequestMethod.GET)
public String getPaymentForm(#ModelAttribute("commandBean") Order commandBean)
{
return "paymentForm";
}
#RequestMapping("/payment", method=RequestMethod.GET)
public String savePayment(#ModelAttribute("commandBean") Order commandBean, BindingResult result)
{
[ Save the payment data ];
return "redirect:confirmation";
}
#RequestMapping("/confirmation", method=RequestMethod.GET)
public String getConfirmationForm(#ModelAttribute("commandBean") Order commandBean)
{
return "confirmationForm";
}
#RequestMapping("/confirmation", method=RequestMethod.GET)
public String saveOrder(#ModelAttribute("commandBean") Order commandBean, BindingResult result, SessionStatus status)
{
[ Save the payment data ];
status.setComplete();
return "redirect:/order";
}
#ModelAttribute("commandBean")
public Order getOrder()
{
return new Order();
}
}
If a user makes a request to the application that would trigger the "getCustomerForm" method (i.e., http://mysite.com/order), and there's already a "commandBean" session attribute, then "getOrder" is not called. I need to make sure that a new Order object is created in this circumstance. Do I just have to repopulate it manually in getCustomerForm?
Thoughts? Please let me know if I'm not making myself clear.
Yes, sounds like you may have to repopulate it manually in getCustomerForm - if an attribute is part of the #SessionAttributes and present in the session, then like you said #ModelAttribute method is not called on it.
An alternative may be to define a new controller with only getCustomerForm method along with the #ModelAttribute method but without the #SessionAttributes on the type so that you can guarantee that #ModelAttribute method is called, and then continue with the existing #RequestMapped methods in the existing controller.
Related
In a Servlet, you can include an #Override service method which gets called before the doGet or doPost, is there a way to achieve the same in a Spring #Controller?
Or more precisely, in each method in the Controller, I need to make sure an Entity (in this case, a Product) exists and redirect otherwise, like so, so how would one achieve that in Spring? Note that I also need the Product available in each Method.
#Controller
#RequestMapping("/product/{prod_id}/attribute")
public class AttributeController {
#Autowired
private AttributeService attributeService;
#RequestMapping(value = "/add", method = RequestMethod.GET)
public String add(Model model, #PathVariable Long prod_id) {
Product product = attributeService.getProduct(prod_id);
if (product == null) {
return "products/products";
}
model.addAttribute("product", product);
model.addAttribute("attribute", new Attribute());
return "products/attribute_add";
}
#RequestMapping(value = "/add", method = RequestMethod.POST)
public String save(Model model, #PathVariable Long prod_id, #Valid Attribute attribute, BindingResult result) {
Product product = attributeService.getProduct(prod_id);
if (product == null) {
return "products/products";
}
// ...
}
// ...
}
This can be done with HandlerInterceptor. All you need to do is to extend HandlerInterceptorAdapter#preHandle and then register your interceptor through WebMvcConfigurer#addInterceptors. You can choose to use interceptor with all your mappings or with some specific mappers through InterceptorRegistration object with is returned by InterceptorRegistry#addInterceptor method.
By the way, HandlerInterceptors are useful to do some utility operations with requests and responses in general, like logging, adding headers, authentication, etc. For business-related operations I would recommend to use ControllerAdvice with custom business-oriented exceptions. In this case it would be a method which retrieves Product from database and throws custom exception if not found.
Is there a way to add an attribute to all paths of a certain user?
I.e I am trying to reach the current logged in administrator on all pages the administrator can reach, but I don't want to add this attribute to every single controller.
Something like this, where I don't need to return anything:
#RequestMapping(value = {"admin/**"}, method = RequestMethod.GET)
public void adminPaths(ModelMap model) {
model.addAttribute("user", getPrincipal());
}
You can use #ModelAttributes on a method in a controller. An #ModelAttribute on a method indicates the purpose of that method is to add one or more model attributes to all controller methods:
#Controller
#RequestMapping("/admin")
public class AdminController {
...
#ModelAttribute
public void populateModel(Model model) {
model.addAttribute("user", getPrincipal());
// add more ...
}
...
}
#ModelAttribute methods in a controller are invoked before #RequestMapping methods, within the same controller.
For truly wildcard matching, you can use ControllerAdvice and ModelAttributes on methods together. Something like following:
#ControllerAdvice(annotations = Controller.class)
public class AdminPopulatorAdvice {
#ModelAttribute
public void populateModel(HttpServletRequest request, Model model) {
// examine the request
// if its path contains /admin, then add attribute
model.addAttribute("user", getPrincipal());
// add more ...
}
}
I forgot to mention that I was using Spring security.
Bohuslav pointed me into the right direction and ended up here: https://docs.spring.io/spring-security/site/docs/current/reference/html/taglibs.html
I have a Controller configured on Spring, and I have to workout a DB connection through it to call DAO operations.
This connection is actually available in a session variable, which is not accessible at the momment to the Spring Controller due to it is not HttpServlet inherited.
What is the right way to this Controller access the session variables? Must I implement methods doGet and doPost, inherited from HttpServlet, in order to manipulate the request object? Can it rattle Spring controll over the class?
Thanks for responding.
#Controller
public class SpringController {
#RequestMapping("/create")
public String form(MyCar myCar) {
/*That's where I have to retrieve hibernateSession from
* HttpSession and pass to DAO class do its work.
*/
MyCarDAO myCarDao = new MyCarDAO(session);
myCarDao.saveOrUpdate(myCar);
return "WEB-INF/views/projeto/novo.jsp";
}
}
You can add a HttpSession parameter to your method:
#RequestMapping("/create")
public String form(MyCar myCar, HttpSession session) {
...
}
Spring will automatically add the session parameter when the method is called.
Check the documentation of RequestMapping for possible parameters
Suppose that you declare 3 session attributes, but use only 1 of them in your handler method parameters, so:
#SessionAttributes({ "abc", "def", "ghi" })
public class BindingTestController {
#ModelAttribute("abc")
public String createABC() {
return "abc";
}
#RequestMapping(method = RequestMethod.GET)
public void onGet(#ModelAttribute("abc") String something) {
// do nothing :)
}
#RequestMapping(method = RequestMethod.POST)
public void onPost(#ModelAttribute("abc") String something, BindingResult bindingResult, SessionStatus sessionStatus) {
sessionStatus.setComplete();
}
}
There are lots of example if hit it in google
IMO Right way should be to store the connection in a session-scoped bean instead of a session variable.
Use
#Scope(value = "session")
(cf. http://static.springsource.org/spring/docs/3.0.0.M3/reference/html/ch04s04.html)
I'm getting some weird binding issue with Spring MVC 3.
My Controller request mapping looks like this:
#RequestMapping
public String save(HttpServletRequest req,
#ModelAttribute("userEditForm") UserEditForm form,
BindingResult formBindingResult,
ModelMap model,
#ModelAttribute("session") AdminSession session) {
// some validation etc
}
The UserEditForm:
public class UserEditForm {
private User user;
public User getUser() { ... }
public void setUser(User user) { ... }
}
The AdminSession:
public class AdminSession {
private User user;
public User getUser() { ... }
public void setUser() { ...}
}
What's happening is that when I submit my form, Spring is binding the User as I expect in my UserEditForm object, however, the AdminSession is also having it's User bound by Spring, in so far as it's property values are also updated.
I'm going to assume it's due to having a user property in both #ModelAttribute objects.
I thought that having the BindingResult after the UserEditForm form in the method signature would stop this? The objects are separate instances, and my form elements reference the UserEditForm object:
<#spring.bind "userEditForm.user.name" />
<input name="${spring.status.expression}" />
I've noticed that in the generated HTML it's outputting:
<input name="user.name" />
Hardcoding the name as userEditForm.user.name gives me errors, so that's not the way forward.
Is there anyway to stop this from happening?
That's the default behavior when you annotate a handler method parameter with the #ModelAttribute. Spring takes the request properties and matches them to properties of the objects annotated with #ModelAttribute. That's what Spring looks at when deciding what to do: your annotations.
Since both UserEditForm and AdminSession are annotated with #ModelAttribute and both have a User property, a request property named user.name will get bound to both User properties.
You tried to include the command name in the input name and got an error. That's because when binding occurs it occurs on your command object and Spring looks for properties on it (the bindinf path is relative to the command object) and off course the expression does not find any property with that name. If you want to use a full name you could wrap the form in another object and use that for your command instead, something like this:
public class UserEditFormWrapper {
private UserEditForm form;
public UserEditForm getForm() {
return form;
}
public void setForm(UserEditForm form) {
this.form = form;
}
}
Now you can use an expression like this in your inputs: form.user.name and when you submit to your handler method that now looks like this:
#RequestMapping
public String save(HttpServletRequest req,
#ModelAttribute("userEditForm") UserEditFormWrapper formWrapper,
BindingResult formBindingResult,
ModelMap model,
#ModelAttribute("session") AdminSession session) {
UserEditForm form = formWrapper.getForm();
// some validation etc
}
the binding won't be triggered since AdminSession does not have a form property.
That's one way to solve this but it's kind of a hack. You don't want to have the request parameters bound to AdminSession but that's part of your model so you must have created it somewhere and placed it on the model, right? If so, then remove it from the method's parameters and just get it from the model, something like:
#RequestMapping(value = "/test", method = { RequestMethod.POST })
public String handlePost(HttpServletRequest req,
#ModelAttribute("userEditForm") UserEditForm form,
BindingResult formBindingResult, ModelMap model) {
AdminSession session = (AdminSession) model.get("session");
// some validation etc
}
I'm using Spring 3.1 and have web pages using validation. The field-level validation, and the display of errors, works OK. My problem is with Model attributes not being available during the validation form display.
Let's say I've code:
#RequestMapping(value="/edit", method=RequestMethod.GET)
public String getEdit(#RequestParam("id") Long id, Model model) {
model.addAttribute("mytitle", "Hello There");
return "editObject"
}
#RequestMapping(value="/edit", method=RequestMethod.POST)
public String postEdit(#RequestParam("id") Long id, #Valid #ModelAttribute("object") MyData object, BindingResult result) {
if(result.hasErrors()) {
return "editObject";
}
[snip]
}
If I have an error the hasErrors() is detected and short-circuits to the map "editObject". However, the model attributes aren't available.
What do I use here? I tried adding a Model reference to the postEdit parameter list and adding in again things like the "mytitle" attribute.
Thanks,
Jerome.
When postEdit controller render the editObject view, you are in a new request, so you are loosing the model (previously set for getEdit action).
What you need to do it re-set any values needed in a new model:
#RequestMapping(value="/edit", method=RequestMethod.POST)
public String postEdit(#RequestParam("id") Long id, #Valid #ModelAttribute("object") MyData object, BindingResult result, Model model) {
if(result.hasErrors()) {
model.addAttribute("mytitle", "Hello There");
model.addAttribute("object", object);
return "editObject";
}
If you need to keep those attributes for different views you can store them in a session for example (so that you won't have to re-set them for each new request).