Pass information /current ID/ to child Controller with #PathVariable SpringBoot - java

but want to ask one question. First in my app i want to register User in one form and after redirect to another form where User must fill UserDetails form. These are two Entities User and UserDetail with OneToOne Relation. In my second Controller UserDetailController I want to pass UserId property from User, I made research but cant find the answer how. I try with #PathVariable, but its not working.
Here part of my code:
abstract class BaseController {
protected ModelAndView view(String view, ModelAndView modelAndView) {
modelAndView.setViewName(view);
return modelAndView;
}
protected ModelAndView view(String view) {
return this.view(view, new ModelAndView());
}
protected ModelAndView redirect(String route) {
return this.view("redirect:" + route);
}
}
UserController
#GetMapping("/register")
public ModelAndView register() {
return this.view("register");
}
#PostMapping("/register")
public ModelAndView registerConfirm(#ModelAttribute UserRegisterBindingModel userRegisterBindingModel,
HttpSession session, HttpServletRequest request
) {
if (!userRegisterBindingModel.getPassword()
.equals(userRegisterBindingModel.getConfirmPassword())) {
return this.view("register");
}
this.userService.createUser(this.modelMapper.map(userRegisterBindingModel, UserServiceModel.class));
String id = request.getParameter("userId");
return this.redirect("user-profile/" + id);
}
UserDetailController
#GetMapping("/user-profile/{id}")
public ModelAndView add(#PathVariable String id) {
return this.view("user-profile");
}
This code doesn't work, because "id" return NULL. How to pass from register to user-profile/{id} --> id ?
Thаnks for helping me.

In registerConfirmMethod
model.addAttribute("userId", userId);
return new ModelAndView("redirect:/user-profile", model);
In last method you would have to add
ModelAndView modelAndView = new ModelAndView("user-profile");
modelAndView.addObject("id", id);
return modelAndView;

You can use RedirectView something like this
When you add attribute with "id"
variable '{id}' matches with a model attribute, so it will be automatically substituted as pathparam in redirect URL
#PostMapping("/register")
public RedirectView registerConfirm(#ModelAttribute UserRegisterBindingModel userRegisterBindingModel,
HttpSession session, HttpServletRequest request, Model model
) {
this.userService.createUser(this.modelMapper.
map(userRegisterBindingModel, UserServiceModel.class));
String id = request.getParameter("userId");
model.addAttribute("id", id);
RedirectView rv = new RedirectView();
rv.setContextRelative(true);
rv.setUrl("/user-profile/{id}");
return rv;
}
and in your user-profile controller
#GetMapping("/user-profie/{id}")
public ModelAndView add(#PathVariable("id") String id) {
// sout id
return this.view("user-profile");
}

Related

Spring Return after Validation

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

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.

Spring MVC forward to new URL keeping existing

I am trying to redirect to a previous page is a domain object is invalid. For example with a domain object of Address.
#Autowired
private Address address;
#RequestMapping(method = RequestMethod.GET)
public String addressPage(
ModelMap model
) throws StdAddFault {
model.addAttribute("address", address);
return TILE_GET_STARTED_ADDRESS;
}
#RequestMapping(value = "/validate", method = RequestMethod.POST)
public String selectAddress(
#Valid Address address,
BindingResult result,
Model model,
HttpSession session
) throws StdAddFault {
if(result.hasErrors()) {
model.addAttribute("address", address);
return "/address";
}
...
}
Problem is after the request instead of being /address it is /address/validate. This seems a little misleading but if i use a redirect or a forward i lose my information.
Is there any way to make the url after the contoller /address (for invalid data only)?
Update
I confirmed the above does not work. This does work but I am concerned one request may overwrite another's Address. Is this a valid concern?
if(result.hasErrors()) {
this.address = address;
return "redirect:/get-started/address";
}
I think what you're after is an "address" model attribute stored in session. This will retain unique address object per user-session
#Controller
#SessionAttributes({"address"})
#RequestMapping("/address")
public class AddressController {
/**
* Injected model reference will contain 'address' if it exists in session
*/
#RequestMapping(method = RequestMethod.GET)
public String addressPage(Model model) {
if(!model.containsAttribute("address"))
model.addAttribute("address", new Address());
return "address";
}
/**
* This handler maps to form selecting / updating the address object
* at the end of this method the address object is automatically
* stored in session
*/
#RequestMapping(value = "/validate", method = RequestMethod.POST)
public String selectAddress(#ModelAttribute("address") Address address) {
// do stuff with the newly updated address..
}
}
If in any other handler you need the address object, all you need to do is just inject #ModelAttribute("address") Address address on the argument
I can do it one of 2 ways but I am not sure which one to use..
1.)
if(result.hasErrors()) {
this.address=address;
return "redirect:/get-started/address";
}
My concerns here revolve around object lifecycle using the Autowired. Could one call overwrite the address on another?
The other option is to use session...
private String getMainPage(
ModelMap model,
HttpSession session
){
if(session.getAttribute("address") == null){
session.setAttribute("address", new Address());
}
model.addAttribute("mainNav", MAIN_NAV);
model.addAttribute("subNav", SUB_NAV);
model.addAttribute("address", session.getAttribute("address"));
return TILE_GET_STARTED_ADDRESS;
}
I would really like to avoid the session though.
Ok to get a new Object each time make your Controller implement BeanFactoryAware.
Then implement the method setBeanFactory something like this.
private Address address.
public void setBeanFactory(BeanFactory context) {
address = (Address)context.getBean("myAddress");
}
So everytime you call setBeanFactory this will get an Address object and because of the scope = prototype it will be a new object.
This is the answer I finally went with
#Controller
#RequestMapping("/address")
public class AddressController {
private String getMainPage(
ModelMap model,
HttpSession session
){
if(!model.containsAttribute("address"))
model.addAttribute("address", new Address());
...
return TILE_GET_STARTED_ADDRESS;
}
#RequestMapping(value = URL_ADDRESS_SELECT, method = RequestMethod.POST)
public String validateAddress(
#Valid Address address,
BindingResult result,
Model model,
RedirectAttributes attr,
HttpSession session
) throws StdAddFault {
if(result.hasErrors()) {
attr.addFlashAttribute("org.springframework.validation.BindingResult.address", result);
attr.addFlashAttribute("address", address);
return "redirect:/address";
}
....
}
}
This question + you guys helped me out a bunch!

Removing duplication from Spring controllers

I have been looking for a way to somehow reduce the amount of code that is duplicated with subtle variance in my Spring MVC controllers, but searching through the SO questions so far has only yielded some questions without any satisfactory answers.
One example of duplication that I want to remove is this, where the user creation page and the role creation page share similarities:
#RequestMapping(value = "user/create", method = RequestMethod.GET)
public String create(#ModelAttribute("user") User user, BindingResult errors) {
LOG.debug("Displaying user creation page.");
return "user/create";
}
#RequestMapping(value = "role/create", method = RequestMethod.GET)
public String create(#ModelAttribute("role") Role role, BindingResult errors) {
LOG.debug("Displaying role creation page.");
return "role/create";
}
A slightly more involved variant of duplication that I would like to remove is the one for posting the create form:
#RequestMapping(value = "user/create", method = RequestMethod.POST)
public String save(#ModelAttribute("user") User user, BindingResult errors) {
LOG.debug("Entering save ({})", user);
validator.validate(user, errors);
validator.validatePassword(user, errors);
validator.validateUsernameAvailable(user, errors);
String encodedPassword = encoder.encode(user.getPassword());
user.setPassword(encodedPassword);
if (errors.hasErrors()) {
return create(user, errors);
} else {
service.save(user);
}
return "redirect:/user/index/1";
}
#RequestMapping(value = "role/create", method = RequestMethod.POST)
public String save(#ModelAttribute("role") Role role, BindingResult errors) {
LOG.debug("Entering save({})", role);
validator.validate(role, errors);
if (errors.hasErrors()) {
return create(role, errors);
} else {
service.save(role);
}
return "redirect:/index";
}
This example includes a validate then save if correct and a redirect to the error page if things don't go as planned.
How to remove this duplication?
Spring uses your handler method parameter types to create class instances from the request parameters or body. As such, there is no way to create a handler (#RequestMapping) method that could take an Object and check if it is either a Role or a User. (Technically you could have both parameters and just check which one isn't null, but that is terrible design).
Consequently, you need a handler method for each. This makes sense since, even through the logic is similar, it is still specific to the exact type of model object you are trying to create. You perform different validation, call a different service method, and return a different view name.
I say your code is fine.
Thought I would provide the solution that I settled on in the hope that it might help someone. My gf suggested that I use the name of the entity as a path variable for the controller, and this has proved to provide a very nice solution for the problem at hand.
The two methods now look like this:
#RequestMapping(value = "{entityName}/create", method = RequestMethod.GET)
public String create(#PathVariable("entityName") String entityName, #ModelAttribute("entity") BaseEntity entity, BindingResult errors) {
LOG.debug("Displaying create page for entity named: [{}]", entityName);
return handlerFactory.getHandler(entityName).getCreateView();
}
#RequestMapping(value = "{entityName}/create", method = RequestMethod.POST)
public String save(#PathVariable("entityName") String entityName, #ModelAttribute("entity") BaseEntity entity, BindingResult errors) {
LOG.debug("Saving entity of type {}", entityName);
CrudHandler handler = handlerFactory.getHandler(entityName);
handler.getCreateValidator().validate(entity, errors);
if (errors.hasErrors()) {
return create(entityName, entity, errors);
}
handler.preSave(entity);
handler.getService().save(entity);
return "redirect:" + DASHBOARD_URL;
}
The CrudHandler interface has implementations for each entity, and provides the controller with the entity specific classes that it needs, such as service and validator. A sample CrudHandler implementation looks like this for me:
#Component
public class RoleCrudHandler implements CrudHandler {
private static final String ENTITY_NAME = "role";
public static final String CREATE_VIEW = "role/create";
public static final String EDIT_VIEW = "role/edit";
#Resource
private RoleService roleService;
#Resource
private RoleValidator validator;
#Resource
private CrudHandlerFactory handlerFactory;
#PostConstruct
public void init() {
handlerFactory.register(ENTITY_NAME, this);
}
#Override
public GenericService getService() {
return roleService;
}
#Override
public Validator getCreateValidator() {
return validator;
}
#Override
public Validator getUpdateValidator() {
return validator;
}
#Override
public BaseEntity createEntity() {
return new Role();
}
#Override
public void preSave(BaseEntity entity) {
}
#Override
public String getCreateView() {
return CREATE_VIEW;
}
#Override
public String getUpdateView() {
return EDIT_VIEW;
}
}
If someone sees some ways to improve this, feel free to share. Hope this will be of use for someone.

better way to feed a listbox

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) {
...
}
}

Categories

Resources