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.
Related
I have a page to add a user "/user/userAdd". In GET, i populate a list of Countries. In POST, i validate the User object from the formsubmit. If it has error, i return back to the same page with error msg. My problem is I just do a simple return "/user/userAdd"; the Country list is not populated. If I do a return "redirect:/user/userAdd"; i am loosing the previous user input. How should I handle this?
#RequestMapping(value = "/user/userAdd", method = RequestMethod.GET)
public void getUserAdd(Model aaModel) {
aaModel.addAttribute("user", new User());
List<Country> llistCountry = this.caService.findCountryAll();
aaModel.addAttribute("countrys", llistCountry);
}
#RequestMapping(value = "/user/userAdd", method = RequestMethod.POST)
public String postUserAdd(#ModelAttribute("user") #Valid User user,
BindingResult aaResult, SessionStatus aaStatus) {
if (aaResult.hasErrors()) {
return "/user/userAdd";
} else {
user = this.caService.saveUser(user);
aaStatus.setComplete();
return "redirect:/login";
}
}
I was also facing similar issues in my spring project. I would recommend changing your POST method to following
#RequestMapping(value = "/user/userAdd", method = RequestMethod.POST)
public String postUserAdd(#ModelAttribute("user") #Valid User user,
BindingResult aaResult, Model aaModel, SessionStatus aaStatus) {
if (aaResult.hasErrors()) {
List<Country> llistCountry = this.caService.findCountryAll();
aaModel.addAttribute("countrys", llistCountry);
return "/user/userAdd";
} else {
user = this.caService.saveUser(user);
aaStatus.setComplete();
return "redirect:/login";
}
}
Here, the list is again added to the model and it will also retain the previously selected values(if any) in UI.
Hope this helps
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.
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 :/
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.
With ModelAttribute annotation, we can feed many thing like listbox.
Is it better to feed each lisbox with a different method in the controler or to use a form and have a list of object for every listbox?
If the controller class is used for different requests where some have this listboxes and some not, (for example the Controller handles the show, create and update functions for an entity, where only the create and update pages have that list boxes) then popluating the model with the #ModelAttribute annotated method would mean, that this methods will be executed even if there values are not needed. -- I my humble opinnion this would be bad.
I hope I understand your question right, if not please add a example for each of the two choises you want to compare.
#RequestMapping("/users")
#Controller
TheWayIPreferController() {
#RequestMapping(params = "form", method = RequestMethod.GET)
public ModelAndView createForm() {
ModelMap uiModel = new ModelMap();
uiModel.addAttribute("userCreateCommand", new UserCreateCommand());
uiModel.addAttribute("securityRoles", this.securityRoleDao.readAll()));
uiModel.addAttribute("salutations", this.salutationDao.readAll()));
uiModel.addAttribute("locales", this.localeDao.readAll());
return new ModelAndView("users/create", uiModel);
}
#RequestMapping(method = RequestMethod.POST)
public ModelAndView create(final #Valid UserCreateCommand userCreateCommand, final BindingResult bindingResult) {
...
}
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ModelAndView show(#PathVariable("id") final User user) {
...
}
}
instead of:
#RequestMapping("/users")
#Controller
TheWayIDiscourageController(){
#ModelAttribute("securityRoles")
public List<SecurityRoles> getSecurityRoles(){
return this.securityRoleDao.readAll();
}
#ModelAttribute("salutations")
public List<SecurityRoles> getSalutations(){
return this.salutationDao.readAll());
}
#ModelAttribute("locales")
public List<SecurityRoles> getLocals(){
return this.localeDao.readAll();
}
#RequestMapping(params = "form", method = RequestMethod.GET)
public ModelAndView createForm() {
return new ModelAndView("users/create", "userCreateCommand", new UserCreateCommand());
}
#RequestMapping(method = RequestMethod.POST)
public ModelAndView create(final #Valid UserCreateCommand userCreateCommand, final BindingResult bindingResult) {
...
}
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ModelAndView show(#PathVariable("id") final User user) {
...
}
}