/** * 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; }