Repository: docker-java/docker-java
Pull request #208 (2015-05-03 12:27:20)

Added PullEventStreamItem and EventStreamReader to stream the reading of...
... events.

Similar to the `FrameReader` a decorator for input streams that allows you to read typed data from pull, push and build commands.


Patch:
diff --git a/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java b/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java
index ec013d42..925946db 100644
--- a/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java
+++ b/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java
@@ -1,12 +1,12 @@
 package com.github.dockerjava.api.command;
 
+import com.github.dockerjava.api.model.AuthConfigurations;
+import com.github.dockerjava.api.model.EventStreamItem;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 
-import com.github.dockerjava.api.model.AuthConfigurations;
-import com.github.dockerjava.api.model.EventStreamItem;
-
 /**
  * 
  * Build an image from Dockerfile.
@@ -61,6 +61,9 @@ public interface BuildImageCmd extends DockerCmd<BuildImageCmd.Response>{
 	public static interface Exec extends DockerCmdExec<BuildImageCmd, BuildImageCmd.Response> {
 	}
 
+	/**
+	 * @see  {@link com.github.dockerjava.core.command.EventStreamReader}
+	 */
   public static abstract class Response extends InputStream {
     public abstract Iterable<EventStreamItem> getItems() throws IOException;
   }
diff --git a/src/main/java/com/github/dockerjava/api/command/PullImageCmd.java b/src/main/java/com/github/dockerjava/api/command/PullImageCmd.java
index c39617dd..4b5fc4c3 100644
--- a/src/main/java/com/github/dockerjava/api/command/PullImageCmd.java
+++ b/src/main/java/com/github/dockerjava/api/command/PullImageCmd.java
@@ -1,6 +1,7 @@
 package com.github.dockerjava.api.command;
 
 import com.github.dockerjava.api.model.AuthConfig;
+import com.github.dockerjava.core.command.EventStreamReader;
 
 import java.io.InputStream;
 
@@ -33,6 +34,8 @@ public interface PullImageCmd extends DockerCmd<InputStream>{
     /**
      * Its the responsibility of the caller to consume and/or close the {@link InputStream} to prevent
 	 * connection leaks.
+     *
+     * @see {@link EventStreamReader}
      */
     @Override
     public InputStream exec();
diff --git a/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java b/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java
index 80e16c6d..9ed38beb 100644
--- a/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java
+++ b/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java
@@ -1,11 +1,12 @@
 package com.github.dockerjava.api.command;
 
-import java.io.IOException;
-import java.io.InputStream;
-
 import com.github.dockerjava.api.NotFoundException;
 import com.github.dockerjava.api.model.AuthConfig;
 import com.github.dockerjava.api.model.PushEventStreamItem;
+import com.github.dockerjava.core.command.EventStreamReader;
+
+import java.io.IOException;
+import java.io.InputStream;
 
 /**
  * Push the latest image to the repository.
@@ -40,6 +41,9 @@ public interface PushImageCmd extends DockerCmd<PushImageCmd.Response>{
 	public static interface Exec extends DockerCmdExec<PushImageCmd, Response> {
 	}
 
+	/**
+	 * @see {@link EventStreamReader}
+	 */
   	public static abstract class Response extends InputStream {
 	  public abstract Iterable<PushEventStreamItem> getItems() throws IOException;
 	}
diff --git a/src/main/java/com/github/dockerjava/api/model/PullEventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/PullEventStreamItem.java
new file mode 100644
index 00000000..4a3a0b50
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/PullEventStreamItem.java
@@ -0,0 +1,59 @@
+package com.github.dockerjava.api.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Objects;
+
+import java.io.Serializable;
+
+/**
+ * Represents an item returned from pull
+ */
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class PullEventStreamItem implements Serializable {
+
+	private static final long serialVersionUID = -5187169652557467828L;
+
+	@JsonProperty("status")
+    private String status;
+
+    @JsonProperty("progress")
+    private String progress;
+
+    @JsonProperty("progressDetail")
+    private ProgressDetail progressDetail;
+
+
+    public String getStatus() {
+        return status;
+    }
+
+    public String getProgress() {
+        return progress;
+    }
+
+    public ProgressDetail getProgressDetail() {
+        return progressDetail;
+    }
+
+    @JsonIgnoreProperties(ignoreUnknown=true)
+    public static class ProgressDetail implements Serializable {
+        @JsonProperty("current")
+        int current;
+
+
+        @Override
+        public String toString() {
+            return "current " + current;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("status", status)
+                .add("progress", progress)
+                .add("progressDetail", progressDetail)
+                .toString();
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/core/command/EventStreamReader.java b/src/main/java/com/github/dockerjava/core/command/EventStreamReader.java
new file mode 100644
index 00000000..b8975224
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/core/command/EventStreamReader.java
@@ -0,0 +1,35 @@
+package com.github.dockerjava.core.command;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class EventStreamReader<I> implements AutoCloseable {
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+    private final Class<I> type;
+    private final InputStream inputStream;
+
+    public EventStreamReader(InputStream inputStream, Class<I> type) {
+        this.inputStream = inputStream;
+        this.type = type;
+    }
+
+    public I readItem() throws IOException {
+        try {
+            return objectMapper.readValue(inputStream, type);
+        } catch (IOException e) {
+            // dirty, but works
+            if (e.getMessage().equals("Stream closed")) {
+                return null;
+            }
+            throw e;
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        inputStream.close();
+    }
+}


Test patch:
diff --git a/src/test/java/com/github/dockerjava/core/command/DockerfileFixture.java b/src/test/java/com/github/dockerjava/core/command/DockerfileFixture.java
index 2d7ca7c8..840f63da 100644
--- a/src/test/java/com/github/dockerjava/core/command/DockerfileFixture.java
+++ b/src/test/java/com/github/dockerjava/core/command/DockerfileFixture.java
@@ -8,6 +8,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.io.IOException;
 
 /**
  * Start and stop a single container for testing.
@@ -25,7 +26,7 @@ public class DockerfileFixture implements AutoCloseable {
         this.directory = directory;
     }
 
-    public void open() throws Exception {
+    public void open() throws IOException {
 
         LOGGER.info("building {}", directory);
         dockerClient
@@ -79,7 +80,7 @@ public class DockerfileFixture implements AutoCloseable {
                         .removeImageCmd(repository)
                         .withForce()
                         .exec();
-            } catch (InternalServerErrorException e) {
+            } catch (NotFoundException | InternalServerErrorException e) {
                 LOGGER.info("ignoring {}", e.getMessage());
             }
             repository = null;
diff --git a/src/test/java/com/github/dockerjava/core/command/EventStreamReaderITest.java b/src/test/java/com/github/dockerjava/core/command/EventStreamReaderITest.java
new file mode 100644
index 00000000..dfc330d9
--- /dev/null
+++ b/src/test/java/com/github/dockerjava/core/command/EventStreamReaderITest.java
@@ -0,0 +1,72 @@
+package com.github.dockerjava.core.command;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.model.EventStreamItem;
+import com.github.dockerjava.api.model.PullEventStreamItem;
+import com.github.dockerjava.core.DockerClientBuilder;
+import org.testng.annotations.AfterTest;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Test;
+
+import java.io.File;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.beans.HasPropertyWithValue.hasProperty;
+import static org.hamcrest.core.AllOf.allOf;
+import static org.hamcrest.core.IsEqual.equalTo;
+import static org.hamcrest.core.IsNull.nullValue;
+import static org.testng.AssertJUnit.assertNull;
+
+
+@Test(groups = "integration")
+public class EventStreamReaderITest  {
+
+    private DockerClient dockerClient;
+
+    @BeforeTest
+    public void setUp() throws Exception {
+        dockerClient = DockerClientBuilder.getInstance().build();
+    }
+
+    @AfterTest
+    public void tearDown() throws Exception {
+        dockerClient.close();
+    }
+
+    @Test
+    public void pullCanBeStreamed() throws Exception {
+
+        try (EventStreamReader<PullEventStreamItem> reader = new EventStreamReader<>(
+                dockerClient.pullImageCmd("busybox:latest").exec(),
+                PullEventStreamItem.class)
+        ) {;
+            assertThat(reader.readItem(),
+                    allOf(
+                            hasProperty("status", equalTo("Pulling repository busybox")),
+                            hasProperty("progress", nullValue()),
+                            hasProperty("progressDetail", nullValue())
+                    )
+                );
+            assertNull(reader.readItem());
+        }
+    }
+
+    @Test
+    public void buildCanBeStreamed() throws Exception {
+
+        try (EventStreamReader<EventStreamItem> reader = new EventStreamReader<>(
+                dockerClient.buildImageCmd(new File("src/test/resources/eventStreamReaderDockerfile")).exec(),
+                EventStreamItem.class)
+        ) {
+            assertThat(reader.readItem(),
+                    allOf(
+                            hasProperty("stream", equalTo("Step 0 : FROM busybox:latest\n")),
+                            hasProperty("error", nullValue()),
+                            hasProperty("errorDetail", nullValue())
+                    )
+            );
+            assertNull(reader.readItem());
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/com/github/dockerjava/core/command/FrameReaderITest.java b/src/test/java/com/github/dockerjava/core/command/FrameReaderITest.java
index 4654d83d..0cf08e95 100644
--- a/src/test/java/com/github/dockerjava/core/command/FrameReaderITest.java
+++ b/src/test/java/com/github/dockerjava/core/command/FrameReaderITest.java
@@ -1,44 +1,37 @@
 package com.github.dockerjava.core.command;
 
 
+import com.github.dockerjava.api.DockerClient;
 import com.github.dockerjava.api.model.Frame;
 import com.github.dockerjava.api.model.StreamType;
-import com.github.dockerjava.client.AbstractDockerClientTest;
-import org.testng.annotations.AfterMethod;
+import com.github.dockerjava.core.DockerClientBuilder;
 import org.testng.annotations.AfterTest;
-import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.BeforeTest;
 import org.testng.annotations.Test;
 
 import java.io.IOException;
 import java.io.InputStream;
 
+import static org.testng.Assert.assertEquals;
+import static org.testng.AssertJUnit.assertNull;
+
 @Test(groups = "integration")
-public class FrameReaderITest extends AbstractDockerClientTest {
+public class FrameReaderITest  {
 
+    private DockerClient dockerClient;
     private DockerfileFixture dockerfileFixture;
 
     @BeforeTest
-    @Override
-    public void beforeTest() {
-        super.beforeTest();
+    public void beforeTest() throws Exception {
+        dockerClient = DockerClientBuilder.getInstance().build();
         dockerfileFixture = new DockerfileFixture(dockerClient, "frameReaderDockerfile");
-    }
-
-    @BeforeMethod
-    public void createAndStartDockerContainer() throws Exception {
         dockerfileFixture.open();
     }
 
-    @AfterMethod
+    @AfterTest
     public void deleteDockerContainerImage() throws Exception {
         dockerfileFixture.close();
-    }
-
-    @AfterTest
-    @Override
-    public void afterTest() {
-        super.afterTest();
+        dockerClient.close();
     }
 
     @Test
diff --git a/src/test/resources/eventStreamReaderDockerfile/Dockerfile b/src/test/resources/eventStreamReaderDockerfile/Dockerfile
new file mode 100644
index 00000000..cdd3bba7
--- /dev/null
+++ b/src/test/resources/eventStreamReaderDockerfile/Dockerfile
@@ -0,0 +1,5 @@
+FROM busybox:latest
+
+RUN true
+
+CMD ["true"]
\ No newline at end of file