Skip to content

Commit 816e328

Browse files
committed
Reactive support for @ModelAttribute argument
Issue: SPR-14542
1 parent 3230ca6 commit 816e328

File tree

5 files changed

+834
-5
lines changed

5 files changed

+834
-5
lines changed

spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,10 @@ public void putAll(Map<? extends String, ?> map) {
5454

5555
private void removeBindingResultIfNecessary(String key, Object value) {
5656
if (!key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
57-
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
58-
BindingResult bindingResult = (BindingResult) get(bindingResultKey);
59-
if (bindingResult != null && bindingResult.getTarget() != value) {
60-
remove(bindingResultKey);
57+
String resultKey = BindingResult.MODEL_KEY_PREFIX + key;
58+
BindingResult result = (BindingResult) get(resultKey);
59+
if (result != null && result.getTarget() != value) {
60+
remove(resultKey);
6161
}
6262
}
6363
}

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractMessageReaderArgumentResolver.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.springframework.web.server.ServerWebExchange;
4444
import org.springframework.web.server.ServerWebInputException;
4545
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
46+
import org.springframework.web.bind.WebExchangeBindException;
4647

4748
/**
4849
* Abstract base class for argument resolvers that resolve method arguments
@@ -216,7 +217,7 @@ protected void validate(Object target, Object[] validationHints,
216217
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
217218
binder.validate(validationHints);
218219
if (binder.getBindingResult().hasErrors()) {
219-
throw new ServerWebInputException("Validation failed", param);
220+
throw new WebExchangeBindException(param, binder.getBindingResult());
220221
}
221222
}
222223

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.reactive.result.method.annotation;
17+
18+
import java.lang.annotation.Annotation;
19+
import java.util.Map;
20+
21+
import reactor.core.publisher.Mono;
22+
import reactor.core.publisher.MonoProcessor;
23+
24+
import org.springframework.beans.BeanUtils;
25+
import org.springframework.core.MethodParameter;
26+
import org.springframework.core.ReactiveAdapter;
27+
import org.springframework.core.ReactiveAdapter.Descriptor;
28+
import org.springframework.core.ReactiveAdapterRegistry;
29+
import org.springframework.core.ResolvableType;
30+
import org.springframework.core.annotation.AnnotationUtils;
31+
import org.springframework.util.Assert;
32+
import org.springframework.util.ClassUtils;
33+
import org.springframework.util.StringUtils;
34+
import org.springframework.validation.BindingResult;
35+
import org.springframework.validation.Errors;
36+
import org.springframework.validation.annotation.Validated;
37+
import org.springframework.web.bind.WebExchangeBindException;
38+
import org.springframework.web.bind.WebExchangeDataBinder;
39+
import org.springframework.web.bind.annotation.ModelAttribute;
40+
import org.springframework.web.reactive.result.method.BindingContext;
41+
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
42+
import org.springframework.web.server.ServerWebExchange;
43+
44+
/**
45+
* Resolve {@code @ModelAttribute} annotated method arguments.
46+
*
47+
* <p>Model attributes are sourced from the model, or created using a default
48+
* constructor and then added to the model. Once created the attribute is
49+
* populated via data binding to the request (form data, query params).
50+
* Validation also may be applied if the argument is annotated with
51+
* {@code @javax.validation.Valid} or Spring's own
52+
* {@code @org.springframework.validation.annotation.Validated}.
53+
*
54+
* <p>When this handler is created with {@code useDefaultResolution=true}
55+
* any non-simple type argument and return value is regarded as a model
56+
* attribute with or without the presence of an {@code @ModelAttribute}.
57+
*
58+
* @author Rossen Stoyanchev
59+
* @since 5.0
60+
*/
61+
public class ModelAttributeMethodArgumentResolver implements HandlerMethodArgumentResolver {
62+
63+
private final boolean useDefaultResolution;
64+
65+
private final ReactiveAdapterRegistry adapterRegistry;
66+
67+
68+
/**
69+
* Class constructor.
70+
* @param useDefaultResolution if "true", non-simple method arguments and
71+
* return values are considered model attributes with or without a
72+
* {@code @ModelAttribute} annotation present.
73+
* @param registry for adapting to other reactive types from and to Mono
74+
*/
75+
public ModelAttributeMethodArgumentResolver(boolean useDefaultResolution,
76+
ReactiveAdapterRegistry registry) {
77+
78+
Assert.notNull(registry, "'ReactiveAdapterRegistry' is required.");
79+
this.useDefaultResolution = useDefaultResolution;
80+
this.adapterRegistry = registry;
81+
}
82+
83+
84+
/**
85+
* Return the configured {@link ReactiveAdapterRegistry}.
86+
*/
87+
public ReactiveAdapterRegistry getAdapterRegistry() {
88+
return this.adapterRegistry;
89+
}
90+
91+
92+
@Override
93+
public boolean supportsParameter(MethodParameter parameter) {
94+
if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
95+
return true;
96+
}
97+
if (this.useDefaultResolution) {
98+
Class<?> clazz = parameter.getParameterType();
99+
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(clazz);
100+
if (adapter != null) {
101+
Descriptor descriptor = adapter.getDescriptor();
102+
if (descriptor.isNoValue() || descriptor.isMultiValue()) {
103+
return false;
104+
}
105+
clazz = ResolvableType.forMethodParameter(parameter).getGeneric(0).getRawClass();
106+
}
107+
return !BeanUtils.isSimpleProperty(clazz);
108+
}
109+
return false;
110+
}
111+
112+
@Override
113+
public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext context,
114+
ServerWebExchange exchange) {
115+
116+
ResolvableType type = ResolvableType.forMethodParameter(parameter);
117+
ReactiveAdapter adapterTo = getAdapterRegistry().getAdapterTo(type.resolve());
118+
Class<?> valueType = (adapterTo != null ? type.resolveGeneric(0) : parameter.getParameterType());
119+
String name = getAttributeName(valueType, parameter);
120+
Mono<?> valueMono = getAttributeMono(name, valueType, parameter, context, exchange);
121+
122+
Map<String, Object> model = context.getModel().asMap();
123+
MonoProcessor<BindingResult> bindingResultMono = MonoProcessor.create();
124+
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultMono);
125+
126+
return valueMono.then(value -> {
127+
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
128+
return binder.bind(exchange)
129+
.doOnError(bindingResultMono::onError)
130+
.doOnSuccess(aVoid -> {
131+
validateIfApplicable(binder, parameter);
132+
BindingResult errors = binder.getBindingResult();
133+
model.put(BindingResult.MODEL_KEY_PREFIX + name, errors);
134+
model.put(name, value);
135+
bindingResultMono.onNext(errors);
136+
})
137+
.then(Mono.fromCallable(() -> {
138+
BindingResult errors = binder.getBindingResult();
139+
if (adapterTo != null) {
140+
return adapterTo.fromPublisher(errors.hasErrors() ?
141+
Mono.error(new WebExchangeBindException(parameter, errors)) :
142+
Mono.just(value));
143+
}
144+
else {
145+
if (errors.hasErrors() && checkErrorsArgument(parameter)) {
146+
throw new WebExchangeBindException(parameter, errors);
147+
}
148+
return value;
149+
}
150+
}));
151+
});
152+
}
153+
154+
private String getAttributeName(Class<?> valueType, MethodParameter parameter) {
155+
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
156+
String name = (ann != null ? ann.value() : null);
157+
// TODO: Conventions does not deal with async wrappers
158+
return StringUtils.hasText(name) ? name : ClassUtils.getShortNameAsProperty(valueType);
159+
}
160+
161+
private Mono<?> getAttributeMono(String attributeName, Class<?> attributeType,
162+
MethodParameter param, BindingContext context, ServerWebExchange exchange) {
163+
164+
Object attribute = context.getModel().asMap().get(attributeName);
165+
if (attribute == null) {
166+
attribute = createAttribute(attributeName, attributeType, param, context, exchange);
167+
}
168+
if (attribute != null) {
169+
ReactiveAdapter adapterFrom = getAdapterRegistry().getAdapterFrom(null, attribute);
170+
if (adapterFrom != null) {
171+
return adapterFrom.toMono(attribute);
172+
}
173+
}
174+
return Mono.justOrEmpty(attribute);
175+
}
176+
177+
protected Object createAttribute(String attributeName, Class<?> attributeType,
178+
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
179+
180+
return BeanUtils.instantiateClass(attributeType);
181+
}
182+
183+
protected boolean checkErrorsArgument(MethodParameter methodParam) {
184+
int i = methodParam.getParameterIndex();
185+
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
186+
return paramTypes.length <= (i + 1) || !Errors.class.isAssignableFrom(paramTypes[i + 1]);
187+
}
188+
189+
protected void validateIfApplicable(WebExchangeDataBinder binder, MethodParameter parameter) {
190+
Annotation[] annotations = parameter.getParameterAnnotations();
191+
for (Annotation ann : annotations) {
192+
Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class);
193+
if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
194+
Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann));
195+
Object hintArray = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
196+
binder.validate(hintArray);
197+
}
198+
}
199+
}
200+
201+
}

0 commit comments

Comments
 (0)