Fix issue with resolving Errors controller argument
The ErrorsMethodArgumentResolver expects the preceding @ModelAttribute in the controller method signature to be the last one added in the model -- an assumption that can break if a model attribute is added earlier (e.g. through a @ModelAttribute method) and more attributes are added as well. This fix ensures when an @ModelAttribute is resolved as a controller method argument it has the highest index in the model. Issue: SPR-9378
This commit is contained in:
parent
e04b322110
commit
c499df2315
|
|
@ -17,10 +17,10 @@
|
||||||
package org.springframework.web.method.annotation;
|
package org.springframework.web.method.annotation;
|
||||||
|
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.core.annotation.AnnotationUtils;
|
import org.springframework.core.annotation.AnnotationUtils;
|
||||||
|
|
@ -31,7 +31,6 @@ import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.support.WebDataBinderFactory;
|
import org.springframework.web.bind.support.WebDataBinderFactory;
|
||||||
import org.springframework.web.bind.support.WebRequestDataBinder;
|
import org.springframework.web.bind.support.WebRequestDataBinder;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.method.annotation.ModelFactory;
|
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
|
||||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
@ -99,10 +98,10 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
|
||||||
String name = ModelFactory.getNameForParameter(parameter);
|
String name = ModelFactory.getNameForParameter(parameter);
|
||||||
Object target = (mavContainer.containsAttribute(name)) ?
|
Object attribute = (mavContainer.containsAttribute(name)) ?
|
||||||
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
|
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
|
||||||
|
|
||||||
WebDataBinder binder = binderFactory.createBinder(request, target, name);
|
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
|
||||||
if (binder.getTarget() != null) {
|
if (binder.getTarget() != null) {
|
||||||
bindRequestParameters(binder, request);
|
bindRequestParameters(binder, request);
|
||||||
validateIfApplicable(binder, parameter);
|
validateIfApplicable(binder, parameter);
|
||||||
|
|
@ -113,7 +112,12 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mavContainer.addAllAttributes(binder.getBindingResult().getModel());
|
// Add resolved attribute and BindingResult at the end of the model
|
||||||
|
|
||||||
|
Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
|
||||||
|
mavContainer.removeAttributes(bindingResultModel);
|
||||||
|
mavContainer.addAllAttributes(bindingResultModel);
|
||||||
|
|
||||||
return binder.getTarget();
|
return binder.getTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2011 the original author or authors.
|
* Copyright 2002-2012 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -188,7 +188,7 @@ public class ModelAndViewContainer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the supplied attribute to the underlying model.
|
* Add the supplied attribute to the underlying model.
|
||||||
* @see ModelMap#addAttribute(String, Object)
|
* A shortcut for {@code getModel().addAttribute(String, Object)}.
|
||||||
*/
|
*/
|
||||||
public ModelAndViewContainer addAttribute(String name, Object value) {
|
public ModelAndViewContainer addAttribute(String name, Object value) {
|
||||||
getModel().addAttribute(name, value);
|
getModel().addAttribute(name, value);
|
||||||
|
|
@ -197,7 +197,7 @@ public class ModelAndViewContainer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the supplied attribute to the underlying model.
|
* Add the supplied attribute to the underlying model.
|
||||||
* @see Model#addAttribute(Object)
|
* A shortcut for {@code getModel().addAttribute(Object)}.
|
||||||
*/
|
*/
|
||||||
public ModelAndViewContainer addAttribute(Object value) {
|
public ModelAndViewContainer addAttribute(Object value) {
|
||||||
getModel().addAttribute(value);
|
getModel().addAttribute(value);
|
||||||
|
|
@ -206,7 +206,7 @@ public class ModelAndViewContainer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy all attributes to the underlying model.
|
* Copy all attributes to the underlying model.
|
||||||
* @see ModelMap#addAllAttributes(Map)
|
* A shortcut for {@code getModel().addAllAttributes(Map)}.
|
||||||
*/
|
*/
|
||||||
public ModelAndViewContainer addAllAttributes(Map<String, ?> attributes) {
|
public ModelAndViewContainer addAllAttributes(Map<String, ?> attributes) {
|
||||||
getModel().addAllAttributes(attributes);
|
getModel().addAllAttributes(attributes);
|
||||||
|
|
@ -216,16 +216,28 @@ public class ModelAndViewContainer {
|
||||||
/**
|
/**
|
||||||
* Copy attributes in the supplied <code>Map</code> with existing objects of
|
* Copy attributes in the supplied <code>Map</code> with existing objects of
|
||||||
* the same name taking precedence (i.e. not getting replaced).
|
* the same name taking precedence (i.e. not getting replaced).
|
||||||
* @see ModelMap#mergeAttributes(Map)
|
* A shortcut for {@code getModel().mergeAttributes(Map<String, ?>)}.
|
||||||
*/
|
*/
|
||||||
public ModelAndViewContainer mergeAttributes(Map<String, ?> attributes) {
|
public ModelAndViewContainer mergeAttributes(Map<String, ?> attributes) {
|
||||||
getModel().mergeAttributes(attributes);
|
getModel().mergeAttributes(attributes);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given attributes from the model.
|
||||||
|
*/
|
||||||
|
public ModelAndViewContainer removeAttributes(Map<String, ?> attributes) {
|
||||||
|
if (attributes != null) {
|
||||||
|
for (String key : attributes.keySet()) {
|
||||||
|
getModel().remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the underlying model contains the given attribute name.
|
* Whether the underlying model contains the given attribute name.
|
||||||
* @see ModelMap#containsAttribute(String)
|
* A shortcut for {@code getModel().containsAttribute(String)}.
|
||||||
*/
|
*/
|
||||||
public boolean containsAttribute(String name) {
|
public boolean containsAttribute(String name) {
|
||||||
return getModel().containsAttribute(name);
|
return getModel().containsAttribute(name);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2011 the original author or authors.
|
* Copyright 2002-2012 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|
@ -27,6 +27,7 @@ import org.springframework.beans.TestBean;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.mock.web.MockHttpServletRequest;
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
import org.springframework.validation.Errors;
|
import org.springframework.validation.Errors;
|
||||||
import org.springframework.web.bind.WebDataBinder;
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
|
@ -141,13 +142,13 @@ public class ModelAttributeMethodProcessorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getAttributeFromModel() throws Exception {
|
public void resovleArgumentFromModel() throws Exception {
|
||||||
testGetAttributeFromModel("attrName", paramNamedValidModelAttr);
|
getAttributeFromModel("attrName", paramNamedValidModelAttr);
|
||||||
testGetAttributeFromModel("testBean", paramModelAttr);
|
getAttributeFromModel("testBean", paramModelAttr);
|
||||||
testGetAttributeFromModel("testBean", paramNonSimpleType);
|
getAttributeFromModel("testBean", paramNonSimpleType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void testGetAttributeFromModel(String expectedAttributeName, MethodParameter param) throws Exception {
|
private void getAttributeFromModel(String expectedAttributeName, MethodParameter param) throws Exception {
|
||||||
Object target = new TestBean();
|
Object target = new TestBean();
|
||||||
mavContainer.addAttribute(expectedAttributeName, target);
|
mavContainer.addAttribute(expectedAttributeName, target);
|
||||||
|
|
||||||
|
|
@ -162,7 +163,7 @@ public class ModelAttributeMethodProcessorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void createAttribute() throws Exception {
|
public void resovleArgumentViaDefaultConstructor() throws Exception {
|
||||||
WebDataBinder dataBinder = new WebRequestDataBinder(null);
|
WebDataBinder dataBinder = new WebRequestDataBinder(null);
|
||||||
|
|
||||||
WebDataBinderFactory factory = createMock(WebDataBinderFactory.class);
|
WebDataBinderFactory factory = createMock(WebDataBinderFactory.class);
|
||||||
|
|
@ -175,13 +176,14 @@ public class ModelAttributeMethodProcessorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void automaticValidation() throws Exception {
|
public void resovleArgumentValidation() throws Exception {
|
||||||
|
String name = "attrName";
|
||||||
Object target = new TestBean();
|
Object target = new TestBean();
|
||||||
mavContainer.addAttribute("attrName", target);
|
mavContainer.addAttribute(name, target);
|
||||||
|
|
||||||
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target);
|
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
|
||||||
WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class);
|
WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class);
|
||||||
expect(binderFactory.createBinder(webRequest, target, "attrName")).andReturn(dataBinder);
|
expect(binderFactory.createBinder(webRequest, target, name)).andReturn(dataBinder);
|
||||||
replay(binderFactory);
|
replay(binderFactory);
|
||||||
|
|
||||||
processor.resolveArgument(paramNamedValidModelAttr, mavContainer, webRequest, binderFactory);
|
processor.resolveArgument(paramNamedValidModelAttr, mavContainer, webRequest, binderFactory);
|
||||||
|
|
@ -191,20 +193,46 @@ public class ModelAttributeMethodProcessorTests {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected=BindException.class)
|
@Test(expected=BindException.class)
|
||||||
public void bindException() throws Exception {
|
public void resovleArgumentBindException() throws Exception {
|
||||||
|
String name = "testBean";
|
||||||
Object target = new TestBean();
|
Object target = new TestBean();
|
||||||
mavContainer.getModel().addAttribute(target);
|
mavContainer.getModel().addAttribute(target);
|
||||||
|
|
||||||
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target);
|
StubRequestDataBinder dataBinder = new StubRequestDataBinder(target, name);
|
||||||
dataBinder.getBindingResult().reject("error");
|
dataBinder.getBindingResult().reject("error");
|
||||||
|
|
||||||
WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class);
|
WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class);
|
||||||
expect(binderFactory.createBinder(webRequest, target, "testBean")).andReturn(dataBinder);
|
expect(binderFactory.createBinder(webRequest, target, name)).andReturn(dataBinder);
|
||||||
replay(binderFactory);
|
replay(binderFactory);
|
||||||
|
|
||||||
processor.resolveArgument(paramNonSimpleType, mavContainer, webRequest, binderFactory);
|
processor.resolveArgument(paramNonSimpleType, mavContainer, webRequest, binderFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SPR-9378
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resolveArgumentOrdering() throws Exception {
|
||||||
|
String name = "testBean";
|
||||||
|
Object testBean = new TestBean(name);
|
||||||
|
mavContainer.addAttribute(name, testBean);
|
||||||
|
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, testBean);
|
||||||
|
|
||||||
|
Object anotherTestBean = new TestBean();
|
||||||
|
mavContainer.addAttribute("anotherTestBean", anotherTestBean);
|
||||||
|
|
||||||
|
StubRequestDataBinder dataBinder = new StubRequestDataBinder(testBean, name);
|
||||||
|
WebDataBinderFactory binderFactory = createMock(WebDataBinderFactory.class);
|
||||||
|
expect(binderFactory.createBinder(webRequest, testBean, name)).andReturn(dataBinder);
|
||||||
|
replay(binderFactory);
|
||||||
|
|
||||||
|
processor.resolveArgument(paramModelAttr, mavContainer, webRequest, binderFactory);
|
||||||
|
|
||||||
|
assertSame("Resolved attribute should be updated to be last in the order",
|
||||||
|
testBean, mavContainer.getModel().values().toArray()[1]);
|
||||||
|
assertSame("BindingResult of resolved attribute should be last in the order",
|
||||||
|
dataBinder.getBindingResult(), mavContainer.getModel().values().toArray()[2]);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void handleAnnotatedReturnValue() throws Exception {
|
public void handleAnnotatedReturnValue() throws Exception {
|
||||||
processor.handleReturnValue("expected", returnParamNamedModelAttr, mavContainer, webRequest);
|
processor.handleReturnValue("expected", returnParamNamedModelAttr, mavContainer, webRequest);
|
||||||
|
|
@ -225,8 +253,8 @@ public class ModelAttributeMethodProcessorTests {
|
||||||
|
|
||||||
private boolean validateInvoked;
|
private boolean validateInvoked;
|
||||||
|
|
||||||
public StubRequestDataBinder(Object target) {
|
public StubRequestDataBinder(Object target, String objectName) {
|
||||||
super(target);
|
super(target, objectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBindInvoked() {
|
public boolean isBindInvoked() {
|
||||||
|
|
@ -248,6 +276,8 @@ public class ModelAttributeMethodProcessorTests {
|
||||||
public void validate(Object... validationHints) {
|
public void validate(Object... validationHints) {
|
||||||
validateInvoked = true;
|
validateInvoked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER })
|
@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER })
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ Changes in version 3.2 M1
|
||||||
* add Servlet 3.0 based async support
|
* add Servlet 3.0 based async support
|
||||||
* fix issue with encoded params in UriComponentsBuilder
|
* fix issue with encoded params in UriComponentsBuilder
|
||||||
* add Jackson 2 HttpMessageConverter and View types
|
* add Jackson 2 HttpMessageConverter and View types
|
||||||
|
* add pretty print option to Jackson HttpMessageConverter and View types
|
||||||
|
* fix issue with resolving Errors controller method argument
|
||||||
|
|
||||||
Changes in version 3.1.1 (2012-02-16)
|
Changes in version 3.1.1 (2012-02-16)
|
||||||
-------------------------------------
|
-------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue