Spring lost model attribute between GET and POST - java

The get method will prepare the model and send to the "add person" jsp
It will also set the "mode" attribute to be "add" (so add and edit can share same jsp)
When the processSubmit result hasErrors ,mode attribute is gone
How to maintain mode attribute between calls?
#RequestMapping(value="/people/def/add" , method = RequestMethod.GET)
public String personAdd(#ModelAttribute("person") Person person,Model map) {
map.addAttribute("mode", "add");
//DO SOME LOGIC
return "personAdd";
}
#RequestMapping(value="/people/def/add" , method = RequestMethod.POST)
public String processSubmit(#ModelAttribute("person") Person person,BindingResult result) {
personValidator.validate(person, result);
if (result.hasErrors()) {
//MODE ATTRIBUTE IS LOST
return "personAdd";

Request attributes live only for the life of request. So, if you want "mode" back in Post, you may have to submit it back as part of POST, may be by using hidden form control in your web form.
You have to add "#RequestParam("mode") String mode" to your "processSubmit" method to retrieve the value of mode from HTTP POST parameters

Related

Should I put the ID of my entity in the URL or into the form as a hidden field?

I think in terms of REST, the ID should be placed into the URL, something like:
https://example.com/module/[ID]
and then I call GET, PUT, DELETE on that URL. That's kind of clear I think. In Spring MVC controllers, I'd get the ID with #PathVariable. Works.
Now, my practical problem with Spring MVC is, that if I do this, I have to NOT include the ID as part of the form (as well), Spring emits warnings of type
Skipping URI variable 'id' since the request contains a bind value with the same name.
otherwise. And it also makes kind of sense to only send it once, right? What would you do if they don't match??
That would be fine, but I do have a custom validator for my form backing bean, that needs to know the ID! (It needs to check if a certain unique name is already being used for a different entity instance, but cannot without knowing the ID of the submitted form).
I haven't found a good way to tell the validator that ID from #PathVariable, since the validation happens even before code in my controller method is executed.
How would you solve this dilemma?
This is my Controller (modified):
#Controller
#RequestMapping("/channels")
#RoleRestricted(resource = RoleResource.CHANNEL_ADMIN)
public class ChannelAdminController
{
protected ChannelService channelService;
protected ChannelEditFormValidator formValidator;
#Autowired
public ChannelAdminController(ChannelService channelService, ChannelEditFormValidator formValidator)
{
this.channelService = channelService;
this.formValidator = formValidator;
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.GET)
public String editChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") ChannelEditForm channelEditForm, Model model)
{
if (channelId > 0)
{
// Populate from persistent entity
}
else
{
// Prepare form with default values
}
return "channel/admin/channel-edit";
}
#RequestMapping(value = "/{channelId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long channelId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
try
{
// Has to validate in controller if the name is already used by another channel, since in the validator, we don't know the channelId
Long nameChannelId = channelService.getChannelIdByName(channelEditForm.getName());
if (nameChannelId != null && !nameChannelId.equals(channelId))
result.rejectValue("name", "channel:admin.f1.error.name");
}
catch (EmptyResultDataAccessException e)
{
// That's fine, new valid unique name (not so fine using an exception for this, but you know...)
}
if (result.hasErrors())
{
return "channel/admin/channel-edit";
}
// Copy properties from form to ChannelEditRequest DTO
// ...
// Save
// ...
redirectAttributes.addFlashAttribute("successMessage", new SuccessMessage.Builder("channel:admin.f1.success", "Success!").build());
// POST-REDIRECT-GET
return "redirect:/channels/" + channelId + "/admin";
}
#InitBinder("channelForm")
protected void initBinder(WebDataBinder binder)
{
binder.setValidator(formValidator);
}
}
I think I finally found the solution.
As it turns out Spring binds path variables to form beans, too! I haven't found this documented anywhere, and wouldn't have expected it, but when trying to rename the path variable, like #DavidW suggested (which I would have expected to only have a local effect in my controller method), I realized that some things got broken, because of the before-mentioned.
So, basically, the solution is to have the ID property on the form-backing object, too, BUT not including a hidden input field in the HTML form. This way Spring will use the path variable and populate it on the form. The local #PathVariable parameter in the controller method can even be skipped.
The cleanest way to solve this, I think, is to let the database handle the duplicates: Add a unique constraint to the database column. (or JPA by adding a #UniqueConstraint)
But you still have to catch the database exception and transform it to a user friendly message.
This way you can keep the spring MVC validator simple: only validate fields, without needing to query the database.
Could you not simply disambiguate the 2 (URI template variables vs. parameters) by using a different name for your URI template variable?
#RequestMapping(value = "/{chanId}/admin", method = RequestMethod.PUT)
public String saveChannel(#PathVariable Long chanId, #ModelAttribute("channelForm") #Valid ChannelEditForm channelEditForm, BindingResult result, Model model, RedirectAttributes redirectAttributes)
{
[...]
What ever you said is correct the correct way to design rest api is to mention the resource id in path variable if you look at some examples from the swagger now as open api you could find similar examples there
for you the correct solution would be to use a custom validator like this
import javax.validation.Validator;`
import org.apache.commons.lang3.StringUtils;`
import org.springframework.validation.Errors;`
importorg.springframework.validation.beanvalidation.CustomValidatorBean;`
public class MyValidator extends CustomValidatorBean {`
public void myvalidate(Object target,Errors errors,String flag,Profile profile){
super.validate(target,errors);
if(StringUtils.isEmpty(profile.name())){
errors.rejectValue("name", "NotBlank.profilereg.name", new Object[] { "name" }, "Missing Required Fields");
}
}
}
This would make sure all the fields are validated and you dont need to pass the id in the form.

How to combine GET & POST HTTP methods for the same view in Spring web app?

I have a view that is rendered with this method:
#RequestMapping("/employee/{id}")
public String showSpecificEmployee(#PathVariable String id, Model model){
model.addAttribute("employee", employeeService.findEmployeeById(new Long(id)));
DateCommand dateCommand = new DateCommand();
dateCommand.setEmployeeId(new Long(id));
model.addAttribute("date", dateCommand);
return "specificEmployee";
}
The view displayes some basic information about the Employee. On the same view, I do have a form to choose a month and filter the information by Date.
After the Date is chosen, I would like to have the view 'refreshed' with updated information. That means I do have a POST & GET methods bound to the same view.
#RequestMapping("/passdate")
public String updateWorkmonth(#ModelAttribute DateCommand dateCommand, Model model){
model.addAttribute("employee", employeeService.findEmployeeWithFilteredWorkdaysAndPayments(dateCommand.getEmployeeId(), dateCommand.getActualDate()));
model.addAttribute("date", dateCommand);
return "specificEmployee";
}
After the second method is invoked looks like
http://localhost:8080/passdate?employeeId=1&actualDate=2018-02, but I want it to be /employee/{id}. How do I combine those 2 methods, so they point to the same URL?
If I set #RequestMapping("/employee/{id}") on both methods, I keep getting an error.
You actually need only one GET method
#RequestMapping("/employee/{id}")
and optionally passed
#RequestParam("actualDate")
You can redirect user to that url. Just replace in method updateWorkmonth one line
return "specificEmployee";
with
return "redirect:/employee/" + dateCommand.getEmployeeId();
You can specify the type of HTTP request you want in the #RequestMapping parametters
When you don't specify it, it uses GET by default
#RequestMapping(value = "/employee/{id}",method = RequestMethod.POST)

How to keep an model attribute after a redirection?

I have an object that I fill in a form in the view "submit".
After that, it post the object "WelcomeMessageFinder" in the view "return".
I call a service with that use this object. If the service fails, I want to redirect to the view "submit" and keep the form filled with the previous values.
My issue is that I don't find how to keep the "WelcomeMessageFinder" object after the redirect. It always create a new empty object.
Here is my code :
#Controller
#SessionAttributes("welcomeMessageFinder")
public class SandBoxController extends PortalWebuiController {
#ModelAttribute("welcomeMessageFinder")
public WelcomeMessageFinder welcomeMessageFinder() {
return new WelcomeMessageFinder();
}
#RequestMapping(value = "/submit", method = RequestMethod.GET)
public String submit(WelcomeMessageFinder welcomeMessageFinder, Model model, SessionStatus sessionStatus, HttpSession httpSession) {
// On Init : a new WelcomeMessageFinder is created
// After redirect : I want to keep the filled WelcomeMessageFinder but a new one is created ...
model.addAttribute("zenithUserSession", zenithUserSession);
return "submitwelcomemessage";
}
#RequestMapping(value = "/return", method = RequestMethod.POST)
public String retun(
WelcomeMessageFinder welcomeMessageFinder,
Model model,
RedirectAttributes redirectAttributes,
SessionStatus sessionStatus, HttpSession httpSession) {
// welcomeMessageFinder contains the parameters I enter in the form.
redirectAttributes.addFlashAttribute("welcomeMessageFinder", welcomeMessageFinder);
return "redirect:/submit";
}
}
What can I do to keep the same WelcomeMessageFinder object before and after the redirect ?
I find this question that says that I can't use SessionAttributes with redirect because it doesn't keep the session. And it says to use RedirectAttributes but the attributes seems to be reinitialized.
EDIT :
I finally found my error. This code works, the problem is with my class WelcomeMessageFinder. To add an object in the flash session, this object need to be Serializable. I forget to implements Serializable in my class.
After adding this, it works fine.
I finally found my error. This code works, the problem is with my class WelcomeMessageFinder. To add an object in the flash session, this object need to be Serializable. I forget to implements Serializable in my class.
After adding this, it works fine.
it is because of this piece of code
"#ModelAttribute("welcomeMessageFinder")
public WelcomeMessageFinder welcomeMessageFinder() {
return new WelcomeMessageFinder();
}"
. it is ALWAYS executed before any requestmapping method is called

sending variable back to server in spring mvc

I am trying to reload the same page with different content that varies depending on which link is clicked on the page. The url pattern for the page is "/owners", which triggers the running of this method in OwnerController.java:
#RequestMapping(value = "/owners", method = RequestMethod.GET)
public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) {
Collection<Owner> results = this.clinicService.findOwnerByLastName("");
model.put("selections", results);
model.put("sel_owner",this.clinicService.findOwnerById(ownerId));//ownerId is not defined yet
return "owners/ownersList";
}
The jsp includes a dynamically generated list of ownerId integer values, each of which can be used to send a unique ownerId back to the server. What do I need to add to my jsp in order to get ownerId to have a user-specified value when processFindForm() is run? In other words, what does the hyperlink need to look like?
You are using GET request type.If you want to send any parameter to controller then you have to use either #RequestParam or #PathParam annotations based on requirement as an arguments to controller method. In your case it will be something like...
public String processFindForm(#RequestParam("ownerID") String ownerId, BindingResult result, Map<String, Object> model) {... }
Take a look on this link as well: http://www.byteslounge.com/tutorials/spring-mvc-requestmapping-example

spring form tags to have default values

in My jsp I am using spring form tags to bind and update data.
my scenario is to show default values when user enters the page. these default values should come from database depending on some conditions.
But when user edits the data and submit, I want to save them in databse without updating the default values.
Any suggestion is greatly appreciated!
Standard approach to a form page is to have a controller with two methods, one for GET and one for POST. You'll also need an object to bind to the form.
The method that handles the GET sets up the bind object and puts it in the model, then returns the view for the form page. The bind object here can be created with default values which you can get from anywhere. Your bind object will probably have some key, like an ID. The default one can have a zero/default key.
The method that handles the POST takes the bind object as a parameter. It probably validates the values then inserts into the database. Its likely the database will generate the key/id.
Here's an example;
#Controller
public class PersonController {
#RequestMapping(value="/person.do", method=RequestMethod.GET)
public ModelAndView setup() {
ModelAndView response = new ModelAndView("person");
//Create default bind object, can get values
//from database if you like. Here they're just
//hard coded.
Person person = new Person();
person.setName("Joe Bloggs");
response.addObject("person", person);
return response;
}
#RequestMapping(value="/person.do", method=RequestMethod.POST)
public ModelAndView post(#ModelAttribute("person") Person person,
BindingResult result) {
Validator.validate(person, result);
if (result.hasErrors()) {
ModelAndView response = new ModelAndView("person");
response.addObject("person", person);
return response;
} else {
personDao.store(person);
}
return new ModelAndView("redirect:nextPage.do");
}
}
The form will be populated with any values you supply in the backing object. I don't understand the second part of your question.

Categories