001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.mvc.method.annotation;
018
019import java.io.IOException;
020import java.util.LinkedHashSet;
021import java.util.Set;
022
023import org.springframework.http.MediaType;
024import org.springframework.http.server.ServerHttpResponse;
025import org.springframework.util.Assert;
026
027/**
028 * A controller method return value type for asynchronous request processing
029 * where one or more objects are written to the response.
030 *
031 * <p>While {@link org.springframework.web.context.request.async.DeferredResult}
032 * is used to produce a single result, a {@code ResponseBodyEmitter} can be used
033 * to send multiple objects where each object is written with a compatible
034 * {@link org.springframework.http.converter.HttpMessageConverter}.
035 *
036 * <p>Supported as a return type on its own as well as within a
037 * {@link org.springframework.http.ResponseEntity}.
038 *
039 * <pre>
040 * &#064;RequestMapping(value="/stream", method=RequestMethod.GET)
041 * public ResponseBodyEmitter handle() {
042 *         ResponseBodyEmitter emitter = new ResponseBodyEmitter();
043 *         // Pass the emitter to another component...
044 *         return emitter;
045 * }
046 *
047 * // in another thread
048 * emitter.send(foo1);
049 *
050 * // and again
051 * emitter.send(foo2);
052 *
053 * // and done
054 * emitter.complete();
055 * </pre>
056 *
057 * @author Rossen Stoyanchev
058 * @author Juergen Hoeller
059 * @since 4.2
060 */
061public class ResponseBodyEmitter {
062
063        private final Long timeout;
064
065        private final Set<DataWithMediaType> earlySendAttempts = new LinkedHashSet<DataWithMediaType>(8);
066
067        private Handler handler;
068
069        private boolean complete;
070
071        private Throwable failure;
072
073        private final DefaultCallback timeoutCallback = new DefaultCallback();
074
075        private final DefaultCallback completionCallback = new DefaultCallback();
076
077
078        /**
079         * Create a new ResponseBodyEmitter instance.
080         */
081        public ResponseBodyEmitter() {
082                this.timeout = null;
083        }
084
085        /**
086         * Create a ResponseBodyEmitter with a custom timeout value.
087         * <p>By default not set in which case the default configured in the MVC
088         * Java Config or the MVC namespace is used, or if that's not set, then the
089         * timeout depends on the default of the underlying server.
090         * @param timeout timeout value in milliseconds
091         */
092        public ResponseBodyEmitter(Long timeout) {
093                this.timeout = timeout;
094        }
095
096
097        /**
098         * Return the configured timeout value, if any.
099         */
100        public Long getTimeout() {
101                return this.timeout;
102        }
103
104
105        synchronized void initialize(Handler handler) throws IOException {
106                this.handler = handler;
107
108                for (DataWithMediaType sendAttempt : this.earlySendAttempts) {
109                        sendInternal(sendAttempt.getData(), sendAttempt.getMediaType());
110                }
111                this.earlySendAttempts.clear();
112
113                if (this.complete) {
114                        if (this.failure != null) {
115                                this.handler.completeWithError(this.failure);
116                        }
117                        else {
118                                this.handler.complete();
119                        }
120                }
121                else {
122                        this.handler.onTimeout(this.timeoutCallback);
123                        this.handler.onCompletion(this.completionCallback);
124                }
125        }
126
127        /**
128         * Invoked after the response is updated with the status code and headers,
129         * if the ResponseBodyEmitter is wrapped in a ResponseEntity, but before the
130         * response is committed, i.e. before the response body has been written to.
131         * <p>The default implementation is empty.
132         */
133        protected void extendResponse(ServerHttpResponse outputMessage) {
134        }
135
136        /**
137         * Write the given object to the response.
138         * <p>If any exception occurs a dispatch is made back to the app server where
139         * Spring MVC will pass the exception through its exception handling mechanism.
140         * @param object the object to write
141         * @throws IOException raised when an I/O error occurs
142         * @throws java.lang.IllegalStateException wraps any other errors
143         */
144        public void send(Object object) throws IOException {
145                send(object, null);
146        }
147
148        /**
149         * Write the given object to the response also using a MediaType hint.
150         * <p>If any exception occurs a dispatch is made back to the app server where
151         * Spring MVC will pass the exception through its exception handling mechanism.
152         * @param object the object to write
153         * @param mediaType a MediaType hint for selecting an HttpMessageConverter
154         * @throws IOException raised when an I/O error occurs
155         * @throws java.lang.IllegalStateException wraps any other errors
156         */
157        public synchronized void send(Object object, MediaType mediaType) throws IOException {
158                Assert.state(!this.complete, "ResponseBodyEmitter is already set complete");
159                sendInternal(object, mediaType);
160        }
161
162        private void sendInternal(Object object, MediaType mediaType) throws IOException {
163                if (object != null) {
164                        if (this.handler != null) {
165                                try {
166                                        this.handler.send(object, mediaType);
167                                }
168                                catch (IOException ex) {
169                                        throw ex;
170                                }
171                                catch (Throwable ex) {
172                                        throw new IllegalStateException("Failed to send " + object, ex);
173                                }
174                        }
175                        else {
176                                this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
177                        }
178                }
179        }
180
181        /**
182         * Complete request processing.
183         * <p>A dispatch is made into the app server where Spring MVC completes
184         * asynchronous request processing.
185         * <p><strong>Note:</strong> you do not need to call this method after an
186         * {@link IOException} from any of the {@code send} methods. The Servlet
187         * container will generate an error notification that Spring MVC will process
188         * and handle through the exception resolver mechanism and then complete.
189         */
190        public synchronized void complete() {
191                this.complete = true;
192                if (this.handler != null) {
193                        this.handler.complete();
194                }
195        }
196
197        /**
198         * Complete request processing with an error.
199         * <p>A dispatch is made into the app server where Spring MVC will pass the
200         * exception through its exception handling mechanism.
201         */
202        public synchronized void completeWithError(Throwable ex) {
203                this.complete = true;
204                this.failure = ex;
205                if (this.handler != null) {
206                        this.handler.completeWithError(ex);
207                }
208        }
209
210        /**
211         * Register code to invoke when the async request times out. This method is
212         * called from a container thread when an async request times out.
213         */
214        public synchronized void onTimeout(Runnable callback) {
215                this.timeoutCallback.setDelegate(callback);
216        }
217
218        /**
219         * Register code to invoke when the async request completes. This method is
220         * called from a container thread when an async request completed for any
221         * reason including timeout and network error. This method is useful for
222         * detecting that a {@code ResponseBodyEmitter} instance is no longer usable.
223         */
224        public synchronized void onCompletion(Runnable callback) {
225                this.completionCallback.setDelegate(callback);
226        }
227
228
229        /**
230         * Handle sent objects and complete request processing.
231         */
232        interface Handler {
233
234                void send(Object data, MediaType mediaType) throws IOException;
235
236                void complete();
237
238                void completeWithError(Throwable failure);
239
240                void onTimeout(Runnable callback);
241
242                void onCompletion(Runnable callback);
243        }
244
245
246        /**
247         * A simple holder of data to be written along with a MediaType hint for
248         * selecting a message converter to write with.
249         */
250        public static class DataWithMediaType {
251
252                private final Object data;
253
254                private final MediaType mediaType;
255
256                public DataWithMediaType(Object data, MediaType mediaType) {
257                        this.data = data;
258                        this.mediaType = mediaType;
259                }
260
261                public Object getData() {
262                        return this.data;
263                }
264
265                public MediaType getMediaType() {
266                        return this.mediaType;
267                }
268        }
269
270
271        private class DefaultCallback implements Runnable {
272
273                private Runnable delegate;
274
275                public void setDelegate(Runnable delegate) {
276                        this.delegate = delegate;
277                }
278
279                @Override
280                public void run() {
281                        ResponseBodyEmitter.this.complete = true;
282                        if (this.delegate != null) {
283                                this.delegate.run();
284                        }
285                }
286        }
287
288}