Merge pull request #1304 from jclouds/dynect-zone-1.5.x

finished zone support for dynect
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTApi.java
index 13280df..f22be79 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTApi.java
@@ -21,8 +21,10 @@
 import java.util.concurrent.TimeUnit;
 
 import org.jclouds.concurrent.Timeout;
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.features.SessionApi;
 import org.jclouds.dynect.v3.features.ZoneApi;
+import org.jclouds.javax.annotation.Nullable;
 import org.jclouds.rest.annotations.Delegate;
 
 /**
@@ -36,6 +38,15 @@
  */
 @Timeout(duration = 30, timeUnit = TimeUnit.SECONDS)
 public interface DynECTApi {
+   /**
+    * returns the current status of a job.
+    * 
+    * @param jobId
+    *           The ID of the job
+    * @return null, if not found
+    */
+   @Nullable
+   Job getJob(long jobId);
 
    /**
     * Provides synchronous access to Session features.
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTAsyncApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTAsyncApi.java
index 761864c..05da149 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTAsyncApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTAsyncApi.java
@@ -20,12 +20,24 @@
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.features.SessionAsyncApi;
 import org.jclouds.dynect.v3.features.ZoneAsyncApi;
+import org.jclouds.dynect.v3.filters.SessionManager;
 import org.jclouds.rest.annotations.Delegate;
+import org.jclouds.rest.annotations.ExceptionParser;
 import org.jclouds.rest.annotations.Headers;
+import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404;
+
+import com.google.common.util.concurrent.ListenableFuture;
 
 /**
  * Provides access to DynECT Managed DNS through the API2 api
@@ -35,10 +47,19 @@
  * @see <a href="https://manage.dynect.net/help/docs/api2/rest/" />
  * @author Adrian Cole
  */
-// required for all calls
-@Produces(APPLICATION_JSON)
-@Headers(keys = "API-Version", values = "{jclouds.api-version}")
 public interface DynECTAsyncApi {
+   /**
+    * @see DynECTApi#getJob
+    */
+   @Named("GetJob")
+   @GET
+   @Path("/Job/{jobId}")
+   @Produces(APPLICATION_JSON)
+   @RequestFilters(SessionManager.class)
+   @Headers(keys = "API-Version", values = "{jclouds.api-version}")
+   @ExceptionParser(ReturnNullOnNotFoundOr404.class)
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> getJob(@PathParam("jobId") long jobId);
 
    /**
     * Provides asynchronous access to Session features.
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java
index b803b9e..2f93c3a 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java
@@ -29,8 +29,10 @@
 import org.jclouds.dynect.v3.features.ZoneApi;
 import org.jclouds.dynect.v3.features.ZoneAsyncApi;
 import org.jclouds.dynect.v3.filters.SessionManager;
+import org.jclouds.dynect.v3.handlers.GetJobRedirectionRetryHandler;
 import org.jclouds.http.HttpRetryHandler;
 import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.handlers.RedirectionRetryHandler;
 import org.jclouds.rest.ConfiguresRestClient;
 import org.jclouds.rest.config.RestClientModule;
 
@@ -61,6 +63,7 @@
    protected void configure() {
       // binding explicitly ensures singleton despite multiple linked bindings
       bind(SessionManager.class);
+      bind(RedirectionRetryHandler.class).to(GetJobRedirectionRetryHandler.class);
       super.configure();
       // Bind apis that are used directly vs via DynECTApi
       bindClientAndAsyncClient(binder(), SessionApi.class, SessionAsyncApi.class);
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/CreatePrimaryZone.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/CreatePrimaryZone.java
new file mode 100644
index 0000000..69a1594
--- /dev/null
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/CreatePrimaryZone.java
@@ -0,0 +1,161 @@
+/**
+ * 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, String 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.dynect.v3.domain;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.inject.Named;
+
+import org.jclouds.dynect.v3.domain.Zone.SerialStyle;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+
+/**
+ * @author Adrian Cole
+ */
+public final class CreatePrimaryZone {
+   // persisted via path param
+   private final transient String fqdn;
+   @Named("rname")
+   private final String contact;
+   @Named("serial_style")
+   private final SerialStyle serialStyle;
+   @Named("ttl")
+   private final int defaultTTL;
+
+   private CreatePrimaryZone(String fqdn, String contact, SerialStyle serialStyle, int defaultTTL) {
+      this.fqdn = checkNotNull(fqdn, "fqdn");
+      this.contact = checkNotNull(contact, "contact for %s", fqdn);
+      this.defaultTTL = checkNotNull(defaultTTL, "defaultTTL for %s", fqdn);
+      this.serialStyle = checkNotNull(serialStyle, "serialStyle for %s", serialStyle);
+   }
+
+   /**
+    * The fqdn of the zone to create
+    */
+   public String getFQDN() {
+      return fqdn;
+   }
+
+   /**
+    * The administrative contact or {@code rname} for the {@code SOA} record.
+    */
+   public String getContact() {
+      return contact;
+   }
+
+   /**
+    * Default TTL (in seconds) for records in the zone.  Defaults to 3600.
+    */
+   public int getDefaultTTL() {
+      return defaultTTL;
+   }
+
+   /**
+    * The style of the zone's serial. Defaults to {@link SerialStyle#INCREMENT}.
+    */
+   public SerialStyle getSerialStyle() {
+      return serialStyle;
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(fqdn, contact);
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj)
+         return true;
+      if (obj == null || getClass() != obj.getClass())
+         return false;
+      CreatePrimaryZone that = CreatePrimaryZone.class.cast(obj);
+      return equal(this.fqdn, that.fqdn) && equal(this.contact, that.contact);
+   }
+
+   @Override
+   public String toString() {
+      return toStringHelper(this).omitNullValues().add("fqdn", fqdn).add("contact", contact)
+            .add("defaultTTL", defaultTTL).add("serialStyle", serialStyle).toString();
+   }
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public Builder toBuilder() {
+      return builder().from(this);
+   }
+
+   public final static class Builder {
+      private String fqdn;
+      private String contact;
+      private int defaultTTL = 3600;
+      private SerialStyle serialStyle = SerialStyle.INCREMENT;
+
+      /**
+       * @see CreatePrimaryZone#getFQDN()
+       */
+      public Builder fqdn(String fqdn) {
+         this.fqdn = fqdn;
+         return this;
+      }
+
+      /**
+       * @see CreatePrimaryZone#getContact()
+       */
+      public Builder contact(String contact) {
+         this.contact = contact;
+         return this;
+      }
+
+      /**
+       * @see CreatePrimaryZone#getDefaultTTL()
+       */
+      public Builder defaultTTL(int defaultTTL) {
+         this.defaultTTL = defaultTTL;
+         return this;
+      }
+
+      /**
+       * @see CreatePrimaryZone#getSerialStyle()
+       */
+      public Builder serialStyle(SerialStyle serialStyle) {
+         this.serialStyle = serialStyle;
+         return this;
+      }
+
+      public CreatePrimaryZone build() {
+         return new CreatePrimaryZone(fqdn, contact, serialStyle, defaultTTL);
+      }
+
+      public Builder from(CreatePrimaryZone in) {
+         return this.fqdn(in.fqdn).contact(in.contact).serialStyle(in.serialStyle).defaultTTL(in.defaultTTL);
+      }
+   }
+
+   public static final class ToFQDN implements Function<Object, String> {
+      public String apply(Object in) {
+         return CreatePrimaryZone.class.cast(in).getFQDN();
+      }
+   }
+}
\ No newline at end of file
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Job.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Job.java
new file mode 100644
index 0000000..6247997
--- /dev/null
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Job.java
@@ -0,0 +1,99 @@
+/**
+ * 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.dynect.v3.domain;
+
+import static com.google.common.base.Objects.equal;
+import static com.google.common.base.Objects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.beans.ConstructorProperties;
+
+import javax.inject.Named;
+
+import com.google.common.base.Objects;
+
+/**
+ * @author Adrian Cole
+ */
+public final class Job {
+
+   @Named("job_id")
+   private final long id;
+   private final Status status;
+
+   @ConstructorProperties({ "job_id", "status" })
+   private Job(long id, Status status) {
+      this.id = checkNotNull(id, "id");
+      this.status = checkNotNull(status, "status for %s", id);
+   }
+
+   /**
+    * The ID of the job.
+    */
+   public long getId() {
+      return id;
+   }
+
+   /**
+    * The current status of the job.
+    */
+   public Status getStatus() {
+      return status;
+   }
+
+   public enum Status {
+      SUCCESS, FAILURE, UNRECOGNIZED;
+
+      public static Status fromValue(String status) {
+         try {
+            return valueOf(checkNotNull(status, "status").toUpperCase());
+         } catch (IllegalArgumentException e) {
+            return UNRECOGNIZED;
+         }
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(id);
+   }
+
+   @Override
+   public boolean equals(Object obj) {
+      if (this == obj)
+         return true;
+      if (obj == null || getClass() != obj.getClass())
+         return false;
+      Job that = Job.class.cast(obj);
+      return equal(this.id, that.id);
+   }
+
+   @Override
+   public String toString() {
+      return toStringHelper(this).add("id", id).add("status", status).toString();
+   }
+
+   public static Job success(long id) {
+      return new Job(id, Status.SUCCESS);
+   }
+
+   public static Job failure(long id) {
+      return new Job(id, Status.FAILURE);
+   }
+}
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Zone.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Zone.java
index c4cda63..9d064ec 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Zone.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/domain/Zone.java
@@ -34,7 +34,7 @@
  */
 public final class Zone {
 
-   private final String name;
+   private final String fqdn;
    @Named("zone_type")
    private final Type type;
    private final int serial;
@@ -92,18 +92,18 @@
    }
 
    @ConstructorProperties({ "zone", "zone_type", "serial", "serial_style" })
-   private Zone(String name, Type type, int serial, SerialStyle serialStyle) {
-      this.name = checkNotNull(name, "name");
-      this.type = checkNotNull(type, "type for %s", name);
-      this.serial = checkNotNull(serial, "serial for %s", name);
+   private Zone(String fqdn, Type type, int serial, SerialStyle serialStyle) {
+      this.fqdn = checkNotNull(fqdn, "fqdn");
+      this.type = checkNotNull(type, "type for %s", fqdn);
+      this.serial = checkNotNull(serial, "serial for %s", fqdn);
       this.serialStyle = checkNotNull(serialStyle, "serialStyle for %s", serialStyle);
    }
 
    /**
-    * The name of the requested zone
+    * The fqdn of the requested zone
     */
-   public String getName() {
-      return name;
+   public String getFQDN() {
+      return fqdn;
    }
 
    /**
@@ -129,7 +129,7 @@
 
    @Override
    public int hashCode() {
-      return Objects.hashCode(name, type);
+      return Objects.hashCode(fqdn, type);
    }
 
    @Override
@@ -139,12 +139,12 @@
       if (obj == null || getClass() != obj.getClass())
          return false;
       Zone that = Zone.class.cast(obj);
-      return equal(this.name, that.name) && equal(this.type, that.type);
+      return equal(this.fqdn, that.fqdn) && equal(this.type, that.type);
    }
 
    @Override
    public String toString() {
-      return toStringHelper(this).omitNullValues().add("name", name).add("type", type).add("serial", serial)
+      return toStringHelper(this).omitNullValues().add("fqdn", fqdn).add("type", type).add("serial", serial)
             .add("serialStyle", serialStyle).toString();
    }
 
@@ -157,16 +157,16 @@
    }
 
    public final static class Builder {
-      private String name;
+      private String fqdn;
       private Type type;
       private int serial;
       private SerialStyle serialStyle;
 
       /**
-       * @see Zone#getName()
+       * @see Zone#getFQDN()
        */
-      public Builder name(String name) {
-         this.name = name;
+      public Builder fqdn(String fqdn) {
+         this.fqdn = fqdn;
          return this;
       }
 
@@ -195,11 +195,11 @@
       }
 
       public Zone build() {
-         return new Zone(name, type, serial, serialStyle);
+         return new Zone(fqdn, type, serial, serialStyle);
       }
 
       public Builder from(Zone in) {
-         return this.name(in.name).type(in.type).serial(in.serial).serialStyle(in.serialStyle);
+         return this.fqdn(in.fqdn).type(in.type).serial(in.serial).serialStyle(in.serialStyle);
       }
    }
 }
\ No newline at end of file
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java
index 35a7f47..ff3490f 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneApi.java
@@ -21,7 +21,10 @@
 import java.util.concurrent.TimeUnit;
 
 import org.jclouds.concurrent.Timeout;
+import org.jclouds.dynect.v3.domain.CreatePrimaryZone;
+import org.jclouds.dynect.v3.domain.Job;
 import org.jclouds.dynect.v3.domain.Zone;
+import org.jclouds.dynect.v3.domain.Zone.SerialStyle;
 import org.jclouds.javax.annotation.Nullable;
 
 import com.google.common.collect.FluentIterable;
@@ -38,14 +41,69 @@
    FluentIterable<String> list();
 
    /**
-    * Retrieves information about the specified zone, including its nameserver
-    * configuration
+    * Creates a new primary zone.
     * 
-    * @param name
-    *           name of the zone to get information about. ex
-    *           {@code Z1PA6795UKMFR9}
+    * @param zone
+    *           required parameters to create the zone.
+    * @return unpublished zone
+    */
+   Zone create(CreatePrimaryZone zone);
+
+   /**
+    * Creates a new primary zone with one hour default TTL and
+    * {@link SerialStyle#INCREMENT}
+    * 
+    * @param fqdn
+    *           fqdn of the zone to create {@ex. jclouds.org}
+    * @param contact
+    *           email address of the contact
+    * @return unpublished zone
+    */
+   Zone createWithContact(String fqdn, String contact);
+
+   /**
+    * Retrieves information about the specified zone.
+    * 
+    * @param fqdn
+    *           fqdn of the zone to get information about. ex
+    *           {@code jclouds.org}
     * @return null if not found
     */
    @Nullable
-   Zone get(String name);
+   Zone get(String fqdn);
+
+   /**
+    * deletes the specified zone.
+    * 
+    * @param fqdn
+    *           fqdn of the zone to delete ex {@code jclouds.org}
+    * @return null if not found
+    */
+   @Nullable
+   Job delete(String fqdn);
+
+   /**
+    * Publishes the current zone
+    * 
+    * @param fqdn
+    *           fqdn of the zone to publish. ex
+    *           {@code jclouds.org}
+    */
+   Zone publish(String fqdn);
+
+   /**
+    * freezes the specified zone.
+    * 
+    * @param fqdn
+    *           fqdn of the zone to freeze ex {@code jclouds.org}
+    */
+   Job freeze(String fqdn);
+   
+   /**
+    * thaws the specified zone.
+    * 
+    * @param fqdn
+    *           fqdn of the zone to thaw ex {@code jclouds.org}
+    */
+   Job thaw(String fqdn);
 }
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java
index 0be95c9..2ae22a9 100644
--- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/features/ZoneAsyncApi.java
@@ -21,20 +21,33 @@
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
 import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 
 import org.jclouds.dynect.v3.domain.Zone;
+import org.jclouds.dynect.v3.domain.CreatePrimaryZone;
+import org.jclouds.dynect.v3.domain.CreatePrimaryZone.ToFQDN;
+import org.jclouds.dynect.v3.domain.Job;
+import org.jclouds.dynect.v3.domain.Zone;
 import org.jclouds.dynect.v3.filters.SessionManager;
 import org.jclouds.dynect.v3.functions.ExtractNames;
+import org.jclouds.rest.annotations.BinderParam;
 import org.jclouds.rest.annotations.ExceptionParser;
 import org.jclouds.rest.annotations.Headers;
+import org.jclouds.rest.annotations.ParamParser;
+import org.jclouds.rest.annotations.Payload;
+import org.jclouds.rest.annotations.PayloadParam;
 import org.jclouds.rest.annotations.RequestFilters;
 import org.jclouds.rest.annotations.SelectJson;
 import org.jclouds.rest.annotations.Transform;
 import org.jclouds.rest.functions.ReturnNullOnNotFoundOr404;
+import org.jclouds.rest.binders.BindToJsonPayload;
 
 import com.google.common.collect.FluentIterable;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -56,19 +69,79 @@
    /**
     * @see ZoneApi#list
     */
-   @Named("GET:ZoneList")
+   @Named("ListZones")
    @GET
    @SelectJson("data")
    @Transform(ExtractNames.class)
    ListenableFuture<FluentIterable<String>> list();
-   
+
    /**
-    * @see ZoneApi#isValid
+    * @see ZoneApi#get
     */
-   @Named("GET:Zone")
+   @Named("GetZone")
    @GET
-   @Path("/{name}")
+   @Path("/{fqdn}")
    @SelectJson("data")
    @ExceptionParser(ReturnNullOnNotFoundOr404.class)
-   ListenableFuture<Zone> get(@PathParam("name") String name);
+   ListenableFuture<Zone> get(@PathParam("fqdn") String fqdn);
+
+   /**
+    * @see ZoneApi#create
+    */
+   @Named("CreatePrimaryZone")
+   @POST
+   @Path("/{fqdn}")
+   @SelectJson("data")
+   ListenableFuture<Zone> create(
+         @PathParam("fqdn") @ParamParser(ToFQDN.class) @BinderParam(BindToJsonPayload.class) CreatePrimaryZone createZone);
+
+   /**
+    * @see ZoneApi#createWithContact
+    */
+   @Named("CreatePrimaryZone")
+   @POST
+   @Payload("%7B\"rname\":\"{contact}\",\"serial_style\":\"increment\",\"ttl\":3600%7D")
+   @Path("/{fqdn}")
+   @SelectJson("data")
+   ListenableFuture<Zone> createWithContact(@PathParam("fqdn") String fqdn, @PayloadParam("contact") String contact);
+
+   /**
+    * @see ZoneApi#delete
+    */
+   @Named("DeleteZone")
+   @DELETE
+   @Path("/{fqdn}")
+   @ExceptionParser(ReturnNullOnNotFoundOr404.class)
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> delete(@PathParam("fqdn") String fqdn);
+
+   /**
+    * @see ZoneApi#publish
+    */
+   @Named("PublishZone")
+   @PUT
+   @Payload("{\"publish\":true}")
+   @Path("/{fqdn}")
+   @SelectJson("data")
+   ListenableFuture<Zone> publish(@PathParam("fqdn") String fqdn);
+   
+   /**
+    * @see ZoneApi#freeze
+    */
+   @Named("FreezeZone")
+   @PUT
+   @Path("/{fqdn}")
+   @Payload("{\"freeze\":true}")
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> freeze(@PathParam("fqdn") String fqdn);
+
+   /**
+    * @see ZoneApi#thaw
+    */
+   @Named("ThawZone")
+   @PUT
+   @Path("/{fqdn}")
+   @Payload("{\"thaw\":true}")
+   @Consumes(APPLICATION_JSON)
+   ListenableFuture<Job> thaw(@PathParam("fqdn") String fqdn);
 }
diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/handlers/GetJobRedirectionRetryHandler.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/handlers/GetJobRedirectionRetryHandler.java
new file mode 100644
index 0000000..2b0d99a
--- /dev/null
+++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/handlers/GetJobRedirectionRetryHandler.java
@@ -0,0 +1,64 @@
+/**
+ * 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.dynect.v3.handlers;
+
+import static com.google.common.net.HttpHeaders.LOCATION;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.jclouds.http.HttpCommand;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.http.handlers.BackoffLimitedRetryHandler;
+import org.jclouds.http.handlers.RedirectionRetryHandler;
+import org.jclouds.io.Payload;
+import org.jclouds.io.Payloads;
+
+/**
+ * if the redirection URL is a Job, do not replay the original request; just get
+ * the job.
+ * 
+ * @author Adrian Cole
+ */
+@Singleton
+public class GetJobRedirectionRetryHandler extends RedirectionRetryHandler {
+   
+   private final Payload emptyPayload;
+
+   @Inject
+   protected GetJobRedirectionRetryHandler(BackoffLimitedRetryHandler backoffHandler) {
+      super(backoffHandler);
+      this.emptyPayload = Payloads.newPayload(new byte[]{});
+      this.emptyPayload.getContentMetadata().setContentType(APPLICATION_JSON);     
+   }
+
+   @Override
+   public boolean shouldRetryRequest(HttpCommand command, HttpResponse response) {
+      String location = response.getFirstHeaderOrNull(LOCATION);
+      if (location != null && location.indexOf("Job") != -1) {
+         HttpRequest getRequest = command.getCurrentRequest().toBuilder()
+                                                             .method("GET")
+                                                             .payload(emptyPayload).build();
+         command.setCurrentRequest(getRequest);
+      }
+      return super.shouldRetryRequest(command, response);
+   }
+}
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java
index 420f302..b1cdc9f 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiExpectTest.java
@@ -18,12 +18,15 @@
  */
 package org.jclouds.dynect.v3.features;
 
+import static com.google.common.net.HttpHeaders.ACCEPT;
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 
 import org.jclouds.dynect.v3.DynECTApi;
+import org.jclouds.dynect.v3.domain.CreatePrimaryZone;
 import org.jclouds.dynect.v3.internal.BaseDynECTApiExpectTest;
+import org.jclouds.dynect.v3.parse.DeleteZoneResponseTest;
 import org.jclouds.dynect.v3.parse.GetZoneResponseTest;
 import org.jclouds.dynect.v3.parse.ListZonesResponseTest;
 import org.jclouds.http.HttpRequest;
@@ -35,7 +38,6 @@
  */
 @Test(groups = "unit", testName = "ZoneApiExpectTest")
 public class ZoneApiExpectTest extends BaseDynECTApiExpectTest {
-
    HttpRequest get = HttpRequest.builder().method("GET")
          .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
          .addHeader("API-Version", "3.3.7")
@@ -51,8 +53,30 @@
       assertEquals(success.getZoneApi().get("jclouds.org").toString(),
                    new GetZoneResponseTest().expected().toString());
    }
+   
+   HttpRequest create = HttpRequest.builder().method("POST")
+         .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
+         .addHeader("API-Version", "3.3.7")
+         .addHeader("Auth-Token", authToken)
+         .payload(stringPayload("{\"rname\":\"jimmy@jclouds.org\",\"serial_style\":\"increment\",\"ttl\":3600}"))
+         .build();   
 
-   public void testGetWhenResponseError2401() {
+   public void testCreateWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, getResponse);
+      assertEquals(success.getZoneApi().create(CreatePrimaryZone.builder()
+                                                                .fqdn("jclouds.org")
+                                                                .contact("jimmy@jclouds.org")
+                                                                .build()).toString(),
+                   new GetZoneResponseTest().expected().toString());
+   }
+
+   public void testCreateWithContactWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, create, getResponse);
+      assertEquals(success.getZoneApi().createWithContact("jclouds.org", "jimmy@jclouds.org").toString(),
+                   new GetZoneResponseTest().expected().toString());
+   }
+
+   public void testGetWhenResponseIs404() {
       DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, get, notFound);
       assertNull(fail.getZoneApi().get("jclouds.org"));
    }
@@ -72,4 +96,67 @@
       assertEquals(success.getZoneApi().list().toString(),
                    new ListZonesResponseTest().expected().toString());
    }
+   
+   HttpRequest delete = HttpRequest.builder().method("DELETE")
+         .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
+         .addHeader("API-Version", "3.3.7")
+         .addHeader(ACCEPT, APPLICATION_JSON)
+         .addHeader("Auth-Token", authToken)
+         .payload(emptyJsonPayload())
+         .build();
+
+   HttpResponse deleteResponse = HttpResponse.builder().statusCode(200)
+         .payload(payloadFromResourceWithContentType("/delete_zone.json", APPLICATION_JSON)).build();
+
+   public void testDeleteWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, delete, deleteResponse);
+      assertEquals(success.getZoneApi().delete("jclouds.org").toString(),
+                   new DeleteZoneResponseTest().expected().toString());
+   }
+
+   public void testDeleteWhenResponseIs404() {
+      DynECTApi fail = requestsSendResponses(createSession, createSessionResponse, delete, notFound);
+      assertNull(fail.getZoneApi().delete("jclouds.org"));
+   }
+
+   HttpRequest publish = HttpRequest.builder().method("PUT")
+         .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
+         .addHeader("API-Version", "3.3.7")
+         .addHeader("Auth-Token", authToken)
+         .payload(stringPayload("{\"publish\":true}"))
+         .build();   
+
+   public void testPublishWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, publish, getResponse);
+      assertEquals(success.getZoneApi().publish("jclouds.org").toString(),
+                   new GetZoneResponseTest().expected().toString());
+   }
+
+   HttpRequest freeze = HttpRequest.builder().method("PUT")
+         .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
+         .addHeader("API-Version", "3.3.7")
+         .addHeader(ACCEPT, APPLICATION_JSON)
+         .addHeader("Auth-Token", authToken)
+         .payload(stringPayload("{\"freeze\":true}"))
+         .build();   
+
+   public void testFreezeWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, freeze, deleteResponse);
+      assertEquals(success.getZoneApi().freeze("jclouds.org").toString(),
+                   new DeleteZoneResponseTest().expected().toString());
+   }
+
+   HttpRequest thaw = HttpRequest.builder().method("PUT")
+         .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
+         .addHeader("API-Version", "3.3.7")
+         .addHeader(ACCEPT, APPLICATION_JSON)
+         .addHeader("Auth-Token", authToken)
+         .payload(stringPayload("{\"thaw\":true}"))
+         .build();   
+
+   public void testThawWhenResponseIs2xx() {
+      DynECTApi success = requestsSendResponses(createSession, createSessionResponse, thaw, deleteResponse);
+      assertEquals(success.getZoneApi().thaw("jclouds.org").toString(),
+                   new DeleteZoneResponseTest().expected().toString());
+   }
 }
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java
index a2eb3f0..0eb9e07 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/features/ZoneApiLiveTest.java
@@ -20,10 +20,15 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.logging.Logger.getAnonymousLogger;
+import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNull;
 
+import org.jclouds.JcloudsVersion;
+import org.jclouds.dynect.v3.domain.Job;
+import org.jclouds.dynect.v3.domain.Job.Status;
 import org.jclouds.dynect.v3.domain.Zone;
 import org.jclouds.dynect.v3.internal.BaseDynECTApiLiveTest;
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableList;
@@ -35,7 +40,7 @@
 public class ZoneApiLiveTest extends BaseDynECTApiLiveTest {
 
    private void checkZone(Zone zone) {
-      checkNotNull(zone.getName(), "Name cannot be null for a Zone: %s", zone);
+      checkNotNull(zone.getFQDN(), "FQDN cannot be null for a Zone: %s", zone);
       checkNotNull(zone.getSerial(), "Serial cannot be null for a Zone: %s", zone);
    }
 
@@ -44,19 +49,74 @@
       ImmutableList<String> zones = api().list().toImmutableList();
       getAnonymousLogger().info("zones: " + zones.size());
 
-      for (String zoneName : zones) {
-         Zone zone = api().get(zoneName);
-         checkNotNull(zone, "zone was null for Zone: %s", zoneName);
+      for (String fqdn : zones) {
+         Zone zone = api().get(fqdn);
+         checkNotNull(zone, "zone was null for Zone: %s", fqdn);
          checkZone(zone);
       }
    }
 
    @Test
    public void testGetZoneWhenNotFound() {
-      assertNull(api().get("AAAAAAAAAAAAAAAA"));
+      assertNull(api().get("AAAAAAAAAAAAAAAA.foo.com"));
+   }
+
+   @Test
+   public void testDeleteZoneWhenNotFound() {
+      assertNull(api().delete("AAAAAAAAAAAAAAAA.foo.com"));
+   }
+
+   String fqdn = System.getProperty("user.name").replace('.', '-') + ".zone.dynecttest.jclouds.org";
+   String contact = JcloudsVersion.get() + "@jclouds.org";
+
+   @Test
+   public void testCreateZone() {
+      Zone zone = api().createWithContact(fqdn, contact);
+      checkNotNull(zone, "unable to create zone %s", fqdn);
+      getAnonymousLogger().info("created zone: " + zone);
+      checkZone(zone);
+   }
+
+   @Test(dependsOnMethods = "testCreateZone")
+   public void testPublishZone() {
+      Zone zone = api().publish(fqdn);
+      checkNotNull(zone, "unable to publish zone %s", fqdn);
+      getAnonymousLogger().info("published zone: " + zone);
+      checkZone(zone);
+   }
+
+   @Test(dependsOnMethods = "testPublishZone")
+   public void testFreezeZone() {
+      Job job = api().freeze(fqdn);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
+      // TODO: determine how to prove it is frozen
+   }
+
+   @Test(dependsOnMethods = "testFreezeZone")
+   public void testThawZone() {
+      Job job = api().thaw(fqdn);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
+      // TODO: determine how to prove it is thawed
+   }
+
+   @Test(dependsOnMethods = "testThawZone")
+   public void testDeleteZone() {
+      Job job = api().delete(fqdn);
+      assertEquals(job.getStatus(), Status.SUCCESS);
+      assertEquals(context.getApi().getJob(job.getId()), job);
+      assertNull(api().get(fqdn), "job " + job + " didn't delete zone" + fqdn);
    }
 
    protected ZoneApi api() {
       return context.getApi().getZoneApi();
    }
+
+   @Override
+   @AfterClass(groups = "live")
+   protected void tearDownContext() {
+      api().delete(fqdn);
+      super.tearDownContext();
+   }
 }
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/handlers/GetJobRedirectionRetryHandlerExpectTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/handlers/GetJobRedirectionRetryHandlerExpectTest.java
new file mode 100644
index 0000000..17456bc
--- /dev/null
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/handlers/GetJobRedirectionRetryHandlerExpectTest.java
@@ -0,0 +1,70 @@
+/**
+ * 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.dynect.v3.handlers;
+
+import static com.google.common.net.HttpHeaders.ACCEPT;
+import static com.google.common.net.HttpHeaders.LOCATION;
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import org.jclouds.dynect.v3.DynECTApi;
+import org.jclouds.dynect.v3.internal.BaseDynECTApiExpectTest;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.testng.annotations.Test;
+
+/**
+ * 
+ * @author Adrian Cole
+ */
+@Test(groups = "unit", testName = "GetJobRedirectionRetryHandlerExpectTest")
+public class GetJobRedirectionRetryHandlerExpectTest extends BaseDynECTApiExpectTest {
+
+   public void testRedirectOnJobLocationSwitchesToGETAndNoPayload() {
+
+      HttpRequest thaw = HttpRequest.builder().method("PUT")
+                                    .endpoint("https://api2.dynect.net/REST/Zone/jclouds.org")
+                                    .addHeader("API-Version", "3.3.7")
+                                    .addHeader(ACCEPT, APPLICATION_JSON)
+                                    .addHeader("Auth-Token", authToken)
+                                    .payload(stringPayload("{\"thaw\":true}"))
+                                    .build();
+
+      HttpResponse redirectResponse = HttpResponse.builder() 
+                                                  .statusCode(317)
+                                                  .addHeader(LOCATION, "https://api2.dynect.net/REST/Job/1234")
+                                                  .build();
+
+      HttpRequest job = HttpRequest.builder().method("GET")
+                                   .endpoint("https://api2.dynect.net/REST/Job/1234")
+                                   .addHeader("API-Version", "3.3.7")
+                                   .addHeader(ACCEPT, APPLICATION_JSON)
+                                   .addHeader("Auth-Token", authToken)
+                                   .payload(emptyJsonPayload())
+                                   .build();
+
+      HttpResponse success = HttpResponse.builder().statusCode(200)
+            .payload(payloadFromResourceWithContentType("/delete_zone.json", APPLICATION_JSON)).build();
+
+      DynECTApi apiThatRedirects = requestsSendResponses(createSession, createSessionResponse, thaw, redirectResponse,
+            job, success);
+      
+      apiThatRedirects.getZoneApi().thaw("jclouds.org");
+
+   }
+}
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java
index 1c50c99..bafed02 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java
@@ -44,6 +44,12 @@
       return p;
    }
 
+   public static Payload stringPayload(String json) {
+      Payload p = Payloads.newPayload(json);
+      p.getContentMetadata().setContentType(APPLICATION_JSON);
+      return p;
+   }
+
    @Override
    protected HttpRequestComparisonType compareHttpRequestAsType(HttpRequest input) {
       return HttpRequestComparisonType.JSON;
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/DeleteZoneResponseTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/DeleteZoneResponseTest.java
new file mode 100644
index 0000000..c4bd6ee
--- /dev/null
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/DeleteZoneResponseTest.java
@@ -0,0 +1,45 @@
+/**
+ * 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.dynect.v3.parse;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.core.MediaType;
+
+import org.jclouds.dynect.v3.domain.Job;
+import org.jclouds.dynect.v3.internal.BaseDynECTParseTest;
+import org.testng.annotations.Test;
+
+/**
+ * @author Adrian Cole
+ */
+@Test(groups = "unit")
+public class DeleteZoneResponseTest extends BaseDynECTParseTest<Job> {
+
+   @Override
+   public String resource() {
+      return "/delete_zone.json";
+   }
+
+   @Override
+   @Consumes(MediaType.APPLICATION_JSON)
+   public Job expected() {
+      return Job.success(262989027l);
+   }
+}
diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/GetZoneResponseTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/GetZoneResponseTest.java
index ce2de76..83731f4 100644
--- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/GetZoneResponseTest.java
+++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/parse/GetZoneResponseTest.java
@@ -44,6 +44,6 @@
    @SelectJson("data")
    @Consumes(MediaType.APPLICATION_JSON)
    public Zone expected() {
-      return Zone.builder().type(PRIMARY).serialStyle(INCREMENT).serial(5).name("jclouds.org").build();
+      return Zone.builder().type(PRIMARY).serialStyle(INCREMENT).serial(5).fqdn("jclouds.org").build();
    }
 }
\ No newline at end of file
diff --git a/labs/dynect/src/test/resources/delete_zone.json b/labs/dynect/src/test/resources/delete_zone.json
new file mode 100644
index 0000000..aca65be
--- /dev/null
+++ b/labs/dynect/src/test/resources/delete_zone.json
@@ -0,0 +1 @@
+{"status": "success", "data": {}, "job_id": 262989027, "msgs": [{"INFO": "remove: Zone removed", "SOURCE": "BLL", "ERR_CD": null, "LVL": "INFO"}]}
\ No newline at end of file