From 1707e972edae447e9829b0082b94c804c67467df Mon Sep 17 00:00:00 2001 From: Mihai Andronache Date: Sun, 4 Feb 2018 21:23:11 +0200 Subject: [PATCH] read plain or chunked body + tests --- .../com/amihaiemil/docker/SocketResponse.java | 75 +++++++- .../docker/SocketResponseTestCase.java | 161 +++++++++++++++++- 2 files changed, 223 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/amihaiemil/docker/SocketResponse.java b/src/main/java/com/amihaiemil/docker/SocketResponse.java index acd0a51d..142db6a1 100644 --- a/src/main/java/com/amihaiemil/docker/SocketResponse.java +++ b/src/main/java/com/amihaiemil/docker/SocketResponse.java @@ -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 { @@ -114,14 +112,41 @@ public Map> headers() { @Override public String body() { - return this.response.substring(this.response.indexOf("\n\n")).trim(); + final Map> 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 @@ -135,4 +160,42 @@ public T as(final Class 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 chunks = Arrays.asList( + this.content.split("\n") + ); + for(int idx = 1; idx < chunks.size() - 1; idx += 2) { + concat += chunks.get(idx) + " "; + } + return concat.trim(); + } + } } diff --git a/src/test/java/com/amihaiemil/docker/SocketResponseTestCase.java b/src/test/java/com/amihaiemil/docker/SocketResponseTestCase.java index 88a6dc20..fa945bc0 100644 --- a/src/test/java/com/amihaiemil/docker/SocketResponseTestCase.java +++ b/src/test/java/com/amihaiemil/docker/SocketResponseTestCase.java @@ -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; @@ -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> headers = resp.headers(); MatcherAssert.assertThat(headers.size(), Matchers.is(5)); @@ -154,4 +155,150 @@ public void noHeaders() { final Map> 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()); + } + }