Skip to content

Commit 3230ca6

Browse files
committed
Add ConcurrentModel
This commit adds a Model implementation based on ConcurrentHashMap for use in Spring Web Reactive. Issue: SPR-14542
1 parent 9b57437 commit 3230ca6

File tree

10 files changed

+237
-27
lines changed

10 files changed

+237
-27
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 bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
58+
BindingResult bindingResult = (BindingResult) get(bindingResultKey);
59+
if (bindingResult != null && bindingResult.getTarget() != value) {
60+
remove(bindingResultKey);
61+
}
62+
}
63+
}
64+
65+
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
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.ConcurrentModel;
27+
import org.springframework.ui.Model;
2828
import org.springframework.util.Assert;
2929

3030
/**
@@ -42,7 +42,7 @@ public class HandlerResult {
4242

4343
private final ResolvableType returnType;
4444

45-
private final ModelMap model;
45+
private final Model model;
4646

4747
private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
4848

@@ -64,13 +64,13 @@ public HandlerResult(Object handler, Object returnValue, MethodParameter returnT
6464
* @param returnType the return value type
6565
* @param model the model used for request handling
6666
*/
67-
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, ModelMap model) {
67+
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, Model model) {
6868
Assert.notNull(handler, "'handler' is required");
6969
Assert.notNull(returnType, "'returnType' is required");
7070
this.handler = handler;
7171
this.returnValue = Optional.ofNullable(returnValue);
7272
this.returnType = ResolvableType.forMethodParameter(returnType);
73-
this.model = (model != null ? model : new ExtendedModelMap());
73+
this.model = (model != null ? model : new ConcurrentModel());
7474
}
7575

7676

@@ -107,7 +107,7 @@ public MethodParameter getReturnTypeSource() {
107107
* Return the model used during request handling with attributes that may be
108108
* used to render HTML templates with.
109109
*/
110-
public ModelMap getModel() {
110+
public Model getModel() {
111111
return this.model;
112112
}
113113

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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.core.GenericTypeResolver;
3333
import org.springframework.core.MethodParameter;
3434
import org.springframework.core.ParameterNameDiscoverer;
35+
import org.springframework.ui.Model;
3536
import org.springframework.ui.ModelMap;
3637
import org.springframework.util.ClassUtils;
3738
import org.springframework.util.ObjectUtils;
@@ -101,7 +102,7 @@ public Mono<HandlerResult> invoke(ServerWebExchange exchange,
101102
return resolveArguments(exchange, bindingContext, providedArgs).then(args -> {
102103
try {
103104
Object value = doInvoke(args);
104-
ModelMap model = bindingContext.getModel();
105+
Model model = bindingContext.getModel();
105106
HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model);
106107
return Mono.just(handlerResult);
107108
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import org.springframework.beans.factory.config.BeanExpressionResolver;
2828
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2929
import org.springframework.core.MethodParameter;
30-
import org.springframework.ui.ModelMap;
30+
import org.springframework.ui.Model;
3131
import org.springframework.web.bind.WebDataBinder;
3232
import org.springframework.web.bind.annotation.ValueConstants;
3333
import org.springframework.web.reactive.result.method.BindingContext;
@@ -87,7 +87,7 @@ public Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bi
8787
"Specified name must not resolve to null: [" + namedValueInfo.name + "]"));
8888
}
8989

90-
ModelMap model = bindingContext.getModel();
90+
Model model = bindingContext.getModel();
9191

9292
return resolveName(resolvedName.toString(), nestedParameter, exchange)
9393
.map(arg -> {
@@ -186,7 +186,7 @@ private Object applyConversion(Object value, NamedValueInfo namedValueInfo, Meth
186186
}
187187

188188
private Mono<Object> getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter,
189-
BindingContext bindingContext, ModelMap model, ServerWebExchange exchange) {
189+
BindingContext bindingContext, Model model, ServerWebExchange exchange) {
190190

191191
Object value = null;
192192
try {
@@ -263,7 +263,7 @@ else if (paramType.isPrimitive()) {
263263
*/
264264
@SuppressWarnings("UnusedParameters")
265265
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
266-
ModelMap model, ServerWebExchange exchange) {
266+
Model model, ServerWebExchange exchange) {
267267
}
268268

269269

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2323
import org.springframework.core.MethodParameter;
2424
import org.springframework.core.convert.converter.Converter;
25-
import org.springframework.ui.ModelMap;
25+
import org.springframework.ui.Model;
2626
import org.springframework.util.StringUtils;
2727
import org.springframework.web.bind.annotation.PathVariable;
2828
import org.springframework.web.bind.annotation.ValueConstants;
@@ -93,7 +93,7 @@ protected void handleMissingValue(String name, MethodParameter parameter) {
9393
@Override
9494
@SuppressWarnings("unchecked")
9595
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
96-
ModelMap model, ServerWebExchange exchange) {
96+
Model model, ServerWebExchange exchange) {
9797

9898
// TODO: View.PATH_VARIABLES ?
9999
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ private Mono<HandlerResult> handleException(Throwable ex, HandlerMethod handlerM
329329
logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod());
330330
}
331331
invocable.setArgumentResolvers(getArgumentResolvers());
332-
bindingContext.getModel().clear();
332+
bindingContext.getModel().asMap().clear();
333333
return invocable.invoke(exchange, bindingContext, ex);
334334
}
335335
catch (Throwable invocationEx) {

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
206206
.defaultIfEmpty(result.getModel())
207207
.then(model -> getDefaultViewNameMono(exchange, result));
208208
}
209-
Map<String, ?> model = result.getModel();
209+
Map<String, ?> model = result.getModel().asMap();
210210
return viewMono.then(view -> {
211211
updateResponseStatus(result.getReturnTypeSource(), exchange);
212212
if (view instanceof View) {

0 commit comments

Comments
 (0)