|
|
|
@ -45,6 +45,7 @@ import org.springframework.core.io.buffer.DataBufferUtils; |
|
|
|
import org.springframework.http.HttpHeaders; |
|
|
|
import org.springframework.http.HttpHeaders; |
|
|
|
import org.springframework.http.HttpStatusCode; |
|
|
|
import org.springframework.http.HttpStatusCode; |
|
|
|
import org.springframework.http.MediaType; |
|
|
|
import org.springframework.http.MediaType; |
|
|
|
|
|
|
|
import org.springframework.http.codec.ServerSentEvent; |
|
|
|
import org.springframework.http.server.reactive.ServerHttpRequest; |
|
|
|
import org.springframework.http.server.reactive.ServerHttpRequest; |
|
|
|
import org.springframework.http.server.reactive.ServerHttpResponse; |
|
|
|
import org.springframework.http.server.reactive.ServerHttpResponse; |
|
|
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator; |
|
|
|
import org.springframework.http.server.reactive.ServerHttpResponseDecorator; |
|
|
|
@ -101,7 +102,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
|
|
|
|
|
|
|
|
private final List<View> defaultViews = new ArrayList<>(4); |
|
|
|
private final List<View> defaultViews = new ArrayList<>(4); |
|
|
|
|
|
|
|
|
|
|
|
private final List<StreamHandler> streamHandlers = List.of(new SseStreamHandler()); |
|
|
|
private final SseStreamHandler sseHandler = new SseStreamHandler(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
@ -175,7 +176,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
returnType = returnType.getNested(2); |
|
|
|
returnType = returnType.getNested(2); |
|
|
|
|
|
|
|
|
|
|
|
if (adapter.isMultiValue()) { |
|
|
|
if (adapter.isMultiValue()) { |
|
|
|
return Fragment.class.isAssignableFrom(type); |
|
|
|
return (Fragment.class.isAssignableFrom(type) || isSseFragmentStream(returnType)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -194,8 +195,13 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private static boolean isFragmentCollection(ResolvableType returnType) { |
|
|
|
private static boolean isFragmentCollection(ResolvableType returnType) { |
|
|
|
Class<?> clazz = returnType.resolve(Object.class); |
|
|
|
return (Collection.class.isAssignableFrom(returnType.resolve(Object.class)) && |
|
|
|
return (Collection.class.isAssignableFrom(clazz) && Fragment.class.equals(returnType.getNested(2).resolve())); |
|
|
|
Fragment.class.equals(returnType.getNested(2).resolve())); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static boolean isSseFragmentStream(ResolvableType returnType) { |
|
|
|
|
|
|
|
return (ServerSentEvent.class.equals(returnType.resolve()) && |
|
|
|
|
|
|
|
Fragment.class.equals(returnType.getNested(2).resolve())); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
|
@ -204,9 +210,15 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
Mono<Object> valueMono; |
|
|
|
Mono<Object> valueMono; |
|
|
|
ResolvableType valueType; |
|
|
|
ResolvableType valueType; |
|
|
|
ReactiveAdapter adapter = getAdapter(result); |
|
|
|
ReactiveAdapter adapter = getAdapter(result); |
|
|
|
|
|
|
|
BindingContext bindingContext = result.getBindingContext(); |
|
|
|
|
|
|
|
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); |
|
|
|
|
|
|
|
|
|
|
|
if (adapter != null) { |
|
|
|
if (adapter != null) { |
|
|
|
if (adapter.isMultiValue()) { |
|
|
|
if (adapter.isMultiValue()) { |
|
|
|
|
|
|
|
if (isSseFragmentStream(result.getReturnType().getNested(2))) { |
|
|
|
|
|
|
|
return handleSseFragmentStream(exchange, result, adapter, locale, bindingContext); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
valueMono = (result.getReturnValue() != null ? |
|
|
|
valueMono = (result.getReturnValue() != null ? |
|
|
|
Mono.just(FragmentsRendering.fragmentsPublisher(adapter.toPublisher(result.getReturnValue())).build()) : |
|
|
|
Mono.just(FragmentsRendering.fragmentsPublisher(adapter.toPublisher(result.getReturnValue())).build()) : |
|
|
|
Mono.empty()); |
|
|
|
Mono.empty()); |
|
|
|
@ -233,8 +245,6 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
Mono<List<View>> viewsMono; |
|
|
|
Mono<List<View>> viewsMono; |
|
|
|
Model model = result.getModel(); |
|
|
|
Model model = result.getModel(); |
|
|
|
MethodParameter parameter = result.getReturnTypeSource(); |
|
|
|
MethodParameter parameter = result.getReturnTypeSource(); |
|
|
|
BindingContext bindingContext = result.getBindingContext(); |
|
|
|
|
|
|
|
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Class<?> clazz = valueType.toClass(); |
|
|
|
Class<?> clazz = valueType.toClass(); |
|
|
|
if (clazz == Object.class) { |
|
|
|
if (clazz == Object.class) { |
|
|
|
@ -277,13 +287,15 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
response.getHeaders().putAll(render.headers()); |
|
|
|
response.getHeaders().putAll(render.headers()); |
|
|
|
bindingContext.updateModel(exchange); |
|
|
|
bindingContext.updateModel(exchange); |
|
|
|
|
|
|
|
|
|
|
|
StreamHandler streamHandler = getStreamHandler(exchange); |
|
|
|
StreamHandler streamHandler = |
|
|
|
|
|
|
|
(this.sseHandler.supports(exchange.getRequest()) ? this.sseHandler : null); |
|
|
|
|
|
|
|
|
|
|
|
if (streamHandler != null) { |
|
|
|
if (streamHandler != null) { |
|
|
|
streamHandler.updateResponse(exchange); |
|
|
|
streamHandler.updateResponse(exchange); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Flux<Flux<DataBuffer>> renderFlux = render.fragments() |
|
|
|
Flux<Flux<DataBuffer>> renderFlux = render.fragments() |
|
|
|
.concatMap(fragment -> renderFragment(fragment, streamHandler, locale, bindingContext, exchange)) |
|
|
|
.concatMap(fragment -> renderFragment(fragment, null, streamHandler, locale, bindingContext, exchange)) |
|
|
|
.doOnDiscard(DataBuffer.class, DataBufferUtils::release); |
|
|
|
.doOnDiscard(DataBuffer.class, DataBufferUtils::release); |
|
|
|
|
|
|
|
|
|
|
|
return response.writeAndFlushWith(renderFlux); |
|
|
|
return response.writeAndFlushWith(renderFlux); |
|
|
|
@ -338,9 +350,29 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private Mono<Void> handleSseFragmentStream( |
|
|
|
|
|
|
|
ServerWebExchange exchange, HandlerResult result, ReactiveAdapter adapter, Locale locale, |
|
|
|
|
|
|
|
BindingContext bindingContext) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.sseHandler.updateResponse(exchange); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Flux<ServerSentEvent<Fragment>> eventFlux = |
|
|
|
|
|
|
|
Flux.from(adapter.toPublisher(result.getReturnValue())); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Flux<Flux<DataBuffer>> dataBufferFlux = eventFlux |
|
|
|
|
|
|
|
.concatMap(event -> renderFragment(event.data(), event, this.sseHandler, locale, bindingContext, exchange)) |
|
|
|
|
|
|
|
.doOnDiscard(DataBuffer.class, DataBufferUtils::release); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return exchange.getResponse().writeAndFlushWith(dataBufferFlux); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
private Mono<Flux<DataBuffer>> renderFragment( |
|
|
|
private Mono<Flux<DataBuffer>> renderFragment( |
|
|
|
Fragment fragment, @Nullable StreamHandler streamHandler, Locale locale, |
|
|
|
@Nullable Fragment fragment, @Nullable Object streamingHints, @Nullable StreamHandler streamHandler, |
|
|
|
BindingContext bindingContext, ServerWebExchange exchange) { |
|
|
|
Locale locale, BindingContext bindingContext, ServerWebExchange exchange) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (fragment == null) { |
|
|
|
|
|
|
|
return Mono.empty(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Merge attributes from top-level model
|
|
|
|
// Merge attributes from top-level model
|
|
|
|
fragment.mergeAttributes(bindingContext.getModel()); |
|
|
|
fragment.mergeAttributes(bindingContext.getModel()); |
|
|
|
@ -355,8 +387,11 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
Map<String, Object> model = fragment.model(); |
|
|
|
Map<String, Object> model = fragment.model(); |
|
|
|
|
|
|
|
|
|
|
|
if (streamHandler != null) { |
|
|
|
if (streamHandler != null) { |
|
|
|
return selectedViews.flatMap(views -> render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange)) |
|
|
|
return selectedViews |
|
|
|
.then(Mono.fromSupplier(() -> streamHandler.format(response.getBodyFlux(), fragment, exchange))); |
|
|
|
.flatMap(views -> |
|
|
|
|
|
|
|
render(views, model, MediaType.TEXT_HTML, bindingContext, mutatedExchange)) |
|
|
|
|
|
|
|
.then(Mono.fromSupplier(() -> streamHandler.format( |
|
|
|
|
|
|
|
response.getBodyFlux(), fragment, streamingHints, exchange))); |
|
|
|
} |
|
|
|
} |
|
|
|
else { |
|
|
|
else { |
|
|
|
return selectedViews.flatMap(views -> render(views, model, null, bindingContext, mutatedExchange)) |
|
|
|
return selectedViews.flatMap(views -> render(views, model, null, bindingContext, mutatedExchange)) |
|
|
|
@ -364,16 +399,6 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@Nullable |
|
|
|
|
|
|
|
private StreamHandler getStreamHandler(ServerWebExchange exchange) { |
|
|
|
|
|
|
|
for (StreamHandler handler : this.streamHandlers) { |
|
|
|
|
|
|
|
if (handler.supports(exchange.getRequest())) { |
|
|
|
|
|
|
|
return handler; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return null; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private String getNameForReturnValue(MethodParameter returnType) { |
|
|
|
private String getNameForReturnValue(MethodParameter returnType) { |
|
|
|
return Optional.ofNullable(returnType.getMethodAnnotation(ModelAttribute.class)) |
|
|
|
return Optional.ofNullable(returnType.getMethodAnnotation(ModelAttribute.class)) |
|
|
|
.filter(ann -> StringUtils.hasText(ann.value())) |
|
|
|
.filter(ann -> StringUtils.hasText(ann.value())) |
|
|
|
@ -499,10 +524,13 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
* Format the given fragment. |
|
|
|
* Format the given fragment. |
|
|
|
* @param fragmentContent the fragment serialized to data buffers |
|
|
|
* @param fragmentContent the fragment serialized to data buffers |
|
|
|
* @param fragment the fragment being rendered |
|
|
|
* @param fragment the fragment being rendered |
|
|
|
|
|
|
|
* @param streamingHints extra hints for the stream format (e.g. ServerSentEvent wrapper) |
|
|
|
* @param exchange the current exchange |
|
|
|
* @param exchange the current exchange |
|
|
|
* @return the formatted fragment |
|
|
|
* @return the formatted fragment |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
Flux<DataBuffer> format(Flux<DataBuffer> fragmentContent, Fragment fragment, ServerWebExchange exchange); |
|
|
|
Flux<DataBuffer> format( |
|
|
|
|
|
|
|
Flux<DataBuffer> fragmentContent, Fragment fragment, @Nullable Object streamingHints, |
|
|
|
|
|
|
|
ServerWebExchange exchange); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -540,16 +568,21 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp |
|
|
|
|
|
|
|
|
|
|
|
@Override |
|
|
|
@Override |
|
|
|
public Flux<DataBuffer> format( |
|
|
|
public Flux<DataBuffer> format( |
|
|
|
Flux<DataBuffer> fragmentFlux, Fragment fragment, ServerWebExchange exchange) { |
|
|
|
Flux<DataBuffer> fragmentFlux, Fragment fragment, @Nullable Object hints, |
|
|
|
|
|
|
|
ServerWebExchange exchange) { |
|
|
|
|
|
|
|
|
|
|
|
MediaType mediaType = exchange.getResponse().getHeaders().getContentType(); |
|
|
|
MediaType mediaType = exchange.getResponse().getHeaders().getContentType(); |
|
|
|
Charset charset = (mediaType != null && mediaType.getCharset() != null ? |
|
|
|
Charset charset = (mediaType != null && mediaType.getCharset() != null ? |
|
|
|
mediaType.getCharset() : StandardCharsets.UTF_8); |
|
|
|
mediaType.getCharset() : StandardCharsets.UTF_8); |
|
|
|
|
|
|
|
Assert.state(hints == null || hints instanceof ServerSentEvent, "Expected ServerSentEvent"); |
|
|
|
|
|
|
|
|
|
|
|
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); |
|
|
|
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); |
|
|
|
|
|
|
|
|
|
|
|
String eventLine = (fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : ""); |
|
|
|
ServerSentEvent<?> sse = (ServerSentEvent<?>) hints; |
|
|
|
DataBuffer prefix = encodeText(eventLine + "data:", charset, bufferFactory); |
|
|
|
CharSequence eventText = (sse != null ? sse.format() : |
|
|
|
|
|
|
|
(fragment.viewName() != null ? "event:" + fragment.viewName() + "\n" : "") + "data:"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DataBuffer prefix = encodeText(eventText.toString(), charset, bufferFactory); |
|
|
|
DataBuffer suffix = encodeText("\n\n", charset, bufferFactory); |
|
|
|
DataBuffer suffix = encodeText("\n\n", charset, bufferFactory); |
|
|
|
|
|
|
|
|
|
|
|
Mono<DataBuffer> content = DataBufferUtils.join(fragmentFlux) |
|
|
|
Mono<DataBuffer> content = DataBufferUtils.join(fragmentFlux) |
|
|
|
|