Skip to content

Commit 6a96c26

Browse files
committed
Add HttpRange tests, set Accept-Range header, polish
Issue: SPR-10805
1 parent da48739 commit 6a96c26

File tree

5 files changed

+196
-135
lines changed

5 files changed

+196
-135
lines changed

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

Lines changed: 57 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
import org.springframework.util.StringUtils;
2828

2929
/**
30-
* Represents an HTTP (byte) range, as used in the {@code Range} header.
30+
* Represents an HTTP (byte) range for use with the HTTP {@code "Range"} header.
3131
*
3232
* @author Arjen Poutsma
3333
* @see <a href="http://tools.ietf.org/html/rfc7233">HTTP/1.1: Range Requests</a>
@@ -41,57 +41,51 @@ public abstract class HttpRange {
4141

4242

4343
/**
44-
* Creates a {@code HttpRange} that ranges from the given position to the end of the
45-
* representation.
44+
* Return the start of the range given the total length of a representation.
45+
* @param length the length of the representation
46+
* @return the start of this range for the representation
47+
*/
48+
public abstract long getRangeStart(long length);
49+
50+
/**
51+
* Return the end of the range (inclusive) given the total length of a representation.
52+
* @param length the length of the representation
53+
* @return the end of the range for the representation
54+
*/
55+
public abstract long getRangeEnd(long length);
56+
57+
58+
/**
59+
* Create an {@code HttpRange} from the given position to the end.
4660
* @param firstBytePos the first byte position
47-
* @return a byte range that ranges from {@code firstBytePos} till the end
61+
* @return a byte range that ranges from {@code firstPos} till the end
4862
* @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
4963
*/
5064
public static HttpRange createByteRange(long firstBytePos) {
5165
return new ByteRange(firstBytePos, null);
5266
}
5367

5468
/**
55-
* Creates a {@code HttpRange} that ranges from the given fist position to the given
56-
* last position.
69+
* Create a {@code HttpRange} from the given fist to last position.
5770
* @param firstBytePos the first byte position
5871
* @param lastBytePos the last byte position
59-
* @return a byte range that ranges from {@code firstBytePos} till {@code lastBytePos}
72+
* @return a byte range that ranges from {@code firstPos} till {@code lastPos}
6073
* @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
6174
*/
6275
public static HttpRange createByteRange(long firstBytePos, long lastBytePos) {
63-
Assert.isTrue(firstBytePos <= lastBytePos,
64-
"\"firstBytePost\" should be " + "less then or equal to \"lastBytePos\"");
6576
return new ByteRange(firstBytePos, lastBytePos);
6677
}
6778

6879
/**
69-
* Creates a {@code HttpRange} that ranges over the last given number of bytes.
70-
* @param suffixLength the number of bytes
80+
* Create an {@code HttpRange} that ranges over the last given number of bytes.
81+
* @param suffixLength the number of bytes for the range
7182
* @return a byte range that ranges over the last {@code suffixLength} number of bytes
7283
* @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
7384
*/
7485
public static HttpRange createSuffixRange(long suffixLength) {
7586
return new SuffixByteRange(suffixLength);
7687
}
7788

78-
79-
/**
80-
* Return the start of this range, given the total length of the representation.
81-
* @param length the length of the representation.
82-
* @return the start of this range
83-
*/
84-
public abstract long getRangeStart(long length);
85-
86-
/**
87-
* Return the end of this range (inclusive), given the total length of the
88-
* representation.
89-
* @param length the length of the representation.
90-
* @return the end of this range
91-
*/
92-
public abstract long getRangeEnd(long length);
93-
94-
9589
/**
9690
* Parse the given, comma-separated string into a list of {@code HttpRange} objects.
9791
* <p>This method can be used to parse an {@code Range} header.
@@ -104,8 +98,7 @@ public static List<HttpRange> parseRanges(String ranges) {
10498
return Collections.emptyList();
10599
}
106100
if (!ranges.startsWith(BYTE_RANGE_PREFIX)) {
107-
throw new IllegalArgumentException("Range \"" + ranges + "\" does not " +
108-
"start with \"" + BYTE_RANGE_PREFIX + "\"");
101+
throw new IllegalArgumentException("Range '" + ranges + "' does not start with 'bytes='");
109102
}
110103
ranges = ranges.substring(BYTE_RANGE_PREFIX.length());
111104

@@ -118,36 +111,25 @@ public static List<HttpRange> parseRanges(String ranges) {
118111
}
119112

120113
private static HttpRange parseRange(String range) {
121-
if (range == null) {
122-
return null;
123-
}
114+
Assert.notNull(range);
124115
int dashIdx = range.indexOf('-');
125-
if (dashIdx < 0) {
126-
throw new IllegalArgumentException("Range '\"" + range + "\" does not" +
127-
"contain \"-\"");
128-
}
129-
else if (dashIdx > 0) {
130-
// standard byte range, i.e. "bytes=0-500"
116+
if (dashIdx > 0) {
131117
long firstPos = Long.parseLong(range.substring(0, dashIdx));
132-
ByteRange byteRange;
133118
if (dashIdx < range.length() - 1) {
134-
long lastPos =
135-
Long.parseLong(range.substring(dashIdx + 1, range.length()));
136-
byteRange = new ByteRange(firstPos, lastPos);
119+
Long lastPos = Long.parseLong(range.substring(dashIdx + 1, range.length()));
120+
return new ByteRange(firstPos, lastPos);
137121
}
138122
else {
139-
byteRange = new ByteRange(firstPos, null);
123+
return new ByteRange(firstPos, null);
140124
}
141-
if (!byteRange.validate()) {
142-
throw new IllegalArgumentException("Invalid Range \"" + range + "\"");
143-
}
144-
return byteRange;
145125
}
146-
else { // dashIdx == 0
147-
// suffix byte range, i.e. "bytes=-500"
126+
else if (dashIdx == 0) {
148127
long suffixLength = Long.parseLong(range.substring(1));
149128
return new SuffixByteRange(suffixLength);
150129
}
130+
else {
131+
throw new IllegalArgumentException("Range '" + range + "' does not contain \"-\"");
132+
}
151133
}
152134

153135
/**
@@ -157,6 +139,7 @@ else if (dashIdx > 0) {
157139
* @return the string representation
158140
*/
159141
public static String toString(Collection<HttpRange> ranges) {
142+
Assert.notNull(ranges);
160143
StringBuilder builder = new StringBuilder(BYTE_RANGE_PREFIX);
161144
for (Iterator<HttpRange> iterator = ranges.iterator(); iterator.hasNext(); ) {
162145
HttpRange range = iterator.next();
@@ -177,6 +160,7 @@ public String toString() {
177160

178161
abstract void appendTo(StringBuilder builder);
179162

163+
180164
/**
181165
* Represents an HTTP/1.1 byte range, with a first and optional last position.
182166
* @see <a href="http://tools.ietf.org/html/rfc7233#section-2.1">Byte Ranges</a>
@@ -189,11 +173,23 @@ private static class ByteRange extends HttpRange {
189173

190174
private final Long lastPos;
191175

176+
192177
private ByteRange(long firstPos, Long lastPos) {
178+
assertPositions(firstPos, lastPos);
193179
this.firstPos = firstPos;
194180
this.lastPos = lastPos;
195181
}
196182

183+
private void assertPositions(long firstBytePos, Long lastBytePos) {
184+
if (firstBytePos < 0) {
185+
throw new IllegalArgumentException("Invalid firstPos=" + firstBytePos);
186+
}
187+
if (lastBytePos != null && lastBytePos < firstBytePos) {
188+
throw new IllegalArgumentException("firstPost= " + firstBytePos +
189+
" should be less then or equal to lastBytePosition=" + lastBytePos);
190+
}
191+
}
192+
197193
@Override
198194
public long getRangeStart(long length) {
199195
return this.firstPos;
@@ -206,7 +202,6 @@ public long getRangeEnd(long length) {
206202
}
207203
else {
208204
return length - 1;
209-
210205
}
211206
}
212207

@@ -219,18 +214,6 @@ void appendTo(StringBuilder builder) {
219214
}
220215
}
221216

222-
boolean validate() {
223-
if (this.firstPos < 0) {
224-
return false;
225-
}
226-
if (this.lastPos == null) {
227-
return true;
228-
}
229-
else {
230-
return this.firstPos <= this.lastPos;
231-
}
232-
}
233-
234217
@Override
235218
public boolean equals(Object o) {
236219
if (this == o) {
@@ -239,11 +222,8 @@ public boolean equals(Object o) {
239222
if (!(o instanceof ByteRange)) {
240223
return false;
241224
}
242-
243225
ByteRange other = (ByteRange) o;
244-
245-
return this.firstPos == other.firstPos &&
246-
ObjectUtils.nullSafeEquals(this.lastPos, other.lastPos);
226+
return this.firstPos == other.firstPos && ObjectUtils.nullSafeEquals(this.lastPos, other.lastPos);
247227
}
248228

249229
@Override
@@ -252,7 +232,6 @@ public int hashCode() {
252232
hashCode = 31 * hashCode + ObjectUtils.nullSafeHashCode(this.lastPos);
253233
return hashCode;
254234
}
255-
256235
}
257236

258237
/**
@@ -264,15 +243,14 @@ private static class SuffixByteRange extends HttpRange {
264243

265244
private final long suffixLength;
266245

246+
267247
private SuffixByteRange(long suffixLength) {
248+
if (suffixLength < 0) {
249+
throw new IllegalArgumentException("Invalid suffixLength=" + suffixLength);
250+
}
268251
this.suffixLength = suffixLength;
269252
}
270253

271-
@Override
272-
void appendTo(StringBuilder builder) {
273-
builder.append('-');
274-
builder.append(this.suffixLength);
275-
}
276254

277255
@Override
278256
public long getRangeStart(long length) {
@@ -289,6 +267,12 @@ public long getRangeEnd(long length) {
289267
return length - 1;
290268
}
291269

270+
@Override
271+
void appendTo(StringBuilder builder) {
272+
builder.append('-');
273+
builder.append(this.suffixLength);
274+
}
275+
292276
@Override
293277
public boolean equals(Object o) {
294278
if (this == o) {

spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.junit.Test;
3737

3838
/**
39+
* Unit tests for {@link org.springframework.http.HttpHeaders}.
3940
* @author Arjen Poutsma
4041
*/
4142
public class HttpHeadersTests {
@@ -266,16 +267,4 @@ public void getAllowEmptySet() {
266267
assertThat(headers.getAllow(), Matchers.emptyCollectionOf(HttpMethod.class));
267268
}
268269

269-
@Test
270-
public void range() {
271-
List<HttpRange> ranges = new ArrayList<>();
272-
ranges.add(HttpRange.createByteRange(0, 499));
273-
ranges.add(HttpRange.createByteRange(9500));
274-
ranges.add(HttpRange.createSuffixRange(500));
275-
276-
headers.setRange(ranges);
277-
assertEquals("Invalid Range header", ranges, headers.getRange());
278-
assertEquals("Invalid Range header", "bytes=0-499, 9500-, -500", headers.getFirst("Range"));
279-
}
280-
281270
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright 2002-2015 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+
package org.springframework.http;
17+
18+
import static org.junit.Assert.*;
19+
20+
import java.util.ArrayList;
21+
import java.util.List;
22+
23+
import org.junit.Test;
24+
25+
/**
26+
* Unit tests for {@link org.springframework.http.HttpRange}.
27+
* @author Rossen Stoyanchev
28+
*/
29+
public class HttpRangeTests {
30+
31+
32+
@Test(expected = IllegalArgumentException.class)
33+
public void invalidFirstPosition() throws Exception {
34+
HttpRange.createByteRange(-1);
35+
}
36+
37+
@Test(expected = IllegalArgumentException.class)
38+
public void invalidLastLessThanFirst() throws Exception {
39+
HttpRange.createByteRange(10, 9);
40+
}
41+
42+
@Test(expected = IllegalArgumentException.class)
43+
public void invalidSuffixLength() throws Exception {
44+
HttpRange.createSuffixRange(-1);
45+
}
46+
47+
@Test
48+
public void byteRange() throws Exception {
49+
HttpRange range = HttpRange.createByteRange(0, 499);
50+
assertEquals(0, range.getRangeStart(1000));
51+
assertEquals(499, range.getRangeEnd(1000));
52+
}
53+
54+
@Test
55+
public void byteRangeWithoutLastPosition() throws Exception {
56+
HttpRange range = HttpRange.createByteRange(9500);
57+
assertEquals(9500, range.getRangeStart(10000));
58+
assertEquals(9999, range.getRangeEnd(10000));
59+
}
60+
61+
@Test
62+
public void byteRangeOfZeroLength() throws Exception {
63+
HttpRange range = HttpRange.createByteRange(9500, 9500);
64+
assertEquals(9500, range.getRangeStart(10000));
65+
assertEquals(9500, range.getRangeEnd(10000));
66+
}
67+
68+
@Test
69+
public void suffixRange() throws Exception {
70+
HttpRange range = HttpRange.createSuffixRange(500);
71+
assertEquals(500, range.getRangeStart(1000));
72+
assertEquals(999, range.getRangeEnd(1000));
73+
}
74+
75+
@Test
76+
public void suffixRangeShorterThanRepresentation() throws Exception {
77+
HttpRange range = HttpRange.createSuffixRange(500);
78+
assertEquals(0, range.getRangeStart(350));
79+
assertEquals(349, range.getRangeEnd(350));
80+
}
81+
82+
@Test
83+
public void parseRanges() throws Exception {
84+
List<HttpRange> ranges = HttpRange.parseRanges("bytes=0-0,500-,-1");
85+
assertEquals(3, ranges.size());
86+
assertEquals(0, ranges.get(0).getRangeStart(1000));
87+
assertEquals(0, ranges.get(0).getRangeEnd(1000));
88+
assertEquals(500, ranges.get(1).getRangeStart(1000));
89+
assertEquals(999, ranges.get(1).getRangeEnd(1000));
90+
assertEquals(999, ranges.get(2).getRangeStart(1000));
91+
assertEquals(999, ranges.get(2).getRangeEnd(1000));
92+
}
93+
94+
@Test
95+
public void rangeToString() {
96+
List<HttpRange> ranges = new ArrayList<>();
97+
ranges.add(HttpRange.createByteRange(0, 499));
98+
ranges.add(HttpRange.createByteRange(9500));
99+
ranges.add(HttpRange.createSuffixRange(500));
100+
assertEquals("Invalid Range header", "bytes=0-499, 9500-, -500", HttpRange.toString(ranges));
101+
}
102+
103+
}

0 commit comments

Comments
 (0)