You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by ad...@apache.org on 2013/09/28 21:14:14 UTC

git commit: JCLOUDS-298. Add ObjectApi to openstack-swift

Updated Branches:
  refs/heads/master f5e4012f9 -> 2150c8b65


JCLOUDS-298. Add ObjectApi to openstack-swift


Project: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/commit/2150c8b6
Tree: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/tree/2150c8b6
Diff: http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/diff/2150c8b6

Branch: refs/heads/master
Commit: 2150c8b65c15aaf0d2865ad894e9cdf86d0a8cb4
Parents: f5e4012
Author: Adrian Cole <ad...@gmail.com>
Authored: Sat Sep 28 09:50:40 2013 -0700
Committer: Adrian Cole <ad...@gmail.com>
Committed: Sat Sep 28 12:10:53 2013 -0700

----------------------------------------------------------------------
 .../jclouds/openstack/swift/v1/SwiftApi.java    |   7 +-
 .../swift/v1/binders/BindMetadataToHeaders.java |  12 +
 .../openstack/swift/v1/domain/SwiftObject.java  | 219 +++++++++++++
 .../openstack/swift/v1/features/ObjectApi.java  | 202 +++++++++++-
 .../swift/v1/functions/ETagHeader.java          |  31 ++
 .../v1/functions/ParseAccountFromHeaders.java   |   4 +-
 .../v1/functions/ParseContainerFromHeaders.java |   2 +-
 .../v1/functions/ParseObjectFromResponse.java   |  59 ++++
 .../swift/v1/features/ObjectApiLiveTest.java    | 139 +++++++++
 .../swift/v1/features/ObjectApiMockTest.java    | 309 +++++++++++++++++++
 10 files changed, 970 insertions(+), 14 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java
index 547c356..f803c47 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApi.java
@@ -19,6 +19,9 @@ package org.jclouds.openstack.swift.v1;
 import java.io.Closeable;
 import java.util.Set;
 
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
 import org.jclouds.javax.annotation.Nullable;
 import org.jclouds.location.Region;
 import org.jclouds.location.functions.RegionToEndpoint;
@@ -50,5 +53,7 @@ public interface SwiftApi extends Closeable {
    ContainerApi containerApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region);
 
    @Delegate
-   ObjectApi objectApiInRegion(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region);
+   @Path("/{containerName}")
+   ObjectApi objectApiInRegionForContainer(@EndpointParam(parser = RegionToEndpoint.class) @Nullable String region,
+         @PathParam("containerName") String containerName);
 }

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
index dd3f803..262938e 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
@@ -78,6 +78,18 @@ public abstract class BindMetadataToHeaders implements Binder {
       }
    }
 
+   public static class BindObjectMetadataToHeaders extends BindMetadataToHeaders {
+      BindObjectMetadataToHeaders() {
+         super("x-object-meta-");
+      }
+   }
+
+   public static class BindRemoveObjectMetadataToHeaders extends BindMetadataToHeaders.ForRemoval {
+      BindRemoveObjectMetadataToHeaders() {
+         super("x-object-meta-");
+      }
+   }
+
    /**
     * @see <a
     *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/delete-account-metadata.html">documentation</a>

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java
new file mode 100644
index 0000000..6904436
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/domain/SwiftObject.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.openstack.swift.v1.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 java.util.Date;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.jclouds.io.Payload;
+import org.jclouds.io.Payloads;
+import org.jclouds.openstack.swift.v1.features.ObjectApi;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Objects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * @see <a
+ *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/retrieve-object.html">api
+ *      doc</a>
+ */
+public class SwiftObject implements Comparable<SwiftObject> {
+
+   private final String name;
+   private final String hash;
+   private final Date lastModified;
+   private final Map<String, String> metadata;
+   private final Payload payload;
+
+   @ConstructorProperties({ "name", "hash", "bytes", "content_type", "last_modified" })
+   protected SwiftObject(String name, String hash, long bytes, String contentType, Date lastModified) {
+      this(name, hash, lastModified, ImmutableMap.<String, String> of(), payload(bytes, contentType));
+   }
+
+   protected SwiftObject(String name, String hash, Date lastModified, Map<String, String> metadata, Payload payload) {
+      this.name = checkNotNull(name, "name");
+      this.hash = checkNotNull(hash, "hash of %s", name);
+      this.lastModified = checkNotNull(lastModified, "lastModified of %s", name);
+      this.metadata = metadata == null ? ImmutableMap.<String, String> of() : metadata;
+      this.payload = checkNotNull(payload, "payload of %s", name);
+   }
+
+   public String name() {
+      return name;
+   }
+
+   public String hash() {
+      return hash;
+   }
+
+   public Date lastModified() {
+      return lastModified;
+   }
+
+   /**
+    * Empty except in {@link ObjectApi#head(String) GetObjectMetadata} or
+    * {@link ObjectApi#get(String) GetObject} commands.
+    * 
+    * <h3>Note</h3>
+    * 
+    * In current swift implementations, headers keys are lower-cased. This means
+    * characters such as turkish will probably not work out well.
+    */
+   public Map<String, String> metadata() {
+      return metadata;
+   }
+
+   /**
+    * Only has a {@link Payload#getInput()} when retrieved via the
+    * {@link ObjectApi#get(String) GetObject} command.
+    */
+   public Payload payload() {
+      return payload;
+   }
+
+   @Override
+   public boolean equals(Object object) {
+      if (this == object) {
+         return true;
+      }
+      if (object instanceof SwiftObject) {
+         final SwiftObject that = SwiftObject.class.cast(object);
+         return equal(name(), that.name()) //
+               && equal(hash(), that.hash());
+      } else {
+         return false;
+      }
+   }
+
+   @Override
+   public int hashCode() {
+      return Objects.hashCode(name(), hash());
+   }
+
+   @Override
+   public String toString() {
+      return string().toString();
+   }
+
+   protected ToStringHelper string() {
+      return toStringHelper("") //
+            .add("name", name()) //
+            .add("hash", hash()) //
+            .add("lastModified", lastModified()) //
+            .add("metadata", metadata());
+   }
+
+   @Override
+   public int compareTo(SwiftObject that) {
+      if (that == null)
+         return 1;
+      if (this == that)
+         return 0;
+      return this.name().compareTo(that.name());
+   }
+
+   public static Builder builder() {
+      return new Builder();
+   }
+
+   public Builder toBuilder() {
+      return builder().fromObject(this);
+   }
+
+   public static class Builder {
+      protected String name;
+      protected String hash;
+      protected Date lastModified;
+      protected Payload payload;
+      protected Map<String, String> metadata = ImmutableMap.of();
+
+      /**
+       * @see SwiftObject#name()
+       */
+      public Builder name(String name) {
+         this.name = checkNotNull(name, "name");
+         return this;
+      }
+
+      /**
+       * @see SwiftObject#hash()
+       */
+      public Builder hash(String hash) {
+         this.hash = hash;
+         return this;
+      }
+
+      /**
+       * @see SwiftObject#lastModified()
+       */
+      public Builder lastModified(Date lastModified) {
+         this.lastModified = lastModified;
+         return this;
+      }
+
+      /**
+       * @see SwiftObject#payload()
+       */
+      public Builder payload(Payload payload) {
+         this.payload = payload;
+         return this;
+      }
+
+      /**
+       * Will lower-case all metadata keys due to a swift implementation
+       * decision.
+       * 
+       * @see SwiftObject#metadata()
+       */
+      public Builder metadata(Map<String, String> metadata) {
+         ImmutableMap.Builder<String, String> builder = ImmutableMap.<String, String> builder();
+         for (Entry<String, String> entry : checkNotNull(metadata, "metadata").entrySet()) {
+            builder.put(entry.getKey().toLowerCase(), entry.getValue());
+         }
+         this.metadata = builder.build();
+         return this;
+      }
+
+      public SwiftObject build() {
+         return new SwiftObject(name, hash, lastModified, metadata, payload);
+      }
+
+      public Builder fromObject(SwiftObject from) {
+         return name(from.name()) //
+               .hash(from.hash()) //
+               .lastModified(from.lastModified()) //
+               .metadata(from.metadata()) //
+               .payload(from.payload());
+      }
+   }
+
+   private static final byte[] NO_CONTENT = new byte[] {};
+
+   private static Payload payload(long bytes, String contentType) {
+      Payload payload = Payloads.newByteArrayPayload(NO_CONTENT);
+      payload.getContentMetadata().setContentLength(bytes);
+      payload.getContentMetadata().setContentType(contentType);
+      return payload;
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
index 93775a4..2a3ee1e 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
@@ -16,25 +16,207 @@
  */
 package org.jclouds.openstack.swift.v1.features;
 
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import java.util.Map;
+
+import javax.inject.Named;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HEAD;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.QueryParam;
+
+import org.jclouds.Fallbacks.EmptyFluentIterableOnNotFoundOr404;
+import org.jclouds.Fallbacks.FalseOnNotFoundOr404;
+import org.jclouds.Fallbacks.NullOnNotFoundOr404;
+import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.options.GetOptions;
+import org.jclouds.io.Payload;
+import org.jclouds.javax.annotation.Nullable;
 import org.jclouds.openstack.keystone.v2_0.filters.AuthenticateRequest;
+import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindObjectMetadataToHeaders;
+import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindRemoveObjectMetadataToHeaders;
+import org.jclouds.openstack.swift.v1.domain.SwiftObject;
+import org.jclouds.openstack.swift.v1.functions.ETagHeader;
+import org.jclouds.openstack.swift.v1.functions.ParseObjectFromResponse;
+import org.jclouds.rest.Binder;
+import org.jclouds.rest.annotations.BinderParam;
+import org.jclouds.rest.annotations.Fallback;
+import org.jclouds.rest.annotations.QueryParams;
 import org.jclouds.rest.annotations.RequestFilters;
+import org.jclouds.rest.annotations.ResponseParser;
+
+import com.google.common.collect.FluentIterable;
 
 /**
- * Storage Object Services An object represents the data and any metadata for the files stored in
- * the system. Through the ReST interface, metadata for an object can be included by adding custom
- * HTTP headers to the request and the data payload as the request body. Objects cannot exceed 5GB
- * and must have names that do not exceed 1024 bytes after URL encoding. However, objects larger
- * than 5GB can be segmented and then concatenated together so that you can upload 5 GB segments and
- * download a single concatenated object. You can work with the segments and manifests directly with
- * HTTP requests.
- * 
- * @author Adrian Cole
- * @author Zack Shoylev
  * @see <a href=
  *      "http://docs.openstack.org/api/openstack-object-storage/1.0/content/storage-object-services.html"
  *      >api doc</a>
  */
 @RequestFilters(AuthenticateRequest.class)
+@Consumes(APPLICATION_JSON)
 public interface ObjectApi {
 
+   /**
+    * Lists up to 10,000 objects.
+    * 
+    * @return a list of existing storage objects ordered by name.
+    */
+   @Named("ListObjects")
+   @GET
+   @QueryParams(keys = "format", values = "json")
+   @Fallback(EmptyFluentIterableOnNotFoundOr404.class)
+   @Path("/")
+   FluentIterable<SwiftObject> listFirstPage();
+
+   /**
+    * Lists up to 10,000 objects, starting at {@code marker}.
+    * 
+    * @param marker
+    *           lexicographic position to start list.
+    * 
+    * @return a list of existing storage objects ordered by name.
+    */
+   @Named("ListObjects")
+   @GET
+   @QueryParams(keys = "format", values = "json")
+   @Fallback(EmptyFluentIterableOnNotFoundOr404.class)
+   @Path("/")
+   FluentIterable<SwiftObject> listAt(@QueryParam("marker") String marker);
+
+   /**
+    * Creates or updates an object.
+    * 
+    * @param objectName
+    *           corresponds to {@link SwiftObject#name()}.
+    * @param payload
+    *           corresponds to {@link SwiftObject#payload()}.
+    * @param metadata
+    *           corresponds to {@link SwiftObject#metadata()}.
+    * @see <a
+    *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/create-update-object.html">
+    *      Create or Update Object API</a>
+    * 
+    * @return {@link SwiftObject#hash()} of the object.
+    */
+   @Named("CreateOrUpdateObject")
+   @PUT
+   @ResponseParser(ETagHeader.class)
+   @Path("/{objectName}")
+   String createOrUpdate(@PathParam("objectName") String objectName, @BinderParam(SetPayload.class) Payload payload,
+         @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> metadata);
+
+   static class SetPayload implements Binder {
+      @SuppressWarnings("unchecked")
+      @Override
+      public <R extends HttpRequest> R bindToRequest(R request, Object input) {
+         return (R) request.toBuilder().payload(Payload.class.cast(input)).build();
+      }
+   }
+
+   /**
+    * Gets the {@link SwiftObject} metadata without its
+    * {@link Payload#getInput() body}.
+    * 
+    * @param objectName
+    *           corresponds to {@link SwiftObject#name()}.
+    * @return the Object or null, if not found.
+    * 
+    * @see <a
+    *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/retrieve-object-metadata.html">
+    *      Get Object Metadata API</a>
+    */
+   @Named("GetObjectMetadata")
+   @HEAD
+   @ResponseParser(ParseObjectFromResponse.class)
+   @Fallback(NullOnNotFoundOr404.class)
+   @Path("/{objectName}")
+   @Nullable
+   SwiftObject head(@PathParam("objectName") String objectName);
+
+   /**
+    * Gets the {@link SwiftObject} including its {@link Payload#getInput() body}.
+    * 
+    * @param objectName
+    *           corresponds to {@link SwiftObject#name()}.
+    * @param options options to control the download.
+    * 
+    * @return the Object or null, if not found.
+    * 
+    * @see <a
+    *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/retrieve-object.html">
+    *      Get Object API</a>
+    */
+   @Named("GetObject")
+   @GET
+   @ResponseParser(ParseObjectFromResponse.class)
+   @Fallback(NullOnNotFoundOr404.class)
+   @Path("/{objectName}")
+   @Nullable
+   SwiftObject get(@PathParam("objectName") String objectName, GetOptions options);
+
+   /**
+    * Creates or updates the Object metadata.
+    * 
+    * @param objectName
+    *           corresponds to {@link SwiftObject#name()}.
+    * @param metadata
+    *           the Object metadata to create or update.
+    * 
+    * @see <a
+    *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/Update_Container_Metadata-d1e1900.html">
+    *      Create or Update Object Metadata API</a>
+    * 
+    * @return <code>true</code> if the Object Metadata was successfully created
+    *         or updated, false if not.
+    */
+   @Named("UpdateObjectMetadata")
+   @POST
+   @Fallback(FalseOnNotFoundOr404.class)
+   @Path("/{objectName}")
+   boolean updateMetadata(@PathParam("objectName") String objectName,
+         @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> metadata);
+
+   /**
+    * Deletes Object metadata.
+    * 
+    * @param objectName
+    *           corresponds to {@link SwiftObject#name()}.
+    * @param metadata
+    *           the Object metadata to delete.
+    * 
+    * @return <code>true</code> if the Object Metadata was successfully deleted,
+    *         false if not.
+    * 
+    * @see <a
+    *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/delete-object-metadata.html">
+    *      Delete Object Metadata API</a>
+    */
+   @Named("DeleteObjectMetadata")
+   @POST
+   @Fallback(FalseOnNotFoundOr404.class)
+   @Path("/{objectName}")
+   boolean deleteMetadata(@PathParam("objectName") String objectName,
+         @BinderParam(BindRemoveObjectMetadataToHeaders.class) Map<String, String> metadata);
+
+   /**
+    * Deletes a object, if present.
+    * 
+    * @param objectName
+    *           corresponds to {@link SwiftObject#name()}.
+    * @see <a
+    *      href="http://docs.openstack.org/api/openstack-object-storage/1.0/content/delete-object.html">
+    *      Delete Object API</a>
+    */
+   @Named("DeleteObject")
+   @DELETE
+   @Fallback(VoidOnNotFoundOr404.class)
+   @Path("/{objectName}")
+   void delete(@PathParam("objectName") String objectName);
 }

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java
new file mode 100644
index 0000000..f5c9dd2
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ETagHeader.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.openstack.swift.v1.functions;
+
+import static com.google.common.net.HttpHeaders.ETAG;
+
+import org.jclouds.http.HttpResponse;
+
+import com.google.common.base.Function;
+
+public class ETagHeader implements Function<HttpResponse, String> {
+
+   @Override
+   public String apply(HttpResponse from) {
+      return from.getFirstHeaderOrNull(ETAG);
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java
index b03c8ec..f31cbac 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseAccountFromHeaders.java
@@ -27,8 +27,8 @@ public class ParseAccountFromHeaders implements Function<HttpResponse, Account>
    public Account apply(HttpResponse from) {
       return Account.builder() //
             .bytesUsed(Long.parseLong(from.getFirstHeaderOrNull("X-Account-Bytes-Used"))) //
-            .containerCount(Integer.parseInt(from.getFirstHeaderOrNull("X-Account-Container-Count"))) //
-            .objectCount(Integer.parseInt(from.getFirstHeaderOrNull("X-Account-Object-Count"))) //
+            .containerCount(Long.parseLong(from.getFirstHeaderOrNull("X-Account-Container-Count"))) //
+            .objectCount(Long.parseLong(from.getFirstHeaderOrNull("X-Account-Object-Count"))) //
             .metadata(EntriesWithoutMetaPrefix.INSTANCE.apply(from.getHeaders())).build();
    }
 }

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java
index 5e66202..e246af5 100644
--- a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseContainerFromHeaders.java
@@ -34,7 +34,7 @@ public class ParseContainerFromHeaders implements Function<HttpResponse, Contain
       return Container.builder() //
             .name(name) //
             .bytesUsed(Long.parseLong(from.getFirstHeaderOrNull("X-Container-Bytes-Used"))) //
-            .objectCount(Integer.parseInt(from.getFirstHeaderOrNull("X-Container-Object-Count"))) //
+            .objectCount(Long.parseLong(from.getFirstHeaderOrNull("X-Container-Object-Count"))) //
             .metadata(EntriesWithoutMetaPrefix.INSTANCE.apply(from.getHeaders())).build();
    }
 

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java
new file mode 100644
index 0000000..c5533a4
--- /dev/null
+++ b/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/functions/ParseObjectFromResponse.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.openstack.swift.v1.functions;
+
+import static com.google.common.net.HttpHeaders.ETAG;
+import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
+
+import javax.inject.Inject;
+
+import org.jclouds.date.DateService;
+import org.jclouds.http.HttpRequest;
+import org.jclouds.http.HttpResponse;
+import org.jclouds.openstack.swift.v1.domain.SwiftObject;
+import org.jclouds.rest.InvocationContext;
+import org.jclouds.rest.internal.GeneratedHttpRequest;
+
+import com.google.common.base.Function;
+
+public class ParseObjectFromResponse implements Function<HttpResponse, SwiftObject>,
+      InvocationContext<ParseObjectFromResponse> {
+   private final DateService dates;
+
+   @Inject
+   ParseObjectFromResponse(DateService dates) {
+      this.dates = dates;
+   }
+
+   private String name;
+
+   @Override
+   public SwiftObject apply(HttpResponse from) {
+      return SwiftObject.builder() //
+            .name(name) //
+            .hash(from.getFirstHeaderOrNull(ETAG)) //
+            .payload(from.getPayload()) //
+            .lastModified(dates.rfc822DateParse(from.getFirstHeaderOrNull(LAST_MODIFIED))) //
+            .metadata(EntriesWithoutMetaPrefix.INSTANCE.apply(from.getHeaders())).build();
+   }
+
+   @Override
+   public ParseObjectFromResponse setContext(HttpRequest request) {
+      this.name = GeneratedHttpRequest.class.cast(request).getInvocation().getArgs().get(0).toString();
+      return this;
+   }
+}

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
index a9e83e7..4960fed 100644
--- a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
+++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
@@ -16,13 +16,152 @@
  */
 package org.jclouds.openstack.swift.v1.features;
 
+import static org.jclouds.http.options.GetOptions.Builder.tail;
+import static org.jclouds.io.Payloads.newStringPayload;
+import static org.jclouds.util.Strings2.toStringAndClose;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.TimeUnit;
+
+import org.jclouds.http.options.GetOptions;
+import org.jclouds.openstack.swift.v1.domain.SwiftObject;
 import org.jclouds.openstack.swift.v1.internal.BaseSwiftApiLiveTest;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableMap;
+
 /**
  * @author Adrian Cole
  */
 @Test(groups = "live", testName = "ObjectApiLiveTest")
 public class ObjectApiLiveTest extends BaseSwiftApiLiveTest {
 
+   private String name = getClass().getSimpleName();
+   private String containerName = getClass().getSimpleName() + "Container";
+
+   @Test
+   public void list() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         ObjectApi objectApi = api.objectApiInRegionForContainer(regionId, containerName);
+         FluentIterable<SwiftObject> response = objectApi.listFirstPage();
+         assertNotNull(response);
+         for (SwiftObject object : response) {
+            checkObject(object);
+         }
+      }
+   }
+
+   static void checkObject(SwiftObject object) {
+      assertNotNull(object.name());
+      assertNotNull(object.hash());
+      assertTrue(object.lastModified().getTime() <= System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5));
+      assertNotNull(object.payload().getContentMetadata().getContentLength());
+      assertNotNull(object.payload().getContentMetadata().getContentType());
+   }
+
+   public void metadata() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).head(name);
+         assertEquals(object.name(), name);
+         checkObject(object);
+         assertEquals(toStringAndClose(object.payload().getInput()), "");
+      }
+   }
+
+   public void get() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).get(name, GetOptions.NONE);
+         assertEquals(object.name(), name);
+         checkObject(object);
+         assertEquals(toStringAndClose(object.payload().getInput()), "swifty");
+      }
+   }
+
+   public void getOptions() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName).get(name, tail(1));
+         assertEquals(object.name(), name);
+         checkObject(object);
+         assertEquals(toStringAndClose(object.payload().getInput()), "y");
+      }
+   }
+
+   public void listAt() throws Exception {
+      String lexicographicallyBeforeName = name.substring(0, name.length() - 1);
+      for (String regionId : api.configuredRegions()) {
+         SwiftObject object = api.objectApiInRegionForContainer(regionId, containerName)
+               .listAt(lexicographicallyBeforeName).get(0);
+         assertEquals(object.name(), name);
+         checkObject(object);
+      }
+   }
+
+   public void updateMetadata() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         ObjectApi objectApi = api.objectApiInRegionForContainer(regionId, containerName);
+         ;
+
+         Map<String, String> meta = ImmutableMap.of("MyAdd1", "foo", "MyAdd2", "bar");
+
+         assertTrue(objectApi.updateMetadata(name, meta));
+
+         containerHasMetadata(objectApi, name, meta);
+      }
+   }
+
+   public void deleteMetadata() throws Exception {
+      for (String regionId : api.configuredRegions()) {
+         ObjectApi objectApi = api.objectApiInRegionForContainer(regionId, containerName);
+
+         Map<String, String> meta = ImmutableMap.of("MyDelete1", "foo", "MyDelete2", "bar");
+
+         assertTrue(objectApi.updateMetadata(name, meta));
+         containerHasMetadata(objectApi, name, meta);
+
+         assertTrue(objectApi.deleteMetadata(name, meta));
+         SwiftObject object = objectApi.head(name);
+         for (Entry<String, String> entry : meta.entrySet()) {
+            // note keys are returned in lower-case!
+            assertFalse(object.metadata().containsKey(entry.getKey().toLowerCase()));
+         }
+      }
+   }
+
+   static void containerHasMetadata(ObjectApi objectApi, String name, Map<String, String> meta) {
+      SwiftObject object = objectApi.head(name);
+      for (Entry<String, String> entry : meta.entrySet()) {
+         // note keys are returned in lower-case!
+         assertEquals(object.metadata().get(entry.getKey().toLowerCase()), entry.getValue(), //
+               object + " didn't have metadata: " + entry);
+      }
+   }
+
+   @Override
+   @BeforeClass(groups = "live")
+   public void setup() {
+      super.setup();
+      for (String regionId : api.configuredRegions()) {
+         api.containerApiInRegion(regionId).createIfAbsent(containerName);
+         api.objectApiInRegionForContainer(regionId, containerName).createOrUpdate(name, newStringPayload("swifty"),
+               ImmutableMap.<String, String> of());
+      }
+   }
+
+   @Override
+   @AfterClass(groups = "live")
+   public void tearDown() {
+      for (String regionId : api.configuredRegions()) {
+         api.objectApiInRegionForContainer(regionId, containerName).delete(name);
+         api.containerApiInRegion(regionId).deleteIfEmpty(containerName);
+      }
+      super.tearDown();
+   }
 }

http://git-wip-us.apache.org/repos/asf/incubator-jclouds-labs-openstack/blob/2150c8b6/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
----------------------------------------------------------------------
diff --git a/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
new file mode 100644
index 0000000..63c9974
--- /dev/null
+++ b/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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.openstack.swift.v1.features;
+
+import static com.google.common.base.Charsets.US_ASCII;
+import static com.google.common.net.HttpHeaders.RANGE;
+import static org.jclouds.http.options.GetOptions.Builder.tail;
+import static org.jclouds.io.Payloads.newStringPayload;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.jclouds.date.internal.SimpleDateFormatDateService;
+import org.jclouds.io.Payload;
+import org.jclouds.io.Payloads;
+import org.jclouds.openstack.swift.v1.SwiftApi;
+import org.jclouds.openstack.swift.v1.domain.SwiftObject;
+import org.jclouds.openstack.swift.v1.internal.BaseSwiftMockTest;
+import org.jclouds.util.Strings2;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+
+@Test
+public class ObjectApiMockTest extends BaseSwiftMockTest {
+   SimpleDateFormatDateService dates = new SimpleDateFormatDateService();
+
+   String objectList = "" //
+         + "[\n" //
+         + "   {\"name\":\"test_obj_1\",\n" //
+         + "    \"hash\":\"4281c348eaf83e70ddce0e07221c3d28\",\n" //
+         + "    \"bytes\":14,\n" //
+         + "    \"content_type\":\"application\\/octet-stream\",\n" //
+         + "    \"last_modified\":\"2009-02-03T05:26:32.612278\"},\n" //
+         + "   {\"name\":\"test_obj_2\",\n" //
+         + "    \"hash\":\"b039efe731ad111bc1b0ef221c3849d0\",\n" //
+         + "    \"bytes\":64,\n" //
+         + "    \"content_type\":\"application\\/octet-stream\",\n" //
+         + "    \"last_modified\":\"2009-02-03T05:26:32.612278\"},\n" //
+         + "]";
+
+   ImmutableList<SwiftObject> parsedObjects = ImmutableList.of(//
+         SwiftObject.builder() //
+               .name("test_obj_1") //
+               .hash("4281c348eaf83e70ddce0e07221c3d28") //
+               .payload(payload(14, "application/octet-stream")) //
+               .lastModified(dates.iso8601DateParse("2009-02-03T05:26:32.612278")).build(), //
+         SwiftObject.builder() //
+               .name("test_obj_2") //
+               .hash("b039efe731ad111bc1b0ef221c3849d0") //
+               .payload(payload(64l, "application/octet-stream")) //
+               .lastModified(dates.iso8601DateParse("2009-02-03T05:26:32.612278")).build());
+
+   public void listFirstPage() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(new MockResponse().setBody(objectList));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         ImmutableList<SwiftObject> objects = api.objectApiInRegionForContainer("DFW", "myContainer").listFirstPage()
+               .toList();
+         assertEquals(objects, parsedObjects);
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         assertEquals(server.takeRequest().getRequestLine(),
+               "GET /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/?format=json HTTP/1.1");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void listAt() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(new MockResponse().setBody(objectList));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         ImmutableList<SwiftObject> objects = api.objectApiInRegionForContainer("DFW", "myContainer").listAt("test")
+               .toList();
+         assertEquals(objects, parsedObjects);
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         assertEquals(server.takeRequest().getRequestLine(),
+               "GET /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/?format=json&marker=test HTTP/1.1");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void createOrUpdate() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(new MockResponse() //
+            .setResponseCode(201) //
+            .addHeader("ETag", "d9f5eb4bba4e2f2f046e54611bc8196b"));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         assertEquals(
+               api.objectApiInRegionForContainer("DFW", "myContainer").createOrUpdate("myObject",
+                     newStringPayload("swifty"), metadata), "d9f5eb4bba4e2f2f046e54611bc8196b");
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest createOrUpdate = server.takeRequest();
+         assertEquals(createOrUpdate.getRequestLine(),
+               "PUT /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+         assertEquals(new String(createOrUpdate.getBody()), "swifty");
+         for (Entry<String, String> entry : metadata.entrySet()) {
+            assertEquals(createOrUpdate.getHeader("x-object-meta-" + entry.getKey().toLowerCase()), entry.getValue());
+         }
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   /** upper-cases first char, and lower-cases rest!! **/
+   public void headKnowingServerMessesWithMetadataKeyCaseFormat() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(objectResponse() //
+            // note silly casing
+            .addHeader("X-Object-Meta-Apiname", "swift") //
+            .addHeader("X-Object-Meta-Apiversion", "v1.1"));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         SwiftObject object = api.objectApiInRegionForContainer("DFW", "myContainer").head("myObject");
+         assertEquals(object.name(), "myObject");
+         assertEquals(object.hash(), "8a964ee2a5e88be344f36c22562a6486");
+         assertEquals(object.lastModified(), dates.rfc822DateParse("Fri, 12 Jun 2010 13:40:18 GMT"));
+         for (Entry<String, String> entry : object.metadata().entrySet()) {
+            assertEquals(object.metadata().get(entry.getKey().toLowerCase()), entry.getValue());
+         }
+         assertEquals(object.payload().getContentMetadata().getContentLength(), new Long(4));
+         assertEquals(object.payload().getContentMetadata().getContentType(), "text/plain; charset=UTF-8");
+         assertEquals(Strings2.toStringAndClose(object.payload().getInput()), "");
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         assertEquals(server.takeRequest().getRequestLine(),
+               "HEAD /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void get() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(objectResponse() //
+            // note silly casing
+            .addHeader("X-Object-Meta-Apiname", "swift") //
+            .addHeader("X-Object-Meta-Apiversion", "v1.1"));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         SwiftObject object = api.objectApiInRegionForContainer("DFW", "myContainer").get("myObject", tail(1));
+         assertEquals(object.name(), "myObject");
+         assertEquals(object.hash(), "8a964ee2a5e88be344f36c22562a6486");
+         assertEquals(object.lastModified(), dates.rfc822DateParse("Fri, 12 Jun 2010 13:40:18 GMT"));
+         for (Entry<String, String> entry : object.metadata().entrySet()) {
+            assertEquals(object.metadata().get(entry.getKey().toLowerCase()), entry.getValue());
+         }
+         assertEquals(object.payload().getContentMetadata().getContentLength(), new Long(4));
+         assertEquals(object.payload().getContentMetadata().getContentType(), "text/plain; charset=UTF-8");
+         // note MWS doesn't process Range header at the moment
+         assertEquals(Strings2.toStringAndClose(object.payload().getInput()), "ABCD");
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest get = server.takeRequest();
+         assertEquals(get.getRequestLine(),
+               "GET /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+         assertEquals(get.getHeader(RANGE), "bytes=-1");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void updateMetadata() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(objectResponse() //
+            .addHeader("X-Object-Meta-ApiName", "swift") //
+            .addHeader("X-Object-Meta-ApiVersion", "v1.1"));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         assertTrue(api.objectApiInRegionForContainer("DFW", "myContainer").updateMetadata("myObject", metadata));
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest replaceRequest = server.takeRequest();
+         assertEquals(replaceRequest.getRequestLine(),
+               "POST /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+         for (Entry<String, String> entry : metadata.entrySet()) {
+            assertEquals(replaceRequest.getHeader("x-object-meta-" + entry.getKey().toLowerCase()), entry.getValue());
+         }
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void deleteMetadata() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(objectResponse());
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         assertTrue(api.objectApiInRegionForContainer("DFW", "myContainer").deleteMetadata("myObject", metadata));
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest deleteRequest = server.takeRequest();
+         assertEquals(deleteRequest.getRequestLine(),
+               "POST /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+         for (String key : metadata.keySet()) {
+            assertEquals(deleteRequest.getHeader("x-remove-object-meta-" + key.toLowerCase()), "ignored");
+         }
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void delete() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(new MockResponse().setResponseCode(204));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         api.objectApiInRegionForContainer("DFW", "myContainer").delete("myObject");
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest deleteRequest = server.takeRequest();
+         assertEquals(deleteRequest.getRequestLine(),
+               "DELETE /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   public void alreadyDeleted() throws Exception {
+      MockWebServer server = mockSwiftServer();
+      server.enqueue(new MockResponse().setBody(access));
+      server.enqueue(new MockResponse().setResponseCode(404));
+
+      try {
+         SwiftApi api = swiftApi(server.getUrl("/").toString());
+         api.objectApiInRegionForContainer("DFW", "myContainer").delete("myObject");
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+         RecordedRequest deleteRequest = server.takeRequest();
+         assertEquals(deleteRequest.getRequestLine(),
+               "DELETE /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject HTTP/1.1");
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   private final static Map<String, String> metadata = ImmutableMap.of("ApiName", "swift", "ApiVersion", "v1.1");
+
+   public static MockResponse objectResponse() {
+      return new MockResponse() //
+            .addHeader("Last-Modified", "Fri, 12 Jun 2010 13:40:18 GMT") //
+            .addHeader("ETag", "8a964ee2a5e88be344f36c22562a6486") //
+            // TODO: MWS doesn't allow you to return content length w/o content
+            // on HEAD!
+            .setBody("ABCD".getBytes(US_ASCII)) //
+            .addHeader("Content-Length", "4").addHeader("Content-Type", "text/plain; charset=UTF-8");
+   }
+
+   private static final byte[] NO_CONTENT = new byte[] {};
+
+   private static Payload payload(long bytes, String contentType) {
+      Payload payload = Payloads.newByteArrayPayload(NO_CONTENT);
+      payload.getContentMetadata().setContentLength(bytes);
+      payload.getContentMetadata().setContentType(contentType);
+      return payload;
+   }
+}