Skip to content

Commit 5ac31fb

Browse files
committed
Refactor HTTP Range support with ResourceRegion
Prior to this commit, the `ResourceHttpMessageConverter` would support all HTTP Range requests and `MethodProcessors` would "wrap" controller handler return values with a `HttpRangeResource` to support that use case in Controllers. This commit refactors that support in several ways: * a new ResourceRegion class has been introduced * a new, separate, ResourceRegionHttpMessageConverter handles the HTTP range use cases when serving static resources with the ResourceHttpRequestHandler * the support of HTTP range requests on Controller handlers has been removed until a better solution is found Issue: SPR-14221, SPR-13834
1 parent 7737c3c commit 5ac31fb

File tree

16 files changed

+627
-468
lines changed

16 files changed

+627
-468
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.core.io;
18+
19+
import org.springframework.util.Assert;
20+
21+
/**
22+
* Region of a {@link Resource} implementation, materialized by a {@code position}
23+
* within the {@link Resource} and a byte {@code count} for the length of that region.
24+
* @author Arjen Poutsma
25+
* @since 4.3.0
26+
*/
27+
public class ResourceRegion {
28+
29+
private final Resource resource;
30+
31+
private final long position;
32+
33+
private final long count;
34+
35+
/**
36+
* Create a new {@code ResourceRegion} from a given {@link Resource}.
37+
* This region of a resource is reprensented by a start {@code position}
38+
* and a byte {@code count} within the given {@code Resource}.
39+
* @param resource a Resource
40+
* @param position the start position of the region in that resource
41+
* @param count the byte count of the region in that resource
42+
*/
43+
public ResourceRegion(Resource resource, long position, long count) {
44+
Assert.notNull(resource, "'resource' must not be null");
45+
Assert.isTrue(position >= 0, "'position' must be larger than or equal to 0");
46+
Assert.isTrue(count >= 0, "'count' must be larger than or equal to 0");
47+
this.resource = resource;
48+
this.position = position;
49+
this.count = count;
50+
}
51+
52+
/**
53+
* Return the underlying {@link Resource} for this {@code ResourceRegion}
54+
*/
55+
public Resource getResource() {
56+
return this.resource;
57+
}
58+
59+
/**
60+
* Return the start position of this region in the underlying {@link Resource}
61+
*/
62+
public long getPosition() {
63+
return this.position;
64+
}
65+
66+
/**
67+
* Return the byte count of this region in the underlying {@link Resource}
68+
*/
69+
public long getCount() {
70+
return this.count;
71+
}
72+
}

spring-core/src/main/java/org/springframework/util/StreamUtils.java

+38
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
*
3838
* @author Juergen Hoeller
3939
* @author Phillip Webb
40+
* @author Brian Clozel
4041
* @since 3.2.2
4142
* @see FileCopyUtils
4243
*/
@@ -131,6 +132,43 @@ public static int copy(InputStream in, OutputStream out) throws IOException {
131132
return byteCount;
132133
}
133134

135+
/**
136+
* Copy a range of content of the given InputStream to the given OutputStream.
137+
* <p>If the specified range exceeds the length of the InputStream, this copies
138+
* up to the end of the stream and returns the actual number of copied bytes.
139+
* <p>Leaves both streams open when done.
140+
* @param in the InputStream to copy from
141+
* @param out the OutputStream to copy to
142+
* @param start the position to start copying from
143+
* @param end the position to end copying
144+
* @return the number of bytes copied
145+
* @throws IOException in case of I/O errors
146+
* @since 4.3.0
147+
*/
148+
public static long copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
149+
long skipped = in.skip(start);
150+
if (skipped < start) {
151+
throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required.");
152+
}
153+
long bytesToCopy = end - start + 1;
154+
byte buffer[] = new byte[StreamUtils.BUFFER_SIZE];
155+
while (bytesToCopy > 0) {
156+
int bytesRead = in.read(buffer);
157+
if (bytesRead == -1) {
158+
break;
159+
}
160+
else if (bytesRead <= bytesToCopy) {
161+
out.write(buffer, 0, bytesRead);
162+
bytesToCopy -= bytesRead;
163+
}
164+
else {
165+
out.write(buffer, 0, (int) bytesToCopy);
166+
bytesToCopy = 0;
167+
}
168+
}
169+
return end - start + 1 - bytesToCopy;
170+
}
171+
134172
/**
135173
* Drain the remaining content of the given InputStream.
136174
* Leaves the InputStream open when done.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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.core.io;
18+
19+
import static org.mockito.Mockito.mock;
20+
21+
import org.junit.Test;
22+
23+
/**
24+
* Unit tests for the {@link ResourceRegion} class.
25+
*
26+
* @author Brian Clozel
27+
*/
28+
public class ResourceRegionTests {
29+
30+
@Test(expected = IllegalArgumentException.class)
31+
public void shouldThrowExceptionWithNullResource() {
32+
new ResourceRegion(null, 0, 1);
33+
}
34+
35+
@Test(expected = IllegalArgumentException.class)
36+
public void shouldThrowExceptionForNegativePosition() {
37+
new ResourceRegion(mock(Resource.class), -1, 1);
38+
}
39+
40+
@Test(expected = IllegalArgumentException.class)
41+
public void shouldThrowExceptionForNegativeCount() {
42+
new ResourceRegion(mock(Resource.class), 0, -1);
43+
}
44+
}

spring-core/src/test/java/org/springframework/util/StreamUtilsTests.java

+10
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.io.InputStream;
2222
import java.io.OutputStream;
2323
import java.nio.charset.Charset;
24+
import java.util.Arrays;
2425
import java.util.Random;
2526
import java.util.UUID;
2627

@@ -93,6 +94,15 @@ public void copyStream() throws Exception {
9394
verify(out, never()).close();
9495
}
9596

97+
@Test
98+
public void copyRange() throws Exception {
99+
ByteArrayOutputStream out = spy(new ByteArrayOutputStream());
100+
StreamUtils.copyRange(new ByteArrayInputStream(bytes), out, 0, 100);
101+
byte[] range = Arrays.copyOfRange(bytes, 0, 101);
102+
assertThat(out.toByteArray(), equalTo(range));
103+
verify(out, never()).close();
104+
}
105+
96106
@Test
97107
public void nonClosingInputStream() throws Exception {
98108
InputStream source = mock(InputStream.class);

spring-web/src/main/java/org/springframework/http/HttpRange.java

+48
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@
1616

1717
package org.springframework.http;
1818

19+
import java.io.IOException;
1920
import java.util.ArrayList;
2021
import java.util.Collection;
2122
import java.util.Collections;
2223
import java.util.Iterator;
2324
import java.util.List;
2425

26+
import org.springframework.core.io.InputStreamResource;
27+
import org.springframework.core.io.Resource;
28+
import org.springframework.core.io.ResourceRegion;
2529
import org.springframework.util.Assert;
2630
import org.springframework.util.ObjectUtils;
2731
import org.springframework.util.StringUtils;
@@ -55,6 +59,30 @@ public abstract class HttpRange {
5559
*/
5660
public abstract long getRangeEnd(long length);
5761

62+
/**
63+
* Turn a {@code Resource} into a {@link ResourceRegion} using the range
64+
* information contained in the current {@code HttpRange}.
65+
* @param resource the {@code Resource} to select the region from
66+
* @return the selected region of the given {@code Resource}
67+
* @since 4.3.0
68+
*/
69+
public ResourceRegion toResourceRegion(Resource resource) {
70+
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
71+
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
72+
Assert.isTrue(InputStreamResource.class != resource.getClass(),
73+
"Can't convert an InputStreamResource to a ResourceRegion");
74+
try {
75+
long contentLength = resource.contentLength();
76+
Assert.isTrue(contentLength > 0, "Resource content length should be > 0");
77+
long start = getRangeStart(contentLength);
78+
long end = getRangeEnd(contentLength);
79+
return new ResourceRegion(resource, start, end - start + 1);
80+
}
81+
catch (IOException exc) {
82+
throw new IllegalArgumentException("Can't convert this Resource to a ResourceRegion", exc);
83+
}
84+
}
85+
5886

5987
/**
6088
* Create an {@code HttpRange} from the given position to the end.
@@ -133,6 +161,26 @@ else if (dashIdx == 0) {
133161
}
134162
}
135163

164+
/**
165+
* Convert each {@code HttpRange} into a {@code ResourceRegion},
166+
* selecting the appropriate segment of the given {@code Resource}
167+
* using the HTTP Range information.
168+
*
169+
* @param ranges the list of ranges
170+
* @param resource the resource to select the regions from
171+
* @return the list of regions for the given resource
172+
*/
173+
public static List<ResourceRegion> toResourceRegions(List<HttpRange> ranges, Resource resource) {
174+
if(ranges == null || ranges.size() == 0) {
175+
return Collections.emptyList();
176+
}
177+
List<ResourceRegion> regions = new ArrayList<ResourceRegion>(ranges.size());
178+
for(HttpRange range : ranges) {
179+
regions.add(range.toResourceRegion(resource));
180+
}
181+
return regions;
182+
}
183+
136184
/**
137185
* Return a string representation of the given list of {@code HttpRange} objects.
138186
* <p>This method can be used to for an {@code Range} header.

spring-web/src/main/java/org/springframework/http/HttpRangeResource.java

-123
This file was deleted.

0 commit comments

Comments
 (0)