Merge pull request #1301 from jclouds/dynect-zone

finished zone support for dynect
diff --git a/apis/s3/pom.xml b/apis/s3/pom.xml
index 364e233..17de087 100644
--- a/apis/s3/pom.xml
+++ b/apis/s3/pom.xml
@@ -87,6 +87,11 @@
       <artifactId>log4j</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.mockwebserver</groupId>
+      <artifactId>mockwebserver</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <profiles>
diff --git a/apis/s3/src/test/java/org/jclouds/s3/S3ClientMockTest.java b/apis/s3/src/test/java/org/jclouds/s3/S3ClientMockTest.java
new file mode 100644
index 0000000..6931d49
--- /dev/null
+++ b/apis/s3/src/test/java/org/jclouds/s3/S3ClientMockTest.java
@@ -0,0 +1,84 @@
+/**
+ * Licensed to jclouds, Inc. (jclouds) under one or more
+ * contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  jclouds licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.jclouds.s3;
+
+import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor;
+import static org.jclouds.s3.reference.S3Constants.PROPERTY_S3_VIRTUAL_HOST_BUCKETS;
+import static org.testng.Assert.assertEquals;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Properties;
+import java.util.Set;
+
+import org.jclouds.ContextBuilder;
+import org.jclouds.concurrent.config.ExecutorServiceModule;
+import org.jclouds.rest.RestContext;
+import org.jclouds.s3.domain.S3Object;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Module;
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+import com.google.mockwebserver.RecordedRequest;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test
+public class S3ClientMockTest {
+
+   private static final Set<Module> modules = ImmutableSet.<Module> of(
+         new ExecutorServiceModule(sameThreadExecutor(), sameThreadExecutor()));
+
+   static RestContext<? extends S3Client,? extends  S3AsyncClient> getContext(URL server) {
+      Properties overrides = new Properties();
+      overrides.setProperty(PROPERTY_S3_VIRTUAL_HOST_BUCKETS, "false");
+
+      return ContextBuilder.newBuilder("s3")
+                           .credentials("accessKey", "secretKey")
+                           .endpoint(server.toString())
+                           .modules(modules)
+                           .overrides(overrides)
+                           .build(S3ApiMetadata.CONTEXT_TOKEN);
+   }
+
+   public void testZeroLengthPutHasContentLengthHeader() throws IOException, InterruptedException {
+      MockWebServer server = new MockWebServer();
+      server.enqueue(new MockResponse().setBody("").addHeader(ETAG, "ABCDEF"));
+      server.play();
+
+      S3Client client = getContext(server.getUrl("/")).getApi();
+      S3Object nada = client.newS3Object();
+      nada.getMetadata().setKey("object");
+      nada.setPayload(new byte[] {});
+      
+      assertEquals(client.putObject("bucket", nada), "ABCDEF");
+
+      RecordedRequest request = server.takeRequest();
+      assertEquals(request.getRequestLine(), "PUT /bucket/object HTTP/1.1");
+      assertEquals(request.getHeaders(CONTENT_LENGTH), ImmutableList.of("0"));
+      server.shutdown();
+   }
+}
diff --git a/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java b/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java
index 21d4c9b..0816e20 100644
--- a/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java
+++ b/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java
@@ -214,27 +214,33 @@
             // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6755625
             checkArgument(length < Integer.MAX_VALUE,
                   "JDK 1.6 does not support >2GB chunks. Use chunked encoding, if possible.");
-            connection.setRequestProperty(CONTENT_LENGTH, length + "");
             if (length > 0) {
                writePayloadToConnection(payload, connection);
+            } else {
+               writeNothing(connection);
             }
          }
       } else {
-         connection.setRequestProperty(CONTENT_LENGTH, "0");
-         // for some reason POST/PUT undoes the content length header above.
-         if (ImmutableSet.of("POST", "PUT").contains(connection.getRequestMethod())) {
-            connection.setFixedLengthStreamingMode(0);
-            connection.setDoOutput(true);
-         }
+         writeNothing(connection);
       }
       return connection;
    }
 
+   protected void writeNothing(HttpURLConnection connection) {
+      connection.setRequestProperty(CONTENT_LENGTH, "0");
+      // for some reason POST/PUT undoes the content length header above.
+      if (ImmutableSet.of("POST", "PUT").contains(connection.getRequestMethod())) {
+         connection.setFixedLengthStreamingMode(0);
+         connection.setDoOutput(true);
+      }
+   }
+
    void writePayloadToConnection(Payload payload, HttpURLConnection connection) throws IOException {
       Long length = payload.getContentMetadata().getContentLength();
+      connection.setRequestProperty(CONTENT_LENGTH, length.toString());
+      connection.setRequestProperty("Expect", "100-continue");
       connection.setFixedLengthStreamingMode(length.intValue());
       connection.setDoOutput(true);
-      connection.setRequestProperty("Expect", "100-continue");
       CountingOutputStream out = new CountingOutputStream(connection.getOutputStream());
       try {
          payload.writeTo(out);
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncApi.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncApi.java
index 22f0807..d097195 100644
--- a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncApi.java
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/JenkinsAsyncApi.java
@@ -18,6 +18,7 @@
  */
 package org.jclouds.jenkins.v1;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -48,6 +49,7 @@
    /**
     * @see JenkinsApi#getMaster
     */
+   @Named("GetMaster")
    @GET
    @Path("/api/json")
    @Consumes(MediaType.APPLICATION_JSON)
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncApi.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncApi.java
index b0f8ed1..81607f7 100644
--- a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncApi.java
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/ComputerAsyncApi.java
@@ -18,6 +18,7 @@
  */
 package org.jclouds.jenkins.v1.features;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -48,6 +49,7 @@
    /**
     * @see ComputerApi#getView
     */
+   @Named("ListComputers")
    @GET
    @Path("/computer/api/json")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -56,6 +58,7 @@
    /**
     * @see ComputerApi#get
     */
+   @Named("GetComputer")
    @GET
    @Path("/computer/{displayName}/api/json")
    @Consumes(MediaType.APPLICATION_JSON)
diff --git a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/JobAsyncApi.java b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/JobAsyncApi.java
index 0d257a5..4dd8ea1 100644
--- a/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/JobAsyncApi.java
+++ b/labs/jenkins/src/main/java/org/jclouds/jenkins/v1/features/JobAsyncApi.java
@@ -20,6 +20,7 @@
 
 import java.util.Map;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
@@ -55,6 +56,7 @@
    /**
     * @see JobApi#createFromXML
     */
+   @Named("CreateItem")
    @POST
    @Path("/createItem")
    @Produces(MediaType.TEXT_XML)
@@ -63,6 +65,7 @@
    /**
     * @see JobApi#get
     */
+   @Named("GetJob")
    @GET
    @Path("/job/{displayName}/api/json")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -72,6 +75,7 @@
    /**
     * @see JobApi#delete
     */
+   @Named("DeleteJob")
    @POST
    @Path("/job/{displayName}/doDelete")
    @Fallback(VoidOn302Or404.class)
@@ -80,6 +84,7 @@
    /**
     * @see JobApi#buildJob
     */
+   @Named("Build")
    @POST
    @Path("/job/{displayName}/build")
    @Fallback(NullOnNotFoundOr404.class)
@@ -88,6 +93,7 @@
    /**
     * @see JobApi#buildJobWithParameters
     */
+   @Named("BuildWithParameters")
    @POST
    @Path("/job/{displayName}/buildWithParameters")
    @Fallback(NullOnNotFoundOr404.class)
@@ -97,6 +103,7 @@
    /**
     * @see JobApi#fetchConfigXML
     */
+   @Named("GetConfigXML")
    @GET
    @Path("/job/{displayName}/config.xml")
    @Fallback(NullOnNotFoundOr404.class)
@@ -105,6 +112,7 @@
    /**
     * @see JobApi#lastBuild
     */
+   @Named("GetLastBuild")
    @GET
    @Path("/job/{displayName}/lastBuild/api/json")
    @Consumes(MediaType.APPLICATION_JSON)
diff --git a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatacenterAsyncApi.java b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatacenterAsyncApi.java
index b0c98eb..d01a6ec 100644
--- a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatacenterAsyncApi.java
+++ b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatacenterAsyncApi.java
@@ -21,6 +21,7 @@
 import java.net.URI;
 import java.util.Map;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -48,6 +49,7 @@
    /**
     * @see DatacenterApi#getDatacenters
     */
+   @Named("ListDataCenters")
    @GET
    @Path("/my/datacenters")
    @Consumes(MediaType.APPLICATION_JSON)
diff --git a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatasetAsyncApi.java b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatasetAsyncApi.java
index 9f8ac60..c7fb3e6 100644
--- a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatasetAsyncApi.java
+++ b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/DatasetAsyncApi.java
@@ -2,6 +2,7 @@
 
 import java.util.Set;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -32,6 +33,7 @@
    /**
     * @see DatasetApi#list
     */
+   @Named("ListDatasets")
    @GET
    @Path("/my/datasets")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -41,6 +43,7 @@
    /**
     * @see DatasetApi#get
     */
+   @Named("GetDataset")
    @GET
    @Path("/my/datasets/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
diff --git a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/KeyAsyncApi.java b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/KeyAsyncApi.java
index 146c7fa..b2e5d2f 100644
--- a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/KeyAsyncApi.java
+++ b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/KeyAsyncApi.java
@@ -2,6 +2,7 @@
 
 import java.util.Set;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -34,6 +35,7 @@
    /**
     * @see KeyApi#list
     */
+   @Named("ListKeys")
    @GET
    @Path("/my/keys")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -43,6 +45,7 @@
    /**
     * @see KeyApi#get
     */
+   @Named("GetKey")
    @GET
    @Path("/my/keys/{name}")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -52,6 +55,7 @@
    /**
     * @see KeyApi#create
     */
+   @Named("CreateKey")
    @POST
    @Path("/my/keys")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -60,6 +64,7 @@
    /**
     * @see KeyApi#delete
     */
+   @Named("DeleteKey")
    @DELETE
    @Consumes(MediaType.APPLICATION_JSON)
    @Path("/my/keys/{name}")
diff --git a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/MachineAsyncApi.java b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/MachineAsyncApi.java
index e1934a6..492564d 100644
--- a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/MachineAsyncApi.java
+++ b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/MachineAsyncApi.java
@@ -20,6 +20,7 @@
 
 import java.util.Set;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
@@ -59,6 +60,7 @@
    /**
     * @see MachineApi#list
     */
+   @Named("ListMachines")
    @GET
    @Path("/my/machines")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -68,6 +70,7 @@
    /**
     * @see MachineApi#get
     */
+   @Named("GetMachine")
    @GET
    @Path("/my/machines/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -77,6 +80,7 @@
    /**
     * @see MachineApi#createWithDataset(String)
     */
+   @Named("CreateMachine")
    @POST
    @Path("/my/machines")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -85,6 +89,7 @@
    /**
     * @see MachineApi#createWithDataset(String, CreateMachineOptions)
     */
+   @Named("CreateMachine")
    @POST
    @Path("/my/machines")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -93,6 +98,7 @@
    /**
     * @see MachineApi#stop
     */
+   @Named("StopMachine")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_FORM_URLENCODED)
@@ -103,6 +109,7 @@
    /**
     * @see MachineApi#start
     */
+   @Named("StartMachine")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_FORM_URLENCODED)
@@ -113,6 +120,7 @@
    /**
     * @see MachineApi#reboot
     */
+   @Named("RestartMachine")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_FORM_URLENCODED)
@@ -123,6 +131,7 @@
    /**
     * @see MachineApi#resize
     */
+   @Named("ResizeMachine")
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_FORM_URLENCODED)
@@ -133,6 +142,7 @@
    /**
     * @see MachineApi#delete
     */
+   @Named("DeleteMachine")
    @DELETE
    @Consumes(MediaType.APPLICATION_JSON)
    @Path("/my/machines/{id}")
diff --git a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/PackageAsyncApi.java b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/PackageAsyncApi.java
index ef14673..6b84f97 100644
--- a/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/PackageAsyncApi.java
+++ b/labs/joyent-cloudapi/src/main/java/org/jclouds/joyent/cloudapi/v6_5/features/PackageAsyncApi.java
@@ -2,6 +2,7 @@
 
 import java.util.Set;
 
+import javax.inject.Named;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
@@ -31,6 +32,7 @@
    /**
     * @see PackageApi#list
     */
+   @Named("ListPackages")
    @GET
    @Path("/my/packages")
    @Consumes(MediaType.APPLICATION_JSON)
@@ -40,6 +42,7 @@
    /**
     * @see PackageApi#get
     */
+   @Named("GetPackage")
    @GET
    @Path("/my/packages/{name}")
    @Consumes(MediaType.APPLICATION_JSON)
diff --git a/project/pom.xml b/project/pom.xml
index 6aaf69f..cfe83a5 100644
--- a/project/pom.xml
+++ b/project/pom.xml
@@ -271,6 +271,11 @@
                 <artifactId>logback-core</artifactId>
                 <version>1.0.7</version>
             </dependency>
+            <dependency>
+                <groupId>com.google.mockwebserver</groupId>
+                <artifactId>mockwebserver</artifactId>
+                <version>20121111</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
     <dependencies>