Skip to content

Commit 6abd4d5

Browse files
committed
Async model attributes resolved before rendering
Issue: SPR-14542
1 parent d163240 commit 6abd4d5

File tree

2 files changed

+207
-188
lines changed

2 files changed

+207
-188
lines changed

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

Lines changed: 122 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,18 @@
1616

1717
package org.springframework.web.reactive.result.view;
1818

19-
import java.lang.reflect.Method;
2019
import java.util.ArrayList;
2120
import java.util.Collections;
2221
import java.util.List;
2322
import java.util.Locale;
2423
import java.util.Map;
2524
import java.util.Optional;
25+
import java.util.stream.Collectors;
2626

2727
import reactor.core.publisher.Flux;
2828
import reactor.core.publisher.Mono;
2929

3030
import org.springframework.beans.BeanUtils;
31-
import org.springframework.core.Conventions;
32-
import org.springframework.core.GenericTypeResolver;
3331
import org.springframework.core.MethodParameter;
3432
import org.springframework.core.Ordered;
3533
import org.springframework.core.ReactiveAdapter;
@@ -38,6 +36,7 @@
3836
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
3937
import org.springframework.http.MediaType;
4038
import org.springframework.ui.Model;
39+
import org.springframework.util.ClassUtils;
4140
import org.springframework.util.StringUtils;
4241
import org.springframework.web.bind.annotation.ModelAttribute;
4342
import org.springframework.web.reactive.HandlerResult;
@@ -77,6 +76,11 @@
7776
public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
7877
implements HandlerResultHandler, Ordered {
7978

79+
private static final Object NO_VALUE = new Object();
80+
81+
private static final Mono<Object> NO_VALUE_MONO = Mono.just(NO_VALUE);
82+
83+
8084
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
8185

8286
private final List<View> defaultViews = new ArrayList<>(4);
@@ -172,89 +176,81 @@ private boolean isSupportedType(Class<?> clazz) {
172176
}
173177

174178
@Override
179+
@SuppressWarnings("unchecked")
175180
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
176181

177-
Mono<Object> valueMono;
182+
Mono<Object> returnValueMono;
178183
ResolvableType elementType;
179-
ResolvableType returnType = result.getReturnType();
184+
ResolvableType parameterType = result.getReturnType();
180185

181186
Optional<Object> optional = result.getReturnValue();
182-
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(returnType.getRawClass(), optional);
187+
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(parameterType.getRawClass(), optional);
188+
183189
if (adapter != null) {
184190
if (optional.isPresent()) {
185191
Mono<?> converted = adapter.toMono(optional);
186-
valueMono = converted.map(o -> o);
192+
returnValueMono = converted.map(o -> o);
187193
}
188194
else {
189-
valueMono = Mono.empty();
195+
returnValueMono = Mono.empty();
190196
}
191197
elementType = adapter.getDescriptor().isNoValue() ?
192-
ResolvableType.forClass(Void.class) : returnType.getGeneric(0);
198+
ResolvableType.forClass(Void.class) : parameterType.getGeneric(0);
193199
}
194200
else {
195-
valueMono = Mono.justOrEmpty(result.getReturnValue());
196-
elementType = returnType;
201+
returnValueMono = Mono.justOrEmpty(result.getReturnValue());
202+
elementType = parameterType;
197203
}
198204

199-
Mono<Object> viewMono;
200-
if (isViewNameOrReference(elementType, result)) {
201-
Mono<Object> viewName = getDefaultViewNameMono(exchange, result);
202-
viewMono = valueMono.otherwiseIfEmpty(viewName);
203-
}
204-
else {
205-
viewMono = valueMono.map(value -> updateModel(value, result))
206-
.defaultIfEmpty(result.getModel())
207-
.then(model -> getDefaultViewNameMono(exchange, result));
208-
}
209-
Map<String, ?> model = result.getModel().asMap();
210-
return viewMono.then(view -> {
211-
updateResponseStatus(result.getReturnTypeSource(), exchange);
212-
if (view instanceof View) {
213-
return ((View) view).render(model, null, exchange);
214-
}
215-
else if (view instanceof CharSequence) {
216-
String viewName = view.toString();
217-
Locale locale = Locale.getDefault(); // TODO
218-
return resolveAndRender(viewName, locale, model, exchange);
205+
return returnValueMono
206+
.otherwiseIfEmpty(exchange.isNotModified() ? Mono.empty() : NO_VALUE_MONO)
207+
.then(returnValue -> {
219208

220-
}
221-
else {
222-
// Should not happen
223-
return Mono.error(new IllegalStateException("Unexpected view type"));
224-
}
225-
});
226-
}
209+
updateResponseStatus(result.getReturnTypeSource(), exchange);
227210

228-
private boolean isViewNameOrReference(ResolvableType elementType, HandlerResult result) {
229-
Class<?> clazz = elementType.getRawClass();
230-
return (View.class.isAssignableFrom(clazz) ||
231-
(CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result)));
232-
}
211+
Mono<List<View>> viewsMono;
212+
Model model = result.getModel();
213+
Locale locale = Locale.getDefault(); // TODO
233214

234-
private Mono<Object> getDefaultViewNameMono(ServerWebExchange exchange, HandlerResult result) {
235-
if (exchange.isNotModified()) {
236-
return Mono.empty();
237-
}
238-
String defaultViewName = getDefaultViewName(result, exchange);
239-
if (defaultViewName != null) {
240-
return Mono.just(defaultViewName);
241-
}
242-
else {
243-
return Mono.error(new IllegalStateException(
244-
"Handler [" + result.getHandler() + "] " +
245-
"neither returned a view name nor a View object"));
246-
}
215+
Class<?> clazz = elementType.getRawClass();
216+
if (clazz == null) {
217+
clazz = returnValue.getClass();
218+
}
219+
220+
if (returnValue == NO_VALUE || Void.class.equals(clazz) || void.class.equals(clazz)) {
221+
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
222+
}
223+
else if (Model.class.isAssignableFrom(clazz)) {
224+
model.addAllAttributes(((Model) returnValue).asMap());
225+
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
226+
}
227+
else if (Map.class.isAssignableFrom(clazz)) {
228+
model.addAllAttributes((Map<String, ?>) returnValue);
229+
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
230+
}
231+
else if (View.class.isAssignableFrom(clazz)) {
232+
viewsMono = Mono.just(Collections.singletonList((View) returnValue));
233+
}
234+
else if (CharSequence.class.isAssignableFrom(clazz) && !hasModelAttributeAnnotation(result)) {
235+
viewsMono = resolveViews(returnValue.toString(), locale);
236+
}
237+
else {
238+
String name = getNameForReturnValue(clazz, result.getReturnTypeSource());
239+
model.addAttribute(name, returnValue);
240+
viewsMono = resolveViews(getDefaultViewName(result, exchange), locale);
241+
}
242+
243+
return resolveAsyncAttributes(model.asMap())
244+
.then(viewsMono)
245+
.then(views -> render(views, model.asMap(), exchange));
246+
});
247247
}
248248

249249
/**
250-
* Translate the given request into a default view name. This is useful when
251-
* the application leaves the view name unspecified.
252-
* <p>The default implementation strips the leading and trailing slash from
253-
* the as well as any extension and uses that as the view name.
254-
* @return the default view name to use; if {@code null} is returned
255-
* processing will result in an IllegalStateException.
250+
* Select a default view name when a controller leaves the view unspecified.
251+
* The default implementation strips the leading and trailing slash from the
252+
* as well as any extension and uses that as the view name.
256253
*/
257-
@SuppressWarnings("UnusedParameters")
258254
protected String getDefaultViewName(HandlerResult result, ServerWebExchange exchange) {
259255
String path = this.pathHelper.getLookupPathForRequest(exchange);
260256
if (path.startsWith("/")) {
@@ -266,79 +262,87 @@ protected String getDefaultViewName(HandlerResult result, ServerWebExchange exch
266262
return StringUtils.stripFilenameExtension(path);
267263
}
268264

269-
@SuppressWarnings("unchecked")
270-
private Object updateModel(Object value, HandlerResult result) {
271-
if (value instanceof Model) {
272-
result.getModel().addAllAttributes(((Model) value).asMap());
273-
}
274-
else if (value instanceof Map) {
275-
result.getModel().addAllAttributes((Map<String, ?>) value);
276-
}
277-
else {
278-
MethodParameter returnType = result.getReturnTypeSource();
279-
String name = getNameForReturnValue(value, returnType);
280-
result.getModel().addAttribute(name, value);
281-
}
282-
return value;
265+
private Mono<List<View>> resolveViews(String viewName, Locale locale) {
266+
return Flux.fromIterable(getViewResolvers())
267+
.concatMap(resolver -> resolver.resolveViewName(viewName, locale))
268+
.collectList()
269+
.map(views -> {
270+
if (views.isEmpty()) {
271+
throw new IllegalStateException(
272+
"Could not resolve view with name '" + viewName + "'.");
273+
}
274+
views.addAll(getDefaultViews());
275+
return views;
276+
});
283277
}
284278

285279
/**
286-
* Derive the model attribute name for the given return value using one of:
287-
* <ol>
288-
* <li>The method {@code ModelAttribute} annotation value
289-
* <li>The declared return type if it is more specific than {@code Object}
290-
* <li>The actual return value type
291-
* </ol>
292-
* @param returnValue the value returned from a method invocation
293-
* @param returnType the return type of the method
294-
* @return the model name, never {@code null} nor empty
280+
* Return the name of a model attribute return value based on the method
281+
* {@code @ModelAttribute} annotation, if present, or derived from the type
282+
* of the return value otherwise.
295283
*/
296-
private static String getNameForReturnValue(Object returnValue, MethodParameter returnType) {
284+
private String getNameForReturnValue(Class<?> returnValueType, MethodParameter returnType) {
297285
ModelAttribute annotation = returnType.getMethodAnnotation(ModelAttribute.class);
298286
if (annotation != null && StringUtils.hasText(annotation.value())) {
299287
return annotation.value();
300288
}
301-
else {
302-
Method method = returnType.getMethod();
303-
Class<?> containingClass = returnType.getContainingClass();
304-
Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
305-
return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
306-
}
289+
// TODO: Conventions does not deal with async wrappers
290+
return ClassUtils.getShortNameAsProperty(returnValueType);
307291
}
308292

309-
private Mono<? extends Void> resolveAndRender(String viewName, Locale locale,
310-
Map<String, ?> model, ServerWebExchange exchange) {
293+
private Mono<Void> resolveAsyncAttributes(Map<String, Object> model) {
311294

312-
return Flux.fromIterable(getViewResolvers())
313-
.concatMap(resolver -> resolver.resolveViewName(viewName, locale))
314-
.switchIfEmpty(Mono.error(
315-
new IllegalStateException(
316-
"Could not resolve view with name '" + viewName + "'.")))
317-
.collectList()
318-
.then(views -> {
319-
views.addAll(getDefaultViews());
295+
List<String> names = new ArrayList<>();
296+
List<Mono<Object>> valueMonos = new ArrayList<>();
320297

321-
List<MediaType> producibleTypes = getProducibleMediaTypes(views);
322-
MediaType bestMediaType = selectMediaType(exchange, () -> producibleTypes);
298+
for (Map.Entry<String, ?> entry : model.entrySet()) {
299+
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(null, entry.getValue());
300+
if (adapter != null) {
301+
names.add(entry.getKey());
302+
valueMonos.add(adapter.toMono(entry.getValue()).defaultIfEmpty(NO_VALUE));
303+
}
304+
}
305+
306+
if (names.isEmpty()) {
307+
return Mono.empty();
308+
}
323309

324-
if (bestMediaType != null) {
325-
for (View view : views) {
326-
for (MediaType supported : view.getSupportedMediaTypes()) {
327-
if (supported.isCompatibleWith(bestMediaType)) {
328-
return view.render(model, bestMediaType, exchange);
329-
}
330-
}
310+
return Mono.when(valueMonos,
311+
values -> {
312+
for (int i=0; i < values.length; i++) {
313+
if (values[i] != NO_VALUE) {
314+
model.put(names.get(i), values[i]);
315+
}
316+
else {
317+
model.remove(names.get(i));
331318
}
332319
}
320+
return NO_VALUE;
321+
})
322+
.then();
323+
}
333324

334-
return Mono.error(new NotAcceptableStatusException(producibleTypes));
335-
});
325+
private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
326+
ServerWebExchange exchange) {
327+
328+
List<MediaType> mediaTypes = getMediaTypes(views);
329+
MediaType bestMediaType = selectMediaType(exchange, () -> mediaTypes);
330+
if (bestMediaType != null) {
331+
for (View view : views) {
332+
for (MediaType mediaType : view.getSupportedMediaTypes()) {
333+
if (mediaType.isCompatibleWith(bestMediaType)) {
334+
return view.render(model, mediaType, exchange);
335+
}
336+
}
337+
}
338+
}
339+
throw new NotAcceptableStatusException(mediaTypes);
336340
}
337341

338-
private List<MediaType> getProducibleMediaTypes(List<View> views) {
339-
List<MediaType> result = new ArrayList<>();
340-
views.forEach(view -> result.addAll(view.getSupportedMediaTypes()));
341-
return result;
342+
private List<MediaType> getMediaTypes(List<View> views) {
343+
return views.stream()
344+
.flatMap(view -> view.getSupportedMediaTypes().stream())
345+
.collect(Collectors.toList());
342346
}
343347

344348
}

0 commit comments

Comments
 (0)