Skip to content

Commit 1ad9e32

Browse files
committed
Byte range refactor
1 parent fbf950b commit 1ad9e32

File tree

7 files changed

+149
-124
lines changed

7 files changed

+149
-124
lines changed
Lines changed: 90 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,133 @@
1-
/**
2-
* Licensed under the Apache License, Version 2.0 (the "License");
3-
* you may not use this file except in compliance with the License.
4-
* You may obtain a copy of the License at
5-
*
6-
* http://www.apache.org/licenses/LICENSE-2.0
7-
*
8-
* Unless required by applicable law or agreed to in writing, software
9-
* distributed under the License is distributed on an "AS IS" BASIS,
10-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11-
* See the License for the specific language governing permissions and
12-
* limitations under the License.
13-
*
14-
* Copyright 2014 Edgar Espina
15-
*/
161
package io.jooby;
172

183
public class ByteRange {
19-
204
private static final String BYTES_EQ = "bytes=";
215

22-
public static final ByteRange NO_RANGE = new ByteRange(-1, -1) {
23-
@Override public ByteRange apply(Context ctx, long contentLength) {
24-
return new ByteRange(0, contentLength);
25-
}
26-
};
6+
private String value;
277

28-
public static final ByteRange NOT_SATISFIABLE = new ByteRange(-1, -1) {
29-
@Override public ByteRange apply(Context ctx, long contentLength) {
30-
throw new Err(StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
31-
}
32-
};
8+
private long start;
9+
10+
private long end;
3311

34-
public final long start;
12+
private long contentLength;
3513

36-
public final long end;
14+
private String contentRange;
3715

38-
private ByteRange(long start, long end) {
16+
private StatusCode statusCode;
17+
18+
private ByteRange(String value, long start, long end, long contentLength, String contentRange,
19+
StatusCode statusCode) {
20+
this.value = value;
3921
this.start = start;
4022
this.end = end;
23+
this.contentLength = contentLength;
24+
this.contentRange = contentRange;
25+
this.statusCode = statusCode;
4126
}
4227

43-
public ByteRange apply(Context ctx, long contentLength) {
44-
long start = this.start;
45-
long end = this.end;
46-
if (start == -1) {
47-
start = contentLength - end;
48-
end = contentLength - 1;
49-
}
50-
if (end == -1 || end > contentLength - 1) {
51-
end = contentLength - 1;
52-
}
53-
if (start > end) {
54-
return NOT_SATISFIABLE;
28+
public long getStart() {
29+
return start;
30+
}
31+
32+
public long getEnd() {
33+
return end;
34+
}
35+
36+
public long getContentLength() {
37+
return contentLength;
38+
}
39+
40+
public String getContentRange() {
41+
return contentRange;
42+
}
43+
44+
public StatusCode getStatusCode() {
45+
return statusCode;
46+
}
47+
48+
public boolean isPartial() {
49+
return statusCode == StatusCode.PARTIAL_CONTENT;
50+
}
51+
52+
public ByteRange apply(Context ctx) {
53+
if (statusCode == StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE) {
54+
// Is throwing the right choice? Probably better to just send the status code and skip error
55+
throw new Err(statusCode, value);
56+
} else if (statusCode == StatusCode.PARTIAL_CONTENT) {
57+
ctx.setHeader("Accept-Ranges", "bytes");
58+
ctx.setHeader("Content-Range", contentRange);
59+
ctx.setContentLength(contentLength);
60+
ctx.setStatusCode(statusCode);
5561
}
56-
// offset
57-
long limit = (end - start + 1);
58-
ctx.setHeader("Accept-Ranges", "bytes");
59-
ctx.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + contentLength);
60-
ctx.setHeader("Content-Length", limit);
61-
ctx.setStatusCode(StatusCode.PARTIAL_CONTENT);
62-
return new ByteRange(start, limit);
62+
return this;
6363
}
6464

65-
public boolean valid() {
66-
return this != NO_RANGE && this != NOT_SATISFIABLE;
65+
@Override public String toString() {
66+
return value;
6767
}
6868

69-
public static ByteRange parse(String value) {
70-
if (value == null) {
71-
return NO_RANGE;
69+
public static ByteRange parse(String value, long contentLength) {
70+
if (contentLength <= 0 || value == null) {
71+
// NOOP
72+
return new ByteRange(value, 0, contentLength, contentLength, "bytes */" + contentLength,
73+
StatusCode.OK);
7274
}
75+
7376
if (!value.startsWith(BYTES_EQ)) {
74-
return NOT_SATISFIABLE;
77+
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
78+
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
7579
}
80+
7681
try {
7782
long[] range = {-1, -1};
7883
int r = 0;
7984
int len = value.length();
8085
int i = BYTES_EQ.length();
81-
int start = i;
86+
int offset = i;
8287
char ch;
8388
// Only Single Byte Range Requests:
8489
while (i < len && (ch = value.charAt(i)) != ',') {
8590
if (ch == '-') {
86-
if (start < i) {
87-
range[r] = Long.parseLong(value.substring(start, i).trim());
91+
if (offset < i) {
92+
range[r] = Long.parseLong(value.substring(offset, i).trim());
8893
}
89-
start = i + 1;
94+
offset = i + 1;
9095
r += 1;
9196
}
9297
i += 1;
9398
}
94-
if (start < i) {
99+
if (offset < i) {
95100
if (r == 0) {
96-
return NOT_SATISFIABLE;
101+
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
102+
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
97103
}
98-
range[r++] = Long.parseLong(value.substring(start, i).trim());
104+
range[r++] = Long.parseLong(value.substring(offset, i).trim());
99105
}
100106
if (r == 0 || (range[0] == -1 && range[1] == -1)) {
101-
return NOT_SATISFIABLE;
107+
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
108+
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
109+
}
110+
111+
long start = range[0];
112+
long end = range[1];
113+
if (start == -1) {
114+
start = contentLength - end;
115+
end = contentLength - 1;
116+
}
117+
if (end == -1 || end > contentLength - 1) {
118+
end = contentLength - 1;
119+
}
120+
if (start > end) {
121+
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
122+
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
102123
}
103-
return new ByteRange(range[0], range[1]);
104-
} catch (NumberFormatException x) {
105-
return NOT_SATISFIABLE;
124+
// offset
125+
long limit = (end - start + 1);
126+
return new ByteRange(value, start, limit, limit,
127+
"bytes " + start + "-" + end + "/" + contentLength, StatusCode.PARTIAL_CONTENT);
128+
} catch (NumberFormatException expected) {
129+
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
130+
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
106131
}
107132
}
108133
}

jooby/src/main/java/io/jooby/ErrorHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public interface ErrorHandler {
4343
.append(" ")
4444
.append(statusCode.reason())
4545
.toString();
46-
ctx.getRouter().getLog().error(msg.toLowerCase(), cause);
46+
ctx.getRouter().getLog().error(msg, cause);
4747

4848
new ContentNegotiation()
4949
.accept(json, () -> {

jooby/src/test/java/io/jooby/ByteRangeTest.java

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,72 +4,69 @@
44

55
import java.util.function.Consumer;
66

7+
import static io.jooby.StatusCode.PARTIAL_CONTENT;
8+
import static io.jooby.StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE;
79
import static org.junit.jupiter.api.Assertions.assertEquals;
810

911
public class ByteRangeTest {
1012

1113
@Test
1214
public void noByteRange() {
13-
range("bytes=-", range -> {
14-
assertEquals(ByteRange.NOT_SATISFIABLE, range);
15-
assertEquals(false, range.valid());
15+
range("bytes=-", 10, range -> {
16+
assertEquals(REQUESTED_RANGE_NOT_SATISFIABLE, range.getStatusCode());
1617
});
17-
range(null, range -> {
18-
assertEquals(ByteRange.NO_RANGE, range);
19-
assertEquals(false, range.valid());
18+
range(null, 10, range -> {
19+
assertEquals(StatusCode.OK, range.getStatusCode());
2020
});
21-
range("foo", range -> {
22-
assertEquals(ByteRange.NOT_SATISFIABLE, range);
23-
assertEquals(false, range.valid());
21+
range("foo", 100, range -> {
22+
assertEquals(REQUESTED_RANGE_NOT_SATISFIABLE, range.getStatusCode());
2423
});
2524

26-
range("bytes=", range -> {
27-
assertEquals(ByteRange.NOT_SATISFIABLE, range);
28-
assertEquals(false, range.valid());
25+
range("bytes=", 10, range -> {
26+
assertEquals(REQUESTED_RANGE_NOT_SATISFIABLE, range.getStatusCode());
2927
});
30-
range("bytes=z-", range -> {
31-
assertEquals(ByteRange.NOT_SATISFIABLE, range);
32-
assertEquals(false, range.valid());
28+
range("bytes=z-", 10, range -> {
29+
assertEquals(REQUESTED_RANGE_NOT_SATISFIABLE, range.getStatusCode());
3330
});
34-
range("bytes=-z", range -> {
35-
assertEquals(ByteRange.NOT_SATISFIABLE, range);
36-
assertEquals(false, range.valid());
37-
});
38-
range("bytes=6", range -> {
39-
assertEquals(ByteRange.NOT_SATISFIABLE, range);
40-
assertEquals(false, range.valid());
31+
range("bytes=-z", 10, range -> {
32+
assertEquals(REQUESTED_RANGE_NOT_SATISFIABLE, range.getStatusCode());
4133
});
4234
}
4335

4436
@Test
4537
public void byteRange() {
46-
range("bytes=1-10", range -> {
47-
assertEquals(true, range.valid());
48-
assertEquals(1, range.start);
49-
assertEquals(10, range.end);
38+
range("bytes=1-10", 10, range -> {
39+
assertEquals(PARTIAL_CONTENT, range.getStatusCode());
40+
assertEquals(1, range.getStart());
41+
assertEquals(9, range.getEnd());
42+
assertEquals(9, range.getContentLength());
43+
assertEquals("bytes 1-9/10", range.getContentRange());
5044
});
5145

52-
range("bytes=99-", range -> {
53-
assertEquals(true, range.valid());
54-
assertEquals(99, range.start);
55-
assertEquals(-1, range.end);
46+
range("bytes=99-", 110, range -> {
47+
assertEquals(PARTIAL_CONTENT, range.getStatusCode());
48+
assertEquals(99, range.getStart());
49+
assertEquals(11, range.getEnd());
50+
assertEquals("bytes 99-109/110", range.getContentRange());
5651
});
5752

58-
range("bytes=-99", range -> {
59-
assertEquals(true, range.valid());
60-
assertEquals(-1, range.start);
61-
assertEquals(99, range.end);
53+
range("bytes=-99", 200, range -> {
54+
assertEquals(PARTIAL_CONTENT, range.getStatusCode());
55+
assertEquals(101, range.getStart());
56+
assertEquals(99, range.getEnd());
57+
assertEquals("bytes 101-199/200", range.getContentRange());
6258
});
6359

6460
// 100-150 is ignored.
65-
range("bytes=0-50, 100-150", range -> {
66-
assertEquals(true, range.valid());
67-
assertEquals(0, range.start);
68-
assertEquals(50, range.end);
61+
range("bytes=0-50, 100-150", 200, range -> {
62+
assertEquals(PARTIAL_CONTENT, range.getStatusCode());
63+
assertEquals(0, range.getStart());
64+
assertEquals(51, range.getEnd());
65+
assertEquals("bytes 0-50/200", range.getContentRange());
6966
});
7067
}
7168

72-
private void range(String value, Consumer<ByteRange> consumer) {
73-
consumer.accept(ByteRange.parse(value));
69+
private void range(String value, long len, Consumer<ByteRange> consumer) {
70+
consumer.accept(ByteRange.parse(value, len));
7471
}
7572
}

modules/server/jooby-jetty/src/main/java/io/jooby/internal/jetty/ByteRangeInputStream.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public class ByteRangeInputStream extends FilterInputStream {
2929

3030
public ByteRangeInputStream(@Nonnull InputStream in, ByteRange range) throws IOException {
3131
super(in);
32-
in.skip(range.start);
33-
left = range.end;
32+
in.skip(range.getStart());
33+
left = range.getEnd();
3434
}
3535

3636
@Override

modules/server/jooby-jetty/src/main/java/io/jooby/internal/jetty/JettyContext.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ private Context sendStreamInternal(@Nonnull InputStream in) {
336336
long len = response.getContentLength();
337337
InputStream stream;
338338
if (len > 0) {
339-
ByteRange range = ByteRange.parse(request.getHeader(HttpHeader.RANGE.asString()))
340-
.apply(this, len);
339+
ByteRange range = ByteRange.parse(request.getHeader(HttpHeader.RANGE.asString()), len)
340+
.apply(this);
341341
stream = new ByteRangeInputStream(in, range);
342342
} else {
343343
response.setHeader(HttpHeader.TRANSFER_ENCODING, HttpHeaderValue.CHUNKED.asString());

modules/server/jooby-netty/src/main/java/io/jooby/internal/netty/NettyContext.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -333,10 +333,12 @@ public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, S
333333
prepareChunked();
334334
long len = responseLength();
335335
ChunkedInput chunkedStream;
336-
if (len > 0) {
337-
ByteRange range = ByteRange.parse(req.headers().get(RANGE)).apply(this, len);
338-
in.skip(range.start);
339-
chunkedStream = new ChunkedLimitedStream(in, bufferSize, range.end);
336+
ByteRange range = ByteRange.parse(req.headers().get(RANGE), len)
337+
.apply(this);
338+
if (range.isPartial()) {
339+
range.apply(this);
340+
in.skip(range.getStart());
341+
chunkedStream = new ChunkedLimitedStream(in, bufferSize, range.getEnd());
340342
} else {
341343
chunkedStream = new ChunkedStream(in, bufferSize);
342344
}
@@ -362,15 +364,16 @@ public NettyContext(ChannelHandlerContext ctx, HttpRequest req, Router router, S
362364
long len = file.size();
363365
setHeaders.set(CONTENT_LENGTH, len);
364366

365-
ByteRange range = ByteRange.parse(req.headers().get(RANGE)).apply(this, len);
367+
ByteRange range = ByteRange.parse(req.headers().get(RANGE), len)
368+
.apply(this);
366369

367370
DefaultHttpResponse rsp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, setHeaders);
368371
responseStarted = true;
369372
ctx.channel().eventLoop().execute(() -> {
370373
// Headers
371374
ctx.write(rsp);
372375
// Body
373-
ctx.write(new DefaultFileRegion(file, range.start, range.end));
376+
ctx.write(new DefaultFileRegion(file, range.getStart(), range.getEnd()));
374377
// Finish
375378
ctx.writeAndFlush(EMPTY_LAST_CONTENT).addListener(this);
376379
});

0 commit comments

Comments
 (0)