Add support for Server-Sent Events
This commit adds ResponseBodyEmitter and SseEmitter (and also ResponseEntity<ResponseBodyEmitter> and ResponseEntity<SseEmitter>) as new return value types supported on @RequestMapping controller methods. See Javadoc on respective types for more details. Issue: SPR-12212
This commit is contained in:
parent
ccb1c13951
commit
a32b5e61d0
|
@ -213,6 +213,12 @@ import java.util.concurrent.Callable;
|
|||
* <li>A {@link org.springframework.util.concurrent.ListenableFuture}
|
||||
* which the application uses to produce a return value in a separate
|
||||
* thread of its own choosing, as an alternative to returning a Callable.
|
||||
* <li>A {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter}
|
||||
* can be used to write multiple objects to the response asynchronously;
|
||||
* also supported as the body within {@code ResponseEntity}.</li>
|
||||
* <li>An {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter}
|
||||
* can be used to write Server-Sent Events to the response asynchronously;
|
||||
* also supported as the body within {@code ResponseEntity}.</li>
|
||||
* <li>{@code void} if the method handles the response itself (by
|
||||
* writing the response content directly, declaring an argument of type
|
||||
* {@link javax.servlet.ServletResponse} / {@link javax.servlet.http.HttpServletResponse}
|
||||
|
|
|
@ -632,6 +632,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
|||
handlers.add(new ModelAndViewMethodReturnValueHandler());
|
||||
handlers.add(new ModelMethodProcessor());
|
||||
handlers.add(new ViewMethodReturnValueHandler());
|
||||
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
|
||||
handlers.add(new HttpEntityMethodProcessor(
|
||||
getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
|
||||
handlers.add(new HttpHeadersReturnValueHandler());
|
||||
|
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
|
||||
/**
|
||||
* A controller method return value type for asynchronous request processing
|
||||
* where one or more objects are written to the response. While
|
||||
* {@link org.springframework.web.context.request.async.DeferredResult DeferredResult}
|
||||
* is used to produce a single result, a {@code ResponseBodyEmitter} can be used
|
||||
* to send multiple objects where each object is written with a compatible
|
||||
* {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}.
|
||||
*
|
||||
* <p>Supported as a return type on its own as well as within a
|
||||
* {@link org.springframework.http.ResponseEntity ResponseEntity}.
|
||||
*
|
||||
* <pre>
|
||||
* @RequestMapping(value="/stream", method=RequestMethod.GET)
|
||||
* public ResponseBodyEmitter handle() {
|
||||
* ResponseBodyEmitter emitter = new ResponseBodyEmitter();
|
||||
* // Pass the emitter to another component...
|
||||
* return emitter;
|
||||
* }
|
||||
*
|
||||
* // in another thread
|
||||
* emitter.send(foo1);
|
||||
*
|
||||
* // and again
|
||||
* emitter.send(foo2);
|
||||
*
|
||||
* // and done
|
||||
* emitter.complete();
|
||||
* </pre>
|
||||
*
|
||||
* <p><strong>Note:</strong> this class is not thread-safe. Callers must ensure
|
||||
* that use from multiple threads is synchronized.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.2
|
||||
*/
|
||||
public class ResponseBodyEmitter {
|
||||
|
||||
private Handler handler;
|
||||
|
||||
/* Cache for objects sent before handler is set. */
|
||||
private final Map<Object, MediaType> initHandlerCache = new LinkedHashMap<Object, MediaType>(10);
|
||||
|
||||
private volatile boolean complete;
|
||||
|
||||
private Throwable failure;
|
||||
|
||||
|
||||
/**
|
||||
* Invoked after the response is updated with the status code and headers,
|
||||
* if the ResponseBodyEmitter is wrapped in a ResponseEntity, but before the
|
||||
* response is committed, i.e. before the response body has been written to.
|
||||
* <p>The default implementation is empty.
|
||||
*/
|
||||
protected void extendResponse(ServerHttpResponse outputMessage) {
|
||||
}
|
||||
|
||||
void initialize(Handler handler) throws IOException {
|
||||
synchronized (this) {
|
||||
this.handler = handler;
|
||||
for (Map.Entry<Object, MediaType> entry : this.initHandlerCache.entrySet()) {
|
||||
try {
|
||||
sendInternal(entry.getKey(), entry.getValue());
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.complete) {
|
||||
if (this.failure != null) {
|
||||
this.handler.completeWithError(this.failure);
|
||||
}
|
||||
else {
|
||||
this.handler.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given object to the response.
|
||||
* <p>If any exception occurs a dispatch is made back to the app server where
|
||||
* Spring MVC will pass the exception through its exception handling mechanism.
|
||||
* @param object the object to write
|
||||
* @throws IOException raised when an I/O error occurs
|
||||
* @throws java.lang.IllegalStateException wraps any other errors
|
||||
*/
|
||||
public void send(Object object) throws IOException {
|
||||
send(object, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given object to the response also using a MediaType hint.
|
||||
* <p>If any exception occurs a dispatch is made back to the app server where
|
||||
* Spring MVC will pass the exception through its exception handling mechanism.
|
||||
* @param object the object to write
|
||||
* @param mediaType a MediaType hint for selecting an HttpMessageConverter
|
||||
* @throws IOException raised when an I/O error occurs
|
||||
* @throws java.lang.IllegalStateException wraps any other errors
|
||||
*/
|
||||
public void send(Object object, MediaType mediaType) throws IOException {
|
||||
Assert.state(!this.complete, "ResponseBodyEmitter is already set complete.");
|
||||
sendInternal(object, mediaType);
|
||||
}
|
||||
|
||||
private void sendInternal(Object object, MediaType mediaType) throws IOException {
|
||||
if (object == null) {
|
||||
return;
|
||||
}
|
||||
if (handler == null) {
|
||||
synchronized (this) {
|
||||
if (handler == null) {
|
||||
this.initHandlerCache.put(object, mediaType);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
this.handler.send(object, mediaType);
|
||||
}
|
||||
catch(IOException ex){
|
||||
this.handler.completeWithError(ex);
|
||||
throw ex;
|
||||
}
|
||||
catch(Throwable ex){
|
||||
this.handler.completeWithError(ex);
|
||||
throw new IllegalStateException("Failed to send " + object, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete request processing.
|
||||
* <p>A dispatch is made into the app server where Spring MVC completes
|
||||
* asynchronous request processing.
|
||||
*/
|
||||
public void complete() {
|
||||
synchronized (this) {
|
||||
this.complete = true;
|
||||
if (handler != null) {
|
||||
this.handler.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete request processing with an error.
|
||||
* <p>A dispatch is made into the app server where Spring MVC will pass the
|
||||
* exception through its exception handling mechanism.
|
||||
*/
|
||||
public void completeWithError(Throwable ex) {
|
||||
synchronized (this) {
|
||||
this.complete = true;
|
||||
this.failure = ex;
|
||||
if (handler != null) {
|
||||
this.handler.completeWithError(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle sent objects and complete request processing.
|
||||
*/
|
||||
interface Handler {
|
||||
|
||||
void send(Object data, MediaType mediaType) throws IOException;
|
||||
|
||||
void complete();
|
||||
|
||||
void completeWithError(Throwable failure);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.context.request.async.DeferredResult;
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
|
||||
/**
|
||||
* Supports return values of type {@link ResponseBodyEmitter} and also
|
||||
* {@code ResponseEntity<ResponseBodyEmitter>}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.2
|
||||
*/
|
||||
public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class);
|
||||
|
||||
private final List<HttpMessageConverter<?>> messageConverters;
|
||||
|
||||
|
||||
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
|
||||
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
|
||||
this.messageConverters = messageConverters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsReturnType(MethodParameter returnType) {
|
||||
if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) {
|
||||
return true;
|
||||
}
|
||||
else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
|
||||
Type paramType = returnType.getGenericParameterType();
|
||||
if (paramType instanceof ParameterizedType) {
|
||||
ParameterizedType type = (ParameterizedType) paramType;
|
||||
Type[] typeArguments = type.getActualTypeArguments();
|
||||
if (typeArguments.length == 1) {
|
||||
return ResponseBodyEmitter.class.isAssignableFrom((Class<?>) typeArguments[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleReturnValue(Object returnValue, MethodParameter returnType,
|
||||
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
|
||||
|
||||
if (returnValue == null) {
|
||||
mavContainer.setRequestHandled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
|
||||
ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
|
||||
|
||||
if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) {
|
||||
ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue;
|
||||
outputMessage.setStatusCode(responseEntity.getStatusCode());
|
||||
outputMessage.getHeaders().putAll(responseEntity.getHeaders());
|
||||
returnValue = responseEntity.getBody();
|
||||
if (returnValue == null) {
|
||||
mavContainer.setRequestHandled(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue);
|
||||
ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue;
|
||||
emitter.extendResponse(outputMessage);
|
||||
|
||||
// Commit the response and wrap to ignore further header changes
|
||||
outputMessage.getBody();
|
||||
outputMessage = new StreamingServletServerHttpResponse(outputMessage);
|
||||
|
||||
DeferredResult<?> deferredResult = new DeferredResult<Object>();
|
||||
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
|
||||
|
||||
HttpMessageConvertingHandler handler = new HttpMessageConvertingHandler(outputMessage, deferredResult);
|
||||
emitter.initialize(handler);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ResponseBodyEmitter.Handler that writes with HttpMessageConverter's.
|
||||
*/
|
||||
private class HttpMessageConvertingHandler implements ResponseBodyEmitter.Handler {
|
||||
|
||||
private final ServerHttpResponse outputMessage;
|
||||
|
||||
private final DeferredResult<?> deferredResult;
|
||||
|
||||
|
||||
public HttpMessageConvertingHandler(ServerHttpResponse outputMessage, DeferredResult<?> deferredResult) {
|
||||
this.outputMessage = outputMessage;
|
||||
this.deferredResult = deferredResult;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void send(Object data, MediaType mediaType) throws IOException {
|
||||
sendInternal(data, mediaType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> void sendInternal(T data, MediaType mediaType) throws IOException {
|
||||
for (HttpMessageConverter<?> converter : ResponseBodyEmitterReturnValueHandler.this.messageConverters) {
|
||||
if (converter.canWrite(data.getClass(), mediaType)) {
|
||||
((HttpMessageConverter<T>) converter).write(data, mediaType, this.outputMessage);
|
||||
this.outputMessage.flush();
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Written [" + data + "] using [" + converter + "]");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No suitable converter for " + data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete() {
|
||||
this.deferredResult.setResult(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeWithError(Throwable failure) {
|
||||
this.deferredResult.setErrorResult(failure);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wrap to silently ignore header changes HttpMessageConverter's that would
|
||||
* otherwise cause HttpHeaders to raise exceptions.
|
||||
*/
|
||||
private static class StreamingServletServerHttpResponse implements ServerHttpResponse {
|
||||
|
||||
private final ServerHttpResponse delegate;
|
||||
|
||||
private final HttpHeaders mutableHeaders = new HttpHeaders();
|
||||
|
||||
|
||||
public StreamingServletServerHttpResponse(ServerHttpResponse delegate) {
|
||||
this.delegate = delegate;
|
||||
this.mutableHeaders.putAll(delegate.getHeaders());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setStatusCode(HttpStatus status) {
|
||||
this.delegate.setStatusCode(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.mutableHeaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getBody() throws IOException {
|
||||
return this.delegate.getBody();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
this.delegate.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
this.delegate.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -25,6 +25,7 @@ import java.util.concurrent.Callable;
|
|||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
@ -260,7 +261,16 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
|
|||
|
||||
@Override
|
||||
public Class<?> getParameterType() {
|
||||
return (this.returnValue != null ? this.returnValue.getClass() : this.returnType.getRawClass());
|
||||
if (this.returnValue != null) {
|
||||
return this.returnValue.getClass();
|
||||
}
|
||||
Class<?> parameterType = super.getParameterType();
|
||||
if (ResponseBodyEmitter.class.isAssignableFrom(parameterType)) {
|
||||
return parameterType;
|
||||
}
|
||||
Assert.isTrue(!ResolvableType.NONE.equals(this.returnType), "Expected one of" +
|
||||
"Callable, DeferredResult, or ListenableFuture: " + super.getParameterType());
|
||||
return this.returnType.getRawClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
|
||||
/**
|
||||
* A specialization of
|
||||
* {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter
|
||||
* ResponseBodyEmitter} for sending
|
||||
* <a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.2
|
||||
*/
|
||||
public class SseEmitter extends ResponseBodyEmitter {
|
||||
|
||||
public static final MediaType TEXT_PLAIN = new MediaType("text", "plain", Charset.forName("UTF-8"));
|
||||
|
||||
|
||||
@Override
|
||||
protected void extendResponse(ServerHttpResponse outputMessage) {
|
||||
super.extendResponse(outputMessage);
|
||||
HttpHeaders headers = outputMessage.getHeaders();
|
||||
if (headers.getContentType() == null) {
|
||||
headers.setContentType(new MediaType("text", "event-stream"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the object formatted as a single SSE "data" line. It's equivalent to:
|
||||
* <pre>
|
||||
*
|
||||
* // static import of SseEmitter.*
|
||||
*
|
||||
* SseEmitter emitter = new SseEmitter();
|
||||
* emitter.send(event().data(myObject));
|
||||
* </pre>
|
||||
* @param object the object to write
|
||||
* @throws IOException raised when an I/O error occurs
|
||||
* @throws java.lang.IllegalStateException wraps any other errors
|
||||
*/
|
||||
@Override
|
||||
public void send(Object object) throws IOException {
|
||||
send(object, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the object formatted as a single SSE "data" line. It's equivalent to:
|
||||
* <pre>
|
||||
*
|
||||
* // static import of SseEmitter.*
|
||||
*
|
||||
* SseEmitter emitter = new SseEmitter();
|
||||
* emitter.send(event().data(myObject, MediaType.APPLICATION_JSON));
|
||||
* </pre>
|
||||
* @param object the object to write
|
||||
* @param mediaType a MediaType hint for selecting an HttpMessageConverter
|
||||
* @throws IOException raised when an I/O error occurs
|
||||
* @throws java.lang.IllegalStateException wraps any other errors
|
||||
*/
|
||||
@Override
|
||||
public void send(Object object, MediaType mediaType) throws IOException {
|
||||
if (object == null) {
|
||||
return;
|
||||
}
|
||||
send(event().data(object, mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an SSE event prepared with the given builder. For example:
|
||||
* <pre>
|
||||
*
|
||||
* // static import of SseEmitter
|
||||
*
|
||||
* SseEmitter emitter = new SseEmitter();
|
||||
* emitter.send(event().name("update").id("1").data(myObject));
|
||||
* </pre>
|
||||
* @param builder a builder for an SSE formatted event.
|
||||
* @throws IOException raised when an I/O error occurs
|
||||
* @throws java.lang.IllegalStateException wraps any other errors
|
||||
*/
|
||||
public void send(SseEventBuilder builder) throws IOException {
|
||||
Map<Object, MediaType> map = builder.build();
|
||||
for (Map.Entry<Object, MediaType> entry : map.entrySet()) {
|
||||
super.send(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public static SseEventBuilder event() {
|
||||
return new DefaultSseEventBuilder();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A builder for an SSE event.
|
||||
*/
|
||||
public interface SseEventBuilder {
|
||||
|
||||
/**
|
||||
* Add an SSE "comment" line.
|
||||
*/
|
||||
SseEventBuilder comment(String comment);
|
||||
|
||||
/**
|
||||
* Add an SSE "event" line.
|
||||
*/
|
||||
SseEventBuilder name(String eventName);
|
||||
|
||||
/**
|
||||
* Add an SSE "id" line.
|
||||
*/
|
||||
SseEventBuilder id(String id);
|
||||
|
||||
/**
|
||||
* Add an SSE "event" line.
|
||||
*/
|
||||
SseEventBuilder reconnectTime(long reconnectTimeMillis);
|
||||
|
||||
/**
|
||||
* Add an SSE "data" line.
|
||||
*/
|
||||
SseEventBuilder data(Object object);
|
||||
|
||||
/**
|
||||
* Add an SSE "data" line.
|
||||
*/
|
||||
SseEventBuilder data(Object object, MediaType mediaType);
|
||||
|
||||
/**
|
||||
* Return a map with objects that represent the data to be written to
|
||||
* the response as well as the required SSE text formatting that
|
||||
* surrounds it.
|
||||
*/
|
||||
Map<Object, MediaType> build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Default implementation of SseEventBuilder.
|
||||
*/
|
||||
private static class DefaultSseEventBuilder implements SseEventBuilder {
|
||||
|
||||
private final Map<Object, MediaType> map = new LinkedHashMap<Object, MediaType>(4);
|
||||
|
||||
private StringBuilder sb;
|
||||
|
||||
|
||||
@Override
|
||||
public SseEventBuilder comment(String comment) {
|
||||
append(":").append(comment != null ? comment : "").append("\n");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEventBuilder name(String name) {
|
||||
append("name:").append(name != null ? name : "").append("\n");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEventBuilder id(String id) {
|
||||
append("id:").append(id != null ? id : "").append("\n");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEventBuilder reconnectTime(long reconnectTimeMillis) {
|
||||
append("retry:").append(String.valueOf(reconnectTimeMillis)).append("\n");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEventBuilder data(Object object) {
|
||||
return data(object, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SseEventBuilder data(Object object, MediaType mediaType) {
|
||||
append("data:");
|
||||
saveAppendedText();
|
||||
this.map.put(object, mediaType);
|
||||
append("\n");
|
||||
return this;
|
||||
}
|
||||
|
||||
DefaultSseEventBuilder append(String text) {
|
||||
if (this.sb == null) {
|
||||
this.sb = new StringBuilder();
|
||||
}
|
||||
this.sb.append(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void saveAppendedText() {
|
||||
if (this.sb != null) {
|
||||
this.map.put(this.sb.toString(), TEXT_PLAIN);
|
||||
this.sb = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, MediaType> build() {
|
||||
if (this.sb == null || this.sb.length() == 0 && this.map.isEmpty()) {
|
||||
return Collections.<Object, MediaType>emptyMap();
|
||||
}
|
||||
append("\n");
|
||||
saveAppendedText();
|
||||
return this.map;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -79,7 +79,7 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder {
|
|||
* {@code "/"} or {@code "*.do"}, the result will be the same as
|
||||
* if calling {@link #fromContextPath(HttpServletRequest)}.
|
||||
*/
|
||||
public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {
|
||||
public static ServletUriComponentsBuilder fromServletMapping(HttpServletRequest request) {
|
||||
ServletUriComponentsBuilder builder = fromContextPath(request);
|
||||
if (StringUtils.hasText(new UrlPathHelper().getPathWithinServletMapping(request))) {
|
||||
builder.path(request.getServletPath());
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.mock.web.test.MockAsyncContext;
|
||||
import org.springframework.mock.web.test.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.test.MockHttpServletResponse;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.async.AsyncWebRequest;
|
||||
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for ResponseBodyEmitterReturnValueHandler.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ResponseBodyEmitterReturnValueHandlerTests {
|
||||
|
||||
private ResponseBodyEmitterReturnValueHandler handler;
|
||||
|
||||
private ModelAndViewContainer mavContainer;
|
||||
|
||||
private NativeWebRequest webRequest;
|
||||
|
||||
private MockHttpServletRequest request;
|
||||
|
||||
private MockHttpServletResponse response;
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
List<HttpMessageConverter<?>> converters = Arrays.asList(
|
||||
new StringHttpMessageConverter(), new MappingJackson2HttpMessageConverter());
|
||||
|
||||
this.handler = new ResponseBodyEmitterReturnValueHandler(converters);
|
||||
this.mavContainer = new ModelAndViewContainer();
|
||||
|
||||
this.request = new MockHttpServletRequest();
|
||||
this.response = new MockHttpServletResponse();
|
||||
this.webRequest = new ServletWebRequest(this.request, this.response);
|
||||
|
||||
AsyncWebRequest asyncWebRequest = new StandardServletAsyncWebRequest(this.request, this.response);
|
||||
WebAsyncUtils.getAsyncManager(this.webRequest).setAsyncWebRequest(asyncWebRequest);
|
||||
this.request.setAsyncSupported(true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void supportsReturnType() throws Exception {
|
||||
assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handle")));
|
||||
assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleSse")));
|
||||
assertTrue(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntity")));
|
||||
assertFalse(this.handler.supportsReturnType(returnType(TestController.class, "handleResponseEntityString")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseBodyEmitter() throws Exception {
|
||||
MethodParameter returnType = returnType(TestController.class, "handle");
|
||||
ResponseBodyEmitter emitter = new ResponseBodyEmitter();
|
||||
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
|
||||
|
||||
assertTrue(this.request.isAsyncStarted());
|
||||
assertEquals("", this.response.getContentAsString());
|
||||
|
||||
SimpleBean bean = new SimpleBean();
|
||||
bean.setId(1L);
|
||||
bean.setName("Joe");
|
||||
emitter.send(bean);
|
||||
emitter.send("\n");
|
||||
|
||||
bean.setId(2L);
|
||||
bean.setName("John");
|
||||
emitter.send(bean);
|
||||
emitter.send("\n");
|
||||
|
||||
bean.setId(3L);
|
||||
bean.setName("Jason");
|
||||
emitter.send(bean);
|
||||
|
||||
assertEquals("{\"id\":1,\"name\":\"Joe\"}\n" +
|
||||
"{\"id\":2,\"name\":\"John\"}\n" +
|
||||
"{\"id\":3,\"name\":\"Jason\"}",
|
||||
this.response.getContentAsString());
|
||||
|
||||
MockAsyncContext asyncContext = (MockAsyncContext) this.request.getAsyncContext();
|
||||
assertNull(asyncContext.getDispatchedPath());
|
||||
|
||||
emitter.complete();
|
||||
assertNotNull(asyncContext.getDispatchedPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sseEmitter() throws Exception {
|
||||
MethodParameter returnType = returnType(TestController.class, "handleSse");
|
||||
SseEmitter emitter = new SseEmitter();
|
||||
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
|
||||
|
||||
assertTrue(this.request.isAsyncStarted());
|
||||
assertEquals(200, this.response.getStatus());
|
||||
assertEquals("text/event-stream", this.response.getContentType());
|
||||
|
||||
SimpleBean bean1 = new SimpleBean();
|
||||
bean1.setId(1L);
|
||||
bean1.setName("Joe");
|
||||
|
||||
SimpleBean bean2 = new SimpleBean();
|
||||
bean2.setId(2L);
|
||||
bean2.setName("John");
|
||||
|
||||
emitter.send(event().comment("a test").name("update").id("1").reconnectTime(5000L).data(bean1).data(bean2));
|
||||
|
||||
assertEquals(":a test\n" +
|
||||
"name:update\n" +
|
||||
"id:1\n" +
|
||||
"retry:5000\n" +
|
||||
"data:{\"id\":1,\"name\":\"Joe\"}\n" +
|
||||
"data:{\"id\":2,\"name\":\"John\"}\n" +
|
||||
"\n",
|
||||
this.response.getContentAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseEntitySse() throws Exception {
|
||||
MethodParameter returnType = returnType(TestController.class, "handleResponseEntitySse");
|
||||
ResponseEntity<SseEmitter> emitter = ResponseEntity.ok().header("foo", "bar").body(new SseEmitter());
|
||||
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
|
||||
|
||||
assertTrue(this.request.isAsyncStarted());
|
||||
assertEquals(200, this.response.getStatus());
|
||||
assertEquals("text/event-stream", this.response.getContentType());
|
||||
assertEquals("bar", this.response.getHeader("foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseEntitySseNoContent() throws Exception {
|
||||
MethodParameter returnType = returnType(TestController.class, "handleResponseEntitySse");
|
||||
ResponseEntity<?> emitter = ResponseEntity.noContent().build();
|
||||
this.handler.handleReturnValue(emitter, returnType, this.mavContainer, this.webRequest);
|
||||
|
||||
assertFalse(this.request.isAsyncStarted());
|
||||
assertEquals(204, this.response.getStatus());
|
||||
}
|
||||
|
||||
|
||||
private MethodParameter returnType(Class<?> clazz, String methodName) throws NoSuchMethodException {
|
||||
Method method = clazz.getDeclaredMethod(methodName);
|
||||
return new MethodParameter(method, -1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestController {
|
||||
|
||||
private ResponseBodyEmitter handle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResponseEntity<ResponseBodyEmitter> handleResponseEntity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private SseEmitter handleSse() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResponseEntity<SseEmitter> handleResponseEntitySse() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResponseEntity<String> handleResponseEntityString() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SimpleBean {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ResponseBodyEmitter}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class ResponseBodyEmitterTests {
|
||||
|
||||
private ResponseBodyEmitter emitter;
|
||||
|
||||
@Mock
|
||||
private ResponseBodyEmitter.Handler handler;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.emitter = new ResponseBodyEmitter();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void sendBeforeHandlerInitialized() throws Exception {
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("bar", MediaType.TEXT_PLAIN);
|
||||
this.emitter.complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendBeforeHandlerInitializedWithError() throws Exception {
|
||||
IllegalStateException ex = new IllegalStateException();
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("bar", MediaType.TEXT_PLAIN);
|
||||
this.emitter.completeWithError(ex);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
this.emitter.initialize(this.handler);
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).completeWithError(ex);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void sendFailsAfterComplete() throws Exception {
|
||||
this.emitter.complete();
|
||||
this.emitter.send("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendAfterHandlerInitialized() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("bar", MediaType.TEXT_PLAIN);
|
||||
this.emitter.complete();
|
||||
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).complete();
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendAfterHandlerInitializedWithError() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
IllegalStateException ex = new IllegalStateException();
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.emitter.send("bar", MediaType.TEXT_PLAIN);
|
||||
this.emitter.completeWithError(ex);
|
||||
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).send("bar", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).completeWithError(ex);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWithError() throws Exception {
|
||||
this.emitter.initialize(this.handler);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
|
||||
IOException failure = new IOException();
|
||||
doThrow(failure).when(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
try {
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
fail("Expected exception");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
// expected
|
||||
}
|
||||
verify(this.handler).send("foo", MediaType.TEXT_PLAIN);
|
||||
verify(this.handler).completeWithError(failure);
|
||||
verifyNoMoreInteractions(this.handler);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event;
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for {@link org.springframework.web.servlet.mvc.method.annotation.SseEmitter}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class SseEmitterTests {
|
||||
|
||||
private SseEmitter emitter;
|
||||
|
||||
private TestHandler handler;
|
||||
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
this.handler = new TestHandler();
|
||||
this.emitter = new SseEmitter();
|
||||
this.emitter.initialize(this.handler);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void send() throws Exception {
|
||||
this.emitter.send("foo");
|
||||
this.handler.assertSentObjectCount(3);
|
||||
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendWithMediaType() throws Exception {
|
||||
this.emitter.send("foo", MediaType.TEXT_PLAIN);
|
||||
this.handler.assertSentObjectCount(3);
|
||||
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(1, "foo", MediaType.TEXT_PLAIN);
|
||||
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendEventEmpty() throws Exception {
|
||||
this.emitter.send(event());
|
||||
this.handler.assertSentObjectCount(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendEventWithDataLine() throws Exception {
|
||||
this.emitter.send(event().data("foo"));
|
||||
this.handler.assertSentObjectCount(3);
|
||||
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendEventWithTwoDataLines() throws Exception {
|
||||
this.emitter.send(event().data("foo").data("bar"));
|
||||
this.handler.assertSentObjectCount(5);
|
||||
this.handler.assertObject(0, "data:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\ndata:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(3, "bar");
|
||||
this.handler.assertObject(4, "\n\n", SseEmitter.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendEventFull() throws Exception {
|
||||
this.emitter.send(event().comment("blah").name("test").reconnectTime(5000L).id("1").data("foo"));
|
||||
this.handler.assertSentObjectCount(3);
|
||||
this.handler.assertObject(0, ":blah\nname:test\nretry:5000\nid:1\ndata:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\n\n", SseEmitter.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendEventFullWithTwoDataLinesInTheMiddle() throws Exception {
|
||||
this.emitter.send(event().comment("blah").data("foo").data("bar").name("test").reconnectTime(5000L).id("1"));
|
||||
this.handler.assertSentObjectCount(5);
|
||||
this.handler.assertObject(0, ":blah\ndata:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(1, "foo");
|
||||
this.handler.assertObject(2, "\ndata:", SseEmitter.TEXT_PLAIN);
|
||||
this.handler.assertObject(3, "bar");
|
||||
this.handler.assertObject(4, "\nname:test\nretry:5000\nid:1\n\n", SseEmitter.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
|
||||
private static class TestHandler implements ResponseBodyEmitter.Handler {
|
||||
|
||||
private List<Object> objects = new ArrayList<>();
|
||||
|
||||
private List<MediaType> mediaTypes = new ArrayList<>();
|
||||
|
||||
|
||||
public void assertSentObjectCount(int size) {
|
||||
assertEquals(size, this.objects.size());
|
||||
}
|
||||
|
||||
public void assertObject(int index, Object object) {
|
||||
assertObject(index, object, null);
|
||||
}
|
||||
|
||||
public void assertObject(int index, Object object, MediaType mediaType) {
|
||||
assertTrue(index <= this.objects.size());
|
||||
assertEquals(object, this.objects.get(index));
|
||||
assertEquals(mediaType, this.mediaTypes.get(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Object data, MediaType mediaType) throws IOException {
|
||||
this.objects.add(data);
|
||||
this.mediaTypes.add(mediaType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void complete() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeWithError(Throwable failure) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -31063,6 +31063,10 @@ The following are the supported return types:
|
|||
value from a thread of its own choosing.
|
||||
* A `ListenableFuture<?>` can be returned when the application wants to produce the return
|
||||
value from a thread of its own choosing.
|
||||
* A `ResponseBodyEmitter` can be returned to write multiple objects to the response
|
||||
asynchronously; also supported as the body within a `ResponseEntity`.
|
||||
* An `SseEmitter` can be returned to write Server-Sent Events to the response
|
||||
asynchronously; also supported as the body within a `ResponseEntity`.
|
||||
* Any other return type is considered to be a single model attribute to be exposed to
|
||||
the view, using the attribute name specified through `@ModelAttribute` at the method
|
||||
level (or the default attribute name based on the return type class name). The model
|
||||
|
|
Loading…
Reference in New Issue