In a Spring-Boot controller during user registration if there is a binding error my controller returns the user to the registration page but in my implementation the model seems to be missing.
For example I commonly see on tutorials
#PostMapping
public String registerUserAccount(#ModelAttribute("user") #Valid UserRegistrationDto userDto,
BindingResult result){
User existing = userService.findByEmail(userDto.getEmail());
if (existing != null){
result.rejectValue("email", null, "There is already an account registered with that email");
}
if (result.hasErrors()){
return "registration";
}
userService.save(userDto);
return "redirect:/registration?success";
}
Which, if there are binding errors returns "registration"
My controller is very similar:
#PostMapping("/user/register")
public String registerNewUser(#ModelAttribute("user") #Valid RegisterUserDTO registerUserDTO,
BindingResult bindingResult,
Model model,
HttpServletRequest request,
Errors errors) {
User existing = userService.findUserByEmail(registerUserDTO.getEmail());
if (existing != null) {
bindingResult.rejectValue("email", null, "There is already an account with that Email");
}
if (bindingResult.hasErrors()) {
return "register";
}
userService.createUser(registerUserDTO);
return "redirect:/registration?success";
}
}
However when I return "register" there is a binding error on the stack at what corresponds to my first thymeleaf tag relating to the object.
If I change the POST controller to add the model explicitly:
if (bindingResult.hasErrors()) {
model.addAttribute(registerUserDTO);
return "register";
}
Then it works, returning the page with the model and previously entered data.
Why am I having to explicitly add the model to the return?
UPDATE
This does not work either:
#PostMapping("/user/register")
public String registerNewUser(#ModelAttribute("user") #Valid RegisterUserDTO registerUserDTO,
BindingResult bindingResult) {
User existing = userService.findUserByEmail(registerUserDTO.getEmail());
if (existing != null) {
bindingResult.rejectValue("email", null, "There is already an account with that Email");
}
if (bindingResult.hasErrors()) {
return "register";
This worked:
#PostMapping("/user/register")
public String registerNewUser(#ModelAttribute("registerUserDTO") #Valid RegisterUserDTO registerUserDTO,
BindingResult bindingResult) {
User existing = userService.findUserByEmail(registerUserDTO.getEmail());
if (existing != null) {
bindingResult.rejectValue("email", null, "There is already an account with that Email");
}
if (bindingResult.hasErrors()) {
return "register";
}
userService.createUser(registerUserDTO);
return "redirect:/registration?success";
}
As M.Denium pointed out the #ModelAttribute name needed to be the same as the instance name of the backing object used in the GET controller. In my GET controller I had :
#GetMapping("/user/register")
String registerForm(Model model) {
RegisterUserDTO registerUserDTO = new RegisterUserDTO();
model.addAttribute(registerUserDTO);
return "register";
}
So the #ModelAttribute needs to match, ie #ModelAttribute("registerUserDTO")
Related
I am new in java world. In service layer i have this updateCustomer method.
Am i handling exceptions correctly?
Is there a better way to do it?
Any pros and cons with my way of doing it?
#Override
public ResponseEntity<String> updateCustomer(String id, Customer customer) throws ResourceNotFoundException, BadRequestBodyException {
log.info("Updating customer.");
if(!Strings.isNullOrEmpty(customer.getCustomerName())
&& !Strings.isNullOrEmpty(customer.getCustomerType())){
Customer existingCustomer = customerRepository.findCustomerById(id)
.orElseThrow(() -> new ResourceNotFoundException("Error: Customer not found for id - " + id));
existingCustomer.setCustomerName(customer.getCustomerName());
existingCustomer.setCustomerType(customer.getCustomerType());
customerRepository.save(existingCustomer);
return ResponseEntity.ok().body("Customer updated successfully.");
} else {
throw new BadRequestBodyException("Error: request body can not be null or empty!");
}
}
Couple of things here.
I would recommend the service layer not to return the ResponseEntity<T> object instead it should return the DTO or just string in your case.
Also for request body validation use #Valid annotation on the request body model in controller or if you want to handle the validation do that in controller not in service.
The controller class should be responsible for to convert/create the ResponseEntity<?> which sends to server.
In your controller class you can define the method with #ExceptionHandler annotation and specify how you would handle that request. Alternative to that if the exception you are handling is thrown from diffrent services then you can define the class with #ControllerAdvice and specify the exception handling methods there.
With regards to your code you should do the following:
//Controller
#Override
public ResponseEntity<String> updateCustomer(String id, Customer customer)
throws ResourceNotFoundException, BadRequestBodyException {
if (Strings.isNullOrEmpty(customer.getCustomerName())
|| Strings.isNullOrEmpty(customer.getCustomerType())) {
throw new BadRequestBodyException("Error: request body can not be null or empty!");
}
log.info("Updating customer.");
return ResponseEntity.ok(customerService.updateCustomer(id, customer);
}
// The below exception handling method can be part of your controller or you can centralise them
// in class annotated with ControllerAdvice
#ExceptionHandler(BadRequestBodyException.class)
public ResponseEntity<Object> handleBadRequestBodyException(BadRequestBodyException ex) {
var response = new HashMap<String, Object>();
response.put("status", "failure");
response.put("message", "Request body Validation failed.");
response.put("detailMessage", ex.getLocalizedMessage());
return ResponseEntity.internalServerError().body(response);
}
#ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Object> handle(ResourceNotFoundException ex) {
var response = new HashMap<String, Object>();
response.put("status", "failure");
response.put("message", "Resource not found.");
response.put("detailMessage", ex.getLocalizedMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
//Service
#Override
public String updateCustomer(String id, Customer customer) throws ResourceNotFoundException, BadRequestBodyException {
log.info("Updating customer.");
Customer existingCustomer = customerRepository
.findCustomerById(id)
.orElseThrow(() -> new ResourceNotFoundException("Error: Customer not found for id - " + id));
existingCustomer.setCustomerName(customer.getCustomerName());
existingCustomer.setCustomerType(customer.getCustomerType());
customerRepository.save(existingCustomer);
return "Customer updated successfully.";
}
Hope this helps!
I have a controller that is supposed to redirect to another endpoint after processing but it only works when I set the active profile to dev. When the profile is not dev, the controller returns to the login page instead of redirecting to the profile endpoint.
This is my controller
public String completeSignUp(
#RequestParam final String token, #RequestParam final String userId,
Model model, RedirectAttributes redirectAttributes, HttpServletRequest httpServletRequest) {
LOG.debug("About to complete the sign-up process with token: {} and userId {}", token, userId);
try {
InputValidationUtility.validateInputs(getClass(), token, userId, model);
UserDto userDto = updateUserAndSendConfirmationEmail(token, userId, model, httpServletRequest);
if (Objects.isNull(userDto) || model.containsAttribute(SignUpControllerConstant.SIGN_UP_ERROR)) {
model.addAttribute(UserConstant.USER_MODEL_KEY, new UserRequestModel());
return SignUpControllerConstant.SIGN_UP_VIEW_NAME;
}
// automatically authenticate the userDto since there will be a redirection to profile page
UserUtility.authenticateUser(userService, userDto.getUsername());
model.addAttribute(SignUpControllerConstant.SIGN_UP_SUCCESS_KEY, true);
model.addAttribute(USER_REQUEST_MODEL_KEY_NAME, new UserRequestModel());
redirectAttributes.addFlashAttribute(ProfileControllerConstant.NEW_PROFILE, true);
LOG.debug("Redirecting to profile page...");
return "redirect:/profile";
} catch (Exception e) {
LOG.error(SignUpControllerConstant.ERROR_CREATING_USER, e);
model.addAttribute(SignUpControllerConstant.ERROR, SignUpControllerConstant.ERROR_CREATING_USER);
return SignUpControllerConstant.SIGN_UP_VIEW_NAME;
}
}
The profile page requires authentication and has the endpoint /profile.
This code works in "dev" profile
I had a bean declared to set cookies to strict which was interfering with the redirection sometimes.
#Bean
WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {
return tomcatServletWebServerFactory -> tomcatServletWebServerFactory.addContextCustomizers(context -> {
Rfc6265CookieProcessor processor = new Rfc6265CookieProcessor();
processor.setSameSiteCookies("strict");
context.setCookieProcessor(processor);
});
}
Redirection worked once I removed it.
this method opens a page when clicking on a link in which I want the value of the message attribute, I did not add the servlet and jsp, how can I display it? my html is empty
#GetMapping("/activate/{code}")
public String activate(ModelMap model, #PathVariable String code) {
boolean isActivated = userService.activateUser(code);
if (isActivated) {
model.addAttribute("message", "User Successfully activated");
System.out.println("Successfully");
} else System.out.println("Activation code not found");
model.addAttribute("message", "Activation code not found");
return "verificationPage";
}
try to use RedirectAttributes
#GetMapping("/activate/{code}")
public String activate(ModelMap model, #PathVariable String code, RedirectAttributes redirAttrs) {
redirAttrs.addFlashAttribute("message", "Here is your message");
}
I have this POST method which only validates a form and returns a confirmation view if the form is validated and I want to send back to the register screen if any field is wrong. In this case if the BindingResult object has errors, the system send the user back to the form screen but the URL shown is "/registerConfirmation" which should only be in case the form has no errors.
#RequestMapping(value="/registerConfirmation", method = RequestMethod.POST)
public ModelAndView confirmRegister(#Valid #ModelAttribute("form") RegistrationForm form, BindingResult result){
logger.info("Sending registration data");
ModelAndView modelAndView = new ModelAndView();
if(result.hasErrors()){
modelAndView.setViewName("register");
modelAndView.addObject("form", form);
return modelAndView;
}
//more code here
return modelAndView;
}
I dont know what I'm missing as I have seen methods like this in many other posts. Any help??
Many thanks!!!
One way to solve your issue is to use redirect:
#RequestMapping(value="/registerConfirmation", method = RequestMethod.POST)
public String confirmRegister(#Valid #ModelAttribute("form") RegistrationForm form, BindingResult result, RedirectAttributes attr){
logger.info("Sending registration data");
if(result.hasErrors()){
attr.addFlashAttribute("org.springframework.validation.BindingResult.form", result);
attr.addFlashAttribute("form", form);
return "redirect:/register";
}
//more code here
return "redirect:/registerConfirmation";
}
and in your register GET method you should check:
#RequestMapping(value="/register", method = RequestMethod.GET)
public String showRegister(Model model) {
....
if (!model.containsAttribute("form")) {
model.addAttribute("form", new RegistrationForm());
}
.....
}
you can read more in this article
Also don't forget to create GET method with registerConfirmation value.
LoginFormController-->Post is invoked after the form is submitted/posted. At the end, it invokes another Controller called LandingFormController-->loadForm.
Well, in the loadForm the values in the Model seems to be empty. Is there a way I can persist a Bean in session or request and get it in the loadForm method? Bonus points: if you could point to some documents to refer :)
Thanks
#Controller
#RequestMapping(value="/login")
public class LoginFormController {
#RequestMapping(method=RequestMethod.POST)
public ModelAndView post(#ModelAttribute User user, BindingResult result, SessionStatus status) {
logger.info("post");
new ReceiptUserValidator().validate(user, result);
if (result.hasErrors()) {
return new ModelAndView("login");
}
else {
logger.info("Email Id: " + user.getEmailId());
//status.setComplete();
Map<String, Object> model = new HashMap<String, Object>();
model.put("userId", user.getEmailId());
model.put("now", new Date().toString());
return new ModelAndView("redirect:/landing.htm", "model", model);
}
}
Controller B below that gets called
#Controller
#RequestMapping(value="/landing")
public class LandingFormController {
protected final Log logger = LogFactory.getLog(getClass());
#RequestMapping(method=RequestMethod.GET)
public String loadForm(Model model) {
logger.info("LandingFormController loadForm: " + model.asMap().keySet());
return "landing";
}
}
The code is performing a redirect which causes the properties placed in the model to be lost. Use flash attributes to pass the attributes to the next controller.
Flash attributes provide a way for one request to store attributes
intended for use in another. This is most commonly needed when
redirecting — for example, the Post/Redirect/Get pattern. Flash
attributes are saved temporarily before the redirect (typically in the
session) to be made available to the request after the redirect and
removed immediately.
LoginFormController
#Controller
#RequestMapping(value="/login")
public class LoginFormController {
#RequestMapping(method=RequestMethod.POST)
public ModelAndView post(#ModelAttribute User user, BindingResult result,
SessionStatus status, final RedirectAttributes redirectAttrs) {
logger.info("post");
new ReceiptUserValidator().validate(user, result);
if (result.hasErrors()) {
return new ModelAndView("login");
}
else {
logger.info("Email Id: " + user.getEmailId());
//status.setComplete();
redirectAttrs.addFlashAttribute("userId", user.getEmailId());
redirectAttrs.addFlashAttribute("now", new Date().toString());
return new ModelAndView("redirect:/landing.htm", "model", model);
}
}
Documentation
As an alternative solution you could simply not perform a redirect from the controller.
Appending retrieving solution.
Modify the loadForm
public String loadForm(#ModelAttribute("user") User user) {
logger.info("LandingFormController loadForm: " + user.getEmailId());
return "landing";
}