/**
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby;
import io.jooby.internal.NoByteRange;
import io.jooby.internal.NotSatisfiableByteRange;
import io.jooby.internal.SingleByteRange;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
/**
* Utility class to compute single byte range requests when response content length is known.
* Jooby support single byte range requests on file responses, like: assets, input stream, files,
* etc.
*
* Single byte range request looks like: bytes=0-100, bytes=100-,
* bytes=-100.
*
* Multiple byte range request are not supported.
*
* @since 2.0.0
* @author edgar
*/
public interface ByteRange {
/**
* Byte range prefix.
*/
String BYTES_RANGE = "bytes=";
/**
* Parse a byte range request value. Example of valid values:
*
* - bytes=0-100
* - bytes=-100
* - bytes=100-
*
* Any non-matching values produces a not satisfiable response.
*
* If value is null or content length less or equal to 0, produces an empty/NOOP
* response.
*
* @param value Valid byte range request value.
* @param contentLength Content length.
* @return Byte range instance.
*/
static @Nonnull ByteRange parse(@Nullable String value, long contentLength) {
if (contentLength <= 0 || value == null) {
// NOOP
return new NoByteRange(contentLength);
}
if (!value.startsWith(SingleByteRange.BYTES_RANGE)) {
return new NotSatisfiableByteRange(value, contentLength);
}
try {
long[] range = {-1, -1};
int r = 0;
int len = value.length();
int i = SingleByteRange.BYTES_RANGE.length();
int offset = i;
char ch;
// Only Single Byte Range Requests:
while (i < len && (ch = value.charAt(i)) != ',') {
if (ch == '-') {
if (offset < i) {
range[r] = Long.parseLong(value.substring(offset, i).trim());
}
offset = i + 1;
r += 1;
}
i += 1;
}
if (offset < i) {
if (r == 0) {
return new NotSatisfiableByteRange(value, contentLength);
}
range[r++] = Long.parseLong(value.substring(offset, i).trim());
}
if (r == 0 || (range[0] == -1 && range[1] == -1)) {
return new NotSatisfiableByteRange(value, contentLength);
}
long start = range[0];
long end = range[1];
if (start == -1) {
start = contentLength - end;
end = contentLength - 1;
}
if (end == -1 || end > contentLength - 1) {
end = contentLength - 1;
}
if (start > end) {
return new NotSatisfiableByteRange(value, contentLength);
}
// offset
long limit = (end - start + 1);
return new SingleByteRange(value, start, limit, limit,
"bytes " + start + "-" + end + "/" + contentLength);
} catch (NumberFormatException expected) {
return new NotSatisfiableByteRange(value, contentLength);
}
}
/**
* Start range or -1.
*
* @return Start range or -1.
*/
long getStart();
/**
* End range or -1.
*
* @return End range or -1.
*/
long getEnd();
/**
* New content length.
*
* @return New content length.
*/
long getContentLength();
/**
* Value for Content-Range response header.
*
* @return Value for Content-Range response header.
*/
@Nonnull String getContentRange();
/**
* For partial requests this method returns {@link StatusCode#PARTIAL_CONTENT}.
*
* For not satisfiable requests this returns {@link StatusCode#REQUESTED_RANGE_NOT_SATISFIABLE}..
*
* Otherwise just returns {@link StatusCode#OK}.
*
* @return Status code.
*/
@Nonnull StatusCode getStatusCode();
/**
* For partial request this method set the following byte range response headers:
*
* - Accept-Ranges
* - Content-Range
* - Content-Length
*
* For not satisfiable requests:
*
* - Throws a {@link StatusCode#REQUESTED_RANGE_NOT_SATISFIABLE}
*
* Otherwise this method does nothing.
*
* @param ctx Web context.
* @return This byte range request.
*/
@Nonnull ByteRange apply(@Nonnull Context ctx);
/**
* For partial requests this method generates a new truncated input stream.
*
* For not satisfiable requests this method throws an exception.
*
* If there is no range to apply this method returns the given input stream.
*
* @param input Input stream.
* @return A truncated input stream for partial request or same input stream.
* @throws IOException When truncation fails.
*/
@Nonnull InputStream apply(@Nonnull InputStream input) throws IOException;
}