From 3230ca6d392a83edd8b539becfd21ae2c27e0fa8 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 2 Nov 2016 15:25:05 +0200 Subject: [PATCH] Add ConcurrentModel This commit adds a Model implementation based on ConcurrentHashMap for use in Spring Web Reactive. Issue: SPR-14542 --- .../springframework/ui/ConcurrentModel.java | 149 ++++++++++++++++++ .../support/BindingAwareConcurrentModel.java | 65 ++++++++ .../web/reactive/HandlerResult.java | 12 +- .../result/method/BindingContext.java | 12 +- .../result/method/InvocableHandlerMethod.java | 3 +- .../AbstractNamedValueArgumentResolver.java | 8 +- .../PathVariableMethodArgumentResolver.java | 4 +- .../RequestMappingHandlerAdapter.java | 2 +- .../view/ViewResolutionResultHandler.java | 2 +- .../ViewResolutionResultHandlerTests.java | 7 +- 10 files changed, 237 insertions(+), 27 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java create mode 100644 spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java diff --git a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java new file mode 100644 index 0000000000..aaccfd5232 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java @@ -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 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}. + *

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. + * @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 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 attributes) { + if (attributes != null) { + for (Map.Entry 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 asMap() { + return this; + } + +} diff --git a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java new file mode 100644 index 0000000000..eac45811dd --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java @@ -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. + * + *

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 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); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java index 910426d240..191a686efc 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/HandlerResult.java @@ -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> 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; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java index f699df1e25..6d74469000 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/BindingContext.java @@ -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; } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java index 3ad4aaffb6..bb091d4a7d 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/InvocableHandlerMethod.java @@ -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); } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java index 30a370d654..3a38916ab2 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/AbstractNamedValueArgumentResolver.java @@ -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 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) { } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java index edc497f4d8..64f83c5700 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/PathVariableMethodArgumentResolver.java @@ -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 ? } diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java index 4eff18628f..d8e8d7349b 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestMappingHandlerAdapter.java @@ -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) { diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index 494c3128ea..570232d647 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -206,7 +206,7 @@ public class ViewResolutionResultHandler extends AbstractHandlerResultHandler .defaultIfEmpty(result.getModel()) .then(model -> getDefaultViewNameMono(exchange, result)); } - Map model = result.getModel(); + Map model = result.getModel().asMap(); return viewMono.then(view -> { updateResponseStatus(result.getReturnTypeSource(), exchange); if (view instanceof View) { diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index f13b8dee7b..39bb5e5dad 100644 --- a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -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);