SseEmitter.java 6.6 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 109
		if (object != null) {
			send(event().data(object, mediaType));
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
		}
	}

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

133 134 135 136 137
	@Override
	public String toString() {
		return "SseEmitter@" + ObjectUtils.getIdentityHexString(this);
	}

138

139
	public static SseEventBuilder event() {
140
		return new SseEventBuilderImpl();
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
	}


	/**
	 * 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.
		 */
177
		SseEventBuilder data(Object object, @Nullable MediaType mediaType);
178 179 180 181

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

187

188 189 190
	/**
	 * Default implementation of SseEventBuilder.
	 */
191
	private static class SseEventBuilderImpl implements SseEventBuilder {
192

193
		private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<>(4);
194 195 196 197 198 199 200 201 202 203 204

		private StringBuilder sb;

		@Override
		public SseEventBuilder comment(String comment) {
			append(":").append(comment != null ? comment : "").append("\n");
			return this;
		}

		@Override
		public SseEventBuilder name(String name) {
205
			append("event:").append(name != null ? name : "").append("\n");
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
			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
227
		public SseEventBuilder data(Object object, @Nullable MediaType mediaType) {
228 229
			append("data:");
			saveAppendedText();
230
			this.dataToSend.add(new DataWithMediaType(object, mediaType));
231 232 233 234
			append("\n");
			return this;
		}

235
		SseEventBuilderImpl append(String text) {
236 237 238 239 240 241 242
			if (this.sb == null) {
				this.sb = new StringBuilder();
			}
			this.sb.append(text);
			return this;
		}

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

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

}