Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 69 additions & 6 deletions src/main/java/com/amihaiemil/docker/SocketResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
* @author Mihai Andronache (amihaiemil@gmail.com)
* @version $Id$
* @since 0.0.1
* @todo #14:30min Refine the body reading logic. Have to take into account
* the Transfer-Encoding header and cover the empty body case.
*/
final class SocketResponse implements Response {

Expand Down Expand Up @@ -114,14 +112,41 @@ public Map<String, List<String>> headers() {

@Override
public String body() {
return this.response.substring(this.response.indexOf("\n\n")).trim();
final Map<String, List<String>> headers = this.headers();
final String body;
if(headers.get("Content-Length") != null) {
final int length = Integer.valueOf(
headers.get("Content-Length").get(0)
);
body = this.response.substring(
this.response.indexOf("\n\n")
).trim().substring(0, length);
} else {
if(headers.get("Transfer-Encoding") == null) {
throw new IllegalStateException(
"Transfer-Encoding header is missing from the response."
);
} else {
final String enc = headers.get("Transfer-Encoding").get(0);
if("chunked".equalsIgnoreCase(enc)) {
body = new ChunkedContent(
this.response.substring(
this.response.indexOf("\n\n")
).trim()
).toString();
} else {
throw new IllegalStateException(
"Only chunked encoding is supported for now."
);
}
}
}
return body;
}

@Override
public byte[] binary() {
return this.response.substring(
this.response.indexOf("\n\n")
).getBytes();
return this.body().getBytes();
}

@Override
Expand All @@ -135,4 +160,42 @@ public <T extends Response> T as(final Class<T> type) {
throw new IllegalStateException(ex);
}
}

/**
* Chunked content from an HTTP response.
*/
final class ChunkedContent {

/**
* Content.
*/
final String content;

/**
* Ctor.
* @param content Chunked content.
*/
ChunkedContent(final String content) {
this.content = content;
}

/**
* Iterate over the lines of the chunked response and concatenate
* the chunks. Skip the first and next every second line since they
* contain the chunk's size. Ignore the last line since it's supposed
* to be 0. This is according to the "Transfer-Encoding: chunked" spec.
* @return The response's concatenated chunks, separated by whitespace.
*/
@Override
public String toString() {
String concat = "";
final List<String> chunks = Arrays.asList(
this.content.split("\n")
);
for(int idx = 1; idx < chunks.size() - 1; idx += 2) {
concat += chunks.get(idx) + " ";
}
return concat.trim();
}
}
}
161 changes: 154 additions & 7 deletions src/test/java/com/amihaiemil/docker/SocketResponseTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
import com.jcabi.http.Response;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -110,12 +111,12 @@ public void headersWithNoContent() {
@Test
public void headersWithontent() {
final String responseString = "HTTP/1.1 200 OK\n"
+ "Api-Version: 1.35\n"
+ "Content-Type: application/json\n"
+ "Docker-Experimental: false\n"
+ "Connection: close\n"
+ "Content-Length: 2\n\n"
+ "OK";
+ "Api-Version: 1.35\n"
+ "Content-Type: application/json\n"
+ "Docker-Experimental: false\n"
+ "Connection: close\n"
+ "Content-Length: 2\n\n"
+ "OK";
final Response resp = new SocketResponse(null, responseString);
final Map<String, List<String>> headers = resp.headers();
MatcherAssert.assertThat(headers.size(), Matchers.is(5));
Expand Down Expand Up @@ -154,4 +155,150 @@ public void noHeaders() {
final Map<String, List<String>> headers = resp.headers();
MatcherAssert.assertThat(headers.isEmpty(), Matchers.is(Boolean.TRUE));
}

/**
* A SocketResponse returns its chunked body.
*/
@Test
public void chunkedBody() {
final String responseString = "HTTP/1.1 200 OK \n"
+ "Content-Type: text/plain \n"
+ "Transfer-Encoding: chunked\n"
+ "\n"
+ "7\n"
+ "Mozilla\n"
+ "9\n"
+ "Developer\n"
+ "7\n"
+ "Network\n"
+ "0\n"
+ "\n";
final Response resp = new SocketResponse(null, responseString);
MatcherAssert.assertThat(
resp.body(),
Matchers.equalTo("Mozilla Developer Network")
);
}

/**
* A SocketResponse returns its plain body.
*/
@Test
public void plainBody() {
final String responseString = "HTTP/1.1 200 OK \n"
+ "Content-Type: text/plain \n"
+ "Content-Length: 25\n"
+ "\n"
+ "Mozilla Developer Network\n\n";
final Response resp = new SocketResponse(null, responseString);
MatcherAssert.assertThat(
resp.body(),
Matchers.equalTo("Mozilla Developer Network")
);
}

/**
* A SocketResponse returns its plain body's bytes.
*/
@Test
public void plainBodyBytes() {
final byte[] bytes = "Mozilla Developer Network".getBytes();
final String responseString = "HTTP/1.1 200 OK \n"
+ "Content-Type: text/plain \n"
+ "Content-Length: 25\n"
+ "\n"
+ "Mozilla Developer Network\n\n";
final Response resp = new SocketResponse(null, responseString);
MatcherAssert.assertThat(
Arrays.equals(bytes, resp.binary()),
Matchers.is(true)
);
}
/**
* A SocketResponse returns its chunked body's bytes.
*/
@Test
public void chunkedBodyBytes() {
final byte[] bytes = "Mozilla Developer Network".getBytes();
final String responseString = "HTTP/1.1 200 OK \n"
+ "Content-Type: text/plain \n"
+ "Transfer-Encoding: chunked\n"
+ "\n"
+ "7\n"
+ "Mozilla\n"
+ "9\n"
+ "Developer\n"
+ "7\n"
+ "Network\n"
+ "0\n"
+ "\n";
final Response resp = new SocketResponse(null, responseString);
MatcherAssert.assertThat(
Arrays.equals(bytes, resp.binary()),
Matchers.is(true)
);
}

/**
* A SocketResponse supports only chunked encoding for now.
*/
@Test
public void onlyChunkedEncodingSupported() {
final byte[] bytes = "Mozilla Developer Network".getBytes();
final String responseString = "HTTP/1.1 200 OK \n"
+ "Content-Type: text/plain \n"
+ "Transfer-Encoding: gzip\n"
+ "\n"
+ "something in gzip";
final Response resp = new SocketResponse(null, responseString);
try {
resp.body();
Assert.fail("IllegalStateException should have been thrown.");
} catch (final IllegalStateException ise) {
MatcherAssert.assertThat(
ise.getMessage(),
Matchers.equalTo("Only chunked encoding is supported for now.")
);
}
}

/**
* When reading the response's body, the header Content-Length
* or Transfer-Encoding have to be present.
*/
@Test
public void contentLengthAndEncodingHeadersMissing() {
final String responseString = "HTTP/1.1 200 OK \n"
+ "Content-Type: text/plain \n"
+ "\n"
+ "both headers missing";
final Response resp = new SocketResponse(null, responseString);
try {
resp.body();
Assert.fail("IllegalStateException should have been thrown.");
} catch (final IllegalStateException ise) {
MatcherAssert.assertThat(
ise.getMessage(),
Matchers.equalTo(
"Transfer-Encoding header is missing from the response."
)
);
}
}

/**
* SocketResponse can return an empty body.
*/
@Test
public void emptyBody() {
final String responseString = "HTTP/1.1 200 OK\n"
+ "Api-Version: 1.35\n"
+ "Content-Type: application/json\n"
+ "Docker-Experimental: false\n"
+ "Connection: close\n"
+ "Content-Length: 0\n\n";
final Response resp = new SocketResponse(null, responseString);
MatcherAssert.assertThat(resp.body(), Matchers.isEmptyString());
}

}