|
16 | 16 |
|
17 | 17 | package org.springframework.web.reactive.result.view;
|
18 | 18 |
|
| 19 | +import java.nio.charset.Charset; |
| 20 | +import java.nio.charset.StandardCharsets; |
19 | 21 | import java.util.ArrayList;
|
20 | 22 | import java.util.Collection;
|
21 | 23 | import java.util.Collections;
|
|
38 | 40 | import org.springframework.core.ResolvableType;
|
39 | 41 | import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
40 | 42 | import org.springframework.core.io.buffer.DataBuffer;
|
| 43 | +import org.springframework.core.io.buffer.DataBufferFactory; |
41 | 44 | import org.springframework.http.HttpHeaders;
|
42 | 45 | import org.springframework.http.HttpStatusCode;
|
43 | 46 | import org.springframework.http.MediaType;
|
| 47 | +import org.springframework.http.server.reactive.ServerHttpRequest; |
44 | 48 | import org.springframework.http.server.reactive.ServerHttpResponse;
|
45 | 49 | import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
46 | 50 | import org.springframework.lang.Nullable;
|
@@ -96,6 +100,8 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp
|
96 | 100 |
|
97 | 101 | private final List<View> defaultViews = new ArrayList<>(4);
|
98 | 102 |
|
| 103 | + private final List<FragmentFormatter> fragmentFormatters = List.of(new SseFragmentFormatter()); |
| 104 | + |
99 | 105 |
|
100 | 106 | /**
|
101 | 107 | * Basic constructor with a default {@link ReactiveAdapterRegistry}.
|
@@ -337,8 +343,22 @@ private Mono<Flux<DataBuffer>> renderFragment(
|
337 | 343 | Mono.just(List.of(fragment.view())) :
|
338 | 344 | resolveViews(fragment.viewName() != null ? fragment.viewName() : getDefaultViewName(exchange), locale));
|
339 | 345 |
|
| 346 | + FragmentFormatter fragmentFormatter = getFragmentFormatter(exchange); |
| 347 | + |
340 | 348 | return selectedViews.flatMap(views -> render(views, fragment.model(), bindingContext, mutatedExchange))
|
341 |
| - .then(Mono.fromSupplier(response::getBodyFlux)); |
| 349 | + .then(Mono.fromSupplier(() -> (fragmentFormatter != null ? |
| 350 | + fragmentFormatter.format(response.getBodyFlux(), fragment, exchange) : |
| 351 | + response.getBodyFlux()))); |
| 352 | + } |
| 353 | + |
| 354 | + @Nullable |
| 355 | + private FragmentFormatter getFragmentFormatter(ServerWebExchange exchange) { |
| 356 | + for (FragmentFormatter formatter : this.fragmentFormatters) { |
| 357 | + if (formatter.supports(exchange.getRequest())) { |
| 358 | + return formatter; |
| 359 | + } |
| 360 | + } |
| 361 | + return null; |
342 | 362 | }
|
343 | 363 |
|
344 | 364 | private String getNameForReturnValue(MethodParameter returnType) {
|
@@ -436,4 +456,71 @@ public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends Data
|
436 | 456 | }
|
437 | 457 | }
|
438 | 458 |
|
| 459 | + |
| 460 | + /** |
| 461 | + * Strategy to render fragment with stream formatting. |
| 462 | + */ |
| 463 | + private interface FragmentFormatter { |
| 464 | + |
| 465 | + /** |
| 466 | + * Whether the formatter supports the given request. |
| 467 | + */ |
| 468 | + boolean supports(ServerHttpRequest request); |
| 469 | + |
| 470 | + /** |
| 471 | + * Format the given fragment. |
| 472 | + * @param fragmentBuffers the fragment serialized to data buffers |
| 473 | + * @param fragment the fragment being rendered |
| 474 | + * @param exchange the current exchange |
| 475 | + * @return the formatted fragment |
| 476 | + */ |
| 477 | + Flux<DataBuffer> format(Flux<DataBuffer> fragmentBuffers, Fragment fragment, ServerWebExchange exchange); |
| 478 | + |
| 479 | + } |
| 480 | + |
| 481 | + |
| 482 | + /** |
| 483 | + * Formatter for Server-Sent Events formatting. |
| 484 | + */ |
| 485 | + private static class SseFragmentFormatter implements FragmentFormatter { |
| 486 | + |
| 487 | + @Override |
| 488 | + public boolean supports(ServerHttpRequest request) { |
| 489 | + String header = request.getHeaders().getFirst(HttpHeaders.ACCEPT); |
| 490 | + return (header != null && header.contains(MediaType.TEXT_EVENT_STREAM_VALUE)); |
| 491 | + } |
| 492 | + |
| 493 | + @Override |
| 494 | + public Flux<DataBuffer> format( |
| 495 | + Flux<DataBuffer> fragmentBuffers, Fragment fragment, ServerWebExchange exchange) { |
| 496 | + |
| 497 | + Charset charset = getCharset(exchange.getRequest()); |
| 498 | + DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); |
| 499 | + |
| 500 | + String eventLine = fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : ""; |
| 501 | + |
| 502 | + return Flux.concat( |
| 503 | + Flux.just(encodeText(eventLine + "data:", charset, bufferFactory)), |
| 504 | + fragmentBuffers, |
| 505 | + Flux.just(encodeText("\n\n", charset, bufferFactory))); |
| 506 | + } |
| 507 | + |
| 508 | + private Charset getCharset(ServerHttpRequest request) { |
| 509 | + for (MediaType mediaType : request.getHeaders().getAccept()) { |
| 510 | + if (mediaType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM)) { |
| 511 | + if (mediaType.getCharset() != null) { |
| 512 | + return mediaType.getCharset(); |
| 513 | + } |
| 514 | + break; |
| 515 | + } |
| 516 | + } |
| 517 | + return StandardCharsets.UTF_8; |
| 518 | + } |
| 519 | + |
| 520 | + private DataBuffer encodeText(String text, Charset charset, DataBufferFactory bufferFactory) { |
| 521 | + byte[] bytes = text.getBytes(charset); |
| 522 | + return bufferFactory.wrap(bytes); |
| 523 | + } |
| 524 | + } |
| 525 | + |
439 | 526 | }
|
0 commit comments