Skip to content

Commit c499df2

Browse files
committed
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
1 parent e04b322 commit c499df2

File tree

4 files changed

+140
-92
lines changed

4 files changed

+140
-92
lines changed

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
package org.springframework.web.method.annotation;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.util.Map;
2021

2122
import org.apache.commons.logging.Log;
2223
import org.apache.commons.logging.LogFactory;
23-
2424
import org.springframework.beans.BeanUtils;
2525
import org.springframework.core.MethodParameter;
2626
import org.springframework.core.annotation.AnnotationUtils;
@@ -31,7 +31,6 @@
3131
import org.springframework.web.bind.support.WebDataBinderFactory;
3232
import org.springframework.web.bind.support.WebRequestDataBinder;
3333
import org.springframework.web.context.request.NativeWebRequest;
34-
import org.springframework.web.method.annotation.ModelFactory;
3534
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
3635
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
3736
import org.springframework.web.method.support.ModelAndViewContainer;
@@ -99,10 +98,10 @@ public final Object resolveArgument(
9998
throws Exception {
10099

101100
String name = ModelFactory.getNameForParameter(parameter);
102-
Object target = (mavContainer.containsAttribute(name)) ?
101+
Object attribute = (mavContainer.containsAttribute(name)) ?
103102
mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);
104103

105-
WebDataBinder binder = binderFactory.createBinder(request, target, name);
104+
WebDataBinder binder = binderFactory.createBinder(request, attribute, name);
106105
if (binder.getTarget() != null) {
107106
bindRequestParameters(binder, request);
108107
validateIfApplicable(binder, parameter);
@@ -113,7 +112,12 @@ public final Object resolveArgument(
113112
}
114113
}
115114

116-
mavContainer.addAllAttributes(binder.getBindingResult().getModel());
115+
// Add resolved attribute and BindingResult at the end of the model
116+
117+
Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
118+
mavContainer.removeAttributes(bindingResultModel);
119+
mavContainer.addAllAttributes(bindingResultModel);
120+
117121
return binder.getTarget();
118122
}
119123

spring-web/src/main/java/org/springframework/web/method/support/ModelAndViewContainer.java

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2011 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,29 +25,29 @@
2525
import org.springframework.web.bind.support.SimpleSessionStatus;
2626

2727
/**
28-
* Records model and view related decisions made by
29-
* {@link HandlerMethodArgumentResolver}s and
30-
* {@link HandlerMethodReturnValueHandler}s during the course of invocation of
28+
* Records model and view related decisions made by
29+
* {@link HandlerMethodArgumentResolver}s and
30+
* {@link HandlerMethodReturnValueHandler}s during the course of invocation of
3131
* a controller method.
32-
*
32+
*
3333
* <p>The {@link #setRequestHandled} flag can be used to indicate the request
3434
* has been handled directly and view resolution is not required.
35-
*
36-
* <p>A default {@link Model} is automatically created at instantiation.
37-
* An alternate model instance may be provided via {@link #setRedirectModel}
38-
* for use in a redirect scenario. When {@link #setRedirectModelScenario} is set
39-
* to {@code true} signalling a redirect scenario, the {@link #getModel()}
35+
*
36+
* <p>A default {@link Model} is automatically created at instantiation.
37+
* An alternate model instance may be provided via {@link #setRedirectModel}
38+
* for use in a redirect scenario. When {@link #setRedirectModelScenario} is set
39+
* to {@code true} signalling a redirect scenario, the {@link #getModel()}
4040
* returns the redirect model instead of the default model.
41-
*
41+
*
4242
* @author Rossen Stoyanchev
4343
* @since 3.1
4444
*/
4545
public class ModelAndViewContainer {
4646

4747
private Object view;
48-
48+
4949
private boolean requestHandled = false;
50-
50+
5151
private final ModelMap defaultModel = new BindingAwareModelMap();
5252

5353
private ModelMap redirectModel;
@@ -57,31 +57,31 @@ public class ModelAndViewContainer {
5757
private boolean ignoreDefaultModelOnRedirect = false;
5858

5959
private final SessionStatus sessionStatus = new SimpleSessionStatus();
60-
60+
6161
/**
6262
* Create a new instance.
6363
*/
6464
public ModelAndViewContainer() {
6565
}
6666

6767
/**
68-
* Set a view name to be resolved by the DispatcherServlet via a ViewResolver.
68+
* Set a view name to be resolved by the DispatcherServlet via a ViewResolver.
6969
* Will override any pre-existing view name or View.
7070
*/
7171
public void setViewName(String viewName) {
7272
this.view = viewName;
7373
}
7474

7575
/**
76-
* Return the view name to be resolved by the DispatcherServlet via a
76+
* Return the view name to be resolved by the DispatcherServlet via a
7777
* ViewResolver, or {@code null} if a View object is set.
7878
*/
7979
public String getViewName() {
8080
return (this.view instanceof String ? (String) this.view : null);
8181
}
82-
82+
8383
/**
84-
* Set a View object to be used by the DispatcherServlet.
84+
* Set a View object to be used by the DispatcherServlet.
8585
* Will override any pre-existing view name or View.
8686
*/
8787
public void setView(Object view) {
@@ -97,28 +97,28 @@ public Object getView() {
9797
}
9898

9999
/**
100-
* Whether the view is a view reference specified via a name to be
100+
* Whether the view is a view reference specified via a name to be
101101
* resolved by the DispatcherServlet via a ViewResolver.
102102
*/
103103
public boolean isViewReference() {
104104
return (this.view instanceof String);
105105
}
106-
106+
107107
/**
108-
* Signal a scenario where the request is handled directly.
109-
* <p>A {@link HandlerMethodReturnValueHandler} may use this flag to
110-
* indicate the response has been fully handled and view resolution
108+
* Signal a scenario where the request is handled directly.
109+
* <p>A {@link HandlerMethodReturnValueHandler} may use this flag to
110+
* indicate the response has been fully handled and view resolution
111111
* is not required (e.g. {@code @ResponseBody}).
112112
* <p>A {@link HandlerMethodArgumentResolver} may also use this flag
113-
* to indicate the presence of an argument (e.g.
114-
* {@code ServletResponse} or {@code OutputStream}) that may lead to
113+
* to indicate the presence of an argument (e.g.
114+
* {@code ServletResponse} or {@code OutputStream}) that may lead to
115115
* a complete response depending on the method return value.
116116
* <p>The default value is {@code true}.
117117
*/
118118
public void setRequestHandled(boolean requestHandled) {
119119
this.requestHandled = requestHandled;
120120
}
121-
121+
122122
/**
123123
* Whether the request is handled directly.
124124
*/
@@ -129,7 +129,7 @@ public boolean isRequestHandled() {
129129
/**
130130
* Return the model to use: the "default" or the "redirect" model.
131131
* <p>The default model is used if {@code "redirectModelScenario=false"} or
132-
* if the redirect model is {@code null} (i.e. it wasn't declared as a
132+
* if the redirect model is {@code null} (i.e. it wasn't declared as a
133133
* method argument) and {@code ignoreDefaultModelOnRedirect=false}.
134134
*/
135135
public ModelMap getModel() {
@@ -140,17 +140,17 @@ public ModelMap getModel() {
140140
return (this.redirectModel != null) ? this.redirectModel : new ModelMap();
141141
}
142142
}
143-
143+
144144
/**
145145
* Whether to use the default model or the redirect model.
146146
*/
147147
private boolean useDefaultModel() {
148148
return !this.redirectModelScenario || ((this.redirectModel == null) && !this.ignoreDefaultModelOnRedirect);
149149
}
150-
150+
151151
/**
152-
* Provide a separate model instance to use in a redirect scenario.
153-
* The provided additional model however is not used used unless
152+
* Provide a separate model instance to use in a redirect scenario.
153+
* The provided additional model however is not used used unless
154154
* {@link #setRedirectModelScenario(boolean)} gets set to {@code true} to signal
155155
* a redirect scenario.
156156
*/
@@ -168,7 +168,7 @@ public void setRedirectModelScenario(boolean redirectModelScenario) {
168168

169169
/**
170170
* When set to {@code true} the default model is never used in a redirect
171-
* scenario. So if a redirect model is not available, an empty model is
171+
* scenario. So if a redirect model is not available, an empty model is
172172
* used instead.
173173
* <p>When set to {@code false} the default model can be used in a redirect
174174
* scenario if a redirect model is not available.
@@ -179,7 +179,7 @@ public void setIgnoreDefaultModelOnRedirect(boolean ignoreDefaultModelOnRedirect
179179
}
180180

181181
/**
182-
* Return the {@link SessionStatus} instance to use that can be used to
182+
* Return the {@link SessionStatus} instance to use that can be used to
183183
* signal that session processing is complete.
184184
*/
185185
public SessionStatus getSessionStatus() {
@@ -188,16 +188,16 @@ public SessionStatus getSessionStatus() {
188188

189189
/**
190190
* Add the supplied attribute to the underlying model.
191-
* @see ModelMap#addAttribute(String, Object)
191+
* A shortcut for {@code getModel().addAttribute(String, Object)}.
192192
*/
193193
public ModelAndViewContainer addAttribute(String name, Object value) {
194194
getModel().addAttribute(name, value);
195195
return this;
196196
}
197-
197+
198198
/**
199199
* Add the supplied attribute to the underlying model.
200-
* @see Model#addAttribute(Object)
200+
* A shortcut for {@code getModel().addAttribute(Object)}.
201201
*/
202202
public ModelAndViewContainer addAttribute(Object value) {
203203
getModel().addAttribute(value);
@@ -206,26 +206,38 @@ public ModelAndViewContainer addAttribute(Object value) {
206206

207207
/**
208208
* Copy all attributes to the underlying model.
209-
* @see ModelMap#addAllAttributes(Map)
209+
* A shortcut for {@code getModel().addAllAttributes(Map)}.
210210
*/
211211
public ModelAndViewContainer addAllAttributes(Map<String, ?> attributes) {
212212
getModel().addAllAttributes(attributes);
213213
return this;
214214
}
215215

216216
/**
217-
* Copy attributes in the supplied <code>Map</code> with existing objects of
217+
* Copy attributes in the supplied <code>Map</code> with existing objects of
218218
* the same name taking precedence (i.e. not getting replaced).
219-
* @see ModelMap#mergeAttributes(Map)
219+
* A shortcut for {@code getModel().mergeAttributes(Map<String, ?>)}.
220220
*/
221221
public ModelAndViewContainer mergeAttributes(Map<String, ?> attributes) {
222222
getModel().mergeAttributes(attributes);
223223
return this;
224224
}
225225

226+
/**
227+
* Remove the given attributes from the model.
228+
*/
229+
public ModelAndViewContainer removeAttributes(Map<String, ?> attributes) {
230+
if (attributes != null) {
231+
for (String key : attributes.keySet()) {
232+
getModel().remove(key);
233+
}
234+
}
235+
return this;
236+
}
237+
226238
/**
227239
* Whether the underlying model contains the given attribute name.
228-
* @see ModelMap#containsAttribute(String)
240+
* A shortcut for {@code getModel().containsAttribute(String)}.
229241
*/
230242
public boolean containsAttribute(String name) {
231243
return getModel().containsAttribute(name);
@@ -257,5 +269,5 @@ public String toString() {
257269
}
258270
return sb.toString();
259271
}
260-
272+
261273
}

0 commit comments

Comments
 (0)