Deal with overlapping #SessionAttributes names? - java

How can I make it so the #SessionAttributes are scoped to their respective controllers, or clean up the #SessionAttributes when switching workflows between controllers prematurely?
Example:
User goes to webpage to edit a dictionary and comes into DictionaryController.java which creates a DictionaryForm.java object and stores it in #SessionAttributes under "form"
Normally, the dictionary entry is fetched in a GET request, then updated on POST and status.setComplete() is called on success;
However if the user does the GET request, then clicks away to another page such as OrganizationController.java the second controller appears to try and reuse the "form" #SessionAttribute object from the other controller and will fail before it even reaches the getOrganization() method. (The nature of how exactly it's failing is undetermined as my eclipse console isn't outputting any exceptions, but I suspect it's because the form types don't match up)
#SessionAttributes("form")
public class DictionaryController {
#ModelAttribute("form")
public DictionaryForm initForm() {
return new DictionaryForm();
}
#RequestMapping(value="/Dictionary" method=RequestMethod.GET)
public String getDictionary(
#ModelAttribute("form") DictionaryForm form) {
...
return "dictionaryView";
}
#RequestMapping(value="/Dictionary" method=RequestMethod.POST)
public String updateDictionary(
#ModelAttribute("form") DictionaryForm form,
SessionStatus status) {
...
status.setComplete();
return "successView";
}
}
#Controller
#SessionAttributes("form")
public class OrganizationController{
#ModelAttribute("form")
public OrganizationForm initForm() {
return new OrganizationForm();
}
#RequestMapping(value="/Organization" method=RequestMethod.GET)
public String getOrganization(
#ModelAttribute("form") OrganizationForm form) {
...
return "orgView";
}
#RequestMapping(value="/Organization" method=RequestMethod.POST)
public String updateOrganization(
#ModelAttribute("form") OrganizationForm form,
SessionStatus status) {
...
status.setComplete();
return "successView";
}
}

Solution I ended up using was having a BaseForm object type that all form types inherit from. Then in my request mapping methods for the GET requests, I would use #ModelAttribute("form") BaseForm form and manually check the form type in the body of the method, and if it doesn't match convert it and restore it in the session. (Replacing it in the session may be unnecessary if you attach it to your model object for the request)
ie.
#RequestMapping(value="/Organization" method=RequestMethod.GET)
public String getOrganization(HttpServletRequest request,
#ModelAttribute("form") BaseForm form) {
if (form.getClass() != OrganizationForm.class) {
form = new OrganizationForm();
request.getSession().setAttribute("form", form);
}
...
return "orgView";
}

Related

Spring form submission with minum boilerplate

I've been trying to figure out what the best practice is for form submission with spring and what the minimum boilerplate is to achieve that.
I think of the following as best practise traits
Validation enabled and form values preserved on validation failure
Disable form re-submission F5 (i.e. use redirects)
Prevent the model values to appear in the URL between redirects (model.clear())
So far I've come up with this.
#Controller
#RequestMapping("/")
public class MyModelController {
#ModelAttribute("myModel")
public MyModel myModel() {
return new MyModel();
}
#GetMapping
public String showPage() {
return "thepage";
}
#PostMapping
public String doAction(
#Valid #ModelAttribute("myModel") MyModel myModel,
BindingResult bindingResult,
Map<String, Object> model,
RedirectAttributes redirectAttrs) throws Exception {
model.clear();
if (bindingResult.hasErrors()) {
redirectAttrs.addFlashAttribute("org.springframework.validation.BindingResult.myModel", bindingResult);
redirectAttrs.addFlashAttribute("myModel", myModel);
} else {
// service logic
}
return "redirect:/thepage";
}
}
Is there a way to do this with less boilerplate code or is this the least amount of code required to achieve this?
First, I wouldn't violate the Post/Redirect/Get (PRG) pattern, meaning I would only redirect if the form is posted successfully.
Second, I would get rid of the BindingResult style altogether. It is fine for simple cases, but once you need more complex notifications to reach the user from service/domain/business logic, things get hairy. Also, your services are not much reusable.
What I would do is pass the bound DTO directly to the service, which would validate the DTO and put a notification in case of errors/warning. This way you can combine business logic validation with JSR 303: Bean Validation.
For that, you can use the Notification Pattern in the service.
Following the Notification Pattern, you would need a generic notification wrapper:
public class Notification<T> {
private List<String> errors = new ArrayList<>();
private T model; // model for which the notifications apply
public Notification<T> pushError(String message) {
this.errors.add(message);
return this;
}
public boolean hasErrors() {
return !this.errors.isEmpty();
}
public void clearErrors() {
this.errors.clear();
}
public String getFirstError() {
if (!hasErrors()) {
return "";
}
return errors.get(0);
}
public List<String> getAllErrors() {
return this.errors;
}
public T getModel() {
return model;
}
public void setModel(T model) {
this.model = model;
}
}
Your service would be something like:
public Notification<MyModel> addMyModel(MyModelDTO myModelDTO){
Notification<MyModel> notification = new Notification();
//if(JSR 303 bean validation errors) -> notification.pushError(...); return notification;
//if(business logic violations) -> notification.pushError(...); return notification;
return notification;
}
And then your controller would be something like:
Notification<MyModel> addAction = service.addMyModel(myModelDTO);
if (addAction.hasErrors()) {
model.addAttribute("myModel", addAction.getModel());
model.addAttribute("notifications", addAction.getAllErrors());
return "myModelView"; // no redirect if errors
}
redirectAttrs.addFlashAttribute("success", "My Model was added successfully");
return "redirect:/thepage";
Although the hasErrors() check is still there, this solution is more extensible as your service can continue evolving with new business rules notifications.
Another approach which I will keep very short, is to throw a custom RuntimeException from your services, this custom RuntimeException can contain the necessary messages/models, and use #ControllerAdvice to catch this generic exception, extract the models and messages from the exception and put them in the model. This way, your controller does nothing but forward the bound DTO to service.
Based on the answer by #isah, if redirect happens only after successful validation the code can be simplified to this:
#Controller
#RequestMapping("/")
public class MyModelController {
#ModelAttribute("myModel")
public MyModel myModel() {
return new MyModel();
}
#GetMapping
public String showPage() {
return "thepage";
}
#PostMapping
public String doAction(
#Valid #ModelAttribute("myModel") MyModel myModel,
BindingResult bindingResult,
RedirectAttributes redirectAttrs) throws Exception {
if (bindingResult.hasErrors()) {
return "thepage";
}
// service logic
redirectAttrs.addFlashAttribute("success", "My Model was added successfully");
return "redirect:/thepage";
}
}
One possible way is to use Archetype for Web forms, Instead of creating simple project, you can choose to create project from existing archetype of web forms. It will provide you with sufficient broiler plate code. You can also make your own archetype.
Have a look at this link to get deeper insight into archetypes.
Link To Archetypes in Java Spring

Forward request to another controller in Spring MVC

I'd like to know if there is a way I can forward a request from one controller to another without actually changing the URL in the browser.
#RequestMapping(value= {"/myurl"})
public ModelAndView handleMyURL(){
if(somecondition == true)
//forward to another controller but keep the url in the browser as /myurl
}
examples that I found online were redirecting to another url which was causing other controllers to handle that. I don't want to change the URL.
Try to return a String instead of ModelAndView, and the String being the forward url.
#RequestMapping({"/myurl"})
public String handleMyURL(Model model) {
if(somecondition == true)
return "forward:/forwardURL";
}
Instead of forwarding, you may just call the controller method directly after getting a reference to it via autowiring. Controllers are normal spring beans:
#Controller
public class MainController {
#Autowired OtherController otherController;
#RequestMapping("/myurl")
public String handleMyURL(Model model) {
otherController.doStuff();
return ...;
}
}
#Controller
public class OtherController {
#RequestMapping("/doStuff")
public String doStuff(Model model) {
...
}
}
As far as I know "forward" of a request will be done internally by the servlet, so there will not be a second request and hence the URL should remain the same. Try using the following code.
#RequestMapping(value= {"/myurl"})
public ModelAndView handleMyURL(){
if(somecondition == true){
return new ModelAndView("forward:/targetURL");
}
}

2 Request Handlers for POST (ResponseBody + "Normal")

I like to implement a REST-API into my SpringMVC application. At the moment, I have one method to handle POST-Requests, which "returns" a rendered ViewScript.
#RequestMapping(method=RequestMethod.POST)
public String onSubmit(User user, Model model)
{
return "success";
}
It would be nice, to add a second method with the #ResponseBody Annotation for POST-Requests, e.g. to send a JSON-Response.
Furthermore, the old Method still has to exists, to handle "normal" Requests.
But a code like this doesn't work:
#RequestMapping(method=RequestMethod.POST)
public String onSubmit(User user, Model model)
{
return "success";
}
#RequestMapping(method=RequestMethod.POST)
#ResponseBody
public Object add(User user, Model model)
{
// [...]
return myObject;
}
With this code, I'm getting a 405 (Method Not Allowed) Error from Tomcat. How can I fix this?
As it stands, Spring has no way to differentiate between these two requests: same URL, same request method.
You can further differentiate by mimetype:
#RequestMapping(method=RequestMethod.POST, headers="content-type=application/json")
Although there are several mimetypes associated with JSON :/ The headers value takes an array, however, so you can narrow/widen it as necessary.
See the headers docs.
Dont USE TWO ANNOTATION. It is a poor option. Just have one more method without annotation. But the method from the old method by checking the below condition.
JUST PASS ONE MORE ARGUMENT FROM UI by query parameter(request="JSON_Request").
#RequestMapping(method=RequestMethod.POST)
public String onSubmit(User user, Model model)
{
if(request="JSON_Request") {
newMethod(user, model);
}
return "success";
}
private Object newMethod(User user, Model model)
{
// [...]
return myObject;
}

Looking for a concise way to add title information to each page in Spring MVC via the Controller's Model object

I have a <TITLE> tag in my JSPs that is set using a value from the request handler:
<title><c:out value="${title}"/></title>
I created a method to do this to try to avoid adding mess to the Controller logic with this extra information.
But I'm still not happy with the way this looks in the code (My actual controller methods are much longer than the examples provided here so I'm trying to minimize and simplify them as much as possible).
Is there a more consise way of adding this information from within the Controller? (It can't be added in the JSPs).
#RequestMapping(value = "/foo", method = RequestMethod.GET)
public final String foo(final ModelMap model) {
addTitle(model, "Desolation Row is the title of this page");
return "foo";
}
#RequestMapping(value = "/goo", method = RequestMethod.GET)
public final String goo(final ModelMap model) {
addTitle(model, "Leopardskin Pillbox Hat is the title of this page");
return "goo";
}
public ModelMap addTitle(ModelMap model, String title) {
model.addAttribute("title", title);
return model;
}
If you want to factor out the addTitle method from your controllers, maybe you can put them in a HandlerInterceptor implementation?
Something like this maybe:
public class TitleInterceptor implements HandlerInterceptor {
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
String requestUrl = (String)request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String title = "";
if ("/url1.htm".equals(requestUrl)) {
title = "Title 1";
} else if ("/url2.htm".equals(requestUrl)) {
title = "Title 2";
}
modelAndView.getModel().put("title", title)
}
}
If you need some processing to determine the title, maybe the modelAndView available to the interceptor will contain the data that will help in determining the title given the url. If no processing is needed, just a simple mapping of a title to a url, you can even implement it as configurable Map during bean configuration in your applicationContext.xml
Some links I found helpful in implementing HandlerInterceptor can be found here:
http://whitesboard.blogspot.com/2009/10/handlerinterceptors-in-spring-web-mvc.html
http://static.springsource.org/spring/docs/2.0.x/api/org/springframework/web/servlet/HandlerInterceptor.html
If you don't want to go down the Interceptor or Aspect road (keeping everything in the Controller):
Create a BaseController that all Controllers extend
Have a HashMap in the BaseController mapping URLs to Titles
Put the addTitle method there too, modifying to return the same string as the JSP name.
BaseController code:
public ModelMap addTitle(ModelMap model, String page) {
model.addAttribute("title", titleMap.get(page));
return page;
}
Controller code becomes:
#RequestMapping(value = "/goo", method = RequestMethod.GET)
public final String goo(final ModelMap model) {
return super.addTitle(model, "goo");
}

Spring 3.0 set and get session attribute

I want to read a domain object (UserVO) from session scope.
I am setting the UserVO in a controller called WelcomeController
#Controller
#RequestMapping("/welcome.htm")
public class WelcomeController {
#RequestMapping(method = RequestMethod.POST)
public String processSubmit(BindingResult result, SessionStatus status,HttpSession session){
User user = loginService.loginUser(loginCredentials);
session.setAttribute("user", user);
return "loginSuccess";
}
}
I am able to use the object in jsp pages <h1>${user.userDetails.firstName}</h1>
But I am not able to read the value from another Controller,
I am trying to read the session attribute as follows:
#Controller
public class InspectionTypeController {
#RequestMapping(value="/addInspectionType.htm", method = RequestMethod.POST )
public String addInspectionType(InspectionType inspectionType, HttpSession session)
{
User user = (User) session.getAttribute("user");
System.out.println("User: "+ user.getUserDetails().getFirstName);
}
}
The code you've shown should work - the HttpSession is shared between the controllers, and you're using the same attribute name. Thus something else is going wrong that you're not showing us.
However, regardless of whether or not it works, Spring provides a more elegant approach to keeping your model objects in the session, using the #SessionAttribute annotation (see docs).
For example (I haven't tested this, but it gives you the idea):
#Controller
#RequestMapping("/welcome.htm")
#SessionAttributes({"user"})
public class WelcomeController {
#RequestMapping(method = RequestMethod.POST)
public String processSubmit(ModelMap modelMap){
User user = loginService.loginUser(loginCredentials);
modelMap.addtAttribute(user);
return "loginSuccess";
}
}
and then
#Controller
#SessionAttributes({"user"})
public class InspectionTypeController {
#RequestMapping(value="/addInspectionType.htm", method = RequestMethod.POST )
public void addInspectionType(InspectionType inspectionType, #ModelAttribute User user) {
System.out.println("User: "+ user.getUserDetails().getFirstName);
}
}
However, if your original code isn't working, then this won't work either, since something else is wrong with your session.
#SessionAttributes works only in context of particular handler, so attribute set in WelcomeController will be visible only in this controller.
Use a parent class to inherit all the controllers and use SessionAttributes over there. Just that this class should be in the package scan of mvc.
May be you have not set your UserVO as Serializable.

Categories