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!
Related
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
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 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.
I am using Spring 3.2.0 MVC. In that I have to store one object to session.
Currently I am using HttpSession set and get attribute to store and retrieve the value.
It returns only the String not Object. I want to use #SessionAttribute when I tried it sets the object in session but I could not retrieve the session object
#RequestMapping(value = "/sample-login", method = RequestMethod.POST)
public String getLoginClient(HttpServletRequest request,ModelMap modelMap) {
String userName = request.getParameter("userName");
String password = request.getParameter("password");
User user = sample.createClient(userName, password);
modelMap.addAttribute("userObject", user);
return "user";
}
#RequestMapping(value = "/user-byName", method = RequestMethod.GET)
public
#ResponseBody
String getUserByName(HttpServletRequest request,#ModelAttribute User user) {
String fas= user.toString();
return fas;
}
Both methods are in same controller. How would I use this to retrieve the object?
#SessionAttributes annotation are used on the class level to :
Mark a model attribute should be persisted to HttpSession after handler methods are executed
Populate your model with previously saved object from HttpSession before handler methods are executed -- if one do exists
So you can use it alongside your #ModelAttribute annotation like in this example:
#Controller
#RequestMapping("/counter")
#SessionAttributes("mycounter")
public class CounterController {
// Checks if there's a model attribute 'mycounter', if not create a new one.
// Since 'mycounter' is labelled as session attribute it will be persisted to
// HttpSession
#RequestMapping(method = GET)
public String get(Model model) {
if(!model.containsAttribute("mycounter")) {
model.addAttribute("mycounter", new MyCounter(0));
}
return "counter";
}
// Obtain 'mycounter' object for this user's session and increment it
#RequestMapping(method = POST)
public String post(#ModelAttribute("mycounter") MyCounter myCounter) {
myCounter.increment();
return "redirect:/counter";
}
}
Also don't forget common noobie pitfall: make sure you make your session objects Serializable.
I want to write rest like method for entity update. In this case I retrieve entity id from url and data from request body. The issue is in binding id with bean. Because neither EntityManager nor Spring-Data Crud Repo haven't update(id, bean) method. So I can set it myself
#RequestMapping(value = "/{id}", method = RequestMethod.POST)
public String update(#PathVariable("id") Long id, #Valid User user, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
user.setId(id); //Very bad
return "usersEdit";
}
user.setId(id); //Bad
repository.save(user);
return "redirect:/users/" + id;
}
or dismiss DRY and put id in forms as private field to.
Is there are any other solutions?
In Spring 3.1 a #ModelAttribute will be instantiated from a path variable if the path variable and the model attribute names are the same and there is a converter to instantiate the model attribute from the path variable value:
#RequestMapping(value="/{account}", method = RequestMethod.PUT)
public String update(#Valid #ModelAttribute Account account, BindingResult result) {
if (result.hasErrors()) {
return "accounts/edit";
}
this.accountManager.saveOrUpdate(account);
return "redirect:../accounts";
}
The full example is available at:
https://github.com/rstoyanchev/spring-mvc-31-demo