Spring MVC redirect with variables - spring

I have following Spring MVC 3.2.4 method:
#RequestMapping(value = "/products/{product}", method = RequestMethod.POST)
public String update(Product product, #Valid #ModelAttribute("productForm") ProductForm productForm, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
return "products/view";
}
mapper.map(productForm, product);
productService.saveProduct(product);
return "redirect:/products/{product}";
}
After success it should redirect back user to detail of product. Problem is that instead of redirecting to page "/products/1" I am redirected to page "/products/Product [code=1234567890, name=Nejaky]". It looks like placeholder {product} is replaced by product.toString() instead of original ID from URL.
I am using built-in Spring Data converter:
<mvc:annotation-driven conversion-service="conversionService">
<mvc:argument-resolvers>
<bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean" />
<bean class="org.springframework.data.repository.support.DomainClassConverter">
<constructor-arg ref="conversionService" />
</bean>
What should I do to make it work correctly and redirect me back to "/products/1" without doing things like "redirect:/product" + product.getId()?

Our story starts in RedirectView source code, in the method replaceUriTemplateVariables.
protected StringBuilder replaceUriTemplateVariables(
String targetUrl, Map<String, Object> model, Map<String, String> currentUriVariables, String encodingScheme)
throws UnsupportedEncodingException {
StringBuilder result = new StringBuilder();
Matcher m = URI_TEMPLATE_VARIABLE_PATTERN.matcher(targetUrl);
int endLastMatch = 0;
while (m.find()) {
String name = m.group(1);
Object value = model.containsKey(name) ? model.remove(name) : currentUriVariables.get(name);
Assert.notNull(value, "Model has no value for '" + name + "'");
result.append(targetUrl.substring(endLastMatch, m.start()));
result.append(UriUtils.encodePathSegment(value.toString(), encodingScheme));
endLastMatch = m.end();
}
result.append(targetUrl.substring(endLastMatch, targetUrl.length()));
return result;
}
As you had predicted, the method uses value.toString() where value is your product object in the Model. No other component like a conversion system is involved here. Your options are as follows:
Use
"redirect:/product" + product.getId()
Add a model attribute called "productId" and use that in your view name
model.addAttribute("productId", product.getId());
"redirect:/product/{productId}"
Or use uri variables. I don't have information on those yet.

Ok, finally found reason for this. I had to annotate product param with #PathVariable. Wondering that it worked without it.

I know it's an old question but for anyone facing the same problem here is the answer
Just inject RedirectAttributes to your controller and use redirectAtrr.addAttribute([attrbuteName],[attributeValue])
#RequestMapping(value = "/products/{product}", method = RequestMethod.POST)
public String update(Product product,#Valid,#ModelAttribute("productForm") ProductForm productForm,BindingResult bindingResult,Model model,RedirectAttributes redirectAttr) {
if (bindingResult.hasErrors()) {
return "products/view";
}
mapper.map(productForm, product);
productService.saveProduct(product);
redirectAttr.addAttributte("productId",product.getId());
return "redirect:/products/{productId}";
}
Read documentation for more understanding.

Related

Spring AOP with RequestDispatcher causes recursive calls

Spring-servlet.xml:
<aop:config>
<aop:advisor advice-ref="interceptor" pointcut="#annotation(Validator)"/>
</aop:config>
<bean id="interceptor" class="org.aopalliance.intercept.MethodInterceptor" />
MethodInterceptor invoke():
if (!valid){
RequestDispatcher rd = request.getRequestDispatcher(errorView);
rd.forward(request, response);
}
Working flow of control:
My interceptor is called before any Spring controller method that is annotated with the Validator annotation. The intention is to validate the request, if validation fails, forward the request to a different view. This is usually working. If there is an error (!valid), the RequestDispatcher.forward is called. This causes another Spring controller method to be called which ultimately shows the error view. This normally works.
Issue:
For some Spring controllers, my RequestDispatcher's errorView causes the request to be forwarded back to the same method causing an infinite loop (invoke()gets called over and over). I think this is because of how the Spring controller's request mappings (see below) are set up.
Error view: #RequestMapping(value = URL, params="error")
Normal view: #RequestMapping(value = URL, params="proceed")
So when the first request is routed it's got 'proceed' in the request params. Then when there's an error and the RequestDispatcher forwards to the view with the 'error' param in the query string, it should forward to the "Error view" method above, but it doesn't. It always forwards to the 'proceed' method causing an infinite loop on the MethodInterceptor invoke(). This seems to be because the 'proceed' parameter is still in the HttpServletRequest. However this isn't easy to fix because the whole point of the interceptor is that it has no knowledge of the Spring controller itself - it only knows if an error occurred, and that it should forward to the error view if an error occurred.
Workaround:
Using the request mappings below, it fixes the issue. This is probably because the HttpServletRequest parameter is overwritten when using the key=value notation.
Error view: #RequestMapping(value = URL, params="view=error")
Normal view: #RequestMapping(value = URL, params="view=proceed")
Question
How can I "properly" fix the issue without resorting to the workaround shown above? Is there a more standard way to forward to the correct spring controller?
Solution#1:
Having configured as following:
Error view: #RequestMapping(value = URL, params="error")
Normal view: #RequestMapping(value = URL, params="proceed")
You could try for redirect as follows:
MethodInterceptor invoke():
if (!valid){
// RequestDispatcher rd = request.getRequestDispatcher(errorView);
// rd.forward(request, response);
response.sendRedirect(errorView);
}
Drawback: the browser would make a second request, therefore the old method parameters are no longer in the httpservletrequest.
WorkArround: To Avoid drawback, You could use Spring MVC Flash Attribute. You could follow this tutorial to know how Flash Attribute works.
Refs:FlashAttributesExample
Solution#2:
How can I "properly" fix the issue without resorting to the workaround
shown above? Is there a more standard way to forward to the correct
spring controller?
You could incorporate by implementing you own RequestMappingHandlerAdapter.
Solution#3:
Here is the code for the aspect:
public class RequestBodyValidatorAspect {
private Validator validator;
#Pointcut("#annotation(org.springframework.web.bind.annotation.RequestMapping)")
private void controllerInvocation() {
}
#Around("controllerInvocation()")
public Object aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
Annotation[][] argAnnotations = method.getParameterAnnotations();
String[] argNames = methodSignature.getParameterNames();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (hasRequestBodyAndValidAnnotations(argAnnotations[i])) {
validateArg(args[i], argNames[i]);
}
}
return joinPoint.proceed(args);
}
private boolean hasRequestBodyAndValidAnnotations(Annotation[] annotations) {
if (annotations.length < 2)
return false;
boolean hasValid = false;
boolean hasRequestBody = false;
for (Annotation annotation : annotations) {
if (Valid.class.isInstance(annotation))
hasValid = true;
else if (RequestBody.class.isInstance(annotation))
hasRequestBody = true;
if (hasValid && hasRequestBody)
return true;
}
return false;
}
#SuppressWarnings({"ThrowableInstanceNeverThrown"})
private void validateArg(Object arg, String argName) {
BindingResult result = getBindingResult(arg, argName);
validator.validate(arg, result);
if (result.hasErrors()) {
throw new HttpMessageConversionException("Validation of controller input parameter failed",
new BindException(result));
}
}
private BindingResult getBindingResult(Object target, String targetName) {
return new BeanPropertyBindingResult(target, targetName);
}
#Required
public void setValidator(Validator validator) {
this.validator = validator;
}
}
One limitation with this work-around is that it can only apply a single validator to all controllers. You can also avoid it.
public class TypeMatchingValidator implements Validator, InitializingBean, ApplicationContextAware {
private ApplicationContext context;
private Collection<Validator> validators;
public void afterPropertiesSet() throws Exception {
findAllValidatorBeans();
}
public boolean supports(Class clazz) {
for (Validator validator : validators) {
if (validator.supports(clazz)) {
return true;
}
}
return false;
}
public void validate(Object target, Errors errors) {
for (Validator validator : validators) {
if (validator.supports(target.getClass())) {
validator.validate(target, errors);
}
}
}
private void findAllValidatorBeans() {
Map<String, Validator> validatorBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, Validator.class, true, false);
validators = validatorBeans.values();
validators.remove(this);
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
}
Spring XML configuration file using the validator aspect and the meta-validator together:
<!-- enable Spring AOP support -->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<!-- declare the validator aspect and inject the validator into it -->
<bean id="validatorAspect" class="com.something.RequestBodyValidatorAspect">
<property name="validator" ref="validator"/>
</bean>
<!-- inject the validator into the DataBinder framework -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="webBindingInitializer">
<bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer" p:validator-ref="validator"/>
</property>
</bean>
<!-- declare the meta-validator bean -->
<bean id="validator" class="com.something.TypeMatchingValidator"/>
<!-- declare all Validator beans, these will be discovered by TypeMatchingValidator -->
<bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
<bean class="com.something.PersonValidator"/>
<bean class="com.something.AccountValidator"/>
Resources Refs:scottfrederick:pring-3-Validation-Aspect
Solution#4:
Yet another solution for form validation using aop , you can check the blog: form-validation-using-aspect-oriented-programming-aop-in-spring-framework

How to avoid redirect loop in spring web mvc

public class ValidateSession extends HandlerInterceptorAdapter {
//before the actual handler will be executed
public boolean preHandle(HttpServletRequest request,HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
if(session.getAttribute("user")==null){
/*ModelAndView mav = new ModelAndView("/login/index");
throw new ModelAndViewDefiningException(mav);*/
ModelAndView mav = new ModelAndView();
mav.setViewName("redirect:/login/index.mars");
throw new ModelAndViewDefiningException(mav);
}
return true;
}
}
In my case if session is expired then user can't access my application, but i am stuck with redirection loop. although i have tried many possible ways but not luck :(
Don't associate the ValidateSession handler with the login/index.mars request. Use mvc interceptor. Exclusions possible since 3.2, I think.
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/yourpath/*"/>
<exclude-mapping path="/login/index.mars"/>
<bean class="org...ValidateSession " />
</mvc:interceptor>
</mvc:interceptors>
I know this is an old post but thought it may be of help since none of the above worked for me.
In the class implementing HandlerInterceptor:
response.sendRedirect("login?logout=true");
then in controller:
#RequestMapping(value = "login", method = RequestMethod.GET)
public String login(ModelMap modelmap, HttpServletRequest request, HttpSession httpSession, #RequestParam(required = false) String logout, #RequestParam(required = false) String reason, #RequestParam(required = false) String message) {
if (reason != null && MvcStatics.ERROR_MAP.get(reason) != null) {
ArrayList<String> errors = new ArrayList<>();
errors.add(MvcStatics.ERROR_MAP.get(reason));
modelmap.addAttribute("errors", errors);
}
if (logout != null && logout.equalsIgnoreCase("true")) {
httpSession.removeAttribute("loggedIn");
httpSession.removeAttribute("user");
httpSession.removeAttribute("token");
modelmap.addAttribute("message", "You have successfully been logged out.");
}}
i experienced the same issue. just remove the "redirect:" appended before "/login/index.mars"
ModelAndView mav = new ModelAndView();
mav.setViewName("redirect:/login/index.mars");
//to this and you redirect ll works fine
mav.setViewName("/login/index.mars");
throw new ModelAndViewDefiningException(mav);

Reference Data is lost from Form when Validation Fails (Spring 3 MVC)

I am new to Spring
I am having a page addContact,in that I am getting dropDown Data from Database in the following way
#RequestMapping("/addContact")
public ModelAndView registerContact(#ModelAttribute Contact contact) {
List<ContactType> contactTypeList = contactdao.getContactTypeList();
Map<Integer,String> contactTypeSelect = new LinkedHashMap<Integer,String>();
Iterator<ContactType> iterator = contactTypeList.iterator();
while (iterator.hasNext()) {
ContactType ct = iterator.next();
contactTypeSelect.put(ct.getContactTypeId(),ct.getContactTypeName());
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("contactTypeSelect", contactTypeSelect);
return new ModelAndView("addContact", "map", map);
}
Now to Insert the Data into Database, I am having following method,
#RequestMapping("/insert")
public String insertData(#Valid Contact contact, BindingResult result, HttpServletRequest request ) {
if (result.hasErrors()) {
return "addContact";
}
else {
HttpSession session = request.getSession();
session.setAttribute("path", request.getSession().getServletContext().getRealPath("/WEB-INF"));
if (contact != null){
contactService.insertData(contact,request);
}
return "redirect:/getList";
}
}
When the validation fails, the drop down data is lost (which is obvious), what is the correct way of achieving the validation.
Create a method annotated with #ModelAttribute which loads the reference data. This method will be called before each #RequestMapping method.
#ModelAttribute("contactTypeSelect")
public List<ContactType> registerContact() {
return contactdao.getContactTypeList();
}
In your form you can use the <form:select ../> tag to render the itemValue and itemLabel.
<form:select items="${contactTypeSelect}" itemLabel="contactTypeName" itemValue="contactTypeId" />
With this you can refactor your addContact method to the following
#RequestMapping("/addContact")
public String registerContact(#ModelAttribute Contact contact) {
return "addContact";
}
You lose the drop down data because you're not adding in the contact map in the insertData method. Pull out the code where you grab the contact data into a separate (private) method and use it in the result.hasErrors() if block as such:
if (result.hasErrors()) {
return new ModelAndView("addContact", "map", map);
}
Also, I strongly suggest adding a method to the #RequestMapping annotation as such:
#RequestMapping("/insert", method=RequestMethod.POST)
This keeps people from making GET calls to this method.

Spring MVC + GWT : Redirect Issue

I am using Spring annotated MVC framework in an app which I am developing.
Following is the issue I am facing:
I have Controller which does a redirect, after a POST:
#RequestMapping(value = "/emdm-viewer-redirect.do", method = RequestMethod.POST)
public ModelAndView getMetricKeysAndRedirect(#RequestParam Object jsonObject, Model model)
{
ModelAndView modelAndView = new ModelAndView("redirect:/mdm-viewer.do");
.....
.....
....//make some service calls and populate value1
...
modelAndView.addobject("param1", value1);
return modelAndView;
}
I have another controller which is mapped to URL mdm-viewer.do (The redirect URL mentioned above):
#RequestMapping(value = "/mdm-viewer.do", method = RequestMethod.GET)
public String getMDMViewer(Model model) {
return "mdmViewer"; //returns a mdmViewer.jsp
}
Please note that the mdmviewer.jsp is a GWT entrypoint which is in classpath.
I have my firebug window open which tells me that a GET request was made for mdm-viewer.do, but it gives me a blank response. In fact, it does not redirect to the new jsp and stays on the same page from where the POST request was made.
However, if I copy the firebug URL and open it in a new window of my browser, I see the expected results.
Any ideas what I am doing wrong here? Tried to google it a lot, but can't find a similar issue anywhere.
Eventually, I returned a ModelAndView back from the POST method using a
#ResponseBody
And in my GWT Module, I used the response.getText() output to do a
#Override
public void onResponseReceived(Request request, Response response) {
if (200 == response.getStatusCode()) {
JSONObject jsonObject = (JSONObject) JSONParser.parse(response.getText());
String viewName = jsonObject.get("viewName").isString().stringValue();
JSONObject jsonParams = jsonObject.get("model").isObject();
Set<String> chartKeys = jsonParams.keySet();
String redirectURL = viewName + "?";
for (String keyString : chartKeys) {
redirectURL = redirectURL + keyString + "=" + jsonParams.get(keyString).isString().stringValue() + "&";
}
Window.open(GWT.getHostPageBaseURL() + redirectURL, "_self", "");
}
}

Apache HttpClient making multipart POST to Spring #Controller class

It seems like there are several posts such as here asking how to use Apache Commons HTTPClient libraries in Java to do a POST to a Servlet. However, it seems like I'm having some problems doing the same thing with a annotated Spring controller method. I've tried a few things but gotten HTTP 401 Bad Request responses from the server. Any examples of doing this would be greatly appreciated.
EDIT: Code I am trying to use:
//Server Side (Java)
#RequestMapping(value = "/create", method = RequestMethod.POST)
public void createDocument(#RequestParam("userId") String userId,
#RequestParam("file") MultipartFile file, HttpServletResponse response) {
// Do some stuff
}
//Client Side (Groovy)
void processJob(InputStream stream, String remoteAddress) {
HttpClient httpclient = new DefaultHttpClient()
httpclient.getParams().setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1)
HttpPost httppost = new HttpPost("http://someurl/rest/create")
MultipartEntity mpEntity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE)
InputStreamBody uploadFilePart = new InputStreamBody(stream, 'application/octet-stream', 'test.file')
mpEntity.addPart('file', uploadFilePart)
mpEntity.addPart('userId', new StringBody('testUser'))
httppost.setEntity(mpEntity)
HttpResponse response = httpclient.execute(httppost);
println(response.statusLine)
}
Still getting 400 Bad Request in the response from the server.
I hate to answer my own question when it shows incompetence, but it turns out the code was fine, this particular controller did not have a CommonsMultipartResolver defined in its servlet-context.xml file (multiple DispatcherServlets...long story :()
Here's what I added to make it work:
<!-- ========================= Resolver DEFINITIONS ========================= -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- one of the properties available; the maximum file size in bytes -->
<property name="maxUploadSize" value="50000000"/>
</bean>
Here is an example from the Spring Reference:
#Controller
public class FileUpoadController {
#RequestMapping(value = "/form", method = RequestMethod.POST)
public String handleFormUpload(#RequestParam("name") String name,
#RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
byte[] bytes = file.getBytes();
// store the bytes somewhere
return "redirect:uploadSuccess";
} else {
return "redirect:uploadFailure";
}
}
}

Resources