Updating DefaultHttpDataSource to allow for http methods other than GET and POST,

as specified by DataSpec.httpMethod.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=207769779
This commit is contained in:
sammon 2018-08-07 13:29:33 -07:00 committed by Oliver Woodman
parent ca473c86c7
commit d3686cf8a2
8 changed files with 209 additions and 57 deletions

View file

@ -92,6 +92,8 @@
([#273](https://github.com/google/ExoPlayer/issues/273)).
* Fix where transitions to clipped media sources happened too early
([#4583](https://github.com/google/ExoPlayer/issues/4583)).
* Add `DataSpec.httpMethod` and update `HttpDataSource` implemenations to support
http HEAD method. Previously, only GET and POST were supported.
### 2.8.3 ###

View file

@ -473,8 +473,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
isContentTypeHeaderSet = isContentTypeHeaderSet || CONTENT_TYPE.equals(key);
requestBuilder.addHeader(key, headerEntry.getValue());
}
if (dataSpec.postBody != null && dataSpec.postBody.length != 0 && !isContentTypeHeaderSet) {
throw new IOException("POST request with non-empty body must set Content-Type");
if (dataSpec.httpBody != null && !isContentTypeHeaderSet) {
throw new IOException("HTTP request with non-empty body must set Content-Type");
}
// Set the Range header.
if (dataSpec.position != 0 || dataSpec.length != C.LENGTH_UNSET) {
@ -494,12 +494,10 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
// requestBuilder.addHeader("Accept-Encoding", "identity");
// }
// Set the method and (if non-empty) the body.
if (dataSpec.postBody != null) {
requestBuilder.setHttpMethod("POST");
if (dataSpec.postBody.length != 0) {
requestBuilder.setUploadDataProvider(new ByteArrayUploadDataProvider(dataSpec.postBody),
executor);
}
requestBuilder.setHttpMethod(dataSpec.getHttpMethodString());
if (dataSpec.httpBody != null) {
requestBuilder.setUploadDataProvider(
new ByteArrayUploadDataProvider(dataSpec.httpBody), executor);
}
return requestBuilder;
}

View file

@ -80,6 +80,7 @@ public final class CronetDataSourceTest {
private DataSpec testDataSpec;
private DataSpec testPostDataSpec;
private DataSpec testHeadDataSpec;
private Map<String, String> testResponseHeader;
private UrlResponseInfo testUrlResponseInfo;
@ -120,6 +121,9 @@ public final class CronetDataSourceTest {
testDataSpec = new DataSpec(Uri.parse(TEST_URL), 0, C.LENGTH_UNSET, null);
testPostDataSpec =
new DataSpec(Uri.parse(TEST_URL), TEST_POST_BODY, 0, 0, C.LENGTH_UNSET, null, 0);
testHeadDataSpec =
new DataSpec(
Uri.parse(TEST_URL), DataSpec.HTTP_METHOD_HEAD, null, 0, 0, C.LENGTH_UNSET, null, 0);
testResponseHeader = new HashMap<>();
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
// This value can be anything since the DataSpec is unset.
@ -332,6 +336,15 @@ public final class CronetDataSourceTest {
}
}
@Test
public void testHeadRequestOpen() throws HttpDataSourceException {
mockResponseStartSuccess();
dataSourceUnderTest.open(testHeadDataSpec);
verify(mockTransferListener)
.onTransferStart(dataSourceUnderTest, testHeadDataSpec, /* isNetwork= */ true);
dataSourceUnderTest.close();
}
@Test
public void testRequestReadTwice() throws HttpDataSourceException {
mockResponseStartSuccess();

View file

@ -296,9 +296,14 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
if (!allowGzip) {
builder.addHeader("Accept-Encoding", "identity");
}
if (dataSpec.postBody != null) {
builder.post(RequestBody.create(null, dataSpec.postBody));
RequestBody requestBody = null;
if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody);
} else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
// OkHttp requires a non-null body for POST requests.
requestBody = RequestBody.create(null, new byte[0]);
}
builder.method(dataSpec.getHttpMethodString(), requestBody);
return builder.build();
}

View file

@ -54,14 +54,33 @@ public final class DataSpec {
*/
public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; // 2
/** The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. */
@IntDef({HTTP_METHOD_GET, HTTP_METHOD_POST, HTTP_METHOD_HEAD})
public @interface HttpMethod {}
public static final int HTTP_METHOD_GET = 1;
public static final int HTTP_METHOD_POST = 2;
public static final int HTTP_METHOD_HEAD = 3;
/**
* The source from which data should be read.
*/
public final Uri uri;
/**
* Body for a POST request, null otherwise.
* The HTTP method, which will be used by {@link HttpDataSource} when requesting this DataSpec.
* This value will be ignored by non-http {@link DataSource}s.
*/
public final @Nullable byte[] postBody;
public final @HttpMethod int httpMethod;
/**
* The HTTP body, null otherwise. If the body is non-null, then httpBody.length will be non-zero.
*/
public final @Nullable byte[] httpBody;
/** @deprecated Use {@link #httpBody} instead. */
@Deprecated public final @Nullable byte[] postBody;
/**
* The absolute position of the data in the full stream.
*/
@ -155,11 +174,13 @@ public final class DataSpec {
}
/**
* Construct a {@link DataSpec} where {@link #position} may differ from {@link
* #absoluteStreamPosition}.
* Construct a {@link DataSpec} by inferring the {@link #httpMethod} based on the {@code postBody}
* parameter. If postBody is non-null, then httpMethod is set to {@link #HTTP_METHOD_POST}. If
* postBody is null, then httpMethod is set to {@link #HTTP_METHOD_GET}.
*
* @param uri {@link #uri}.
* @param postBody {@link #postBody}.
* @param postBody {@link #httpBody} The body of the HTTP request, which is also used to infer the
* {@link #httpMethod}.
* @param absoluteStreamPosition {@link #absoluteStreamPosition}.
* @param position {@link #position}.
* @param length {@link #length}.
@ -174,11 +195,46 @@ public final class DataSpec {
long length,
@Nullable String key,
@Flags int flags) {
this(
uri,
/* httpMethod= */ postBody != null ? HTTP_METHOD_POST : HTTP_METHOD_GET,
/* httpBody= */ postBody,
absoluteStreamPosition,
position,
length,
key,
flags);
}
/**
* Construct a {@link DataSpec} where {@link #position} may differ from {@link
* #absoluteStreamPosition}.
*
* @param uri {@link #uri}.
* @param httpMethod {@link #httpMethod}.
* @param httpBody {@link #httpBody}.
* @param absoluteStreamPosition {@link #absoluteStreamPosition}.
* @param position {@link #position}.
* @param length {@link #length}.
* @param key {@link #key}.
* @param flags {@link #flags}.
*/
public DataSpec(
Uri uri,
@HttpMethod int httpMethod,
@Nullable byte[] httpBody,
long absoluteStreamPosition,
long position,
long length,
@Nullable String key,
@Flags int flags) {
Assertions.checkArgument(absoluteStreamPosition >= 0);
Assertions.checkArgument(position >= 0);
Assertions.checkArgument(length > 0 || length == C.LENGTH_UNSET);
this.uri = uri;
this.postBody = postBody;
this.httpMethod = httpMethod;
this.httpBody = (httpBody != null && httpBody.length != 0) ? httpBody : null;
this.postBody = this.httpBody;
this.absoluteStreamPosition = absoluteStreamPosition;
this.position = position;
this.length = length;
@ -197,8 +253,48 @@ public final class DataSpec {
@Override
public String toString() {
return "DataSpec[" + uri + ", " + Arrays.toString(postBody) + ", " + absoluteStreamPosition
+ ", " + position + ", " + length + ", " + key + ", " + flags + "]";
return "DataSpec["
+ getHttpMethodString()
+ " "
+ uri
+ ", "
+ Arrays.toString(httpBody)
+ ", "
+ absoluteStreamPosition
+ ", "
+ position
+ ", "
+ length
+ ", "
+ key
+ ", "
+ flags
+ "]";
}
/**
* Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@link
* #httpMethod}.
*/
public final String getHttpMethodString() {
return getStringForHttpMethod(httpMethod);
}
/**
* Returns an uppercase HTTP method name (e.g., "GET", "POST", "HEAD") corresponding to the {@code
* httpMethod}.
*/
public static String getStringForHttpMethod(@HttpMethod int httpMethod) {
switch (httpMethod) {
case HTTP_METHOD_GET:
return "GET";
case HTTP_METHOD_POST:
return "POST";
case HTTP_METHOD_HEAD:
return "HEAD";
default:
throw new AssertionError(httpMethod);
}
}
/**
@ -223,8 +319,15 @@ public final class DataSpec {
if (offset == 0 && this.length == length) {
return this;
} else {
return new DataSpec(uri, postBody, absoluteStreamPosition + offset, position + offset, length,
key, flags);
return new DataSpec(
uri,
httpMethod,
httpBody,
absoluteStreamPosition + offset,
position + offset,
length,
key,
flags);
}
}
@ -235,6 +338,7 @@ public final class DataSpec {
* @return The copied {@link DataSpec} with the specified Uri.
*/
public DataSpec withUri(Uri uri) {
return new DataSpec(uri, postBody, absoluteStreamPosition, position, length, key, flags);
return new DataSpec(
uri, httpMethod, httpBody, absoluteStreamPosition, position, length, key, flags);
}
}

View file

@ -20,6 +20,7 @@ import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.Util;
@ -61,6 +62,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
private static final String TAG = "DefaultHttpDataSource";
private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
private static final long MAX_BYTES_TO_DRAIN = 2048;
private static final Pattern CONTENT_RANGE_HEADER =
Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
@ -417,7 +420,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
*/
private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString());
byte[] postBody = dataSpec.postBody;
@HttpMethod int httpMethod = dataSpec.httpMethod;
byte[] httpBody = dataSpec.httpBody;
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
@ -425,28 +429,37 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
if (!allowCrossProtocolRedirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs redirection
// automatically. This is the behavior we want, so use it.
return makeConnection(url, postBody, position, length, allowGzip, true /* followRedirects */);
return makeConnection(
url, httpMethod, httpBody, position, length, allowGzip, true /* followRedirects */);
}
// We need to handle redirects ourselves to allow cross-protocol redirects.
int redirectCount = 0;
while (redirectCount++ <= MAX_REDIRECTS) {
HttpURLConnection connection = makeConnection(
url, postBody, position, length, allowGzip, false /* followRedirects */);
HttpURLConnection connection =
makeConnection(
url, httpMethod, httpBody, position, length, allowGzip, false /* followRedirects */);
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER
|| (postBody == null
&& (responseCode == 307 /* HTTP_TEMP_REDIRECT */
|| responseCode == 308 /* HTTP_PERM_REDIRECT */))) {
// For 300, 301, 302, and 303 POST requests follow the redirect and are transformed into
// GET requests. For 307 and 308 POST requests are not redirected.
postBody = null;
String location = connection.getHeaderField("Location");
String location = connection.getHeaderField("Location");
if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
&& (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER
|| responseCode == HTTP_STATUS_TEMPORARY_REDIRECT
|| responseCode == HTTP_STATUS_PERMANENT_REDIRECT)) {
connection.disconnect();
url = handleRedirect(url, location);
} else if (httpMethod == DataSpec.HTTP_METHOD_POST
&& (responseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| responseCode == HttpURLConnection.HTTP_MOVED_PERM
|| responseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| responseCode == HttpURLConnection.HTTP_SEE_OTHER)) {
// POST request follows the redirect and is transformed into a GET request.
connection.disconnect();
httpMethod = DataSpec.HTTP_METHOD_GET;
httpBody = null;
url = handleRedirect(url, location);
} else {
return connection;
}
@ -460,14 +473,22 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* Configures a connection and opens it.
*
* @param url The url to connect to.
* @param postBody The body data for a POST request.
* @param httpMethod The http method.
* @param httpBody The body data.
* @param position The byte offset of the requested data.
* @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
* @param allowGzip Whether to allow the use of gzip.
* @param followRedirects Whether to follow redirects.
*/
private HttpURLConnection makeConnection(URL url, byte[] postBody, long position,
long length, boolean allowGzip, boolean followRedirects) throws IOException {
private HttpURLConnection makeConnection(
URL url,
@HttpMethod int httpMethod,
byte[] httpBody,
long position,
long length,
boolean allowGzip,
boolean followRedirects)
throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(connectTimeoutMillis);
connection.setReadTimeout(readTimeoutMillis);
@ -491,18 +512,14 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
connection.setRequestProperty("Accept-Encoding", "identity");
}
connection.setInstanceFollowRedirects(followRedirects);
connection.setDoOutput(postBody != null);
if (postBody != null) {
connection.setRequestMethod("POST");
if (postBody.length == 0) {
connection.connect();
} else {
connection.setFixedLengthStreamingMode(postBody.length);
connection.connect();
OutputStream os = connection.getOutputStream();
os.write(postBody);
os.close();
}
connection.setDoOutput(httpBody != null);
connection.setRequestMethod(DataSpec.getStringForHttpMethod(httpMethod));
if (httpBody != null) {
connection.setFixedLengthStreamingMode(httpBody.length);
connection.connect();
OutputStream os = connection.getOutputStream();
os.write(httpBody);
os.close();
} else {
connection.connect();
}

View file

@ -23,6 +23,7 @@ import com.google.android.exoplayer2.upstream.DataSink;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.upstream.FileDataSource;
import com.google.android.exoplayer2.upstream.TeeDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
@ -133,6 +134,7 @@ public final class CacheDataSource implements DataSource {
private boolean currentDataSpecLengthUnset;
private @Nullable Uri uri;
private @Nullable Uri actualUri;
private @HttpMethod int httpMethod;
private int flags;
private @Nullable String key;
private long readPosition;
@ -269,6 +271,7 @@ public final class CacheDataSource implements DataSource {
key = cacheKeyFactory.buildCacheKey(dataSpec);
uri = dataSpec.uri;
actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ uri);
httpMethod = dataSpec.httpMethod;
flags = dataSpec.flags;
readPosition = dataSpec.position;
@ -353,6 +356,7 @@ public final class CacheDataSource implements DataSource {
public void close() throws IOException {
uri = null;
actualUri = null;
httpMethod = DataSpec.HTTP_METHOD_GET;
notifyBytesRead();
try {
closeCurrentSource();
@ -397,7 +401,9 @@ public final class CacheDataSource implements DataSource {
// The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
// from upstream.
nextDataSource = upstreamDataSource;
nextDataSpec = new DataSpec(uri, readPosition, bytesRemaining, key, flags);
nextDataSpec =
new DataSpec(
uri, httpMethod, null, readPosition, readPosition, bytesRemaining, key, flags);
} else if (nextSpan.isCached) {
// Data is cached, read from cache.
Uri fileUri = Uri.fromFile(nextSpan.file);
@ -419,7 +425,8 @@ public final class CacheDataSource implements DataSource {
length = Math.min(length, bytesRemaining);
}
}
nextDataSpec = new DataSpec(uri, readPosition, length, key, flags);
nextDataSpec =
new DataSpec(uri, httpMethod, null, readPosition, readPosition, length, key, flags);
if (cacheWriteDataSource != null) {
nextDataSource = cacheWriteDataSource;
} else {

View file

@ -264,10 +264,16 @@ public final class CacheUtil {
throwExceptionIfInterruptedOrCancelled(isCanceled);
// Create a new dataSpec setting length to C.LENGTH_UNSET to prevent getting an error in
// case the given length exceeds the end of input.
dataSpec = new DataSpec(dataSpec.uri, dataSpec.postBody, absoluteStreamPosition,
dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition,
C.LENGTH_UNSET, dataSpec.key,
dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH);
dataSpec =
new DataSpec(
dataSpec.uri,
dataSpec.httpMethod,
dataSpec.httpBody,
absoluteStreamPosition,
dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition,
C.LENGTH_UNSET,
dataSpec.key,
dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH);
long resolvedLength = dataSource.open(dataSpec);
if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) {
counters.contentLength = dataSpec.absoluteStreamPosition + resolvedLength;