Skip to content

Commit a1320cd

Browse files
committed
Add SSE support to WebMvc.fn
This commit adds support for sending Server-Sent Events in WebMvc.fn, through the ServerResponse.sse method that takes a SseBuilder DSL. It also includes reference documentation. Closes gh-25920
1 parent c7f2f50 commit a1320cd

File tree

10 files changed

+874
-151
lines changed

10 files changed

+874
-151
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.server;
18+
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
22+
import org.springframework.http.HttpHeaders;
23+
import org.springframework.http.HttpStatus;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Implementation of {@code ServerHttpResponse} that delegates all calls to a
28+
* given target {@code ServerHttpResponse}.
29+
*
30+
* @author Arjen Poutsma
31+
* @since 5.3.2
32+
*/
33+
public class DelegatingServerHttpResponse implements ServerHttpResponse {
34+
35+
private final ServerHttpResponse delegate;
36+
37+
/**
38+
* Create a new {@code DelegatingServerHttpResponse}.
39+
* @param delegate the response to delegate to
40+
*/
41+
public DelegatingServerHttpResponse(ServerHttpResponse delegate) {
42+
Assert.notNull(delegate, "Delegate must not be null");
43+
this.delegate = delegate;
44+
}
45+
46+
/**
47+
* Returns the target response that this response delegates to.
48+
* @return the delegate
49+
*/
50+
public ServerHttpResponse getDelegate() {
51+
return this.delegate;
52+
}
53+
54+
@Override
55+
public void setStatusCode(HttpStatus status) {
56+
this.delegate.setStatusCode(status);
57+
}
58+
59+
@Override
60+
public void flush() throws IOException {
61+
this.delegate.flush();
62+
}
63+
64+
@Override
65+
public void close() {
66+
this.delegate.close();
67+
}
68+
69+
@Override
70+
public OutputStream getBody() throws IOException {
71+
return this.delegate.getBody();
72+
}
73+
74+
@Override
75+
public HttpHeaders getHeaders() {
76+
return this.delegate.getHeaders();
77+
}
78+
79+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.servlet.function;
18+
19+
import java.io.IOException;
20+
import java.util.Collection;
21+
import java.util.EnumSet;
22+
import java.util.Set;
23+
24+
import javax.servlet.ServletException;
25+
import javax.servlet.http.Cookie;
26+
import javax.servlet.http.HttpServletRequest;
27+
import javax.servlet.http.HttpServletResponse;
28+
29+
import org.springframework.http.HttpHeaders;
30+
import org.springframework.http.HttpMethod;
31+
import org.springframework.http.HttpStatus;
32+
import org.springframework.lang.Nullable;
33+
import org.springframework.util.CollectionUtils;
34+
import org.springframework.util.LinkedMultiValueMap;
35+
import org.springframework.util.MultiValueMap;
36+
import org.springframework.web.context.request.ServletWebRequest;
37+
import org.springframework.web.servlet.ModelAndView;
38+
39+
/**
40+
* Abstract base class for {@link ServerResponse} implementations.
41+
*
42+
* @author Arjen Poutsma
43+
* @since 5.3.2
44+
*/
45+
abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
46+
47+
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
48+
49+
final int statusCode;
50+
51+
private final HttpHeaders headers;
52+
53+
private final MultiValueMap<String, Cookie> cookies;
54+
55+
protected AbstractServerResponse(
56+
int statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) {
57+
58+
this.statusCode = statusCode;
59+
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
60+
this.cookies =
61+
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
62+
}
63+
64+
@Override
65+
public final HttpStatus statusCode() {
66+
return HttpStatus.valueOf(this.statusCode);
67+
}
68+
69+
@Override
70+
public int rawStatusCode() {
71+
return this.statusCode;
72+
}
73+
74+
@Override
75+
public final HttpHeaders headers() {
76+
return this.headers;
77+
}
78+
79+
@Override
80+
public MultiValueMap<String, Cookie> cookies() {
81+
return this.cookies;
82+
}
83+
84+
@Override
85+
public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response,
86+
Context context) throws ServletException, IOException {
87+
88+
try {
89+
writeStatusAndHeaders(response);
90+
91+
long lastModified = headers().getLastModified();
92+
ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
93+
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
94+
if (SAFE_METHODS.contains(httpMethod) &&
95+
servletWebRequest.checkNotModified(headers().getETag(), lastModified)) {
96+
return null;
97+
}
98+
else {
99+
return writeToInternal(request, response, context);
100+
}
101+
}
102+
catch (Throwable throwable) {
103+
return handleError(throwable, request, response, context);
104+
}
105+
}
106+
107+
private void writeStatusAndHeaders(HttpServletResponse response) {
108+
response.setStatus(this.statusCode);
109+
writeHeaders(response);
110+
writeCookies(response);
111+
}
112+
113+
private void writeHeaders(HttpServletResponse servletResponse) {
114+
this.headers.forEach((headerName, headerValues) -> {
115+
for (String headerValue : headerValues) {
116+
servletResponse.addHeader(headerName, headerValue);
117+
}
118+
});
119+
// HttpServletResponse exposes some headers as properties: we should include those if not already present
120+
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) {
121+
servletResponse.setContentType(this.headers.getContentType().toString());
122+
}
123+
if (servletResponse.getCharacterEncoding() == null &&
124+
this.headers.getContentType() != null &&
125+
this.headers.getContentType().getCharset() != null) {
126+
servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
127+
}
128+
}
129+
130+
private void writeCookies(HttpServletResponse servletResponse) {
131+
this.cookies.values().stream()
132+
.flatMap(Collection::stream)
133+
.forEach(servletResponse::addCookie);
134+
}
135+
136+
@Nullable
137+
protected abstract ModelAndView writeToInternal(
138+
HttpServletRequest request, HttpServletResponse response, Context context)
139+
throws ServletException, IOException;
140+
141+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultEntityResponseBuilder.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,7 @@ public static <T> EntityResponse.Builder<T> fromObject(T t, ParameterizedTypeRef
237237
/**
238238
* Default {@link EntityResponse} implementation for synchronous bodies.
239239
*/
240-
private static class DefaultEntityResponse<T> extends DefaultServerResponseBuilder.AbstractServerResponse
241-
implements EntityResponse<T> {
240+
private static class DefaultEntityResponse<T> extends AbstractServerResponse implements EntityResponse<T> {
242241

243242
private final T entity;
244243

spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultRenderingResponseBuilder.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,7 @@ public RenderingResponse build() {
150150
}
151151

152152

153-
private static final class DefaultRenderingResponse extends DefaultServerResponseBuilder.AbstractServerResponse
154-
implements RenderingResponse {
153+
private static final class DefaultRenderingResponse extends AbstractServerResponse implements RenderingResponse {
155154

156155
private final String name;
157156

spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerResponseBuilder.java

Lines changed: 0 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,16 @@
1616

1717
package org.springframework.web.servlet.function;
1818

19-
import java.io.IOException;
2019
import java.net.URI;
2120
import java.time.Instant;
2221
import java.time.ZonedDateTime;
2322
import java.util.Arrays;
24-
import java.util.Collection;
25-
import java.util.EnumSet;
2623
import java.util.LinkedHashSet;
2724
import java.util.Map;
2825
import java.util.Set;
2926
import java.util.function.BiFunction;
3027
import java.util.function.Consumer;
3128

32-
import javax.servlet.ServletException;
3329
import javax.servlet.http.Cookie;
3430
import javax.servlet.http.HttpServletRequest;
3531
import javax.servlet.http.HttpServletResponse;
@@ -40,12 +36,9 @@
4036
import org.springframework.http.HttpMethod;
4137
import org.springframework.http.HttpStatus;
4238
import org.springframework.http.MediaType;
43-
import org.springframework.lang.Nullable;
4439
import org.springframework.util.Assert;
45-
import org.springframework.util.CollectionUtils;
4640
import org.springframework.util.LinkedMultiValueMap;
4741
import org.springframework.util.MultiValueMap;
48-
import org.springframework.web.context.request.ServletWebRequest;
4942
import org.springframework.web.servlet.ModelAndView;
5043

5144
/**
@@ -224,111 +217,6 @@ public ServerResponse render(String name, Map<String, ?> model) {
224217
}
225218

226219

227-
/**
228-
* Abstract base class for {@link ServerResponse} implementations.
229-
*/
230-
abstract static class AbstractServerResponse extends ErrorHandlingServerResponse {
231-
232-
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
233-
234-
235-
final int statusCode;
236-
237-
private final HttpHeaders headers;
238-
239-
private final MultiValueMap<String, Cookie> cookies;
240-
241-
242-
protected AbstractServerResponse(
243-
int statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) {
244-
245-
this.statusCode = statusCode;
246-
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
247-
this.cookies =
248-
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
249-
}
250-
251-
252-
@Override
253-
public final HttpStatus statusCode() {
254-
return HttpStatus.valueOf(this.statusCode);
255-
}
256-
257-
@Override
258-
public int rawStatusCode() {
259-
return this.statusCode;
260-
}
261-
262-
@Override
263-
public final HttpHeaders headers() {
264-
return this.headers;
265-
}
266-
267-
@Override
268-
public MultiValueMap<String, Cookie> cookies() {
269-
return this.cookies;
270-
}
271-
272-
@Override
273-
public ModelAndView writeTo(HttpServletRequest request, HttpServletResponse response,
274-
Context context) throws ServletException, IOException {
275-
276-
try {
277-
writeStatusAndHeaders(response);
278-
279-
long lastModified = headers().getLastModified();
280-
ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);
281-
HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
282-
if (SAFE_METHODS.contains(httpMethod) &&
283-
servletWebRequest.checkNotModified(headers().getETag(), lastModified)) {
284-
return null;
285-
}
286-
else {
287-
return writeToInternal(request, response, context);
288-
}
289-
}
290-
catch (Throwable throwable) {
291-
return handleError(throwable, request, response, context);
292-
}
293-
}
294-
295-
private void writeStatusAndHeaders(HttpServletResponse response) {
296-
response.setStatus(this.statusCode);
297-
writeHeaders(response);
298-
writeCookies(response);
299-
}
300-
301-
private void writeHeaders(HttpServletResponse servletResponse) {
302-
this.headers.forEach((headerName, headerValues) -> {
303-
for (String headerValue : headerValues) {
304-
servletResponse.addHeader(headerName, headerValue);
305-
}
306-
});
307-
// HttpServletResponse exposes some headers as properties: we should include those if not already present
308-
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) {
309-
servletResponse.setContentType(this.headers.getContentType().toString());
310-
}
311-
if (servletResponse.getCharacterEncoding() == null &&
312-
this.headers.getContentType() != null &&
313-
this.headers.getContentType().getCharset() != null) {
314-
servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
315-
}
316-
}
317-
318-
private void writeCookies(HttpServletResponse servletResponse) {
319-
this.cookies.values().stream()
320-
.flatMap(Collection::stream)
321-
.forEach(servletResponse::addCookie);
322-
}
323-
324-
@Nullable
325-
protected abstract ModelAndView writeToInternal(
326-
HttpServletRequest request, HttpServletResponse response, Context context)
327-
throws ServletException, IOException;
328-
329-
}
330-
331-
332220
private static class WriterFunctionResponse extends AbstractServerResponse {
333221

334222
private final BiFunction<HttpServletRequest, HttpServletResponse, ModelAndView> writeFunction;

0 commit comments

Comments
 (0)