45
45
import org .springframework .http .HttpHeaders ;
46
46
import org .springframework .http .HttpStatusCode ;
47
47
import org .springframework .http .MediaType ;
48
+ import org .springframework .http .codec .ServerSentEvent ;
48
49
import org .springframework .http .server .reactive .ServerHttpRequest ;
49
50
import org .springframework .http .server .reactive .ServerHttpResponse ;
50
51
import org .springframework .http .server .reactive .ServerHttpResponseDecorator ;
@@ -101,7 +102,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
101
102
102
103
private final List <View > defaultViews = new ArrayList <>(4 );
103
104
104
- private final List < StreamHandler > streamHandlers = List . of ( new SseStreamHandler () );
105
+ private final SseStreamHandler sseHandler = new SseStreamHandler ();
105
106
106
107
107
108
/**
@@ -175,7 +176,7 @@ public boolean supports(HandlerResult result) {
175
176
returnType = returnType .getNested (2 );
176
177
177
178
if (adapter .isMultiValue ()) {
178
- return Fragment .class .isAssignableFrom (type );
179
+ return ( Fragment .class .isAssignableFrom (type ) || isSseFragmentStream ( returnType ) );
179
180
}
180
181
}
181
182
@@ -194,8 +195,13 @@ private boolean hasModelAnnotation(MethodParameter parameter) {
194
195
}
195
196
196
197
private static boolean isFragmentCollection (ResolvableType returnType ) {
197
- Class <?> clazz = returnType .resolve (Object .class );
198
- return (Collection .class .isAssignableFrom (clazz ) && Fragment .class .equals (returnType .getNested (2 ).resolve ()));
198
+ return (Collection .class .isAssignableFrom (returnType .resolve (Object .class )) &&
199
+ Fragment .class .equals (returnType .getNested (2 ).resolve ()));
200
+ }
201
+
202
+ private static boolean isSseFragmentStream (ResolvableType returnType ) {
203
+ return (ServerSentEvent .class .equals (returnType .resolve ()) &&
204
+ Fragment .class .equals (returnType .getNested (2 ).resolve ()));
199
205
}
200
206
201
207
@ Override
@@ -204,9 +210,15 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
204
210
Mono <Object > valueMono ;
205
211
ResolvableType valueType ;
206
212
ReactiveAdapter adapter = getAdapter (result );
213
+ BindingContext bindingContext = result .getBindingContext ();
214
+ Locale locale = LocaleContextHolder .getLocale (exchange .getLocaleContext ());
207
215
208
216
if (adapter != null ) {
209
217
if (adapter .isMultiValue ()) {
218
+ if (isSseFragmentStream (result .getReturnType ().getNested (2 ))) {
219
+ return handleSseFragmentStream (exchange , result , adapter , locale , bindingContext );
220
+ }
221
+
210
222
valueMono = (result .getReturnValue () != null ?
211
223
Mono .just (FragmentsRendering .fragmentsPublisher (adapter .toPublisher (result .getReturnValue ())).build ()) :
212
224
Mono .empty ());
@@ -233,8 +245,6 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
233
245
Mono <List <View >> viewsMono ;
234
246
Model model = result .getModel ();
235
247
MethodParameter parameter = result .getReturnTypeSource ();
236
- BindingContext bindingContext = result .getBindingContext ();
237
- Locale locale = LocaleContextHolder .getLocale (exchange .getLocaleContext ());
238
248
239
249
Class <?> clazz = valueType .toClass ();
240
250
if (clazz == Object .class ) {
@@ -277,13 +287,15 @@ else if (FragmentsRendering.class.isAssignableFrom(clazz)) {
277
287
response .getHeaders ().putAll (render .headers ());
278
288
bindingContext .updateModel (exchange );
279
289
280
- StreamHandler streamHandler = getStreamHandler (exchange );
290
+ StreamHandler streamHandler =
291
+ (this .sseHandler .supports (exchange .getRequest ()) ? this .sseHandler : null );
292
+
281
293
if (streamHandler != null ) {
282
294
streamHandler .updateResponse (exchange );
283
295
}
284
296
285
297
Flux <Flux <DataBuffer >> renderFlux = render .fragments ()
286
- .concatMap (fragment -> renderFragment (fragment , streamHandler , locale , bindingContext , exchange ))
298
+ .concatMap (fragment -> renderFragment (fragment , null , streamHandler , locale , bindingContext , exchange ))
287
299
.doOnDiscard (DataBuffer .class , DataBufferUtils ::release );
288
300
289
301
return response .writeAndFlushWith (renderFlux );
@@ -338,9 +350,29 @@ private Mono<List<View>> resolveViews(String viewName, Locale locale) {
338
350
});
339
351
}
340
352
353
+ private Mono <Void > handleSseFragmentStream (
354
+ ServerWebExchange exchange , HandlerResult result , ReactiveAdapter adapter , Locale locale ,
355
+ BindingContext bindingContext ) {
356
+
357
+ this .sseHandler .updateResponse (exchange );
358
+
359
+ Flux <ServerSentEvent <Fragment >> eventFlux =
360
+ Flux .from (adapter .toPublisher (result .getReturnValue ()));
361
+
362
+ Flux <Flux <DataBuffer >> dataBufferFlux = eventFlux
363
+ .concatMap (event -> renderFragment (event .data (), event , this .sseHandler , locale , bindingContext , exchange ))
364
+ .doOnDiscard (DataBuffer .class , DataBufferUtils ::release );
365
+
366
+ return exchange .getResponse ().writeAndFlushWith (dataBufferFlux );
367
+ }
368
+
341
369
private Mono <Flux <DataBuffer >> renderFragment (
342
- Fragment fragment , @ Nullable StreamHandler streamHandler , Locale locale ,
343
- BindingContext bindingContext , ServerWebExchange exchange ) {
370
+ @ Nullable Fragment fragment , @ Nullable Object streamingHints , @ Nullable StreamHandler streamHandler ,
371
+ Locale locale , BindingContext bindingContext , ServerWebExchange exchange ) {
372
+
373
+ if (fragment == null ) {
374
+ return Mono .empty ();
375
+ }
344
376
345
377
// Merge attributes from top-level model
346
378
fragment .mergeAttributes (bindingContext .getModel ());
@@ -355,25 +387,18 @@ private Mono<Flux<DataBuffer>> renderFragment(
355
387
Map <String , Object > model = fragment .model ();
356
388
357
389
if (streamHandler != null ) {
358
- return selectedViews .flatMap (views -> render (views , model , MediaType .TEXT_HTML , bindingContext , mutatedExchange ))
359
- .then (Mono .fromSupplier (() -> streamHandler .format (response .getBodyFlux (), fragment , exchange )));
390
+ return selectedViews
391
+ .flatMap (views ->
392
+ render (views , model , MediaType .TEXT_HTML , bindingContext , mutatedExchange ))
393
+ .then (Mono .fromSupplier (() -> streamHandler .format (
394
+ response .getBodyFlux (), fragment , streamingHints , exchange )));
360
395
}
361
396
else {
362
397
return selectedViews .flatMap (views -> render (views , model , null , bindingContext , mutatedExchange ))
363
398
.then (Mono .fromSupplier (response ::getBodyFlux ));
364
399
}
365
400
}
366
401
367
- @ Nullable
368
- private StreamHandler getStreamHandler (ServerWebExchange exchange ) {
369
- for (StreamHandler handler : this .streamHandlers ) {
370
- if (handler .supports (exchange .getRequest ())) {
371
- return handler ;
372
- }
373
- }
374
- return null ;
375
- }
376
-
377
402
private String getNameForReturnValue (MethodParameter returnType ) {
378
403
return Optional .ofNullable (returnType .getMethodAnnotation (ModelAttribute .class ))
379
404
.filter (ann -> StringUtils .hasText (ann .value ()))
@@ -499,10 +524,13 @@ private interface StreamHandler {
499
524
* Format the given fragment.
500
525
* @param fragmentContent the fragment serialized to data buffers
501
526
* @param fragment the fragment being rendered
527
+ * @param streamingHints extra hints for the stream format (e.g. ServerSentEvent wrapper)
502
528
* @param exchange the current exchange
503
529
* @return the formatted fragment
504
530
*/
505
- Flux <DataBuffer > format (Flux <DataBuffer > fragmentContent , Fragment fragment , ServerWebExchange exchange );
531
+ Flux <DataBuffer > format (
532
+ Flux <DataBuffer > fragmentContent , Fragment fragment , @ Nullable Object streamingHints ,
533
+ ServerWebExchange exchange );
506
534
}
507
535
508
536
@@ -540,16 +568,21 @@ private Charset getCharset(ServerHttpRequest request) {
540
568
541
569
@ Override
542
570
public Flux <DataBuffer > format (
543
- Flux <DataBuffer > fragmentFlux , Fragment fragment , ServerWebExchange exchange ) {
571
+ Flux <DataBuffer > fragmentFlux , Fragment fragment , @ Nullable Object hints ,
572
+ ServerWebExchange exchange ) {
544
573
545
574
MediaType mediaType = exchange .getResponse ().getHeaders ().getContentType ();
546
575
Charset charset = (mediaType != null && mediaType .getCharset () != null ?
547
576
mediaType .getCharset () : StandardCharsets .UTF_8 );
577
+ Assert .state (hints == null || hints instanceof ServerSentEvent , "Expected ServerSentEvent" );
548
578
549
579
DataBufferFactory bufferFactory = exchange .getResponse ().bufferFactory ();
550
580
551
- String eventLine = (fragment .viewName () != null ? "event:" + fragment .viewName () + "\n " : "" );
552
- DataBuffer prefix = encodeText (eventLine + "data:" , charset , bufferFactory );
581
+ ServerSentEvent <?> sse = (ServerSentEvent <?>) hints ;
582
+ CharSequence eventText = (sse != null ? sse .format () :
583
+ (fragment .viewName () != null ? "event:" + fragment .viewName () + "\n " : "" ) + "data:" );
584
+
585
+ DataBuffer prefix = encodeText (eventText .toString (), charset , bufferFactory );
553
586
DataBuffer suffix = encodeText ("\n \n " , charset , bufferFactory );
554
587
555
588
Mono <DataBuffer > content = DataBufferUtils .join (fragmentFlux )
0 commit comments