Support List and Publisher<Fragment> return values
See gh-33162
This commit is contained in:
parent
f2028d2339
commit
54e76c8105
|
@ -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.
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
Loading…
Reference in New Issue