From 54e76c81057593c8cb227108ffbc0f70a5d8271f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Wed, 10 Jul 2024 08:17:34 +0100 Subject: [PATCH] Support List and Publisher return values See gh-33162 --- .../view/ViewResolutionResultHandler.java | 49 ++++++++++++----- ...mentViewResolutionResultHandlerTests.java} | 52 +++++++++++-------- .../ViewResolutionResultHandlerTests.java | 10 +++- 3 files changed, 77 insertions(+), 34 deletions(-) rename spring-webflux/src/test/java/org/springframework/web/reactive/result/view/{FragmentResolutionResultHandlerTests.java => FragmentViewResolutionResultHandlerTests.java} (73%) diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index b3eb55e19d..51dc2baecb 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -17,6 +17,7 @@ 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; @@ -154,13 +155,21 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp return true; } - Class type = result.getReturnType().toClass(); + ResolvableType returnType = result.getReturnType(); + Class type = returnType.toClass(); + ReactiveAdapter adapter = getAdapter(result); if (adapter != null) { if (adapter.isNoValue()) { return true; } - type = result.getReturnType().getGeneric().toClass(); + + type = returnType.getGeneric().toClass(); + returnType = returnType.getNested(2); + + if (adapter.isMultiValue()) { + return Fragment.class.isAssignableFrom(type); + } } return (CharSequence.class.isAssignableFrom(type) || @@ -169,9 +178,19 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp Model.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) || View.class.isAssignableFrom(type) || + isFragmentCollection(returnType.getNested(2)) || !BeanUtils.isSimpleProperty(type)); } + private boolean hasModelAnnotation(MethodParameter parameter) { + return parameter.hasMethodAnnotation(ModelAttribute.class); + } + + private static boolean isFragmentCollection(ResolvableType returnType) { + Class clazz = returnType.resolve(Object.class); + return (Collection.class.isAssignableFrom(clazz) && Fragment.class.equals(returnType.getNested(2).resolve())); + } + @Override @SuppressWarnings("unchecked") public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { @@ -181,14 +200,19 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp if (adapter != null) { if (adapter.isMultiValue()) { - throw new IllegalArgumentException("Multi-value producer: " + result.getReturnType()); + valueMono = (result.getReturnValue() != null ? + Mono.just(FragmentRendering.fromPublisher(adapter.toPublisher(result.getReturnValue())).build()) : + Mono.empty()); + + valueType = ResolvableType.forClass(FragmentRendering.class); } + else { + valueMono = (result.getReturnValue() != null ? + Mono.from(adapter.toPublisher(result.getReturnValue())) : Mono.empty()); - valueMono = (result.getReturnValue() != null ? - Mono.from(adapter.toPublisher(result.getReturnValue())) : Mono.empty()); - - valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) : - result.getReturnType().getGeneric()); + valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) : + result.getReturnType().getGeneric()); + } } else { valueMono = Mono.justOrEmpty(result.getReturnValue()); @@ -210,6 +234,11 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp clazz = returnValue.getClass(); } + if (Collection.class.isAssignableFrom(clazz)) { + returnValue = FragmentRendering.fromCollection((Collection) returnValue).build(); + clazz = FragmentRendering.class; + } + if (returnValue == NO_VALUE || ClassUtils.isVoidType(clazz)) { viewsMono = resolveViews(getDefaultViewName(exchange), locale); } @@ -266,10 +295,6 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp }); } - private boolean hasModelAnnotation(MethodParameter parameter) { - return parameter.hasMethodAnnotation(ModelAttribute.class); - } - /** * Select a default view name when a controller did not specify it. * Use the request path the leading and trailing slash stripped. diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java similarity index 73% rename from spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentResolutionResultHandlerTests.java rename to spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java index 9159877bde..449b9f8b3e 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/FragmentViewResolutionResultHandlerTests.java @@ -33,6 +33,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.HandlerResult; @@ -44,42 +45,46 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.junit.jupiter.api.Named.named; import static org.springframework.web.testfixture.method.ResolvableMethod.on; /** - * Tests for multi-view rendering through {@link ViewResolutionResultHandler}. + * Tests for {@link Fragment} rendering through {@link ViewResolutionResultHandler}. * * @author Rossen Stoyanchev */ -public class FragmentResolutionResultHandlerTests { +public class FragmentViewResolutionResultHandlerTests { - static Stream arguments() { - Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo")); - Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar")); - return Stream.of( - Arguments.of(named("Flux", - FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic())) - .headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) - .build())), - Arguments.of(named("List", - FragmentRendering.fromCollection(List.of(f1, f2)) - .headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) - .build())) - );} + static Stream arguments() { + Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo")); + Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar")); + return Stream.of( + Arguments.of( + FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic())) + .headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) + .build(), + on(Handler.class).resolveReturnType(FragmentRendering.class)), + Arguments.of( + FragmentRendering.fromCollection(List.of(f1, f2)) + .headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) + .build(), + on(Handler.class).resolveReturnType(FragmentRendering.class)), + Arguments.of( + Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic()), + on(Handler.class).resolveReturnType(Flux.class, Fragment.class)), + Arguments.of( + List.of(f1, f2), + on(Handler.class).resolveReturnType(List.class, Fragment.class))); + } @ParameterizedTest @MethodSource("arguments") - void render(FragmentRendering rendering) { - + void render(Object returnValue, MethodParameter parameter) { Locale locale = Locale.ENGLISH; MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); - HandlerResult result = new HandlerResult( - new Handler(), rendering, on(Handler.class).resolveReturnType(FragmentRendering.class), - new BindingContext()); + HandlerResult result = new HandlerResult(new Handler(), returnValue, parameter, new BindingContext()); String body = initHandler().handleResult(exchange, result) .then(Mono.defer(() -> exchange.getResponse().getBodyAsString())) @@ -102,10 +107,15 @@ public class FragmentResolutionResultHandlerTests { } + @SuppressWarnings("unused") private static class Handler { FragmentRendering rendering() { return null; } + Flux fragmentFlux() { return null; } + + List fragmentList() { return null; } + } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java index 5353dcd3f6..bd8e529fb6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java @@ -79,9 +79,14 @@ class ViewResolutionResultHandlerTests { testSupports(on(Handler.class).resolveReturnType(Mono.class, String.class)); testSupports(on(Handler.class).resolveReturnType(Rendering.class)); - testSupports(on(Handler.class).resolveReturnType(FragmentRendering.class)); testSupports(on(Handler.class).resolveReturnType(Mono.class, Rendering.class)); + testSupports(on(Handler.class).resolveReturnType(FragmentRendering.class)); + testSupports(on(Handler.class).resolveReturnType(Flux.class, Fragment.class)); + testSupports(on(Handler.class).resolveReturnType(List.class, Fragment.class)); + testSupports(on(Handler.class).resolveReturnType( + Mono.class, ResolvableType.forClassWithGenerics(List.class, Fragment.class))); + testSupports(on(Handler.class).resolveReturnType(View.class)); testSupports(on(Handler.class).resolveReturnType(Mono.class, View.class)); @@ -436,6 +441,9 @@ class ViewResolutionResultHandlerTests { Mono monoRendering() { return null; } FragmentRendering fragmentRendering() { return null; } + Flux fragmentFlux() { return null; } + Mono> monoFragmentList() { return null; } + List fragmentList() { return null; } View view() { return null; } Mono monoView() { return null; }