SseEmitter.java 6.5 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
34 35 36
 * <a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
 *
 * @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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
		}
	}

	/**
	 * 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
	 */
	@Override
107
	public void send(Object object, @Nullable MediaType mediaType) throws IOException {
108
		send(event().data(object, mediaType));
109 110 111 112 113 114 115 116 117 118 119 120 121
	}

	/**
	 * 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 {
122
		Set<DataWithMediaType> dataToSend = builder.build();
123 124 125 126
		synchronized (this) {
			for (DataWithMediaType entry : dataToSend) {
				super.send(entry.getData(), entry.getMediaType());
			}
127 128 129
		}
	}

130 131 132 133 134
	@Override
	public String toString() {
		return "SseEmitter@" + ObjectUtils.getIdentityHexString(this);
	}

135

136
	public static SseEventBuilder event() {
137
		return new SseEventBuilderImpl();
138 139 140 141 142 143 144 145 146
	}


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

		/**
147
		 * Add an SSE "id" line.
148
		 */
149
		SseEventBuilder id(String id);
150 151 152 153 154 155 156

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

		/**
157
		 * Add an SSE "event" line.
158
		 */
159
		SseEventBuilder reconnectTime(long reconnectTimeMillis);
160 161

		/**
162
		 * Add an SSE "comment" line.
163
		 */
164
		SseEventBuilder comment(String comment);
165 166 167 168 169 170 171 172 173

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

		/**
		 * Add an SSE "data" line.
		 */
174
		SseEventBuilder data(Object object, @Nullable MediaType mediaType);
175 176 177 178

		/**
		 * Return one or more Object-MediaType  pairs to write via
		 * {@link #send(Object, MediaType)}.
J
Juergen Hoeller 已提交
179
		 * @since 4.2.3
180 181
		 */
		Set<DataWithMediaType> build();
182 183
	}

184

185 186 187
	/**
	 * Default implementation of SseEventBuilder.
	 */
188
	private static class SseEventBuilderImpl implements SseEventBuilder {
189

190
		private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<>(4);
191 192 193 194

		private StringBuilder sb;

		@Override
195 196
		public SseEventBuilder id(String id) {
			append("id:").append(id).append("\n");
197 198 199 200 201
			return this;
		}

		@Override
		public SseEventBuilder name(String name) {
202
			append("event:").append(name).append("\n");
203 204 205 206
			return this;
		}

		@Override
207 208
		public SseEventBuilder reconnectTime(long reconnectTimeMillis) {
			append("retry:").append(String.valueOf(reconnectTimeMillis)).append("\n");
209 210 211 212
			return this;
		}

		@Override
213 214
		public SseEventBuilder comment(String comment) {
			append(":").append(comment).append("\n");
215 216 217 218 219 220 221 222 223
			return this;
		}

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

		@Override
224
		public SseEventBuilder data(Object object, @Nullable MediaType mediaType) {
225 226
			append("data:");
			saveAppendedText();
227
			this.dataToSend.add(new DataWithMediaType(object, mediaType));
228 229 230 231
			append("\n");
			return this;
		}

232
		SseEventBuilderImpl append(String text) {
233 234 235 236 237 238 239
			if (this.sb == null) {
				this.sb = new StringBuilder();
			}
			this.sb.append(text);
			return this;
		}

240 241
		@Override
		public Set<DataWithMediaType> build() {
S
stonio 已提交
242
			if (!StringUtils.hasLength(this.sb) && this.dataToSend.isEmpty()) {
243
				return Collections.emptySet();
244
			}
245 246 247
			append("\n");
			saveAppendedText();
			return this.dataToSend;
248 249
		}

250 251 252 253
		private void saveAppendedText() {
			if (this.sb != null) {
				this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN));
				this.sb = null;
254 255 256 257 258
			}
		}
	}

}