SseEmitter.java 6.8 KB
Newer Older
1
/*
S
stonio 已提交
2
 * Copyright 2002-2017 the original author or authors.
3 4 5 6 7 8 9 10 11 12 13 14 15
 *
 * 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.
 */
16

17 18 19
package org.springframework.web.servlet.mvc.method.annotation;

import java.io.IOException;
20
import java.nio.charset.StandardCharsets;
21
import java.util.Collections;
22 23
import java.util.LinkedHashSet;
import java.util.Set;
24 25 26 27

import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
28
import org.springframework.lang.Nullable;
29
import org.springframework.util.ObjectUtils;
S
stonio 已提交
30
import org.springframework.util.StringUtils;
31 32

/**
33
 * A specialization of {@link ResponseBodyEmitter} for sending
S
Spring Operator 已提交
34
 * <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
35 36
 *
 * @author Rossen Stoyanchev
37
 * @author Juergen Hoeller
38 39 40 41
 * @since 4.2
 */
public class SseEmitter extends ResponseBodyEmitter {

42
	static final MediaType TEXT_PLAIN = new MediaType("text", "plain", StandardCharsets.UTF_8);
43

44
	static final MediaType UTF8_TEXT_EVENTSTREAM = new MediaType("text", "event-stream", StandardCharsets.UTF_8);
45

46

47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
	/**
	 * Create a new SseEmitter instance.
	 */
	public SseEmitter() {
		super();
	}

	/**
	 * Create a SseEmitter with a custom timeout value.
	 * <p>By default not set in which case the default configured in the MVC
	 * Java Config or the MVC namespace is used, or if that's not set, then the
	 * timeout depends on the default of the underlying server.
	 * @param timeout timeout value in milliseconds
	 * @since 4.2.2
	 */
	public SseEmitter(Long timeout) {
		super(timeout);
	}


67 68 69
	@Override
	protected void extendResponse(ServerHttpResponse outputMessage) {
		super.extendResponse(outputMessage);
70

71 72
		HttpHeaders headers = outputMessage.getHeaders();
		if (headers.getContentType() == null) {
73
			headers.setContentType(UTF8_TEXT_EVENTSTREAM);
74 75 76 77 78 79 80 81 82 83 84
		}
	}

	/**
	 * 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>
85 86 87 88
	 *
	 * <p>Please, see {@link ResponseBodyEmitter#send(Object) parent Javadoc}
	 * for important notes on exception handling.
	 *
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
	 * @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>
106 107 108 109
	 *
	 * <p>Please, see {@link ResponseBodyEmitter#send(Object) parent Javadoc}
	 * for important notes on exception handling.
	 *
110 111 112 113 114
	 * @param object the object to write
	 * @param mediaType a MediaType hint for selecting an HttpMessageConverter
	 * @throws IOException raised when an I/O error occurs
	 */
	@Override
115
	public void send(Object object, @Nullable MediaType mediaType) throws IOException {
116
		send(event().data(object, mediaType));
117 118 119 120 121 122 123 124 125 126 127 128 129
	}

	/**
	 * 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
	 */
	public void send(SseEventBuilder builder) throws IOException {
130
		Set<DataWithMediaType> dataToSend = builder.build();
131 132 133 134
		synchronized (this) {
			for (DataWithMediaType entry : dataToSend) {
				super.send(entry.getData(), entry.getMediaType());
			}
135 136 137
		}
	}

138 139 140 141 142
	@Override
	public String toString() {
		return "SseEmitter@" + ObjectUtils.getIdentityHexString(this);
	}

143

144
	public static SseEventBuilder event() {
145
		return new SseEventBuilderImpl();
146 147 148 149 150 151 152 153 154
	}


	/**
	 * A builder for an SSE event.
	 */
	public interface SseEventBuilder {

		/**
155
		 * Add an SSE "id" line.
156
		 */
157
		SseEventBuilder id(String id);
158 159 160 161 162 163 164

		/**
		 * Add an SSE "event" line.
		 */
		SseEventBuilder name(String eventName);

		/**
165
		 * Add an SSE "retry" line.
166
		 */
167
		SseEventBuilder reconnectTime(long reconnectTimeMillis);
168 169

		/**
170
		 * Add an SSE "comment" line.
171
		 */
172
		SseEventBuilder comment(String comment);
173 174 175 176 177 178 179 180 181

		/**
		 * Add an SSE "data" line.
		 */
		SseEventBuilder data(Object object);

		/**
		 * Add an SSE "data" line.
		 */
182
		SseEventBuilder data(Object object, @Nullable MediaType mediaType);
183 184 185 186

		/**
		 * Return one or more Object-MediaType  pairs to write via
		 * {@link #send(Object, MediaType)}.
J
Juergen Hoeller 已提交
187
		 * @since 4.2.3
188 189
		 */
		Set<DataWithMediaType> build();
190 191
	}

192

193 194 195
	/**
	 * Default implementation of SseEventBuilder.
	 */
196
	private static class SseEventBuilderImpl implements SseEventBuilder {
197

198
		private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<>(4);
199

200
		@Nullable
201 202 203
		private StringBuilder sb;

		@Override
204 205
		public SseEventBuilder id(String id) {
			append("id:").append(id).append("\n");
206 207 208 209 210
			return this;
		}

		@Override
		public SseEventBuilder name(String name) {
211
			append("event:").append(name).append("\n");
212 213 214 215
			return this;
		}

		@Override
216 217
		public SseEventBuilder reconnectTime(long reconnectTimeMillis) {
			append("retry:").append(String.valueOf(reconnectTimeMillis)).append("\n");
218 219 220 221
			return this;
		}

		@Override
222 223
		public SseEventBuilder comment(String comment) {
			append(":").append(comment).append("\n");
224 225 226 227 228 229 230 231 232
			return this;
		}

		@Override
		public SseEventBuilder data(Object object) {
			return data(object, null);
		}

		@Override
233
		public SseEventBuilder data(Object object, @Nullable MediaType mediaType) {
234 235
			append("data:");
			saveAppendedText();
236
			this.dataToSend.add(new DataWithMediaType(object, mediaType));
237 238 239 240
			append("\n");
			return this;
		}

241
		SseEventBuilderImpl append(String text) {
242 243 244 245 246 247 248
			if (this.sb == null) {
				this.sb = new StringBuilder();
			}
			this.sb.append(text);
			return this;
		}

249 250
		@Override
		public Set<DataWithMediaType> build() {
S
stonio 已提交
251
			if (!StringUtils.hasLength(this.sb) && this.dataToSend.isEmpty()) {
252
				return Collections.emptySet();
253
			}
254 255 256
			append("\n");
			saveAppendedText();
			return this.dataToSend;
257 258
		}

259 260 261 262
		private void saveAppendedText() {
			if (this.sb != null) {
				this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN));
				this.sb = null;
263 264 265 266 267
			}
		}
	}

}