SessionAttributes cause problems with multiple tabs - java

I have a Spring web app (Spring 3.2) and I have used following scenario to handle edit pages:
#Controller
#SessionAttributes(value = { "packet" })
public class PacketController {
#RequestMapping(value = "/edit-packet/{packet_id}", method = RequestMethod.GET)
public String editPacketForm(#PathVariable(value = "packet_id") Long packet_id, Model model)
{
model.addAttribute("packet", packetService.findById(packet_id));
return "packets/packetEdit";
}
POST method:
#RequestMapping(value = "/edit-packet/{packet_id}", method = RequestMethod.POST)
public String packetEditAction(Model model, #Valid #ModelAttribute(value = "packet")
Packet packet, BindingResult result, SessionStatus status)
{
if (result.hasErrors())
{
return "packets/packetEdit";
}
packetService.update(packet);
status.setComplete();
return "redirect:/";
}
Now the problem is what if someone tries to open multiple tabs for /edit-packet/{id} with different ids. With every new open tab the session 'packet' object will be overwritten. Then after trying to submit forms on multiple tabs, first tab will be submitted but it actually change the second packet because in session is second object and second tab will cause error because setComplete has been invoked so there is no 'packet' object in session.
(This is known issue https://jira.spring.io/browse/SPR-4160).
I am trying to implement this solution http://duckranger.com/2012/11/add-conversation-support-to-spring-mvc/ to solve this problem. I copied ConversationalSessionAttributeStore.java
ConversationIdRequestProcessor.java classes and in my servlet-config.xml I made this:
<mvc:annotation-driven />
<bean id="conversationalSessionAttributeStore"
class="com.xx.session.ConversationalSessionAttributeStore">
</bean>
<bean name="requestDataValueProcessor" class="com.xx.session.ConversationIdRequestProcessor" />
But it doesn't work, in my POST methods I don't see any new parameters, did I miss something?
UPDATE: Actually, it started working, but maybe someone has a better idea to solve this issue?
My other idea is to force a new session on every new tab, but it's not a nice solution.

Don't use session attributes, make your controller stateless and simply use the path variable to retrieve the correct model attribute.
#Controller
public class PacketController {
#ModelAttribute
public Packet packet(#PathVariable(value = "packet_id") Long packet_id) {
return packetService.findById(packet_id);
}
#RequestMapping(value = "/edit-packet/{packet_id}", method = RequestMethod.GET)
public String editPacketForm() {
return "packets/packetEdit";
}
#RequestMapping(value = "/edit-packet/{packet_id}", method = RequestMethod.POST)
public String packetEditAction(Model model, #Valid #ModelAttribute(value = "packet")
Packet packet, BindingResult result) {
if (result.hasErrors()) {
return "packets/packetEdit";
}
packetService.update(packet);
return "redirect:/";
}
}
Something like that should do the trick.

Related

Forwarding to another spring controller that receive parameter from POST action

I have a request mapping for a controller let's say it's A, it receives post action and uses its post values as parameter, sometimes the parameters will be very long so that's the reason why it's POST not GET apart from the best practices and security;
RequestMapping(value = "/reports/performance/indicator/{indicatorType}", method = RequestMethod.POST)
public String generatePerformanceReportsIndicator(ModelMap map,HttpServletResponse response, #PathVariable("indicatorType") Long indicatorType,
#RequestParam(value = "siteIds", required = false) List<Long> siteIds,
#RequestParam(value = "timeframeIds", required = false) List<String> timeframeIds,
#RequestParam(value = "showTarget", required = false) String showTarget,Locale locale) {
And then it turned out that in another spring controller I need to forward the request to the first one.
The problem is how I can add post parameters to the request before forwarding it to the first request mapping? is that healthy to say for example?
new FirstController().generatePerformanceReportsIndicator(....);
Given that:
I don't want the first request mapping to use get instead of post of the mentioned reasons.
I don't want to write redundant code by creating another method that extract the parameters as attribute from the model map.
You should not manually call other controllers! What you can do is redirect to them with RedirectAttributes like:
#RequestMapping(value = "/doctor/doEditPatientDetails", method = RequestMethod.POST)
public String editPatientDetails(Model model, #ModelAttribute(value = "user") #Valid User user,
BindingResult result, RedirectAttributes attr, Principal principal) {
if (null != principal) {
if (result.hasErrors()) {
attr.addFlashAttribute("org.springframework.validation.BindingResult.user", result);
attr.addFlashAttribute("user", user);
attr.addAttribute("id", user.getId());
return "redirect:/doctor/editPatient/{id}";
}
}
....
return "redirect:/doctor/patients";
}
#RequestMapping(value = "/doctor/editPatient/{id}", method = RequestMethod.GET)
public String showEditPatient(Model model, #ModelAttribute("id") String id, Principal principal) {
if (null != principal) {
//here you can access the model and do what everything you want with the params.
if (!model.containsAttribute("user")) {
model.addAttribute("user", user);
}
....
}
return "/doctor/editPatient";
}
Note that, to redirect to a link like "redirect:/doctor/editPatient/{id}" you have to add the id in RedirectAttributes. Also not that there are many ways you can achive the same functionality like HttpServletRequest

Spring MVC - The #SessionAttributes and status.setComplete()

I'm facing a problem I don't really know how to solve.
I am developing a Bug Tracker (learning purposes only). I have a page to create a new issue and one page to edit an issue. Both, for now, have their own controllers.
EditIssueController.java
#Controller
#RequestMapping(value = "/issues/{issueId}")
#SessionAttributes("issuePackage")
public class EditIssueController {
#Autowired
private IssueService issueService;
[...]
#ModelAttribute("issuePackage")
public IssueTagEnvironment populateIssue (#PathVariable("issueId") Integer issueId) {
IssueTagEnvironment issueTagEnv = new IssueTagEnvironment();
issueTagEnv.setIssue(issueService.getIssueById(issueId));
return issueTagEnv;
}
#InitBinder
public void initBinder (WebDataBinder binder) {
[...]
}
#RequestMapping(value = "/edit", method = RequestMethod.GET)
public ModelAndView editIssue (#PathVariable("issueId") Integer issueId,
#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage) {
ModelAndView mav = new ModelAndView("/issues/EditIssue");
[...]
IssueTagEnvironment issueTagEnv = new IssueTagEnvironment();
issueTagEnv.setIssue(issueService.getIssueById(issueId));
[...]
mav.addObject("issuePackage", issueTagEnv);
return mav;
}
#RequestMapping(value = "/edit", method = RequestMethod.POST)
public String updateIssue (#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage,
BindingResult result) {
if (result.hasErrors() == true) {
return "redirect:/issues/{issueId}/edit";
}
issueService.updateIssue(issuePackage.getIssue());
return "redirect:/issues/{issueId}";
}
}
CreateIssueController.java
#Controller
#SessionAttributes("issuePackage")
public class CreateIssueController {
#Autowired
private IssueService issueService;
[...]
#ModelAttribute("issuePackage")
public IssueTagEnvironment populateNewIssue () {
return new IssueTagEnvironment();
}
#InitBinder
public void initBinder (WebDataBinder binder) {
[...]
}
#RequestMapping(value = "/issues/CreateIssue", method = RequestMethod.GET)
public ModelAndView createIssueGet (#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage) {
ModelAndView mav = new ModelAndView("/issues/CreateIssue");
[...]
issuePackage.getIssue().setReporter(SecurityUtils.getCurrentUser());
return mav;
}
#RequestMapping(value = "/issues/CreateIssue", method = RequestMethod.POST)
public String createIssuePost (#ModelAttribute("issuePackage") IssueTagEnvironment issuePackage,
BindingResult result,
SessionStatus status) {
if (result.hasErrors() == true) {
return "redirect:/issues/CreateIssue";
}
[...]
issueService.createIssue(issuePackage.getIssue());
status.setComplete();
return "redirect:/issues/" + issuePackage.getIssue().getId();
}
}
So far everything seems correct (and in indeed works). But here are the dragons:
I am within an "edit" page changing data from an existing issue.
Instead of submitting the changes I decide to press the "Go Back" button from the navigator.
Right after that action (Go Back), I decide to create a new issue and... Here it is! The form to create a new issue isn't empty but filled with the information of the previous "edited-but-not-submitted" issue.
I understand what the problem is: The controller is not completing the session/status by executing status.setComplete().
My question here is, how to solve this problem?
Thanks in advance to the community!
For your current example, it is easy to fix , just change createIssueGet method to :
public ModelAndView createIssueGet () {
ModelAndView mav = new ModelAndView("/issues/CreateIssue");
IssueTagEnvironment issuePackage = new IssueTagEnvironment();
ModelAndView mav = new ModelAndView("/issues/CreateIssue");
mav.addAttribute("issuePackage", issuePackage);
[...]
[...]
}
That way you are sure that you always use a fresh IssueTagEnvironment object in that controller. And Spring will put it in session as you put it in model.
But the problem still remains : if you do not properly call status.setComplete(), you leave in session an object that should not be there, and like you said dragons may be there
I stopped using #SessionAttributes for that reason, and only use a hidden field (for the id) and a Converter from the id to a full object using the service layer, hoping it should be in cache and does not hit the database. Not really nice, but not really worse than that.

SpringMVC : POST Redirect GET and error message?

I have a page with a form and a table. When i submit the form, i want it to fill the table.
I tried simply returning the view name, but it doesn't go through the "Get" method.
I saw the Post Redirect Get pattern so i tried it, and it effectively refresh the page like it should. But then the validation errors aren't shown in the tags.
I saw elsewhere that you can use RedirectAttributes and flashAttribute the bindingResult, but it's still not working.
I don't know what is the normal way of doing this thing.
Here's my code :
#Controller
#RequestMapping("/settings")
public class SettingsController {
#Autowired
protected SettingsService settingsService;
#RequestMapping(method = RequestMethod.GET)
public void loadSettings(Model model) {
model.addAttribute("settings", new Settings());
model.addAttribute("settingsList", settingsService.getAllSettings();
}
#RequestMapping(value = "/add", method = RequestMethod.POST)
public String saveSettings(#ModelAttribute("settings") #Valid Settings settings, Errors errors, RedirectAttributes redirectAttributes) {
// code
redirectAttributes.addFlashAttribute("settings", settings);
redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.settings", errors);
return "redirect:/settings";
}
}
I made it work, but i have NO idea why :
Instead of this :
#RequestMapping(method = RequestMethod.GET)
public void loadSettings(Model model) {
model.addAttribute("settings", new Settings());
model.addAttribute("settingsList", settingsService.getAllSettings();
}
I have this :
#RequestMapping(method = RequestMethod.GET)
public void loadSettings(Model model) {
if (!model.containsAttribute("settings")) {
model.addAttribute("settings", new Settings());
}
model.addAttribute("settingsList", settingsService.getAllSettings();
}
And it works, but the form keeps the data posted. It's one or the other :/

How to pass the object from one controller to another in Spring without using Session

I have a requirement where the user selects some data from a form and we need to show that selected data on the next page.
At present we are doing this with a session attribute, but the problem with this is that it overwrites the data if the first page is open in another browser tab, where the data is again selected and submitted. So I just want to get rid of this session attribute while transferring data from one controller to another.
Note: I am using an XML based Spring configuration, so please show a solution using XML, not annotations.
Define RedirectAttributes method parameter in the the handler method that handles form submission from first page:
#RequestMapping("/sendDataToNextPage", method = RequestMethod.POST)
public String submitForm(
#ModelAttribute("formBackingObj") #Valid FormBackingObj formBackingObj,
BindingResult result,
RedirectAttributes redirectAttributes) {
...
DataObject data = new DataObject();
redirectAttributes.addFlashAttribute("dataForNextPage", data);
...
return "redirect:/secondPageURL";
}
The flash attributes are saved temporarily before the redirect (typically in the session) and are available to the request after the redirect and removed immediately.
The above redirect will cause the client (browser) to send a request to /secondPageURL. So you need to have a handler method to handle this request, and there you can get access to the DataObject data set in the submitForm handler method:
#RequestMapping(value = "/secondPageURL", method = RequestMethod.GET)
public String gotoCountrySavePage(
#ModelAttribute("dataForNextPage") DataObject data,
ModelMap model) {
...
//data is the DataObject that was set to redirectAttributes in submitForm method
return "pageToBeShown";
}
Here DataObject data is the object that contains data from the submitForm method.
I worked with this requirement and I used RedirectAttributes, then you can add this redirect attributes to your model. This is an example:
#RequestMapping(value = "/mypath/{myProperty}", method = RequestMethod.POST)
public String submitMyForm(#PathVariable Long myProperty, RedirectAttributes redirectAttributes) {
redirectAttributes.addFlashAttribute("message", "My property is: " + myProperty);
return "redirect:/anotherPage";
}
#RequestMapping(method = RequestMethod.GET)
public String defaultPage(Model model, #RequestParam(required = false) String message) {
if(StringUtils.isNotBlank(message)) {
model.addAttribute("message", message);
}
return "myPage";
}
Hope it helps.
You can use RedirectAttributes ; A specialization of the Model interface that controllers can use to select attributes for a redirect scenario.
public interface RedirectAttributes extends org.springframework.ui.Model
Plus this interface also provide a way to store "Flash Attribute" . Flash Attribute is in FlashMap.
FlashMap : A FlashMap provides a way for one request to store attributes intended for use in another. This is most commonly needed when redirecting from one URL to another.
Quick Example is
#RequestMapping(value = "/accounts", method = RequestMethod.POST)
public String handle(RedirectAttributes redirectAttrs) {
// Save account ...
redirectAttrs.addFlashAttribute("message", "Hello World");
return "redirect:/testUrl/{id}";
}
Reference and detail information are here

How to read flash attributes after redirection in Spring MVC 3.1?

I would like to know how to read a flash attributes after redirection in Spring MVC 3.1.
I have the following code:
#Controller
#RequestMapping("/foo")
public class FooController {
#RequestMapping(value = "/bar", method = RequestMethod.GET)
public ModelAndView handleGet(...) {
// I want to see my flash attributes here!
}
#RequestMapping(value = "/bar", method = RequestMethod.POST)
public ModelAndView handlePost(RedirectAttributes redirectAttrs) {
redirectAttrs.addFlashAttributes("some", "thing");
return new ModelAndView().setViewName("redirect:/foo/bar");
}
}
What I am missing?
Use Model, it should have flash attributes prepopulated:
#RequestMapping(value = "/bar", method = RequestMethod.GET)
public ModelAndView handleGet(Model model) {
String some = (String) model.asMap().get("some");
// do the job
}
or, alternatively, you can use RequestContextUtils#getInputFlashMap:
#RequestMapping(value = "/bar", method = RequestMethod.GET)
public ModelAndView handleGet(HttpServletRequest request) {
Map<String, ?> inputFlashMap = RequestContextUtils.getInputFlashMap(request);
if (inputFlashMap != null) {
String some = (String) inputFlashMap.get("some");
// do the job
}
}
P.S. You can do return return new ModelAndView("redirect:/foo/bar"); in handlePost.
EDIT:
JavaDoc says:
A RedirectAttributes model is empty when the method is called and is
never used unless the method returns a redirect view name or a
RedirectView.
It doesn't mention ModelAndView, so maybe change handlePost to return "redirect:/foo/bar" string or RedirectView:
#RequestMapping(value = "/bar", method = RequestMethod.POST)
public RedirectView handlePost(RedirectAttributes redirectAttrs) {
redirectAttrs.addFlashAttributes("some", "thing");
return new RedirectView("/foo/bar", true);
}
I use RedirectAttributes in my code with RedirectView and model.asMap() method and it works OK.
Try this:
#Controller
public class FooController
{
#RequestMapping(value = "/foo")
public String handleFoo(RedirectAttributes redirectAttrs)
{
redirectAttrs.addFlashAttribute("some", "thing");
return "redirect:/bar";
}
#RequestMapping(value = "/bar")
public void handleBar(#ModelAttribute("some") String some)
{
System.out.println("some=" + some);
}
}
works in Spring MVC 3.2.2
For all those like me who were having problems with seeing the POST url in the browser when a validation would fail.
The POST url is a private url that should not be exposed to users but it was automatically rendered when a validation failed. i.e. if a field was below a minimum length. I was using #Valid. I wanted the original GET url of the form to show at all times even when validation bounced back to the form, so I did the following:
if (validation.hasErrors()) {
redirectAttributes.addFlashAttribute("org.springframework.validation.BindingResult.story", validation);
redirectAttributes.addFlashAttribute("story", story);
return new ModelAndView("redirect:/january/2015");
where story is the form object representation, redirectAttributes are RedirectAttributes you put in the method signature and validation is the BindingResult. /january/2015 is the mapping to the GET controller where the form lives.
After this implementation, in the mapping for /january/2015, story comes in intact as follows:
Story story= (Story) model.asMap().get("story");
//story from the POST method
I had to augment my GET method and check if this was not null. If not null, then send this to the form else I would send a newly initialized Story type to the form as default behaviour before.
In this manner, I am able to return to the form with the bindingresults intact (errors show on form) but have my GET url in place of the post url.

Categories

Resources