Support List and Publisher<Fragment> return values

See gh-33162
This commit is contained in:
rstoyanchev 2024-07-10 08:17:34 +01:00
parent f2028d2339
commit 54e76c8105
3 changed files with 77 additions and 34 deletions

View File

@ -17,6 +17,7 @@
package org.springframework.web.reactive.result.view; package org.springframework.web.reactive.result.view;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@ -154,13 +155,21 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
return true; return true;
} }
Class<?> type = result.getReturnType().toClass(); ResolvableType returnType = result.getReturnType();
Class<?> type = returnType.toClass();
ReactiveAdapter adapter = getAdapter(result); ReactiveAdapter adapter = getAdapter(result);
if (adapter != null) { if (adapter != null) {
if (adapter.isNoValue()) { if (adapter.isNoValue()) {
return true; 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) || return (CharSequence.class.isAssignableFrom(type) ||
@ -169,9 +178,19 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
Model.class.isAssignableFrom(type) || Model.class.isAssignableFrom(type) ||
Map.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type) ||
View.class.isAssignableFrom(type) || View.class.isAssignableFrom(type) ||
isFragmentCollection(returnType.getNested(2)) ||
!BeanUtils.isSimpleProperty(type)); !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 @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) { public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
@ -181,14 +200,19 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
if (adapter != null) { if (adapter != null) {
if (adapter.isMultiValue()) { 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 ? valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) :
Mono.from(adapter.toPublisher(result.getReturnValue())) : Mono.empty()); result.getReturnType().getGeneric());
}
valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) :
result.getReturnType().getGeneric());
} }
else { else {
valueMono = Mono.justOrEmpty(result.getReturnValue()); valueMono = Mono.justOrEmpty(result.getReturnValue());
@ -210,6 +234,11 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
clazz = returnValue.getClass(); clazz = returnValue.getClass();
} }
if (Collection.class.isAssignableFrom(clazz)) {
returnValue = FragmentRendering.fromCollection((Collection<Fragment>) returnValue).build();
clazz = FragmentRendering.class;
}
if (returnValue == NO_VALUE || ClassUtils.isVoidType(clazz)) { if (returnValue == NO_VALUE || ClassUtils.isVoidType(clazz)) {
viewsMono = resolveViews(getDefaultViewName(exchange), locale); 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. * Select a default view name when a controller did not specify it.
* Use the request path the leading and trailing slash stripped. * Use the request path the leading and trailing slash stripped.

View File

@ -33,6 +33,7 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.HandlerResult; 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 org.springframework.web.testfixture.server.MockServerWebExchange;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; 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; 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 * @author Rossen Stoyanchev
*/ */
public class FragmentResolutionResultHandlerTests { public class FragmentViewResolutionResultHandlerTests {
static Stream<Arguments> arguments() { static Stream<Arguments> arguments() {
Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo")); Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo"));
Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar")); Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar"));
return Stream.of( return Stream.of(
Arguments.of(named("Flux", Arguments.of(
FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic())) FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic()))
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) .headers(headers -> headers.setContentType(MediaType.TEXT_HTML))
.build())), .build(),
Arguments.of(named("List", on(Handler.class).resolveReturnType(FragmentRendering.class)),
FragmentRendering.fromCollection(List.of(f1, f2)) Arguments.of(
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML)) FragmentRendering.fromCollection(List.of(f1, f2))
.build())) .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 @ParameterizedTest
@MethodSource("arguments") @MethodSource("arguments")
void render(FragmentRendering rendering) { void render(Object returnValue, MethodParameter parameter) {
Locale locale = Locale.ENGLISH; Locale locale = Locale.ENGLISH;
MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build(); MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build();
MockServerWebExchange exchange = MockServerWebExchange.from(request); MockServerWebExchange exchange = MockServerWebExchange.from(request);
HandlerResult result = new HandlerResult( HandlerResult result = new HandlerResult(new Handler(), returnValue, parameter, new BindingContext());
new Handler(), rendering, on(Handler.class).resolveReturnType(FragmentRendering.class),
new BindingContext());
String body = initHandler().handleResult(exchange, result) String body = initHandler().handleResult(exchange, result)
.then(Mono.defer(() -> exchange.getResponse().getBodyAsString())) .then(Mono.defer(() -> exchange.getResponse().getBodyAsString()))
@ -102,10 +107,15 @@ public class FragmentResolutionResultHandlerTests {
} }
@SuppressWarnings("unused")
private static class Handler { private static class Handler {
FragmentRendering rendering() { return null; } FragmentRendering rendering() { return null; }
Flux<Fragment> fragmentFlux() { return null; }
List<Fragment> fragmentList() { return null; }
} }

View File

@ -79,9 +79,14 @@ class ViewResolutionResultHandlerTests {
testSupports(on(Handler.class).resolveReturnType(Mono.class, String.class)); testSupports(on(Handler.class).resolveReturnType(Mono.class, String.class));
testSupports(on(Handler.class).resolveReturnType(Rendering.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(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(View.class));
testSupports(on(Handler.class).resolveReturnType(Mono.class, View.class)); testSupports(on(Handler.class).resolveReturnType(Mono.class, View.class));
@ -436,6 +441,9 @@ class ViewResolutionResultHandlerTests {
Mono<Rendering> monoRendering() { return null; } Mono<Rendering> monoRendering() { return null; }
FragmentRendering fragmentRendering() { return null; } FragmentRendering fragmentRendering() { return null; }
Flux<Fragment> fragmentFlux() { return null; }
Mono<List<Fragment>> monoFragmentList() { return null; }
List<Fragment> fragmentList() { return null; }
View view() { return null; } View view() { return null; }
Mono<View> monoView() { return null; } Mono<View> monoView() { return null; }