SseEmitter.java 6.4 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;
S
stonio 已提交
28
import org.springframework.util.StringUtils;
29 30

/**
31
 * A specialization of {@link ResponseBodyEmitter} for sending
32 33 34
 * <a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
 *
 * @author Rossen Stoyanchev
35
 * @author Juergen Hoeller
36 37 38 39
 * @since 4.2
 */
public class SseEmitter extends ResponseBodyEmitter {

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

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

44

45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
	/**
	 * 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);
	}


65 66 67
	@Override
	protected void extendResponse(ServerHttpResponse outputMessage) {
		super.extendResponse(outputMessage);
68

69 70
		HttpHeaders headers = outputMessage.getHeaders();
		if (headers.getContentType() == null) {
71
			headers.setContentType(UTF8_TEXT_EVENTSTREAM);
72 73 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
		}
	}

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

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

131

132
	public static SseEventBuilder event() {
133
		return new SseEventBuilderImpl();
134 135 136 137 138 139 140 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
	}


	/**
	 * 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);
171 172 173 174

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

180

181 182 183
	/**
	 * Default implementation of SseEventBuilder.
	 */
184
	private static class SseEventBuilderImpl implements SseEventBuilder {
185

186
		private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<>(4);
187 188 189 190 191 192 193 194 195 196 197

		private StringBuilder sb;

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

		@Override
		public SseEventBuilder name(String name) {
198
			append("event:").append(name != null ? name : "").append("\n");
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
			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();
223
			this.dataToSend.add(new DataWithMediaType(object, mediaType));
224 225 226 227
			append("\n");
			return this;
		}

228
		SseEventBuilderImpl append(String text) {
229 230 231 232 233 234 235
			if (this.sb == null) {
				this.sb = new StringBuilder();
			}
			this.sb.append(text);
			return this;
		}

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

246 247 248 249
		private void saveAppendedText() {
			if (this.sb != null) {
				this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN));
				this.sb = null;
250 251 252 253 254
			}
		}
	}

}