Skip to content

Commit c7bd3b8

Browse files
committed
Support HTTP range requests in Controllers
Prior to this commit, HTTP Range requests were only supported by the ResourceHttpRequestHandler when serving static resources. This commit improves the ResourceHttpMessageConverter that now supports partial writes of Resources. For this, the `HttpEntityMethodProcessor` and `RequestResponseBodyMethodProcessor` now wrap resources with HTTP range information in a `HttpRangeResource`, if necessary. The message converter handle those types and knows how to handle partial writes. Controller methods can now handle Range requests for return types that extend Resource or HttpEntity: @RequestMapping("/example/video.mp4") public Resource handler() { } @RequestMapping("/example/video.mp4") public HttpEntity<Resource> handler() { } Issue: SPR-13834
1 parent 15fe827 commit c7bd3b8

File tree

10 files changed

+699
-326
lines changed

10 files changed

+699
-326
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.InputStream;
22+
import java.net.URI;
23+
import java.net.URL;
24+
import java.util.List;
25+
26+
import org.springframework.core.io.Resource;
27+
import org.springframework.util.Assert;
28+
29+
/**
30+
* Holder that combines a {@link Resource} descriptor with
31+
* {@link HttpRange} information to be used for reading
32+
* selected parts of the resource.
33+
*
34+
* <p>Used as an argument for partial conversion operations in
35+
* {@link org.springframework.http.converter.ResourceHttpMessageConverter}.
36+
*
37+
* @author Brian Clozel
38+
* @since 4.3.0
39+
*/
40+
public class HttpRangeResource implements Resource {
41+
42+
private final List<HttpRange> httpRanges;
43+
44+
private final Resource resource;
45+
46+
public HttpRangeResource(List<HttpRange> httpRanges, Resource resource) {
47+
Assert.notEmpty(httpRanges, "list of HTTP Ranges should not be empty");
48+
this.httpRanges = httpRanges;
49+
this.resource = resource;
50+
}
51+
52+
/**
53+
* Return the list of HTTP (byte) ranges describing the requested
54+
* parts of the Resource, as provided by the HTTP Range request.
55+
*/
56+
public final List<HttpRange> getHttpRanges() {
57+
return httpRanges;
58+
}
59+
60+
@Override
61+
public boolean exists() {
62+
return resource.exists();
63+
}
64+
65+
@Override
66+
public boolean isReadable() {
67+
return resource.isReadable();
68+
}
69+
70+
@Override
71+
public boolean isOpen() {
72+
return resource.isOpen();
73+
}
74+
75+
@Override
76+
public URL getURL() throws IOException {
77+
return resource.getURL();
78+
}
79+
80+
@Override
81+
public URI getURI() throws IOException {
82+
return resource.getURI();
83+
}
84+
85+
@Override
86+
public File getFile() throws IOException {
87+
return resource.getFile();
88+
}
89+
90+
@Override
91+
public long contentLength() throws IOException {
92+
return resource.contentLength();
93+
}
94+
95+
@Override
96+
public long lastModified() throws IOException {
97+
return resource.lastModified();
98+
}
99+
100+
@Override
101+
public Resource createRelative(String relativePath) throws IOException {
102+
return resource.createRelative(relativePath);
103+
}
104+
105+
@Override
106+
public String getFilename() {
107+
return resource.getFilename();
108+
}
109+
110+
@Override
111+
public String getDescription() {
112+
return resource.getDescription();
113+
}
114+
115+
@Override
116+
public InputStream getInputStream() throws IOException {
117+
return resource.getInputStream();
118+
}
119+
}

spring-web/src/main/java/org/springframework/http/converter/ResourceHttpMessageConverter.java

Lines changed: 147 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,32 +16,46 @@
1616

1717
package org.springframework.http.converter;
1818

19+
import java.io.FileNotFoundException;
1920
import java.io.IOException;
2021
import java.io.InputStream;
22+
import java.io.OutputStream;
23+
import java.util.List;
24+
2125
import javax.activation.FileTypeMap;
2226
import javax.activation.MimetypesFileTypeMap;
2327

2428
import org.springframework.core.io.ByteArrayResource;
2529
import org.springframework.core.io.ClassPathResource;
2630
import org.springframework.core.io.InputStreamResource;
2731
import org.springframework.core.io.Resource;
32+
import org.springframework.http.HttpHeaders;
2833
import org.springframework.http.HttpInputMessage;
2934
import org.springframework.http.HttpOutputMessage;
35+
import org.springframework.http.HttpRange;
36+
import org.springframework.http.HttpRangeResource;
3037
import org.springframework.http.MediaType;
38+
import org.springframework.util.Assert;
3139
import org.springframework.util.ClassUtils;
40+
import org.springframework.util.MimeTypeUtils;
3241
import org.springframework.util.StreamUtils;
3342
import org.springframework.util.StringUtils;
3443

3544
/**
36-
* Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}.
45+
* Implementation of {@link HttpMessageConverter} that can read and write {@link Resource Resources}
46+
* and supports byte range requests.
3747
*
3848
* <p>By default, this converter can read all media types. The Java Activation Framework (JAF) -
3949
* if available - is used to determine the {@code Content-Type} of written resources.
4050
* If JAF is not available, {@code application/octet-stream} is used.
4151
*
52+
* <p>This converter supports HTTP byte range requests and can write partial content, when provided
53+
* with an {@link HttpRangeResource} instance containing the required Range information.
54+
*
4255
* @author Arjen Poutsma
4356
* @author Juergen Hoeller
4457
* @author Kazuki Shimizu
58+
* @author Brian Clozel
4559
* @since 3.0.2
4660
*/
4761
public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<Resource> {
@@ -64,7 +78,7 @@ protected boolean supports(Class<?> clazz) {
6478
protected Resource readInternal(Class<? extends Resource> clazz, HttpInputMessage inputMessage)
6579
throws IOException, HttpMessageNotReadableException {
6680

67-
if (InputStreamResource.class == clazz){
81+
if (InputStreamResource.class == clazz) {
6882
return new InputStreamResource(inputMessage.getBody());
6983
}
7084
else if (clazz.isAssignableFrom(ByteArrayResource.class)) {
@@ -94,25 +108,150 @@ protected Long getContentLength(Resource resource, MediaType contentType) throws
94108
return null;
95109
}
96110
long contentLength = resource.contentLength();
111+
if (contentLength > Integer.MAX_VALUE) {
112+
throw new IOException("Resource content too long (beyond Integer.MAX_VALUE): " + resource);
113+
}
97114
return (contentLength < 0 ? null : contentLength);
98115
}
99116

100117
@Override
101118
protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
102119
throws IOException, HttpMessageNotWritableException {
103120

104-
InputStream in = resource.getInputStream();
121+
outputMessage.getHeaders().add(HttpHeaders.ACCEPT_RANGES, "bytes");
122+
if (resource instanceof HttpRangeResource) {
123+
writePartialContent((HttpRangeResource) resource, outputMessage);
124+
}
125+
else {
126+
writeContent(resource, outputMessage);
127+
}
128+
}
129+
130+
protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
131+
throws IOException, HttpMessageNotWritableException {
105132
try {
106-
StreamUtils.copy(in, outputMessage.getBody());
133+
InputStream in = resource.getInputStream();
134+
try {
135+
StreamUtils.copy(in, outputMessage.getBody());
136+
}
137+
catch (NullPointerException ex) {
138+
// ignore, see SPR-13620
139+
}
140+
finally {
141+
try {
142+
in.close();
143+
}
144+
catch (Throwable ex) {
145+
// ignore, see SPR-12999
146+
}
147+
}
107148
}
108-
finally {
149+
catch (FileNotFoundException ex) {
150+
// ignore, see SPR-12999
151+
}
152+
}
153+
154+
/**
155+
* Write parts of the resource as indicated by the request {@code Range} header.
156+
* @param resource the identified resource (never {@code null})
157+
* @param outputMessage current servlet response
158+
* @throws IOException in case of errors while writing the content
159+
*/
160+
protected void writePartialContent(HttpRangeResource resource, HttpOutputMessage outputMessage) throws IOException {
161+
162+
Assert.notNull(resource, "Resource should not be null");
163+
164+
List<HttpRange> ranges = resource.getHttpRanges();
165+
HttpHeaders responseHeaders = outputMessage.getHeaders();
166+
MediaType contentType = responseHeaders.getContentType();
167+
Long length = getContentLength(resource, contentType);
168+
169+
if (ranges.size() == 1) {
170+
HttpRange range = ranges.get(0);
171+
172+
long start = range.getRangeStart(length);
173+
long end = range.getRangeEnd(length);
174+
long rangeLength = end - start + 1;
175+
176+
responseHeaders.add("Content-Range", "bytes " + start + "-" + end + "/" + length);
177+
responseHeaders.setContentLength((int) rangeLength);
178+
179+
InputStream in = resource.getInputStream();
109180
try {
110-
in.close();
181+
copyRange(in, outputMessage.getBody(), start, end);
182+
}
183+
finally {
184+
try {
185+
in.close();
186+
}
187+
catch (IOException ex) {
188+
// ignore
189+
}
190+
}
191+
}
192+
else {
193+
String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
194+
responseHeaders.set(HttpHeaders.CONTENT_TYPE, "multipart/byteranges; boundary=" + boundaryString);
195+
196+
OutputStream out = outputMessage.getBody();
197+
198+
for (HttpRange range : ranges) {
199+
long start = range.getRangeStart(length);
200+
long end = range.getRangeEnd(length);
201+
202+
InputStream in = resource.getInputStream();
203+
204+
// Writing MIME header.
205+
println(out);
206+
print(out, "--" + boundaryString);
207+
println(out);
208+
if (contentType != null) {
209+
print(out, "Content-Type: " + contentType.toString());
210+
println(out);
211+
}
212+
print(out, "Content-Range: bytes " + start + "-" + end + "/" + length);
213+
println(out);
214+
println(out);
215+
216+
// Printing content
217+
copyRange(in, out, start, end);
218+
}
219+
println(out);
220+
print(out, "--" + boundaryString + "--");
221+
}
222+
}
223+
224+
private static void println(OutputStream os) throws IOException {
225+
os.write('\r');
226+
os.write('\n');
227+
}
228+
229+
private static void print(OutputStream os, String buf) throws IOException {
230+
os.write(buf.getBytes("US-ASCII"));
231+
}
232+
233+
private void copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
234+
long skipped = in.skip(start);
235+
if (skipped < start) {
236+
throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required.");
237+
}
238+
239+
long bytesToCopy = end - start + 1;
240+
byte buffer[] = new byte[StreamUtils.BUFFER_SIZE];
241+
while (bytesToCopy > 0) {
242+
int bytesRead = in.read(buffer);
243+
if (bytesRead <= bytesToCopy) {
244+
out.write(buffer, 0, bytesRead);
245+
bytesToCopy -= bytesRead;
246+
}
247+
else {
248+
out.write(buffer, 0, (int) bytesToCopy);
249+
bytesToCopy = 0;
111250
}
112-
catch (IOException ex) {
251+
if (bytesRead == -1) {
252+
break;
113253
}
114254
}
115-
outputMessage.getBody().flush();
116255
}
117256

118257

0 commit comments

Comments
 (0)