Add ConcurrentModel
This commit adds a Model implementation based on ConcurrentHashMap for use in Spring Web Reactive. Issue: SPR-14542
This commit is contained in:
parent
9b57437b7a
commit
3230ca6d39
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.ui;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Implementation of {@link Model} based on a {@link ConcurrentHashMap} for use
|
||||
* in concurrent scenarios. Exposed to handler methods by Spring Web Reactive
|
||||
* typically via a declaration of the {@link Model} interface. There is typically
|
||||
* no need to create it within user code. If necessary a controller method can
|
||||
* return a regular {@code java.util.Map}, or more likely a
|
||||
* {@code java.util.ConcurrentMap}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class ConcurrentModel extends ConcurrentHashMap<String, Object> implements Model {
|
||||
|
||||
/**
|
||||
* Construct a new, empty {@code ConcurrentModel}.
|
||||
*/
|
||||
public ConcurrentModel() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code ModelMap} containing the supplied attribute
|
||||
* under the supplied name.
|
||||
* @see #addAttribute(String, Object)
|
||||
*/
|
||||
public ConcurrentModel(String attributeName, Object attributeValue) {
|
||||
addAttribute(attributeName, attributeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code ModelMap} containing the supplied attribute.
|
||||
* Uses attribute name generation to generate the key for the supplied model
|
||||
* object.
|
||||
* @see #addAttribute(Object)
|
||||
*/
|
||||
public ConcurrentModel(Object attributeValue) {
|
||||
addAttribute(attributeValue);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add the supplied attribute under the supplied name.
|
||||
* @param attributeName the name of the model attribute (never {@code null})
|
||||
* @param attributeValue the model attribute value (can be {@code null})
|
||||
*/
|
||||
public ConcurrentModel addAttribute(String attributeName, Object attributeValue) {
|
||||
Assert.notNull(attributeName, "Model attribute name must not be null");
|
||||
put(attributeName, attributeValue);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the supplied attribute to this {@code Map} using a
|
||||
* {@link org.springframework.core.Conventions#getVariableName generated name}.
|
||||
* <p><emphasis>Note: Empty {@link Collection Collections} are not added to
|
||||
* the model when using this method because we cannot correctly determine
|
||||
* the true convention name. View code should check for {@code null} rather
|
||||
* than for empty collections as is already done by JSTL tags.</emphasis>
|
||||
* @param attributeValue the model attribute value (never {@code null})
|
||||
*/
|
||||
public ConcurrentModel addAttribute(Object attributeValue) {
|
||||
Assert.notNull(attributeValue, "Model object must not be null");
|
||||
if (attributeValue instanceof Collection && ((Collection<?>) attributeValue).isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return addAttribute(Conventions.getVariableName(attributeValue), attributeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all attributes in the supplied {@code Collection} into this
|
||||
* {@code Map}, using attribute name generation for each element.
|
||||
* @see #addAttribute(Object)
|
||||
*/
|
||||
public ConcurrentModel addAllAttributes(Collection<?> attributeValues) {
|
||||
if (attributeValues != null) {
|
||||
for (Object attributeValue : attributeValues) {
|
||||
addAttribute(attributeValue);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all attributes in the supplied {@code Map} into this {@code Map}.
|
||||
* @see #addAttribute(String, Object)
|
||||
*/
|
||||
public ConcurrentModel addAllAttributes(Map<String, ?> attributes) {
|
||||
if (attributes != null) {
|
||||
putAll(attributes);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy all attributes in the supplied {@code Map} into this {@code Map},
|
||||
* with existing objects of the same name taking precedence (i.e. not getting
|
||||
* replaced).
|
||||
*/
|
||||
public ConcurrentModel mergeAttributes(Map<String, ?> attributes) {
|
||||
if (attributes != null) {
|
||||
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (!containsKey(key)) {
|
||||
put(key, entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this model contain an attribute of the given name?
|
||||
* @param attributeName the name of the model attribute (never {@code null})
|
||||
* @return whether this model contains a corresponding attribute
|
||||
*/
|
||||
public boolean containsAttribute(String attributeName) {
|
||||
return containsKey(attributeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> asMap() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.validation.support;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.ui.ConcurrentModel;
|
||||
import org.springframework.validation.BindingResult;
|
||||
|
||||
/**
|
||||
* Sub-class of {@link ConcurrentModel} that automatically removes
|
||||
* the {@link BindingResult} object when its corresponding
|
||||
* target attribute is replaced through regular {@link Map} operations.
|
||||
*
|
||||
* <p>This is the class exposed to controller methods by Spring Web Reactive,
|
||||
* typically consumed through a declaration of the
|
||||
* {@link org.springframework.ui.Model} interface. There is typically
|
||||
* no need to create it within user code. If necessary a controller method can
|
||||
* return a regular {@code java.util.Map}, or more likely a
|
||||
* {@code java.util.ConcurrentMap}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.0
|
||||
* @see BindingResult
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class BindingAwareConcurrentModel extends ConcurrentModel {
|
||||
|
||||
@Override
|
||||
public Object put(String key, Object value) {
|
||||
removeBindingResultIfNecessary(key, value);
|
||||
return super.put(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putAll(Map<? extends String, ?> map) {
|
||||
map.entrySet().forEach(e -> removeBindingResultIfNecessary(e.getKey(), e.getValue()));
|
||||
super.putAll(map);
|
||||
}
|
||||
|
||||
private void removeBindingResultIfNecessary(String key, Object value) {
|
||||
if (!key.startsWith(BindingResult.MODEL_KEY_PREFIX)) {
|
||||
String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + key;
|
||||
BindingResult bindingResult = (BindingResult) get(bindingResultKey);
|
||||
if (bindingResult != null && bindingResult.getTarget() != value) {
|
||||
remove(bindingResultKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -23,8 +23,8 @@ import reactor.core.publisher.Mono;
|
|||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.ui.ConcurrentModel;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
|
@ -42,7 +42,7 @@ public class HandlerResult {
|
|||
|
||||
private final ResolvableType returnType;
|
||||
|
||||
private final ModelMap model;
|
||||
private final Model model;
|
||||
|
||||
private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
|
||||
|
||||
|
@ -64,13 +64,13 @@ public class HandlerResult {
|
|||
* @param returnType the return value type
|
||||
* @param model the model used for request handling
|
||||
*/
|
||||
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, ModelMap model) {
|
||||
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, Model model) {
|
||||
Assert.notNull(handler, "'handler' is required");
|
||||
Assert.notNull(returnType, "'returnType' is required");
|
||||
this.handler = handler;
|
||||
this.returnValue = Optional.ofNullable(returnValue);
|
||||
this.returnType = ResolvableType.forMethodParameter(returnType);
|
||||
this.model = (model != null ? model : new ExtendedModelMap());
|
||||
this.model = (model != null ? model : new ConcurrentModel());
|
||||
}
|
||||
|
||||
|
||||
|
@ -107,7 +107,7 @@ public class HandlerResult {
|
|||
* Return the model used during request handling with attributes that may be
|
||||
* used to render HTML templates with.
|
||||
*/
|
||||
public ModelMap getModel() {
|
||||
public Model getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,8 @@
|
|||
*/
|
||||
package org.springframework.web.reactive.result.method;
|
||||
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.validation.support.BindingAwareModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.support.BindingAwareConcurrentModel;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.WebExchangeDataBinder;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
|
@ -33,12 +32,10 @@ import org.springframework.web.server.ServerWebExchange;
|
|||
*/
|
||||
public class BindingContext {
|
||||
|
||||
private final ModelMap model = new BindingAwareModelMap();
|
||||
private final Model model = new BindingAwareConcurrentModel();
|
||||
|
||||
private final WebBindingInitializer initializer;
|
||||
|
||||
private final TypeConverter simpleValueTypeConverter;
|
||||
|
||||
|
||||
public BindingContext() {
|
||||
this(null);
|
||||
|
@ -46,7 +43,6 @@ public class BindingContext {
|
|||
|
||||
public BindingContext(WebBindingInitializer initializer) {
|
||||
this.initializer = initializer;
|
||||
this.simpleValueTypeConverter = initTypeConverter(initializer);
|
||||
}
|
||||
|
||||
private static WebExchangeDataBinder initTypeConverter(WebBindingInitializer initializer) {
|
||||
|
@ -61,7 +57,7 @@ public class BindingContext {
|
|||
/**
|
||||
* Return the default model.
|
||||
*/
|
||||
public ModelMap getModel() {
|
||||
public Model getModel() {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
|
|||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
@ -101,7 +102,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
|
|||
return resolveArguments(exchange, bindingContext, providedArgs).then(args -> {
|
||||
try {
|
||||
Object value = doInvoke(args);
|
||||
ModelMap model = bindingContext.getModel();
|
||||
Model model = bindingContext.getModel();
|
||||
HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model);
|
||||
return Mono.just(handlerResult);
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import org.springframework.beans.factory.config.BeanExpressionContext;
|
|||
import org.springframework.beans.factory.config.BeanExpressionResolver;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
import org.springframework.web.reactive.result.method.BindingContext;
|
||||
|
@ -87,7 +87,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
|
|||
"Specified name must not resolve to null: [" + namedValueInfo.name + "]"));
|
||||
}
|
||||
|
||||
ModelMap model = bindingContext.getModel();
|
||||
Model model = bindingContext.getModel();
|
||||
|
||||
return resolveName(resolvedName.toString(), nestedParameter, exchange)
|
||||
.map(arg -> {
|
||||
|
@ -186,7 +186,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
|
|||
}
|
||||
|
||||
private Mono<Object> getDefaultValue(NamedValueInfo namedValueInfo, MethodParameter parameter,
|
||||
BindingContext bindingContext, ModelMap model, ServerWebExchange exchange) {
|
||||
BindingContext bindingContext, Model model, ServerWebExchange exchange) {
|
||||
|
||||
Object value = null;
|
||||
try {
|
||||
|
@ -263,7 +263,7 @@ public abstract class AbstractNamedValueArgumentResolver implements HandlerMetho
|
|||
*/
|
||||
@SuppressWarnings("UnusedParameters")
|
||||
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
|
||||
ModelMap model, ServerWebExchange exchange) {
|
||||
Model model, ServerWebExchange exchange) {
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.util.Optional;
|
|||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.ValueConstants;
|
||||
|
@ -93,7 +93,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueSyncAr
|
|||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void handleResolvedValue(Object arg, String name, MethodParameter parameter,
|
||||
ModelMap model, ServerWebExchange exchange) {
|
||||
Model model, ServerWebExchange exchange) {
|
||||
|
||||
// TODO: View.PATH_VARIABLES ?
|
||||
}
|
||||
|
|
|
@ -329,7 +329,7 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
|
|||
logger.debug("Invoking @ExceptionHandler method: " + invocable.getMethod());
|
||||
}
|
||||
invocable.setArgumentResolvers(getArgumentResolvers());
|
||||
bindingContext.getModel().clear();
|
||||
bindingContext.getModel().asMap().clear();
|
||||
return invocable.invoke(exchange, bindingContext, ex);
|
||||
}
|
||||
catch (Throwable invocationEx) {
|
||||
|
|
|
@ -206,7 +206,7 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
|
|||
.defaultIfEmpty(result.getModel())
|
||||
.then(model -> getDefaultViewNameMono(exchange, result));
|
||||
}
|
||||
Map<String, ?> model = result.getModel();
|
||||
Map<String, ?> model = result.getModel().asMap();
|
||||
return viewMono.then(view -> {
|
||||
updateResponseStatus(result.getReturnTypeSource(), exchange);
|
||||
if (view instanceof View) {
|
||||
|
|
|
@ -49,7 +49,6 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
|
|||
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
|
||||
import org.springframework.ui.ExtendedModelMap;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
|
@ -80,7 +79,7 @@ public class ViewResolutionResultHandlerTests {
|
|||
|
||||
private ServerWebExchange exchange;
|
||||
|
||||
private ModelMap model = new ExtendedModelMap();
|
||||
private Model model = new ExtendedModelMap();
|
||||
|
||||
|
||||
@Before
|
||||
|
@ -184,7 +183,7 @@ public class ViewResolutionResultHandlerTests {
|
|||
private void testDefaultViewName(Object returnValue, ResolvableType type)
|
||||
throws URISyntaxException {
|
||||
|
||||
ModelMap model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
Model model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), model);
|
||||
ViewResolutionResultHandler handler = createResultHandler(new TestViewResolver("account"));
|
||||
|
||||
|
@ -290,7 +289,7 @@ public class ViewResolutionResultHandlerTests {
|
|||
private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue,
|
||||
String responseBody, ViewResolver... resolvers) throws URISyntaxException {
|
||||
|
||||
ModelMap model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
Model model = new ExtendedModelMap().addAttribute("id", "123");
|
||||
MethodParameter returnType = resolvableMethod.resolveReturnType();
|
||||
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model);
|
||||
this.request.setUri(path);
|
||||
|
|
Loading…
Reference in New Issue