Repository: docker-java/docker-java
Pull request #146 (2015-02-06 14:36:21)

Create Identifier type.
docker 'Identifiers' are easily to get wrong, as everything is a string and APIs that just take 'string' are therefore prone to error.

Users are used to tags looking like the following
tag
tag:label
host/tag
host/tag:label
host:port/tag
host:port/tag:label

Create a class to parse this into the 'repository' side and the (optional) tag side.


marcuslinke: @magnayn Shouldn't we make use of ```Identifier``` in every image command and remove appropriate ```String``` versions? WDYT? The next release (1.0.0) is will break the API anyway so it would be a good time for it. 
magnayn: You could - IME Identifiers often cause mistakes in clients -- I just didn't want the scope of this PR to go changing APIs that may have some pain for people to change..

magnayn: Rebased because of merge conflict

marcuslinke: Could you add some basic tests for ```Repository``` and ```Identifier``` before I merge please. I think this would be helpful for documentation purposes. Thanks in advance!

Patch:
diff --git a/src/main/java/com/github/dockerjava/api/DockerClient.java b/src/main/java/com/github/dockerjava/api/DockerClient.java
index d905ef10..1a3a285f 100644
--- a/src/main/java/com/github/dockerjava/api/DockerClient.java
+++ b/src/main/java/com/github/dockerjava/api/DockerClient.java
@@ -4,6 +4,7 @@ import java.io.*;
 
 import com.github.dockerjava.api.command.*;
 import com.github.dockerjava.api.model.AuthConfig;
+import com.github.dockerjava.api.model.Identifier;
 
 // https://godoc.org/github.com/fsouza/go-dockerclient
 public interface DockerClient extends Closeable {
@@ -29,6 +30,8 @@ public interface DockerClient extends Closeable {
 
 	public PushImageCmd pushImageCmd(String name);
 
+        public PushImageCmd pushImageCmd(Identifier identifier);
+
 	public CreateImageCmd createImageCmd(String repository,
 			InputStream imageStream);
 
diff --git a/src/main/java/com/github/dockerjava/api/model/Identifier.java b/src/main/java/com/github/dockerjava/api/model/Identifier.java
new file mode 100644
index 00000000..498104e2
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/Identifier.java
@@ -0,0 +1,59 @@
+package com.github.dockerjava.api.model;
+
+
+import com.google.common.base.Objects;
+import com.google.common.base.Optional;
+
+/**
+ * Created by magnayn on 22/07/2014.
+ */
+public class Identifier {
+    public final Repository repository;
+    public final Optional<String> tag;
+
+    public Identifier(Repository repository, String tag) {
+        this.repository = repository;
+
+        if( tag == null )
+            this.tag = Optional.absent();
+        else
+            this.tag = Optional.of(tag);
+    }
+
+
+    /**
+     * Return an identifier that correctly splits up the repository and tag.
+     * There can be &gt; 1 ":"
+     * fred/jim     --&gt; fred/jim, []
+     * fred/jim:123 --&gt;  fred/jim, 123
+     * fred:123/jim:123 --&gt; fred:123/jim, 123
+     *
+     *
+     * @param identifier as a string
+     * @return parsed identifier.
+     */
+    public static Identifier fromCompoundString(String identifier) {
+        String[] parts = identifier.split("/");
+        if( parts.length != 2 ) {
+            String[] rhs = identifier.split(":");
+            if( rhs.length != 2 )
+                return new Identifier( new Repository(identifier), null);
+            else
+                return new Identifier( new Repository(rhs[0]), rhs[1]);
+        }
+
+        String[] rhs = parts[1].split(":");
+        if( rhs.length != 2 )
+            return new Identifier( new Repository(identifier), null);
+
+        return new Identifier( new Repository(parts[0] + "/" + rhs[0]), rhs[1]);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("repository", repository)
+                .add("tag", tag)
+                .toString();
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/api/model/Repository.java b/src/main/java/com/github/dockerjava/api/model/Repository.java
new file mode 100644
index 00000000..673eabee
--- /dev/null
+++ b/src/main/java/com/github/dockerjava/api/model/Repository.java
@@ -0,0 +1,46 @@
+package com.github.dockerjava.api.model;
+
+import com.google.common.base.Objects;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * A repository or image name.
+ */
+public class Repository {
+    public final String name;
+
+    /**
+     * Name may be eg. 'busybox' or '10.0.0.1:5000/fred'
+     * @param name Repository name
+     */
+    public Repository(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Return the URL portion (repository).
+     * Note that this might not actually BE a repository location.
+     * @return
+     * @throws java.net.MalformedURLException
+     */
+    public URL getURL() throws MalformedURLException {
+        return new URL("http://" + name);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                .add("name", name)
+                .toString();
+    }
+
+
+    public String getPath() {
+        if( !name.contains("/") )
+            return name;
+
+        return name.substring(name.indexOf("/") + 1 );
+    }
+}
diff --git a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java
index dd9e566d..06009d87 100644
--- a/src/main/java/com/github/dockerjava/core/DockerClientImpl.java
+++ b/src/main/java/com/github/dockerjava/core/DockerClientImpl.java
@@ -10,6 +10,7 @@ import java.io.InputStream;
 import com.github.dockerjava.api.DockerClient;
 import com.github.dockerjava.api.command.*;
 import com.github.dockerjava.api.model.AuthConfig;
+import com.github.dockerjava.api.model.Identifier;
 import com.github.dockerjava.core.command.*;
 
 /**
@@ -129,8 +130,21 @@ public class DockerClientImpl implements Closeable, DockerClient {
 		return new PushImageCmdImpl(getDockerCmdExecFactory()
 				.createPushImageCmdExec(), name).withAuthConfig(dockerClientConfig.effectiveAuthConfig(name));		
 	}
-    
-    @Override
+
+        @Override
+        public PushImageCmd pushImageCmd(Identifier identifier) {
+          PushImageCmd cmd = pushImageCmd(identifier.repository.name);
+          if( identifier.tag.isPresent() )
+            cmd.withTag(identifier.tag.get());
+
+          AuthConfig cfg = dockerClientConfig.effectiveAuthConfig(identifier.repository.name);
+          if( cfg != null )
+            cmd.withAuthConfig(cfg);
+
+          return cmd;
+        }
+
+        @Override
     public SaveImageCmd saveImageCmd(String name) {
         return new SaveImageCmdImpl(getDockerCmdExecFactory()
             .createSaveImageCmdExec(), name).withAuthConfig(dockerClientConfig.effectiveAuthConfig(name));


Test patch:
diff --git a/src/test/java/com/github/dockerjava/api/model/IdentifierTest.java b/src/test/java/com/github/dockerjava/api/model/IdentifierTest.java
new file mode 100644
index 00000000..d57c0b57
--- /dev/null
+++ b/src/test/java/com/github/dockerjava/api/model/IdentifierTest.java
@@ -0,0 +1,39 @@
+package com.github.dockerjava.api.model;
+
+import junit.framework.TestCase;
+
+
+public class IdentifierTest extends TestCase {
+
+  public void testFromCompoundString() throws Exception {
+
+    Identifier i1 = Identifier.fromCompoundString("10.0.0.1/jim");
+    Identifier i2 = Identifier.fromCompoundString("10.0.0.1/jim:123");
+    Identifier i3 = Identifier.fromCompoundString("10.0.0.1:123/jim:124");
+    Identifier i3A = Identifier.fromCompoundString("10.0.0.1:123/jim:latest");
+
+    assertTrue(!i1.tag.isPresent());
+    assertEquals(i1.repository.name, "10.0.0.1/jim");
+
+    assertTrue(i2.tag.isPresent());
+    assertEquals(i2.tag.get(), "123");
+    assertEquals(i2.repository.name, "10.0.0.1/jim");
+
+    assertTrue(i3.tag.isPresent());
+    assertEquals(i3.tag.get(), "124");
+    assertEquals(i3.repository.name, "10.0.0.1:123/jim");
+    assertEquals(i3.repository.getURL().getPort(), 123);
+    assertEquals(i3A.tag.get(), "latest");
+
+
+    Identifier i4 = Identifier.fromCompoundString("centos:latest");
+    assertTrue(i4.tag.isPresent());
+    assertEquals(i4.tag.get(), "latest");
+
+    Identifier i5 = Identifier.fromCompoundString("busybox");
+    assertTrue(!i5.tag.isPresent());
+
+    Identifier i6 = Identifier.fromCompoundString("10.0.0.1:5000/my-test-image:1234");
+    assertEquals(i6.repository.getPath(), "my-test-image");
+  }
+}
\ No newline at end of file
diff --git a/src/test/java/com/github/dockerjava/api/model/RepositoryTest.java b/src/test/java/com/github/dockerjava/api/model/RepositoryTest.java
new file mode 100644
index 00000000..8a40de28
--- /dev/null
+++ b/src/test/java/com/github/dockerjava/api/model/RepositoryTest.java
@@ -0,0 +1,18 @@
+package com.github.dockerjava.api.model;
+
+import junit.framework.TestCase;
+
+public class RepositoryTest extends TestCase {
+  public void testRepository() throws Exception {
+
+    Repository repo = new Repository("10.0.0.1/jim");
+    Repository repo1 = new Repository("10.0.0.1:1234/jim");
+    Repository repo2 = new Repository("busybox");
+
+    assertEquals("jim", repo.getPath());
+    assertEquals("jim", repo1.getPath());
+    assertEquals("busybox", repo2.getPath());
+
+    assertEquals(1234, repo1.getURL().getPort());
+  }
+}
\ No newline at end of file