This commit is contained in:
Rossen Stoyanchev 2017-09-08 07:53:23 -04:00
parent 8f78c772b5
commit bc470fca30
6 changed files with 48 additions and 39 deletions

View File

@ -243,13 +243,12 @@ public final class ModelFactory {
/** /**
* Derive the model attribute name for a method parameter based on: * Derive the model attribute name for the given method parameter based on
* <ol> * a {@code @ModelAttribute} parameter annotation (if present) or falling
* <li>the parameter {@code @ModelAttribute} annotation value * back on parameter type based conventions.
* <li>the parameter type
* </ol>
* @param parameter a descriptor for the method parameter * @param parameter a descriptor for the method parameter
* @return the derived name (never {@code null} or empty String) * @return the derived name
* @see Conventions#getVariableNameForParameter(MethodParameter)
*/ */
public static String getNameForParameter(MethodParameter parameter) { public static String getNameForParameter(MethodParameter parameter) {
ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);

View File

@ -74,10 +74,7 @@ public class SessionAttributesHandler {
this.attributeNames.addAll(Arrays.asList(annotation.names())); this.attributeNames.addAll(Arrays.asList(annotation.names()));
this.attributeTypes.addAll(Arrays.asList(annotation.types())); this.attributeTypes.addAll(Arrays.asList(annotation.types()));
} }
this.knownAttributeNames.addAll(this.attributeNames);
for (String attributeName : this.attributeNames) {
this.knownAttributeNames.add(attributeName);
}
} }
/** /**
@ -90,7 +87,7 @@ public class SessionAttributesHandler {
/** /**
* Whether the attribute name or type match the names and types specified * Whether the attribute name or type match the names and types specified
* via {@code @SessionAttributes} in underlying controller. * via {@code @SessionAttributes} on the underlying controller.
* <p>Attributes successfully resolved through this method are "remembered" * <p>Attributes successfully resolved through this method are "remembered"
* and subsequently used in {@link #retrieveAttributes(WebRequest)} and * and subsequently used in {@link #retrieveAttributes(WebRequest)} and
* {@link #cleanupAttributes(WebRequest)}. * {@link #cleanupAttributes(WebRequest)}.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2016 the original author or authors. * Copyright 2002-2017 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,12 +30,15 @@ import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.ServletWebRequest;
import static java.util.Arrays.*; import static java.util.Arrays.asList;
import static org.junit.Assert.*; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/** /**
* Test fixture with {@link SessionAttributesHandler}. * Test fixture with {@link SessionAttributesHandler}.
*
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
*/ */
public class SessionAttributesHandlerTests { public class SessionAttributesHandlerTests {
@ -50,10 +53,10 @@ public class SessionAttributesHandlerTests {
@Test @Test
public void isSessionAttribute() throws Exception { public void isSessionAttribute() throws Exception {
assertTrue(sessionAttributesHandler.isHandlerSessionAttribute("attr1", null)); assertTrue(sessionAttributesHandler.isHandlerSessionAttribute("attr1", String.class));
assertTrue(sessionAttributesHandler.isHandlerSessionAttribute("attr2", null)); assertTrue(sessionAttributesHandler.isHandlerSessionAttribute("attr2", String.class));
assertTrue(sessionAttributesHandler.isHandlerSessionAttribute("simple", TestBean.class)); assertTrue(sessionAttributesHandler.isHandlerSessionAttribute("simple", TestBean.class));
assertFalse(sessionAttributesHandler.isHandlerSessionAttribute("simple", null)); assertFalse(sessionAttributesHandler.isHandlerSessionAttribute("simple", String.class));
} }
@Test @Test

View File

@ -24,7 +24,8 @@ import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
/** /**
* Context to assist with processing a request and binding it onto Objects. * Context to assist with binding request data onto Objects and provide access
* to a shared {@link Model} with controller-specific attributes.
* *
* <p>Provides methods to create a {@link WebExchangeDataBinder} for a specific * <p>Provides methods to create a {@link WebExchangeDataBinder} for a specific
* target, command Object to apply data binding and validation to, or without a * target, command Object to apply data binding and validation to, or without a

View File

@ -30,6 +30,7 @@ import org.springframework.core.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.BindingContext;
@ -73,12 +74,13 @@ class ModelInitializer {
List<Mono<HandlerResult>> resultList = new ArrayList<>(); List<Mono<HandlerResult>> resultList = new ArrayList<>();
attributeMethods.forEach(invocable -> resultList.add(invocable.invoke(exchange, bindingContext))); attributeMethods.forEach(invocable -> resultList.add(invocable.invoke(exchange, bindingContext)));
return Mono.zip(resultList, objectArray -> { return Mono
.zip(resultList, objectArray -> {
return Arrays.stream(objectArray) return Arrays.stream(objectArray)
.map(object -> (HandlerResult) object) .map(object -> handleResult(((HandlerResult) object), bindingContext))
.map(handlerResult -> handleResult(handlerResult, bindingContext))
.collect(Collectors.toList()); .collect(Collectors.toList());
}).flatMap(completionList -> Mono.when(completionList)); })
.flatMap(completionList -> Mono.when(completionList));
} }
private Mono<Void> handleResult(HandlerResult handlerResult, BindingContext bindingContext) { private Mono<Void> handleResult(HandlerResult handlerResult, BindingContext bindingContext) {
@ -86,18 +88,19 @@ class ModelInitializer {
if (value != null) { if (value != null) {
ResolvableType type = handlerResult.getReturnType(); ResolvableType type = handlerResult.getReturnType();
ReactiveAdapter adapter = this.adapterRegistry.getAdapter(type.getRawClass(), value); ReactiveAdapter adapter = this.adapterRegistry.getAdapter(type.getRawClass(), value);
if (adapter != null) { if (isAsyncVoidType(type, adapter)) {
Class<?> attributeType = (adapter.isNoValue() ? Void.class : type.resolveGeneric());
if (attributeType == Void.class) {
return Mono.from(adapter.toPublisher(value)); return Mono.from(adapter.toPublisher(value));
} }
}
String name = getAttributeName(handlerResult.getReturnTypeSource()); String name = getAttributeName(handlerResult.getReturnTypeSource());
bindingContext.getModel().asMap().putIfAbsent(name, value); bindingContext.getModel().asMap().putIfAbsent(name, value);
} }
return Mono.empty(); return Mono.empty();
} }
private boolean isAsyncVoidType(ResolvableType type, @Nullable ReactiveAdapter adapter) {
return adapter != null && (adapter.isNoValue() || type.resolveGeneric() == Void.class);
}
private String getAttributeName(MethodParameter param) { private String getAttributeName(MethodParameter param) {
return Optional return Optional
.ofNullable(AnnotatedElementUtils.findMergedAnnotation(param.getAnnotatedElement(), ModelAttribute.class)) .ofNullable(AnnotatedElementUtils.findMergedAnnotation(param.getAnnotatedElement(), ModelAttribute.class))

View File

@ -147,7 +147,6 @@ public abstract class AbstractView implements View, ApplicationContextAware {
* Obtain the ApplicationContext for actual use. * Obtain the ApplicationContext for actual use.
* @return the ApplicationContext (never {@code null}) * @return the ApplicationContext (never {@code null})
* @throws IllegalStateException in case of no ApplicationContext set * @throws IllegalStateException in case of no ApplicationContext set
* @since 5.0
*/ */
protected final ApplicationContext obtainApplicationContext() { protected final ApplicationContext obtainApplicationContext() {
ApplicationContext applicationContext = getApplicationContext(); ApplicationContext applicationContext = getApplicationContext();
@ -191,7 +190,9 @@ public abstract class AbstractView implements View, ApplicationContextAware {
* <p>The default implementation creates a combined output Map that includes * <p>The default implementation creates a combined output Map that includes
* model as well as static attributes with the former taking precedence. * model as well as static attributes with the former taking precedence.
*/ */
protected Mono<Map<String, Object>> getModelAttributes(@Nullable Map<String, ?> model, ServerWebExchange exchange) { protected Mono<Map<String, Object>> getModelAttributes(@Nullable Map<String, ?> model,
ServerWebExchange exchange) {
int size = (model != null ? model.size() : 0); int size = (model != null ? model.size() : 0);
Map<String, Object> attributes = new LinkedHashMap<>(size); Map<String, Object> attributes = new LinkedHashMap<>(size);
@ -203,9 +204,11 @@ public abstract class AbstractView implements View, ApplicationContextAware {
} }
/** /**
* By default, resolve async attributes supported by the {@link ReactiveAdapterRegistry} to their blocking counterparts. * By default, resolve async attributes supported by the
* <p>View implementations capable of taking advantage of reactive types can override this method if needed. * {@link ReactiveAdapterRegistry} to their blocking counterparts.
* @return {@code Mono} to represent when the async attributes have been resolved * <p>View implementations capable of taking advantage of reactive types
* can override this method if needed.
* @return {@code Mono} for the completion of async attributes resolution
*/ */
protected Mono<Void> resolveAsyncAttributes(Map<String, Object> model) { protected Mono<Void> resolveAsyncAttributes(Map<String, Object> model) {
@ -252,8 +255,9 @@ public abstract class AbstractView implements View, ApplicationContextAware {
/** /**
* Create a RequestContext to expose under the specified attribute name. * Create a RequestContext to expose under the specified attribute name.
* <p>The default implementation creates a standard RequestContext instance for the * <p>The default implementation creates a standard RequestContext instance
* given request and model. Can be overridden in subclasses for custom instances. * for the given request and model. Can be overridden in subclasses for
* custom instances.
* @param exchange current exchange * @param exchange current exchange
* @param model combined output Map (never {@code null}), * @param model combined output Map (never {@code null}),
* with dynamic values taking precedence over static attributes * with dynamic values taking precedence over static attributes
@ -269,7 +273,8 @@ public abstract class AbstractView implements View, ApplicationContextAware {
* <p>The default implementation looks in the {@link #getApplicationContext() * <p>The default implementation looks in the {@link #getApplicationContext()
* Spring configuration} for a {@code RequestDataValueProcessor} bean with * Spring configuration} for a {@code RequestDataValueProcessor} bean with
* the name {@link #REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME}. * the name {@link #REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME}.
* @return the RequestDataValueProcessor, or null if there is none at the application context. * @return the RequestDataValueProcessor, or null if there is none at the
* application context.
*/ */
@Nullable @Nullable
protected RequestDataValueProcessor getRequestDataValueProcessor() { protected RequestDataValueProcessor getRequestDataValueProcessor() {
@ -286,7 +291,8 @@ public abstract class AbstractView implements View, ApplicationContextAware {
* with dynamic values taking precedence over static attributes * with dynamic values taking precedence over static attributes
* @param contentType the content type selected to render with which should * @param contentType the content type selected to render with which should
* match one of the {@link #getSupportedMediaTypes() supported media types}. * match one of the {@link #getSupportedMediaTypes() supported media types}.
*@param exchange current exchange @return {@code Mono} to represent when and if rendering succeeds *@param exchange current exchange @return {@code Mono} to represent when
* and if rendering succeeds
*/ */
protected abstract Mono<Void> renderInternal(Map<String, Object> renderAttributes, protected abstract Mono<Void> renderInternal(Map<String, Object> renderAttributes,
@Nullable MediaType contentType, ServerWebExchange exchange); @Nullable MediaType contentType, ServerWebExchange exchange);