Skip to content

Commit 178866c

Browse files
committed
finish byte-range API
1 parent 3c09531 commit 178866c

10 files changed

Lines changed: 398 additions & 127 deletions

File tree

etc/source/jooby-style.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@
163163
<module name="EqualsHashCode"/>
164164
<!-- <module name="HiddenField"/> -->
165165
<module name="IllegalInstantiation"/>
166-
<module name="InnerAssignment"/>
167166
<module name="MagicNumber">
168167
<property name="ignoreNumbers" value="-1, 0, 1, 2, 3, 4, 5" />
169168
</module>

jooby/pom.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<modelVersion>4.0.0</modelVersion>
1313
<artifactId>jooby</artifactId>
1414

15+
<properties>
16+
<shaded.package>io.jooby.internal.\$shaded</shaded.package>
17+
</properties>
1518
<build>
1619
<plugins>
1720
<plugin>
@@ -95,11 +98,11 @@
9598
<relocations>
9699
<relocation>
97100
<pattern>org.objectweb.asm</pattern>
98-
<shadedPattern>io.jooby.internal.lib.asm</shadedPattern>
101+
<shadedPattern>${shaded.package}.asm</shadedPattern>
99102
</relocation>
100103
<relocation>
101104
<pattern>org.apache.commons.io</pattern>
102-
<shadedPattern>io.jooby.internal.lib.commonsio</shadedPattern>
105+
<shadedPattern>${shaded.package}.commonsio</shadedPattern>
103106
</relocation>
104107
</relocations>
105108
</configuration>
Lines changed: 121 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,64 @@
11
package io.jooby;
22

3-
import org.apache.commons.io.input.BoundedInputStream;
3+
import io.jooby.internal.NoByteRange;
4+
import io.jooby.internal.NotSatisfiableByteRange;
5+
import io.jooby.internal.SingleByteRange;
46

7+
import javax.annotation.Nonnull;
8+
import javax.annotation.Nullable;
59
import java.io.IOException;
610
import java.io.InputStream;
711

8-
public class ByteRange {
9-
private static final String BYTES_EQ = "bytes=";
10-
11-
private String value;
12-
13-
private long start;
14-
15-
private long end;
16-
17-
private long contentLength;
18-
19-
private String contentRange;
20-
21-
private StatusCode statusCode;
22-
23-
private ByteRange(String value, long start, long end, long contentLength, String contentRange,
24-
StatusCode statusCode) {
25-
this.value = value;
26-
this.start = start;
27-
this.end = end;
28-
this.contentLength = contentLength;
29-
this.contentRange = contentRange;
30-
this.statusCode = statusCode;
31-
}
32-
33-
public long getStart() {
34-
return start;
35-
}
36-
37-
public long getEnd() {
38-
return end;
39-
}
40-
41-
public long getContentLength() {
42-
return contentLength;
43-
}
44-
45-
public String getContentRange() {
46-
return contentRange;
47-
}
48-
49-
public StatusCode getStatusCode() {
50-
return statusCode;
51-
}
52-
53-
public boolean isPartial() {
54-
return statusCode == StatusCode.PARTIAL_CONTENT;
55-
}
56-
57-
public ByteRange apply(Context ctx) {
58-
if (statusCode == StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE) {
59-
// Is throwing the right choice? Probably better to just send the status code and skip error
60-
throw new Err(statusCode, value);
61-
} else if (statusCode == StatusCode.PARTIAL_CONTENT) {
62-
ctx.setHeader("Accept-Ranges", "bytes");
63-
ctx.setHeader("Content-Range", contentRange);
64-
ctx.setContentLength(contentLength);
65-
ctx.setStatusCode(statusCode);
66-
}
67-
return this;
68-
}
69-
70-
public InputStream apply(InputStream input) throws IOException {
71-
if (statusCode == StatusCode.OK) {
72-
return input;
73-
}
74-
if (statusCode == StatusCode.PARTIAL_CONTENT) {
75-
input.skip(start);
76-
return new BoundedInputStream(input, end);
77-
}
78-
throw new Err(statusCode, value);
79-
}
80-
81-
@Override public String toString() {
82-
return value;
83-
}
84-
85-
public static ByteRange parse(String value, long contentLength) {
12+
/**
13+
* Utility class to compute single byte range requests when response content length is known.
14+
* Jooby support single byte range requests on file responses, like: assets, input stream, files,
15+
* etc.
16+
*
17+
* Single byte range request looks like: <code>bytes=0-100</code>, <code>bytes=100-</code>,
18+
* <code>bytes=-100</code>.
19+
*
20+
* Multiple byte range request are not supported.
21+
*
22+
* @since 2.0.0
23+
* @author edgar
24+
*/
25+
public interface ByteRange {
26+
/**
27+
* Byte range prefix.
28+
*/
29+
String BYTES_RANGE = "bytes=";
30+
31+
/**
32+
* Parse a byte range request value. Example of valid values:
33+
*
34+
* - bytes=0-100
35+
* - bytes=-100
36+
* - bytes=100-
37+
*
38+
* Any non-matching values produces a not satisfiable response.
39+
*
40+
* If value is null or content length less or equal to <code>0</code>, produces an empty/NOOP
41+
* response.
42+
*
43+
* @param value Valid byte range request value.
44+
* @param contentLength Content length.
45+
* @return Byte range instance.
46+
*/
47+
static @Nonnull ByteRange parse(@Nullable String value, long contentLength) {
8648
if (contentLength <= 0 || value == null) {
8749
// NOOP
88-
return new ByteRange(value, 0, contentLength, contentLength, "bytes */" + contentLength,
89-
StatusCode.OK);
50+
return new NoByteRange(contentLength);
9051
}
9152

92-
if (!value.startsWith(BYTES_EQ)) {
93-
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
94-
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
53+
if (!value.startsWith(SingleByteRange.BYTES_RANGE)) {
54+
return new NotSatisfiableByteRange(value, contentLength);
9555
}
9656

9757
try {
9858
long[] range = {-1, -1};
9959
int r = 0;
10060
int len = value.length();
101-
int i = BYTES_EQ.length();
61+
int i = SingleByteRange.BYTES_RANGE.length();
10262
int offset = i;
10363
char ch;
10464
// Only Single Byte Range Requests:
@@ -114,14 +74,12 @@ public static ByteRange parse(String value, long contentLength) {
11474
}
11575
if (offset < i) {
11676
if (r == 0) {
117-
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
118-
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
77+
return new NotSatisfiableByteRange(value, contentLength);
11978
}
12079
range[r++] = Long.parseLong(value.substring(offset, i).trim());
12180
}
12281
if (r == 0 || (range[0] == -1 && range[1] == -1)) {
123-
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
124-
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
82+
return new NotSatisfiableByteRange(value, contentLength);
12583
}
12684

12785
long start = range[0];
@@ -134,16 +92,84 @@ public static ByteRange parse(String value, long contentLength) {
13492
end = contentLength - 1;
13593
}
13694
if (start > end) {
137-
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
138-
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
95+
return new NotSatisfiableByteRange(value, contentLength);
13996
}
14097
// offset
14198
long limit = (end - start + 1);
142-
return new ByteRange(value, start, limit, limit,
143-
"bytes " + start + "-" + end + "/" + contentLength, StatusCode.PARTIAL_CONTENT);
99+
return new SingleByteRange(value, start, limit, limit,
100+
"bytes " + start + "-" + end + "/" + contentLength);
144101
} catch (NumberFormatException expected) {
145-
return new ByteRange(value, 0, 0, contentLength, "bytes */" + contentLength,
146-
StatusCode.REQUESTED_RANGE_NOT_SATISFIABLE);
102+
return new NotSatisfiableByteRange(value, contentLength);
147103
}
148104
}
105+
106+
/**
107+
* Start range or <code>-1</code>.
108+
*
109+
* @return Start range or <code>-1</code>.
110+
*/
111+
long getStart();
112+
113+
/**
114+
* End range or <code>-1</code>.
115+
*
116+
* @return End range or <code>-1</code>.
117+
*/
118+
long getEnd();
119+
120+
/**
121+
* New content length.
122+
*
123+
* @return New content length.
124+
*/
125+
long getContentLength();
126+
127+
/**
128+
* Value for <code>Content-Range</code> response header.
129+
*
130+
* @return Value for <code>Content-Range</code> response header.
131+
*/
132+
@Nonnull String getContentRange();
133+
134+
/**
135+
* For partial requests this method returns {@link StatusCode#PARTIAL_CONTENT}.
136+
*
137+
* For not satisfiable requests this returns {@link StatusCode#REQUESTED_RANGE_NOT_SATISFIABLE}..
138+
*
139+
* Otherwise just returns {@link StatusCode#OK}.
140+
*
141+
* @return Status code.
142+
*/
143+
@Nonnull StatusCode getStatusCode();
144+
145+
/**
146+
* For partial request this method set the following byte range response headers:
147+
*
148+
* - Accept-Ranges
149+
* - Content-Range
150+
* - Content-Length
151+
*
152+
* For not satisfiable requests:
153+
*
154+
* - Throws a {@link StatusCode#REQUESTED_RANGE_NOT_SATISFIABLE}
155+
*
156+
* Otherwise this method does nothing.
157+
*
158+
* @param ctx Web context.
159+
* @return This byte range request.
160+
*/
161+
@Nonnull ByteRange apply(@Nonnull Context ctx);
162+
163+
/**
164+
* For partial requests this method generates a new truncated input stream.
165+
*
166+
* For not satisfiable requests this method throws an exception.
167+
*
168+
* If there is no range to apply this method returns the given input stream.
169+
*
170+
* @param input Input stream.
171+
* @return A truncated input stream for partial request or same input stream.
172+
* @throws IOException When truncation fails.
173+
*/
174+
@Nonnull InputStream apply(@Nonnull InputStream input) throws IOException;
149175
}

jooby/src/main/java/io/jooby/Jooby.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ public Jooby errorCode(@Nonnull Class<? extends Throwable> type,
448448

449449
private Registry checkRegistry() {
450450
if (registry == null) {
451-
throw new Usage("No registry");
451+
throw new IllegalStateException("No registry available");
452452
}
453453
return registry;
454454
}

jooby/src/main/java/io/jooby/Usage.java

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
*/
16+
package io.jooby.internal;
17+
18+
import io.jooby.ByteRange;
19+
import io.jooby.Context;
20+
import io.jooby.StatusCode;
21+
22+
import javax.annotation.Nonnull;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
26+
public class NoByteRange implements ByteRange {
27+
private long contentLength;
28+
29+
public NoByteRange(long contentLength) {
30+
this.contentLength = contentLength;
31+
}
32+
33+
@Override public long getStart() {
34+
return 0;
35+
}
36+
37+
@Override public long getEnd() {
38+
return contentLength;
39+
}
40+
41+
@Override public long getContentLength() {
42+
return contentLength;
43+
}
44+
45+
@Nonnull @Override public StatusCode getStatusCode() {
46+
return StatusCode.OK;
47+
}
48+
49+
@Nonnull @Override public String getContentRange() {
50+
return "bytes */" + contentLength;
51+
}
52+
53+
@Nonnull @Override public ByteRange apply(@Nonnull Context ctx) {
54+
return this;
55+
}
56+
57+
@Nonnull @Override public InputStream apply(@Nonnull InputStream input) throws IOException {
58+
return input;
59+
}
60+
}

0 commit comments

Comments
 (0)