Repository: docker-java/docker-java
Pull request #131 (2015-01-21 15:01:32)

Utility classes and streamed JSON representations
I have some additional commits to come that build on these.

Tag parameters are frequently mis-parsed in client code - provide a mechanism to do so.
Create representations for streamed JSON object types.


marcuslinke: @magnayn I would like to ask you to enrich this PR with these additional commits first please. Don't want to merge incomplete code into master. Thanks!

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 7c2deb63..8a1e6211 100644
--- a/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java
+++ b/src/main/java/com/github/dockerjava/api/command/BuildImageCmd.java
@@ -1,5 +1,9 @@
 package com.github.dockerjava.api.command;
 
+import com.github.dockerjava.api.model.EventStreamItem;
+import com.github.dockerjava.api.model.PushEventStreamItem;
+
+import java.io.IOException;
 import java.io.InputStream;
 
 /**
@@ -9,7 +13,7 @@ import java.io.InputStream;
  * TODO: http://docs.docker.com/reference/builder/#dockerignore
  * 
  */
-public interface BuildImageCmd extends DockerCmd<InputStream>{
+public interface BuildImageCmd extends DockerCmd<BuildImageCmd.Response>{
 
 	public BuildImageCmd withTag(String tag);
 
@@ -37,7 +41,10 @@ public interface BuildImageCmd extends DockerCmd<InputStream>{
 
 	public BuildImageCmd withQuiet(boolean quiet);
 	
-	public static interface Exec extends DockerCmdExec<BuildImageCmd, InputStream> {
+	public static interface Exec extends DockerCmdExec<BuildImageCmd, BuildImageCmd.Response> {
 	}
 
+  public static abstract class Response extends InputStream {
+    public abstract Iterable<EventStreamItem> getItems() throws IOException;
+  }
 }
\ No newline at end of file
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 f50beb01..80e16c6d 100644
--- a/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java
+++ b/src/main/java/com/github/dockerjava/api/command/PushImageCmd.java
@@ -1,16 +1,18 @@
 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;
 
 /**
  * Push the latest image to the repository.
  *
  * @param name The name, e.g. "alexec/busybox" or just "busybox" if you want to default. Not null.
  */
-public interface PushImageCmd extends DockerCmd<InputStream>{
+public interface PushImageCmd extends DockerCmd<PushImageCmd.Response>{
 
 	public String getName();
 
@@ -33,10 +35,13 @@ public interface PushImageCmd extends DockerCmd<InputStream>{
 	/**
 	 * @throws NotFoundException No such image
 	 */
-	@Override
-	public InputStream exec() throws NotFoundException;
+	public Response exec() throws NotFoundException;
 	
-	public static interface Exec extends DockerCmdExec<PushImageCmd, InputStream> {
+	public static interface Exec extends DockerCmdExec<PushImageCmd, Response> {
+	}
+
+  	public static abstract class Response extends InputStream {
+	  public abstract Iterable<PushEventStreamItem> getItems() throws IOException;
 	}
 
 }
\ No newline at end of file
diff --git a/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java
new file mode 100644
index 00000000..6e6ce035
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/EventStreamItem.java
@@ -0,0 +1,64 @@
+package com.github.dockerjava.api.model;
+
+
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+
+import jersey.repackaged.com.google.common.base.Objects;
+
+/**
+ * Represents an event stream
+ */
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class EventStreamItem implements Serializable {
+
+    @JsonProperty("stream")
+    private String stream;
+
+    // {"error":"Error...", "errorDetail":{"code": 123, "message": "Error..."}}
+    @JsonProperty("error")
+    private String error;
+
+    @JsonProperty("errorDetail")
+    private ErrorDetail errorDetail;
+
+    public String getStream() {
+        return stream;
+    }
+
+    public String getError() {
+        return error;
+    }
+
+    public ErrorDetail getErrorDetail() {
+        return errorDetail;
+    }
+
+    @JsonIgnoreProperties(ignoreUnknown=true)
+    public static class ErrorDetail implements Serializable {
+        @JsonProperty("code")
+        String code;
+        @JsonProperty("message")
+        String message;
+
+        @Override
+        public String toString() {
+            return Objects.toStringHelper(this)
+                    .add("code", code)
+                    .add("message", message)
+                    .toString();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("stream", stream)
+                .add("error", error)
+                .add("errorDetail", errorDetail)
+                .toString();
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java b/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java
new file mode 100644
index 00000000..f5e845b2
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/PushEventStreamItem.java
@@ -0,0 +1,58 @@
+package com.github.dockerjava.api.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.io.Serializable;
+
+import jersey.repackaged.com.google.common.base.Objects;
+
+/**
+ * Represents an item returned from push
+ */
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class PushEventStreamItem implements Serializable {
+
+    @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/BuildImageCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java
index 0bdb6db3..468dac7e 100644
--- a/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java
+++ b/src/main/java/com/github/dockerjava/core/command/BuildImageCmdImpl.java
@@ -32,8 +32,7 @@ import static jersey.repackaged.com.google.common.base.Preconditions.*;
  * Build an image from Dockerfile.
  * 
  */
-public class BuildImageCmdImpl extends
-		AbstrDockerCmd<BuildImageCmd, InputStream> implements BuildImageCmd {
+public class BuildImageCmdImpl extends AbstrDockerCmd<BuildImageCmd, BuildImageCmd.Response> implements BuildImageCmd {
 
 	private static final Pattern ADD_OR_COPY_PATTERN = Pattern
 			.compile("^(ADD|COPY)\\s+(.*)\\s+(.*)$");
diff --git a/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java b/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java
index 137c008a..1914f91d 100644
--- a/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java
+++ b/src/main/java/com/github/dockerjava/core/command/PushImageCmdImpl.java
@@ -2,8 +2,6 @@ package com.github.dockerjava.core.command;
 
 import static jersey.repackaged.com.google.common.base.Preconditions.checkNotNull;
 
-import java.io.InputStream;
-
 import com.github.dockerjava.api.NotFoundException;
 import com.github.dockerjava.api.command.PushImageCmd;
 
@@ -12,7 +10,7 @@ import com.github.dockerjava.api.command.PushImageCmd;
  *
  * @param name The name, e.g. "alexec/busybox" or just "busybox" if you want to default. Not null.
  */
-public class PushImageCmdImpl extends AbstrAuthCfgDockerCmd<PushImageCmd, InputStream> implements PushImageCmd  {
+public class PushImageCmdImpl extends AbstrAuthCfgDockerCmd<PushImageCmd, PushImageCmd.Response> implements PushImageCmd  {
 
     private String name;
     private String tag;
@@ -63,7 +61,7 @@ public class PushImageCmdImpl extends AbstrAuthCfgDockerCmd<PushImageCmd, InputS
      * @throws NotFoundException No such image
      */
     @Override
-    public InputStream exec() throws NotFoundException {
+    public Response exec() throws NotFoundException {
     	return super.exec();
     }
 }
diff --git a/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java
index 85c5f18c..faf3d9f2 100644
--- a/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java
+++ b/src/main/java/com/github/dockerjava/jaxrs/BuildImageCmdExec.java
@@ -1,8 +1,12 @@
 package com.github.dockerjava.jaxrs;
 
+
+
 import static javax.ws.rs.client.Entity.entity;
 
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.Iterator;
 
 import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.MediaType;
@@ -13,9 +17,16 @@ import org.glassfish.jersey.client.RequestEntityProcessing;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
 import com.github.dockerjava.api.command.BuildImageCmd;
+import com.github.dockerjava.api.command.PushImageCmd;
+import com.github.dockerjava.api.model.EventStreamItem;
+import com.github.dockerjava.api.model.PushEventStreamItem;
+
+import jersey.repackaged.com.google.common.collect.ImmutableList;
 
-public class BuildImageCmdExec extends AbstrDockerCmdExec<BuildImageCmd, InputStream> implements BuildImageCmd.Exec {
+public class BuildImageCmdExec extends AbstrDockerCmdExec<BuildImageCmd, BuildImageCmd.Response> implements BuildImageCmd.Exec {
 	
 	private static final Logger LOGGER = LoggerFactory
 			.getLogger(BuildImageCmdExec.class);
@@ -25,7 +36,7 @@ public class BuildImageCmdExec extends AbstrDockerCmdExec<BuildImageCmd, InputSt
 	}
 
 	@Override
-	protected InputStream execute(BuildImageCmd command) {
+	protected ResponseImpl execute(BuildImageCmd command) {
 		WebTarget webResource = getBaseResource().path("/build");
 		
 		if(command.getTag() != null) {
@@ -45,12 +56,38 @@ public class BuildImageCmdExec extends AbstrDockerCmdExec<BuildImageCmd, InputSt
 	  webResource.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED);
 	  webResource.property(ClientProperties.CHUNKED_ENCODING_SIZE, 1024*1024);
 
-		LOGGER.debug("POST: {}", webResource);
-		return webResource
-                .request()
-				.accept(MediaType.TEXT_PLAIN)
-				.post(entity(command.getTarInputStream(), "application/tar"), Response.class).readEntity(InputStream.class);
+
+          LOGGER.debug("POST: {}", webResource);
+          InputStream is = webResource
+              .request()
+              .accept(MediaType.TEXT_PLAIN)
+              .post(entity(command.getTarInputStream(), "application/tar"), Response.class).readEntity(InputStream.class);
+
+	  return new ResponseImpl(is);
 		
 	}
+  public static class ResponseImpl extends BuildImageCmd.Response {
+
+    private final InputStream proxy;
+
+    public ResponseImpl(InputStream proxy) {
+      this.proxy = proxy;
+    }
+
+    @Override
+    public Iterable<EventStreamItem> getItems() throws IOException {
+      ObjectMapper mapper = new ObjectMapper();
+      // we'll be reading instances of MyBean
+      ObjectReader reader = mapper.reader(EventStreamItem.class);
+      // and then do other configuration, if any, and read:
+      Iterator<EventStreamItem> items = reader.readValues(proxy);
+
+      return ImmutableList.copyOf(items);
+    }
 
+    @Override
+    public int read() throws IOException {
+      return proxy.read();
+    }
+  }
 }
diff --git a/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java b/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java
index b970d42f..152f31a3 100644
--- a/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java
+++ b/src/main/java/com/github/dockerjava/jaxrs/PushImageCmdExec.java
@@ -1,6 +1,12 @@
 package com.github.dockerjava.jaxrs;
 
+
+
+import static javax.ws.rs.client.Entity.entity;
+
+import java.io.IOException;
 import java.io.InputStream;
+import java.util.Iterator;
 
 import javax.ws.rs.client.WebTarget;
 import javax.ws.rs.core.MediaType;
@@ -8,37 +14,72 @@ import javax.ws.rs.core.MediaType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
 import com.github.dockerjava.api.command.PushImageCmd;
+import com.github.dockerjava.api.command.PushImageCmd.Response;
 import com.github.dockerjava.api.model.AuthConfig;
+import com.github.dockerjava.api.model.PushEventStreamItem;
 
-public class PushImageCmdExec extends
-		AbstrDockerCmdExec<PushImageCmd, InputStream> implements
-		PushImageCmd.Exec {
+// Shaded, but imported
+import jersey.repackaged.com.google.common.collect.ImmutableList;
 
+public class PushImageCmdExec extends AbstrDockerCmdExec<PushImageCmd, Response> implements PushImageCmd.Exec {
+	
 	private static final Logger LOGGER = LoggerFactory
 			.getLogger(PushImageCmdExec.class);
-
+	
 	public PushImageCmdExec(WebTarget baseResource) {
 		super(baseResource);
 	}
 
 	@Override
-	protected InputStream execute(PushImageCmd command) {
-		WebTarget webResource = getBaseResource().path(
-				"/images/" + name(command) + "/push").queryParam("tag",
-				command.getTag());
+	protected ResponseImpl execute(PushImageCmd command) {
+		WebTarget webResource = getBaseResource().path("/images/" + name(command) + "/push")
+		    .queryParam("tag", command.getTag());
 
 		final String registryAuth = registryAuth(command.getAuthConfig());
 		LOGGER.trace("POST: {}", webResource);
-		return webResource.request().header("X-Registry-Auth", registryAuth)
-				.accept(MediaType.APPLICATION_JSON).post(null)
-				.readEntity(InputStream.class);
-	}
+		InputStream is =  webResource
+                .request()
+				.header("X-Registry-Auth", registryAuth)
+				.accept(MediaType.APPLICATION_JSON)
+				.post(
+				    entity(Response.class, MediaType.APPLICATION_JSON)).readEntity(
+			InputStream.class);
 
+	  return new ResponseImpl(is);
+	}
+	
 	private String name(PushImageCmd command) {
 		String name = command.getName();
 		AuthConfig authConfig = command.getAuthConfig();
 		return name.contains("/") ? name : authConfig.getUsername();
 	}
 
+
+  public static class ResponseImpl extends Response {
+
+    private final InputStream proxy;
+
+    ResponseImpl(InputStream proxy) {
+      this.proxy = proxy;
+    }
+
+    @Override
+    public Iterable<PushEventStreamItem> getItems() throws IOException {
+      ObjectMapper mapper = new ObjectMapper();
+      // we'll be reading instances of MyBean
+      ObjectReader reader = mapper.reader(PushEventStreamItem.class);
+      // and then do other configuration, if any, and read:
+      Iterator<PushEventStreamItem> items = reader.readValues(proxy);
+
+      return ImmutableList.copyOf(items);
+    }
+
+    @Override
+    public int read() throws IOException {
+      return proxy.read();
+    }
+  }
 }


Test patch:
diff --git a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java
index c63667a0..4fcf26b6 100644
--- a/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java
+++ b/src/test/java/com/github/dockerjava/core/TestDockerCmdExecFactory.java
@@ -8,6 +8,7 @@ import java.util.List;
 
 import com.github.dockerjava.api.command.*;
 import com.github.dockerjava.api.command.AuthCmd.Exec;
+import com.github.dockerjava.jaxrs.BuildImageCmdExec;
 
 /**
  * Special {@link DockerCmdExecFactory} implementation that collects container and image creations
@@ -92,18 +93,18 @@ public class TestDockerCmdExecFactory implements DockerCmdExecFactory {
 	public BuildImageCmd.Exec createBuildImageCmdExec() {
 		return new BuildImageCmd.Exec() {
 			@Override
-			public InputStream exec(BuildImageCmd command) {
+			public  BuildImageCmd.Response exec(BuildImageCmd command) {
 				// can't detect image id here so tagging it
 				String tag = command.getTag();
 				if(tag == null || "".equals(tag.trim())) {
 					tag = "" + new SecureRandom().nextInt(Integer.MAX_VALUE);
 					command.withTag(tag);
 				}
-				InputStream inputStream = delegate.createBuildImageCmdExec().exec(command); 
+				InputStream inputStream = delegate.createBuildImageCmdExec().exec(command);
 				imageNames.add(tag);
-				return inputStream;
+				return new BuildImageCmdExec.ResponseImpl(inputStream);
 			}
-		};
+                };
 	}
 	
 	@Override
diff --git a/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java b/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java
index 3692f56d..8be2f8f8 100644
--- a/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java
+++ b/src/test/java/com/github/dockerjava/core/command/BuildImageCmdImplTest.java
@@ -27,8 +27,10 @@ import com.github.dockerjava.api.DockerException;
 import com.github.dockerjava.api.command.CreateContainerResponse;
 import com.github.dockerjava.api.command.InspectContainerResponse;
 import com.github.dockerjava.api.command.InspectImageResponse;
+import com.github.dockerjava.api.model.EventStreamItem;
 import com.github.dockerjava.client.AbstractDockerClientTest;
 
+
 @Test(groups = "integration")
 public class BuildImageCmdImplTest extends AbstractDockerClientTest {
 
@@ -170,18 +172,24 @@ public class BuildImageCmdImplTest extends AbstractDockerClientTest {
 	}
 
 	@Test
-	public void testNetCatDockerfileBuilder() throws InterruptedException {
+	public void testNetCatDockerfileBuilder() throws InterruptedException, IOException {
 		File baseDir = new File(Thread.currentThread().getContextClassLoader()
 				.getResource("netcat").getFile());
 
-		InputStream response = dockerClient.buildImageCmd(baseDir).withNoCache().exec();
+		Iterable<EventStreamItem> response = dockerClient.buildImageCmd(baseDir).withNoCache().exec().getItems();
 
-		String fullLog = asString(response);
+                String imageId = null;
+
+		for(EventStreamItem item : response) {
+                  String text = item.getStream();
+                  if( text.startsWith("Successfully built ")) {
+                    imageId = StringUtils.substringBetween(text,
+				"Successfully built ", "\n").trim();
+                  }
+                }
 		
-		assertThat(fullLog, containsString("Successfully built"));
+		assertNotNull(imageId, "Not successful in build");
 
-		String imageId = StringUtils.substringBetween(fullLog,
-				"Successfully built ", "\\n\"}").trim();
 
 		InspectImageResponse inspectImageResponse = dockerClient
 				.inspectImageCmd(imageId).exec();