11package 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 ;
59import java .io .IOException ;
610import 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}
0 commit comments