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