Skip to content

Commit 1f12811

Browse files
committed
Merge reactive @ModelAttribute support
2 parents 9b57437 + 6b73700 commit 1f12811

22 files changed

+2122
-311
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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.ui;
17+
18+
import java.util.Collection;
19+
import java.util.Map;
20+
import java.util.concurrent.ConcurrentHashMap;
21+
22+
import org.springframework.core.Conventions;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* Implementation of {@link Model} based on a {@link ConcurrentHashMap} for use
27+
* in concurrent scenarios. Exposed to handler methods by Spring Web Reactive
28+
* typically via a declaration of the {@link Model} interface. There is typically
29+
* no need to create it within user code. If necessary a controller method can
30+
* return a regular {@code java.util.Map}, or more likely a
31+
* {@code java.util.ConcurrentMap}.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 5.0
35+
*/
36+
@SuppressWarnings("serial")
37+
public class ConcurrentModel extends ConcurrentHashMap<String, Object> implements Model {
38+
39+
/**
40+
* Construct a new, empty {@code ConcurrentModel}.
41+
*/
42+
public ConcurrentModel() {
43+
}
44+
45+
/**
46+
* Construct a new {@code ModelMap} containing the supplied attribute
47+
* under the supplied name.
48+
* @see #addAttribute(String, Object)
49+
*/
50+
public ConcurrentModel(String attributeName, Object attributeValue) {
51+
addAttribute(attributeName, attributeValue);
52+
}
53+
54+
/**
55+
* Construct a new {@code ModelMap} containing the supplied attribute.
56+
* Uses attribute name generation to generate the key for the supplied model
57+
* object.
58+
* @see #addAttribute(Object)
59+
*/
60+
public ConcurrentModel(Object attributeValue) {
61+
addAttribute(attributeValue);
62+
}
63+
64+
65+
/**
66+
* Add the supplied attribute under the supplied name.
67+
* @param attributeName the name of the model attribute (never {@code null})
68+
* @param attributeValue the model attribute value (can be {@code null})
69+
*/
70+
public ConcurrentModel addAttribute(String attributeName, Object attributeValue) {
71+
Assert.notNull(attributeName, "Model attribute name must not be null");
72+
put(attributeName, attributeValue);
73+
return this;
74+
}
75+
76+
/**
77+
* Add the supplied attribute to this {@code Map} using a
78+
* {@link org.springframework.core.Conventions#getVariableName generated name}.
79+
* <p><emphasis>Note: Empty {@link Collection Collections} are not added to
80+
* the model when using this method because we cannot correctly determine
81+
* the true convention name. View code should check for {@code null} rather
82+
* than for empty collections as is already done by JSTL tags.</emphasis>
83+
* @param attributeValue the model attribute value (never {@code null})
84+
*/
85+
public ConcurrentModel addAttribute(Object attributeValue) {
86+
Assert.notNull(attributeValue, "Model object must not be null");
87+
if (attributeValue instanceof Collection && ((Collection<?>) attributeValue).isEmpty()) {
88+
return this;
89+
}
90+
return addAttribute(Conventions.getVariableName(attributeValue), attributeValue);
91+
}
92+
93+
/**
94+
* Copy all attributes in the supplied {@code Collection} into this
95+
* {@code Map}, using attribute name generation for each element.
96+
* @see #addAttribute(Object)
97+
*/
98+
public ConcurrentModel addAllAttributes(Collection<?> attributeValues) {
99+
if (attributeValues != null) {
100+
for (Object attributeValue : attributeValues) {
101+
addAttribute(attributeValue);
102+
}
103+
}
104+
return this;
105+
}
106+
107+
/**
108+
* Copy all attributes in the supplied {@code Map} into this {@code Map}.
109+
* @see #addAttribute(String, Object)
110+
*/
111+
public ConcurrentModel addAllAttributes(Map<String, ?> attributes) {
112+
if (attributes != null) {
113+
putAll(attributes);
114+
}
115+
return this;
116+
}
117+
118+
/**
119+
* Copy all attributes in the supplied {@code Map} into this {@code Map},
120+
* with existing objects of the same name taking precedence (i.e. not getting
121+
* replaced).
122+
*/
123+
public ConcurrentModel mergeAttributes(Map<String, ?> attributes) {
124+
if (attributes != null) {
125+
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
126+
String key = entry.getKey();
127+
if (!containsKey(key)) {
128+
put(key, entry.getValue());
129+
}
130+
}
131+
}
132+
return this;
133+
}
134+
135+
/**
136+
* Does this model contain an attribute of the given name?
137+
* @param attributeName the name of the model attribute (never {@code null})
138+
* @return whether this model contains a corresponding attribute
139+
*/
140+
public boolean containsAttribute(String attributeName) {
141+
return containsKey(attributeName);
142+
}
143+
144+
@Override
145+
public Map<String, Object> asMap() {
146+
return this;
147+
}
148+
149+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2002-2015 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+
17+
package org.springframework.validation.support;
18+
19+
import java.util.Map;
20+
21+
import org.springframework.ui.ConcurrentModel;
22+
import org.springframework.validation.BindingResult;
23+
24+
/**
25+
* Sub-class of {@link ConcurrentModel} that automatically removes
26+
* the {@link BindingResult} object when its corresponding
27+
* target attribute is replaced through regular {@link Map} operations.
28+
*
29+
* <p>This is the class exposed to controller methods by Spring Web Reactive,
30+
* typically consumed through a declaration of the
31+
* {@link org.springframework.ui.Model} interface. There is typically
32+
* no need to create it within user code. If necessary a controller method can
33+
* return a regular {@code java.util.Map}, or more likely a
34+
* {@code java.util.ConcurrentMap}.
35+
*
36+
* @author Rossen Stoyanchev
37+
* @since 5.0
38+
* @see BindingResult
39+
*/
40+
@SuppressWarnings("serial")
41+
public class BindingAwareConcurrentModel extends ConcurrentModel {
42+
43+
@Override
44+
public Object put(String key, Object value) {
45+
removeBindingResultIfNecessary(key, value);
46+
return super.put(key, value);
47+
}
48+
49+
@Override
50+
public void putAll(Map<? extends String, ?> map) {
51+
map.entrySet().forEach(e -> removeBindingResultIfNecessary(e.getKey(), e.getValue()));
52+
super.putAll(map);
53+
}
54+
55+
private void removeBindingResultIfNecessary(String key, Object value) {
56+
if (!key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
57+
String resultKey = BindingResult.MODEL_KEY_PREFIX + key;
58+
BindingResult result = (BindingResult) get(resultKey);
59+
if (result != null && result.getTarget() != value) {
60+
remove(resultKey);
61+
}
62+
}
63+
}
64+
65+
}

spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@
2323

2424
import org.springframework.core.MethodParameter;
2525
import org.springframework.core.ResolvableType;
26-
import org.springframework.ui.ExtendedModelMap;
27-
import org.springframework.ui.ModelMap;
26+
import org.springframework.ui.Model;
2827
import org.springframework.util.Assert;
28+
import org.springframework.web.reactive.result.method.BindingContext;
2929

3030
/**
31-
* Represent the result of the invocation of a handler.
31+
* Represent the result of the invocation of a handler or a handler method.
3232
*
3333
* @author Rossen Stoyanchev
3434
* @since 5.0
@@ -37,12 +37,11 @@ public class HandlerResult {
3737

3838
private final Object handler;
3939

40-
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
41-
private final Optional<Object> returnValue;
40+
private final Object returnValue;
4241

4342
private final ResolvableType returnType;
4443

45-
private final ModelMap model;
44+
private final BindingContext bindingContext;
4645

4746
private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
4847

@@ -62,15 +61,17 @@ public HandlerResult(Object handler, Object returnValue, MethodParameter returnT
6261
* @param handler the handler that handled the request
6362
* @param returnValue the return value from the handler possibly {@code null}
6463
* @param returnType the return value type
65-
* @param model the model used for request handling
64+
* @param context the binding context used for request handling
6665
*/
67-
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, ModelMap model) {
66+
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType,
67+
BindingContext context) {
68+
6869
Assert.notNull(handler, "'handler' is required");
6970
Assert.notNull(returnType, "'returnType' is required");
7071
this.handler = handler;
71-
this.returnValue = Optional.ofNullable(returnValue);
72+
this.returnValue = returnValue;
7273
this.returnType = ResolvableType.forMethodParameter(returnType);
73-
this.model = (model != null ? model : new ExtendedModelMap());
74+
this.bindingContext = (context != null ? context : new BindingContext());
7475
}
7576

7677

@@ -85,7 +86,7 @@ public Object getHandler() {
8586
* Return the value returned from the handler wrapped as {@link Optional}.
8687
*/
8788
public Optional<Object> getReturnValue() {
88-
return this.returnValue;
89+
return Optional.ofNullable(this.returnValue);
8990
}
9091

9192
/**
@@ -104,11 +105,18 @@ public MethodParameter getReturnTypeSource() {
104105
}
105106

106107
/**
107-
* Return the model used during request handling with attributes that may be
108-
* used to render HTML templates with.
108+
* Return the BindingContext used for request handling.
109+
*/
110+
public BindingContext getBindingContext() {
111+
return this.bindingContext;
112+
}
113+
114+
/**
115+
* Return the model used for request handling. This is a shortcut for
116+
* {@code getBindingContext().getModel()}.
109117
*/
110-
public ModelMap getModel() {
111-
return this.model;
118+
public Model getModel() {
119+
return this.bindingContext.getModel();
112120
}
113121

114122
/**

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

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,8 @@
1515
*/
1616
package org.springframework.web.reactive.result.method;
1717

18-
import org.springframework.beans.TypeConverter;
19-
import org.springframework.ui.ModelMap;
20-
import org.springframework.validation.support.BindingAwareModelMap;
18+
import org.springframework.ui.Model;
19+
import org.springframework.validation.support.BindingAwareConcurrentModel;
2120
import org.springframework.web.bind.WebDataBinder;
2221
import org.springframework.web.bind.WebExchangeDataBinder;
2322
import org.springframework.web.bind.support.WebBindingInitializer;
@@ -33,20 +32,17 @@
3332
*/
3433
public class BindingContext {
3534

36-
private final ModelMap model = new BindingAwareModelMap();
35+
private final Model model = new BindingAwareConcurrentModel();
3736

3837
private final WebBindingInitializer initializer;
3938

40-
private final TypeConverter simpleValueTypeConverter;
41-
4239

4340
public BindingContext() {
4441
this(null);
4542
}
4643

4744
public BindingContext(WebBindingInitializer initializer) {
4845
this.initializer = initializer;
49-
this.simpleValueTypeConverter = initTypeConverter(initializer);
5046
}
5147

5248
private static WebExchangeDataBinder initTypeConverter(WebBindingInitializer initializer) {
@@ -61,7 +57,7 @@ private static WebExchangeDataBinder initTypeConverter(WebBindingInitializer ini
6157
/**
6258
* Return the default model.
6359
*/
64-
public ModelMap getModel() {
60+
public Model getModel() {
6561
return this.model;
6662
}
6763

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import org.springframework.core.GenericTypeResolver;
3333
import org.springframework.core.MethodParameter;
3434
import org.springframework.core.ParameterNameDiscoverer;
35-
import org.springframework.ui.ModelMap;
3635
import org.springframework.util.ClassUtils;
3736
import org.springframework.util.ObjectUtils;
3837
import org.springframework.util.ReflectionUtils;
@@ -101,9 +100,8 @@ public Mono<HandlerResult> invoke(ServerWebExchange exchange,
101100
return resolveArguments(exchange, bindingContext, providedArgs).then(args -> {
102101
try {
103102
Object value = doInvoke(args);
104-
ModelMap model = bindingContext.getModel();
105-
HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model);
106-
return Mono.just(handlerResult);
103+
HandlerResult result = new HandlerResult(this, value, getReturnType(), bindingContext);
104+
return Mono.just(result);
107105
}
108106
catch (InvocationTargetException ex) {
109107
return Mono.error(ex.getTargetException());

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

0 commit comments

Comments
 (0)