Skip to content

Commit

Permalink
Allow creation of custom input/output streams in HurlStack (#328)
Browse files Browse the repository at this point in the history
Refactor the InputStream and OutputStream creation during HurlStack request execution into protected helper methods. This allows subclasses to override these methods, e.g. to monitor the progress of writing request bytes to the OutputStream and reading response bytes from the InputStream, as desired.

Co-authored-by: Jeff Davidson <[email protected]>
  • Loading branch information
ndagnas and jpd236 authored Apr 29, 2020
1 parent addc11d commit 455a161
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 18 deletions.
41 changes: 36 additions & 5 deletions src/main/java/com/android/volley/toolbox/HurlStack.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
Expand Down Expand Up @@ -111,7 +112,7 @@ public HttpResponse executeRequest(Request<?> request, Map<String, String> addit
responseCode,
convertHeaders(connection.getHeaderFields()),
connection.getContentLength(),
new UrlConnectionInputStream(connection));
createInputStream(request, connection));
} finally {
if (!keepConnectionOpen) {
connection.disconnect();
Expand Down Expand Up @@ -168,6 +169,19 @@ public void close() throws IOException {
}
}

/**
* Create and return an InputStream from which the response will be read.
*
* <p>May be overridden by subclasses to manipulate or monitor this input stream.
*
* @param request current request.
* @param connection current connection of request.
* @return an InputStream from which the response will be read.
*/
protected InputStream createInputStream(Request<?> request, HttpURLConnection connection) {
return new UrlConnectionInputStream(connection);
}

/**
* Initializes an {@link InputStream} from the given {@link HttpURLConnection}.
*
Expand Down Expand Up @@ -223,7 +237,7 @@ private HttpURLConnection openConnection(URL url, Request<?> request) throws IOE
// NOTE: Any request headers added here (via setRequestProperty or addRequestProperty) should be
// checked against the existing properties in the connection and not overridden if already set.
@SuppressWarnings("deprecation")
/* package */ static void setConnectionParametersForRequest(
/* package */ void setConnectionParametersForRequest(
HttpURLConnection connection, Request<?> request) throws IOException, AuthFailureError {
switch (request.getMethod()) {
case Method.DEPRECATED_GET_OR_POST:
Expand Down Expand Up @@ -270,15 +284,15 @@ private HttpURLConnection openConnection(URL url, Request<?> request) throws IOE
}
}

private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
private void addBodyIfExists(HttpURLConnection connection, Request<?> request)
throws IOException, AuthFailureError {
byte[] body = request.getBody();
if (body != null) {
addBody(connection, request, body);
}
}

private static void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
private void addBody(HttpURLConnection connection, Request<?> request, byte[] body)
throws IOException {
// Prepare output. There is no need to set Content-Length explicitly,
// since this is handled by HttpURLConnection using the size of the prepared
Expand All @@ -289,8 +303,25 @@ private static void addBody(HttpURLConnection connection, Request<?> request, by
connection.setRequestProperty(
HttpHeaderParser.HEADER_CONTENT_TYPE, request.getBodyContentType());
}
DataOutputStream out = new DataOutputStream(connection.getOutputStream());
DataOutputStream out =
new DataOutputStream(createOutputStream(request, connection, body.length));
out.write(body);
out.close();
}

/**
* Create and return an OutputStream to which the request body will be written.
*
* <p>May be overridden by subclasses to manipulate or monitor this output stream.
*
* @param request current request.
* @param connection current connection of request.
* @param length size of stream to write.
* @return an OutputStream to which the request body will be written.
* @throws IOException if an I/O error occurs while creating the stream.
*/
protected OutputStream createOutputStream(
Request<?> request, HttpURLConnection connection, int length) throws IOException {
return connection.getOutputStream();
}
}
104 changes: 91 additions & 13 deletions src/test/java/com/android/volley/toolbox/HurlStackTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,24 @@
package com.android.volley.toolbox;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.android.volley.Header;
import com.android.volley.Request;
import com.android.volley.Request.Method;
import com.android.volley.mock.TestRequest;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
Expand Down Expand Up @@ -62,6 +68,26 @@ public void setUp() throws Exception {
protected HttpURLConnection createConnection(URL url) {
return mMockConnection;
}

@Override
protected InputStream createInputStream(
Request<?> request, HttpURLConnection connection) {
return new MonitoringInputStream(
super.createInputStream(request, connection));
}

@Override
protected OutputStream createOutputStream(
Request<?> request, HttpURLConnection connection, int length)
throws IOException {
if (request instanceof MonitoredRequest) {
return new MonitoringOutputStream(
super.createOutputStream(request, connection, length),
(MonitoredRequest) request,
length);
}
return super.createOutputStream(request, connection, length);
}
};
}

Expand All @@ -70,7 +96,7 @@ public void connectionForDeprecatedGetRequest() throws Exception {
TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet();
assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection, never()).setRequestMethod(anyString());
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -80,7 +106,7 @@ public void connectionForDeprecatedPostRequest() throws Exception {
TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost();
assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("POST");
verify(mMockConnection).setDoOutput(true);
}
Expand All @@ -90,7 +116,7 @@ public void connectionForGetRequest() throws Exception {
TestRequest.Get request = new TestRequest.Get();
assertEquals(request.getMethod(), Method.GET);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("GET");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -100,7 +126,7 @@ public void connectionForPostRequest() throws Exception {
TestRequest.Post request = new TestRequest.Post();
assertEquals(request.getMethod(), Method.POST);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("POST");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -110,7 +136,7 @@ public void connectionForPostWithBodyRequest() throws Exception {
TestRequest.PostWithBody request = new TestRequest.PostWithBody();
assertEquals(request.getMethod(), Method.POST);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("POST");
verify(mMockConnection).setDoOutput(true);
}
Expand All @@ -120,7 +146,7 @@ public void connectionForPutRequest() throws Exception {
TestRequest.Put request = new TestRequest.Put();
assertEquals(request.getMethod(), Method.PUT);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("PUT");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -130,7 +156,7 @@ public void connectionForPutWithBodyRequest() throws Exception {
TestRequest.PutWithBody request = new TestRequest.PutWithBody();
assertEquals(request.getMethod(), Method.PUT);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("PUT");
verify(mMockConnection).setDoOutput(true);
}
Expand All @@ -140,7 +166,7 @@ public void connectionForDeleteRequest() throws Exception {
TestRequest.Delete request = new TestRequest.Delete();
assertEquals(request.getMethod(), Method.DELETE);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("DELETE");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -150,7 +176,7 @@ public void connectionForHeadRequest() throws Exception {
TestRequest.Head request = new TestRequest.Head();
assertEquals(request.getMethod(), Method.HEAD);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("HEAD");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -160,7 +186,7 @@ public void connectionForOptionsRequest() throws Exception {
TestRequest.Options request = new TestRequest.Options();
assertEquals(request.getMethod(), Method.OPTIONS);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("OPTIONS");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -170,7 +196,7 @@ public void connectionForTraceRequest() throws Exception {
TestRequest.Trace request = new TestRequest.Trace();
assertEquals(request.getMethod(), Method.TRACE);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("TRACE");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -180,7 +206,7 @@ public void connectionForPatchRequest() throws Exception {
TestRequest.Patch request = new TestRequest.Patch();
assertEquals(request.getMethod(), Method.PATCH);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("PATCH");
verify(mMockConnection, never()).setDoOutput(true);
}
Expand All @@ -190,7 +216,7 @@ public void connectionForPatchWithBodyRequest() throws Exception {
TestRequest.PatchWithBody request = new TestRequest.PatchWithBody();
assertEquals(request.getMethod(), Method.PATCH);

HurlStack.setConnectionParametersForRequest(mMockConnection, request);
mHurlStack.setConnectionParametersForRequest(mMockConnection, request);
verify(mMockConnection).setRequestMethod("PATCH");
verify(mMockConnection).setDoOutput(true);
}
Expand Down Expand Up @@ -256,4 +282,56 @@ public void convertHeaders() {
expected.add(new Header("HeaderB", "ValueB_2"));
assertEquals(expected, result);
}

@Test
public void interceptResponseStream() throws Exception {
when(mMockConnection.getResponseCode()).thenReturn(HttpURLConnection.HTTP_OK);
when(mMockConnection.getInputStream())
.thenReturn(new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8)));
HttpResponse response =
mHurlStack.executeRequest(
new TestRequest.Get(), Collections.<String, String>emptyMap());
assertTrue(response.getContent() instanceof MonitoringInputStream);
}

@Test
public void interceptRequestStream() throws Exception {
MonitoredRequest request = new MonitoredRequest();
mHurlStack.executeRequest(request, Collections.<String, String>emptyMap());
assertTrue(request.totalRequestBytes > 0);
assertEquals(request.totalRequestBytes, request.requestBytesRead);
}

private static class MonitoringInputStream extends FilterInputStream {
private MonitoringInputStream(InputStream in) {
super(in);
}
}

private static class MonitoringOutputStream extends FilterOutputStream {
private MonitoredRequest request;

private MonitoringOutputStream(OutputStream out, MonitoredRequest request, int length) {
super(out);
this.request = request;
this.request.totalRequestBytes = length;
}

@Override
public void write(int b) throws IOException {
this.request.requestBytesRead++;
out.write(b);
}

@Override
public void write(byte[] b, int off, int len) throws IOException {
this.request.requestBytesRead += len;
out.write(b, off, len);
}
}

private static class MonitoredRequest extends TestRequest.PostWithBody {
int requestBytesRead = 0;
int totalRequestBytes = 0;
}
}

0 comments on commit 455a161

Please sign in to comment.