Repository: docker-java/docker-java
Pull request #200 (2015-04-25 12:12:35)

Created FrameReader to assist reading frames from attach commands.
Hi guy,

I've created a reader as I thought it would be daft to require every client to parse the bytes from the input stream.

Will help with https://github.com/docker-java/docker-java/issues/42 too.

Alex


Patch:
diff --git a/src/main/java/com/github/dockerjava/api/command/LogContainerCmd.java b/src/main/java/com/github/dockerjava/api/command/LogContainerCmd.java
index 74512b39..05e10855 100644
--- a/src/main/java/com/github/dockerjava/api/command/LogContainerCmd.java
+++ b/src/main/java/com/github/dockerjava/api/command/LogContainerCmd.java
@@ -1,9 +1,9 @@
 package com.github.dockerjava.api.command;
 
-import java.io.InputStream;
-
 import com.github.dockerjava.api.NotFoundException;
 
+import java.io.InputStream;
+
 /**
  * Get container logs
  *
@@ -18,6 +18,10 @@ import com.github.dockerjava.api.NotFoundException;
  *            Defaults to false.
  * @param tail
  * 			  - `all` or `<number>`, Output specified number of lines at the end of logs
+ *
+ * Consider wrapping any input stream you get with a frame reader to make reading frame easier.
+ *
+ * @see com.github.dockerjava.core.command.FrameReader
  */
 public interface LogContainerCmd extends DockerCmd<InputStream>{
 
diff --git a/src/main/java/com/github/dockerjava/api/model/Frame.java b/src/main/java/com/github/dockerjava/api/model/Frame.java
new file mode 100644
index 00000000..3860a5db
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/Frame.java
@@ -0,0 +1,47 @@
+package com.github.dockerjava.api.model;
+
+import java.util.Arrays;
+
+/**
+ * Represents a logging frame.
+ */
+public class Frame {
+    private final StreamType streamType;
+    private final byte[] payload;
+
+    public Frame(StreamType streamType, byte[] payload) {
+        this.streamType = streamType;
+        this.payload = payload;
+    }
+
+    public StreamType getStreamType() {
+        return streamType;
+    }
+
+    public byte[] getPayload() {
+        return payload;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s: %s", streamType, new String(payload).trim());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Frame frame = (Frame) o;
+
+        return Arrays.equals(payload, frame.payload) && streamType == frame.streamType;
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = streamType.hashCode();
+        result = 31 * result + Arrays.hashCode(payload);
+        return result;
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/api/model/StreamType.java b/src/main/java/com/github/dockerjava/api/model/StreamType.java
new file mode 100644
index 00000000..eb11c553
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/StreamType.java
@@ -0,0 +1,7 @@
+package com.github.dockerjava.api.model;
+
+public enum StreamType {
+    STDIN,
+    STDOUT,
+    STDERR
+}
diff --git a/src/main/java/com/github/dockerjava/core/command/FrameReader.java b/src/main/java/com/github/dockerjava/core/command/FrameReader.java
new file mode 100644
index 00000000..c3979146
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/core/command/FrameReader.java
@@ -0,0 +1,66 @@
+package com.github.dockerjava.core.command;
+
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.api.model.StreamType;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Breaks the input into frame. Similar to how a buffered reader would readLies.
+ * <p/>
+ * See:  {@link }http://docs.docker.com/v1.6/reference/api/docker_remote_api_v1.13/#attach-to-a-container}
+ */
+public class FrameReader implements AutoCloseable {
+
+    private static final int HEADER_SIZE = 8;
+    private final InputStream inputStream;
+
+    public FrameReader(InputStream inputStream) {
+        this.inputStream = inputStream;
+    }
+
+    private static StreamType streamType(byte streamType) {
+        switch (streamType) {
+            case 0:
+                return StreamType.STDIN;
+            case 1:
+                return StreamType.STDOUT;
+            case 2:
+                return StreamType.STDERR;
+            default:
+                throw new IllegalArgumentException("invalid streamType");
+        }
+    }
+
+    /**
+     * @return A frame, or null if no more frames.
+     */
+    public Frame readFrame() throws IOException {
+        byte[] header = new byte[HEADER_SIZE];
+        int headerSize = inputStream.read(header);
+
+        if (headerSize == -1) {
+            return null;
+        }
+
+        if (headerSize != HEADER_SIZE) {
+            throw new IOException(String.format("header must be %d bytes long, but was %d", HEADER_SIZE, headerSize));
+        }
+
+        int payloadSize = ((header[4] & 0xff) << 24) + ((header[5] & 0xff) << 16) + ((header[6] & 0xff) << 8) + (header[7] & 0xff);
+
+        byte[] payload = new byte[payloadSize];
+        int actualPayloadSize = inputStream.read(payload);
+        if (actualPayloadSize != payloadSize) {
+            throw new IOException(String.format("payload must be %d bytes long, but was %d", payloadSize, actualPayloadSize));
+        }
+
+        return new Frame(streamType(header[0]), payload);
+    }
+
+    @Override
+    public void close() throws IOException {
+        inputStream.close();
+    }
+}


Test patch:
diff --git a/src/test/java/com/github/dockerjava/core/command/FrameReaderTest.java b/src/test/java/com/github/dockerjava/core/command/FrameReaderTest.java
new file mode 100644
index 00000000..2b5e2b64
--- /dev/null
+++ b/src/test/java/com/github/dockerjava/core/command/FrameReaderTest.java
@@ -0,0 +1,57 @@
+package com.github.dockerjava.core.command;
+
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.api.model.StreamType;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
+
+public class FrameReaderTest {
+    public static final int HEADER_SIZE = 8;
+    private final List<Integer> bytes = new ArrayList<>();
+    private final InputStream inputStream = new InputStream() {
+        @Override
+        public int read() throws IOException {
+            return bytes.isEmpty() ? -1 : bytes.remove(0);
+        }
+    };
+    private final FrameReader frameReader = new FrameReader(inputStream);
+
+    @Test
+    public void endOfStreamReturnsNull() throws Exception {
+        assertNull(nextFrame());
+    }
+
+    @Test
+    public void stdInBytesFrameReturnsFrame() throws Exception {
+        assertEquals(nextFrame(0, 0, 0, 0, 0, 0, 0, 0), new Frame(StreamType.STDIN, new byte[0]));
+    }
+
+    private Frame nextFrame(int... bytes) throws IOException {
+        setBytes(bytes);
+        return frameReader.readFrame();
+    }
+
+    @Test
+    public void stdOutBytesFrameReturnsFrame() throws Exception {
+        assertEquals(nextFrame(1, 0, 0, 0, 0, 0, 0, 0), new Frame(StreamType.STDOUT, new byte[0]));
+    }
+
+    @Test
+    public void stdErrBytesFrameReturnsFrame() throws Exception {
+        assertEquals(nextFrame(2, 0, 0, 0, 0, 0, 0, 0), new Frame(StreamType.STDERR, new byte[0]));
+    }
+
+    private void setBytes(int... bytes) {
+        this.bytes.clear();
+        for (int aByte : bytes) {
+            this.bytes.add(aByte);
+        }
+    }
+}
\ No newline at end of file