Skip to content

Commit

Permalink
read plain or chunked body + tests
Browse files Browse the repository at this point in the history
  • Loading branch information
amihaiemil committed Feb 4, 2018
1 parent 080c578 commit 1707e97
Show file tree
Hide file tree
Showing 2 changed files with 223 additions and 13 deletions.
75 changes: 69 additions & 6 deletions src/main/java/com/amihaiemil/docker/SocketResponse.java
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
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());
}

}

1 comment on commit 1707e97

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on 1707e97 Feb 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Puzzle 14-ad81abd3 disappeared from src/main/java/com/amihaiemil/docker/SocketResponse.java, that's why I closed #19. Please, remember that the puzzle was not necessarily removed in this particular commit. Maybe it happened earlier, but we discovered this fact only now.

Please sign in to comment.