Skip to content

Commit 54e76c8

Browse files
committed
Support List and Publisher<Fragment> return values
See gh-33162
1 parent f2028d2 commit 54e76c8

File tree

3 files changed

+78
-35
lines changed

3 files changed

+78
-35
lines changed

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

+37-12
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.reactive.result.view;
1818

1919
import java.util.ArrayList;
20+
import java.util.Collection;
2021
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.Locale;
@@ -154,13 +155,21 @@ public boolean supports(HandlerResult result) {
154155
return true;
155156
}
156157

157-
Class<?> type = result.getReturnType().toClass();
158+
ResolvableType returnType = result.getReturnType();
159+
Class<?> type = returnType.toClass();
160+
158161
ReactiveAdapter adapter = getAdapter(result);
159162
if (adapter != null) {
160163
if (adapter.isNoValue()) {
161164
return true;
162165
}
163-
type = result.getReturnType().getGeneric().toClass();
166+
167+
type = returnType.getGeneric().toClass();
168+
returnType = returnType.getNested(2);
169+
170+
if (adapter.isMultiValue()) {
171+
return Fragment.class.isAssignableFrom(type);
172+
}
164173
}
165174

166175
return (CharSequence.class.isAssignableFrom(type) ||
@@ -169,9 +178,19 @@ public boolean supports(HandlerResult result) {
169178
Model.class.isAssignableFrom(type) ||
170179
Map.class.isAssignableFrom(type) ||
171180
View.class.isAssignableFrom(type) ||
181+
isFragmentCollection(returnType.getNested(2)) ||
172182
!BeanUtils.isSimpleProperty(type));
173183
}
174184

185+
private boolean hasModelAnnotation(MethodParameter parameter) {
186+
return parameter.hasMethodAnnotation(ModelAttribute.class);
187+
}
188+
189+
private static boolean isFragmentCollection(ResolvableType returnType) {
190+
Class<?> clazz = returnType.resolve(Object.class);
191+
return (Collection.class.isAssignableFrom(clazz) && Fragment.class.equals(returnType.getNested(2).resolve()));
192+
}
193+
175194
@Override
176195
@SuppressWarnings("unchecked")
177196
public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result) {
@@ -181,14 +200,19 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
181200

182201
if (adapter != null) {
183202
if (adapter.isMultiValue()) {
184-
throw new IllegalArgumentException("Multi-value producer: " + result.getReturnType());
185-
}
203+
valueMono = (result.getReturnValue() != null ?
204+
Mono.just(FragmentRendering.fromPublisher(adapter.toPublisher(result.getReturnValue())).build()) :
205+
Mono.empty());
186206

187-
valueMono = (result.getReturnValue() != null ?
188-
Mono.from(adapter.toPublisher(result.getReturnValue())) : Mono.empty());
207+
valueType = ResolvableType.forClass(FragmentRendering.class);
208+
}
209+
else {
210+
valueMono = (result.getReturnValue() != null ?
211+
Mono.from(adapter.toPublisher(result.getReturnValue())) : Mono.empty());
189212

190-
valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) :
191-
result.getReturnType().getGeneric());
213+
valueType = (adapter.isNoValue() ? ResolvableType.forClass(Void.class) :
214+
result.getReturnType().getGeneric());
215+
}
192216
}
193217
else {
194218
valueMono = Mono.justOrEmpty(result.getReturnValue());
@@ -210,6 +234,11 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
210234
clazz = returnValue.getClass();
211235
}
212236

237+
if (Collection.class.isAssignableFrom(clazz)) {
238+
returnValue = FragmentRendering.fromCollection((Collection<Fragment>) returnValue).build();
239+
clazz = FragmentRendering.class;
240+
}
241+
213242
if (returnValue == NO_VALUE || ClassUtils.isVoidType(clazz)) {
214243
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
215244
}
@@ -266,10 +295,6 @@ else if (View.class.isAssignableFrom(clazz)) {
266295
});
267296
}
268297

269-
private boolean hasModelAnnotation(MethodParameter parameter) {
270-
return parameter.hasMethodAnnotation(ModelAttribute.class);
271-
}
272-
273298
/**
274299
* Select a default view name when a controller did not specify it.
275300
* Use the request path the leading and trailing slash stripped.
+32-22
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.context.annotation.Bean;
3434
import org.springframework.context.annotation.Configuration;
3535
import org.springframework.context.support.ResourceBundleMessageSource;
36+
import org.springframework.core.MethodParameter;
3637
import org.springframework.http.MediaType;
3738
import org.springframework.web.reactive.BindingContext;
3839
import org.springframework.web.reactive.HandlerResult;
@@ -44,42 +45,46 @@
4445
import org.springframework.web.testfixture.server.MockServerWebExchange;
4546

4647
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
47-
import static org.junit.jupiter.api.Named.named;
4848
import static org.springframework.web.testfixture.method.ResolvableMethod.on;
4949

5050
/**
51-
* Tests for multi-view rendering through {@link ViewResolutionResultHandler}.
51+
* Tests for {@link Fragment} rendering through {@link ViewResolutionResultHandler}.
5252
*
5353
* @author Rossen Stoyanchev
5454
*/
55-
public class FragmentResolutionResultHandlerTests {
56-
57-
static Stream<Arguments> arguments() {
58-
Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo"));
59-
Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar"));
60-
return Stream.of(
61-
Arguments.of(named("Flux",
62-
FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic()))
63-
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML))
64-
.build())),
65-
Arguments.of(named("List",
66-
FragmentRendering.fromCollection(List.of(f1, f2))
67-
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML))
68-
.build()))
69-
);}
55+
public class FragmentViewResolutionResultHandlerTests {
56+
57+
static Stream<Arguments> arguments() {
58+
Fragment f1 = Fragment.create("fragment1", Map.of("foo", "Foo"));
59+
Fragment f2 = Fragment.create("fragment2", Map.of("bar", "Bar"));
60+
return Stream.of(
61+
Arguments.of(
62+
FragmentRendering.fromPublisher(Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic()))
63+
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML))
64+
.build(),
65+
on(Handler.class).resolveReturnType(FragmentRendering.class)),
66+
Arguments.of(
67+
FragmentRendering.fromCollection(List.of(f1, f2))
68+
.headers(headers -> headers.setContentType(MediaType.TEXT_HTML))
69+
.build(),
70+
on(Handler.class).resolveReturnType(FragmentRendering.class)),
71+
Arguments.of(
72+
Flux.just(f1, f2).subscribeOn(Schedulers.boundedElastic()),
73+
on(Handler.class).resolveReturnType(Flux.class, Fragment.class)),
74+
Arguments.of(
75+
List.of(f1, f2),
76+
on(Handler.class).resolveReturnType(List.class, Fragment.class)));
77+
}
7078

7179

7280
@ParameterizedTest
7381
@MethodSource("arguments")
74-
void render(FragmentRendering rendering) {
75-
82+
void render(Object returnValue, MethodParameter parameter) {
7683
Locale locale = Locale.ENGLISH;
7784
MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(locale).build();
7885
MockServerWebExchange exchange = MockServerWebExchange.from(request);
7986

80-
HandlerResult result = new HandlerResult(
81-
new Handler(), rendering, on(Handler.class).resolveReturnType(FragmentRendering.class),
82-
new BindingContext());
87+
HandlerResult result = new HandlerResult(new Handler(), returnValue, parameter, new BindingContext());
8388

8489
String body = initHandler().handleResult(exchange, result)
8590
.then(Mono.defer(() -> exchange.getResponse().getBodyAsString()))
@@ -102,10 +107,15 @@ private ViewResolutionResultHandler initHandler() {
102107
}
103108

104109

110+
@SuppressWarnings("unused")
105111
private static class Handler {
106112

107113
FragmentRendering rendering() { return null; }
108114

115+
Flux<Fragment> fragmentFlux() { return null; }
116+
117+
List<Fragment> fragmentList() { return null; }
118+
109119
}
110120

111121

spring-webflux/src/test/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandlerTests.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,14 @@ void supports() {
7979
testSupports(on(Handler.class).resolveReturnType(Mono.class, String.class));
8080

8181
testSupports(on(Handler.class).resolveReturnType(Rendering.class));
82-
testSupports(on(Handler.class).resolveReturnType(FragmentRendering.class));
8382
testSupports(on(Handler.class).resolveReturnType(Mono.class, Rendering.class));
8483

84+
testSupports(on(Handler.class).resolveReturnType(FragmentRendering.class));
85+
testSupports(on(Handler.class).resolveReturnType(Flux.class, Fragment.class));
86+
testSupports(on(Handler.class).resolveReturnType(List.class, Fragment.class));
87+
testSupports(on(Handler.class).resolveReturnType(
88+
Mono.class, ResolvableType.forClassWithGenerics(List.class, Fragment.class)));
89+
8590
testSupports(on(Handler.class).resolveReturnType(View.class));
8691
testSupports(on(Handler.class).resolveReturnType(Mono.class, View.class));
8792

@@ -436,6 +441,9 @@ private static class Handler {
436441
Mono<Rendering> monoRendering() { return null; }
437442

438443
FragmentRendering fragmentRendering() { return null; }
444+
Flux<Fragment> fragmentFlux() { return null; }
445+
Mono<List<Fragment>> monoFragmentList() { return null; }
446+
List<Fragment> fragmentList() { return null; }
439447

440448
View view() { return null; }
441449
Mono<View> monoView() { return null; }

0 commit comments

Comments
 (0)