Skip to content

Commit

Permalink
Add updateModel to BindingContext
Browse files Browse the repository at this point in the history
The method includes logic that is currently in
ViewResolutionResultHandler but fits well in BindingContext and also
includes the call to saveModel method from the InitBinderBindingContext
subclass, which was called too early until now from
RequestMappingHandlerAdapter before the model has been fully updated.

This mirrors a similar method in ModelFactory on the Spring MVC side
which also combines those two tasks.

Closes gh-30821
  • Loading branch information
rstoyanchev committed Jul 11, 2023
1 parent 15b6626 commit 74972fb
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
package org.springframework.web.reactive;

import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

import reactor.core.publisher.Mono;

import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
import org.springframework.validation.support.BindingAwareConcurrentModel;
import org.springframework.web.bind.support.WebBindingInitializer;
Expand Down Expand Up @@ -57,20 +60,30 @@ public class BindingContext {

private boolean methodValidationApplicable;

private final ReactiveAdapterRegistry reactiveAdapterRegistry;


/**
* Create a new {@code BindingContext}.
* Create an instance without an initializer.
*/
public BindingContext() {
this(null);
}

/**
* Create a new {@code BindingContext} with the given initializer.
* @param initializer the binding initializer to apply (may be {@code null})
* Create an instance with the given initializer, which may be {@code null}.
*/
public BindingContext(@Nullable WebBindingInitializer initializer) {
this(initializer, ReactiveAdapterRegistry.getSharedInstance());
}

/**
* Create an instance with the given initializer and {@code ReactiveAdapterRegistry}.
* @since 6.1
*/
public BindingContext(@Nullable WebBindingInitializer initializer, ReactiveAdapterRegistry registry) {
this.initializer = initializer;
this.reactiveAdapterRegistry = new ReactiveAdapterRegistry();
}


Expand Down Expand Up @@ -151,6 +164,34 @@ protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, Ser
return binder;
}

/**
* Invoked before rendering to add {@link BindingResult} attributes where
* necessary, and also to promote model attributes listed as
* {@code @SessionAttributes} to the session.
* @param exchange the current exchange
* @since 6.1
*/
public void updateModel(ServerWebExchange exchange) {
Map<String, Object> model = getModel().asMap();
for (Map.Entry<String, Object> entry : model.entrySet()) {
String name = entry.getKey();
Object value = entry.getValue();
if (isBindingCandidate(name, value)) {
if (!model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) {
WebExchangeDataBinder binder = createDataBinder(exchange, value, name);
model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
}
}

private boolean isBindingCandidate(String name, @Nullable Object value) {
return (!name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null &&
!value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) &&
this.reactiveAdapterRegistry.getAdapter(null, value) == null &&
!BeanUtils.isSimpleValueType(value.getClass()));
}


/**
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.util.List;

import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
Expand Down Expand Up @@ -52,11 +53,11 @@ class InitBinderBindingContext extends BindingContext {

InitBinderBindingContext(
@Nullable WebBindingInitializer initializer, List<SyncInvocableHandlerMethod> binderMethods,
boolean methodValidationApplicable) {
boolean methodValidationApplicable, ReactiveAdapterRegistry registry) {

super(initializer);
super(initializer, registry);
this.binderMethods = binderMethods;
this.binderMethodContext = new BindingContext(initializer);
this.binderMethodContext = new BindingContext(initializer, registry);
setMethodValidationApplicable(methodValidationApplicable);
}

Expand Down Expand Up @@ -101,8 +102,8 @@ private void invokeBinderMethod(
}

/**
* Provide the context required to apply {@link #saveModel()} after the
* controller method has been invoked.
* Provide the context required to promote model attributes listed as
* {@code @SessionAttributes} to the session during {@link #updateModel}.
*/
public void setSessionContext(SessionAttributesHandler attributesHandler, WebSession session) {
this.saveModelOperation = () -> {
Expand All @@ -115,14 +116,12 @@ public void setSessionContext(SessionAttributesHandler attributesHandler, WebSes
};
}

/**
* Save model attributes in the session based on a type-level declarations
* in an {@code @SessionAttributes} annotation.
*/
public void saveModel() {
@Override
public void updateModel(ServerWebExchange exchange) {
if (this.saveModelOperation != null) {
this.saveModelOperation.run();
}
super.updateModel(exchange);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -186,12 +186,16 @@ public boolean supports(Object handler) {

@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {

Assert.state(this.methodResolver != null &&
this.modelInitializer != null && this.reactiveAdapterRegistry != null, "Not initialized");

HandlerMethod handlerMethod = (HandlerMethod) handler;
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");

InitBinderBindingContext bindingContext = new InitBinderBindingContext(
this.webBindingInitializer, this.methodResolver.getInitBinderMethods(handlerMethod),
this.methodResolver.hasMethodValidator() && handlerMethod.shouldValidateArguments());
this.methodResolver.hasMethodValidator() && handlerMethod.shouldValidateArguments(),
this.reactiveAdapterRegistry);

InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);

Expand All @@ -202,7 +206,6 @@ public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
.initModel(handlerMethod, bindingContext, exchange)
.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
.doOnNext(result -> bindingContext.saveModel())
.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand All @@ -17,7 +17,6 @@
package org.springframework.web.reactive.result.view;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
Expand All @@ -41,9 +40,7 @@
import org.springframework.lang.Nullable;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
Expand Down Expand Up @@ -243,7 +240,7 @@ else if (View.class.isAssignableFrom(clazz)) {
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
}
BindingContext bindingContext = result.getBindingContext();
updateBindingResult(bindingContext, exchange);
bindingContext.updateModel(exchange);
return viewsMono.flatMap(views -> render(views, model.asMap(), bindingContext, exchange));
});
}
Expand Down Expand Up @@ -289,27 +286,6 @@ private String getNameForReturnValue(MethodParameter returnType) {
.orElseGet(() -> Conventions.getVariableNameForParameter(returnType));
}

private void updateBindingResult(BindingContext context, ServerWebExchange exchange) {
Map<String, Object> model = context.getModel().asMap();
for (Map.Entry<String, Object> entry : model.entrySet()) {
String name = entry.getKey();
Object value = entry.getValue();
if (isBindingCandidate(name, value)) {
if (!model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) {
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
}
}

private boolean isBindingCandidate(String name, @Nullable Object value) {
return (!name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null &&
!value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) &&
getAdapterRegistry().getAdapter(null, value) == null &&
!BeanUtils.isSimpleValueType(value.getClass()));
}

private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
BindingContext bindingContext, ServerWebExchange exchange) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ private BindingContext createBindingContext(String methodName, Class<?>... param
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());

return new InitBinderBindingContext(
this.bindingInitializer, Collections.singletonList(handlerMethod), false);
this.bindingInitializer, Collections.singletonList(handlerMethod), false,
ReactiveAdapterRegistry.getSharedInstance());
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public void saveModelAttributeToSession() {
assertThat(session).isNotNull();
assertThat(session.getAttributes()).isEmpty();

context.saveModel();
context.updateModel(this.exchange);
assertThat(session.getAttributes()).hasSize(1);
assertThat(((TestBean) session.getRequiredAttribute("bean")).getName()).isEqualTo("Bean");
}
Expand All @@ -164,7 +164,7 @@ public void retrieveModelAttributeFromSession() {
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
this.modelInitializer.initModel(handlerMethod, context, this.exchange).block(TIMEOUT);

context.saveModel();
context.updateModel(this.exchange);
assertThat(session.getAttributes()).hasSize(1);
assertThat(((TestBean) session.getRequiredAttribute("bean")).getName()).isEqualTo("Session Bean");
}
Expand Down Expand Up @@ -197,7 +197,7 @@ public void clearModelAttributeFromSession() {
this.modelInitializer.initModel(handlerMethod, context, this.exchange).block(TIMEOUT);

context.getSessionStatus().setComplete();
context.saveModel();
context.updateModel(this.exchange);

assertThat(session.getAttributes()).isEmpty();
}
Expand All @@ -211,7 +211,8 @@ private InitBinderBindingContext getBindingContext(Object controller) {
.toList();

WebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
return new InitBinderBindingContext(bindingInitializer, binderMethods, false);
return new InitBinderBindingContext(
bindingInitializer, binderMethods, false, ReactiveAdapterRegistry.getSharedInstance());
}


Expand Down

0 comments on commit 74972fb

Please sign in to comment.