You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jclouds.apache.org by ga...@apache.org on 2015/04/07 00:42:34 UTC

[1/3] jclouds git commit: Allows copying an object and modifying metadata (user and object)

Repository: jclouds
Updated Branches:
  refs/heads/master a6a232a7a -> d8f48c48b


Allows copying an object and modifying metadata (user and object)


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/a1cbec10
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/a1cbec10
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/a1cbec10

Branch: refs/heads/master
Commit: a1cbec10925a05bf0a05c6ddb3f9eb51c7f155ea
Parents: a6a232a
Author: Zack Shoylev <za...@rackspace.com>
Authored: Tue Mar 31 17:30:56 2015 -0500
Committer: Andrew Gaul <ga...@apache.org>
Committed: Mon Apr 6 15:35:58 2015 -0700

----------------------------------------------------------------------
 .../swift/v1/binders/BindMetadataToHeaders.java |  6 +++
 .../openstack/swift/v1/features/ObjectApi.java  | 33 ++++++++++++
 .../swift/v1/features/ObjectApiMockTest.java    | 56 ++++++++++++++++++--
 3 files changed, 91 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/a1cbec10/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
index 9451934..d27e19d 100644
--- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
+++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
@@ -85,6 +85,12 @@ public class BindMetadataToHeaders implements Binder {
       }
    }
 
+   public static class BindHeaderMetadataToHeaders extends BindMetadataToHeaders {
+      BindHeaderMetadataToHeaders() {
+         super("");
+      }
+   }
+
    public static class BindRemoveObjectMetadataToHeaders extends BindMetadataToHeaders.ForRemoval {
       BindRemoveObjectMetadataToHeaders() {
          super(OBJECT_METADATA_PREFIX);

http://git-wip-us.apache.org/repos/asf/jclouds/blob/a1cbec10/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
index 4d4d603..7d89ba3 100644
--- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
+++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
@@ -41,6 +41,7 @@ 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.BindHeaderMetadataToHeaders;
 import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindObjectMetadataToHeaders;
 import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindRemoveObjectMetadataToHeaders;
 import org.jclouds.openstack.swift.v1.binders.SetPayload;
@@ -262,4 +263,36 @@ public interface ObjectApi {
                 @PathParam("sourceContainer") String sourceContainer,
                 @PathParam("sourceObject") String sourceObject);
 
+   /**
+    * Copies an object from one container to another.
+    *
+    * <h3>NOTE</h3>
+    * This is a server side copy.
+    *
+    * @param destinationObject
+    *           the destination object name.
+    * @param sourceContainer
+    *           the source container name.
+    * @param sourceObject
+    *           the source object name.
+    * @param userMetadata
+    *           Freeform metadata for the object, automatically prefixed/escaped
+    * @param objectMetadata
+    *           Unprefixed/unescaped metadata, such as Content-Disposition
+    *
+    * @return {@code true} if the object was successfully copied, {@code false} if not.
+    *
+    * @throws org.jclouds.openstack.swift.v1.CopyObjectException if the source or destination container do not exist.
+    */
+   @Named("object:copy")
+   @PUT
+   @Path("/{destinationObject}")
+   @Headers(keys = OBJECT_COPY_FROM, values = "/{sourceContainer}/{sourceObject}")
+   @Fallback(FalseOnContainerNotFound.class)
+   boolean copy(@PathParam("destinationObject") String destinationObject,
+         @PathParam("sourceContainer") String sourceContainer,
+         @PathParam("sourceObject") String sourceObject,
+         @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> userMetadata,
+         @BinderParam(BindHeaderMetadataToHeaders.class) Map<String, String> objectMetadata);
+
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/a1cbec10/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
index 87166df..81a856e 100644
--- a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
+++ b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiMockTest.java
@@ -39,6 +39,7 @@ import static org.testng.Assert.fail;
 import java.io.IOException;
 import java.net.URI;
 import java.util.Date;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Properties;
@@ -193,7 +194,8 @@ public class ObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi> {
    public void testCreateWithSpacesAndSpecialCharacters() throws Exception {
       MockWebServer server = mockOpenStackServer();
       server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json"))));
-      server.enqueue(addCommonHeaders(new MockResponse().setResponseCode(201).addHeader("ETag", "d9f5eb4bba4e2f2f046e54611bc8196b")));
+      server.enqueue(addCommonHeaders(
+            new MockResponse().setResponseCode(201).addHeader("ETag", "d9f5eb4bba4e2f2f046e54611bc8196b")));
 
       final String containerName = "container # ! special";
       final String objectName = "object # ! special";
@@ -279,7 +281,8 @@ public class ObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi> {
 
          assertEquals(server.getRequestCount(), 2);
          assertAuthentication(server);
-         assertRequest(server.takeRequest(), "HEAD", "/v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject");
+         assertRequest(server.takeRequest(), "HEAD",
+               "/v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/myContainer/myObject");
       } finally {
          server.shutdown();
       }
@@ -339,7 +342,7 @@ public class ObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi> {
 
          fail("testReplaceTimeout test should have failed with an HttpResponseException.");
       } finally {
-         try { 
+         try {
             server.shutdown();
          } catch (IOException e) {
             // MockWebServer 2.1.0 introduces an active wait for its executor termination.
@@ -469,7 +472,7 @@ public class ObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi> {
       try {
          SwiftApi api = api(server.getUrl("/").toString(), "openstack-swift");
          assertTrue(api.getObjectApi("DFW", "foo")
-            .copy("bar.txt", "bar", "foo.txt"));
+               .copy("bar.txt", "bar", "foo.txt"));
 
          assertEquals(server.getRequestCount(), 2);
          assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
@@ -498,6 +501,51 @@ public class ObjectApiMockTest extends BaseOpenStackMockTest<SwiftApi> {
       }
    }
 
+   public void testCopyObjectWithMetadata() throws Exception {
+      MockWebServer server = mockOpenStackServer();
+      server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json"))));
+      server.enqueue(addCommonHeaders(new MockResponse().setResponseCode(201)
+            .addHeader(SwiftHeaders.OBJECT_COPY_FROM, "/bar/foo.txt")));
+
+      try {
+         SwiftApi api = api(server.getUrl("/").toString(), "openstack-swift");
+         assertTrue(api.getObjectApi("DFW", "foo")
+               .copy("bar.txt", "bar", "foo.txt", ImmutableMap.of("someUserHeader", "someUserMetadataValue"),
+                     ImmutableMap.of("Content-Disposition", "attachment; filename=\"fname.ext\"")));
+
+         assertEquals(server.getRequestCount(), 2);
+         assertEquals(server.takeRequest().getRequestLine(), "POST /tokens HTTP/1.1");
+
+         RecordedRequest copyRequest = server.takeRequest();
+         assertEquals(copyRequest.getRequestLine(),
+               "PUT /v1/MossoCloudFS_5bcf396e-39dd-45ff-93a1-712b9aba90a9/foo/bar.txt HTTP/1.1");
+
+         List<String> requestHeaders = copyRequest.getHeaders();
+         assertTrue(requestHeaders.contains("X-Object-Meta-someuserheader: someUserMetadataValue"));
+         assertTrue(requestHeaders.contains("content-disposition: attachment; filename=\"fname.ext\""));
+         assertTrue(requestHeaders.contains(SwiftHeaders.OBJECT_COPY_FROM + ": /bar/foo.txt"));
+      } finally {
+         server.shutdown();
+      }
+   }
+
+   @Test(expectedExceptions = CopyObjectException.class)
+   public void testCopyObjectWithMetadataFail() throws Exception {
+      MockWebServer server = mockOpenStackServer();
+      server.enqueue(addCommonHeaders(new MockResponse().setBody(stringFromResource("/access.json"))));
+      server.enqueue(addCommonHeaders(new MockResponse().setResponseCode(404)
+            .addHeader(SwiftHeaders.OBJECT_COPY_FROM, "/bar/foo.txt")));
+
+      try {
+         SwiftApi api = api(server.getUrl("/").toString(), "openstack-swift");
+         assertTrue(api.getObjectApi("DFW", "foo")
+               .copy("bar.txt", "bar", "foo.txt", ImmutableMap.of("someUserHeader", "someUserMetadataValue"),
+                     ImmutableMap.of("Content-Disposition", "attachment; filename=\"fname.ext\"")));
+      } finally {
+         server.shutdown();
+      }
+   }
+
    private static final Map<String, String> metadata = ImmutableMap.of("ApiName", "swift", "ApiVersion", "v1.1");
 
    static MockResponse objectResponse() {


[3/3] jclouds git commit: JCLOUDS-651: Copy Swift system metadata

Posted by ga...@apache.org.
JCLOUDS-651: Copy Swift system metadata


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/d8f48c48
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/d8f48c48
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/d8f48c48

Branch: refs/heads/master
Commit: d8f48c48b41e229e89c4b5db2641e15003b99277
Parents: 576005a
Author: Andrew Gaul <ga...@apache.org>
Authored: Thu Apr 2 17:16:36 2015 -0700
Committer: Andrew Gaul <ga...@apache.org>
Committed: Mon Apr 6 15:35:59 2015 -0700

----------------------------------------------------------------------
 .../blobstore/RegionScopedSwiftBlobStore.java   | 49 ++++++++++++++------
 .../internal/BaseBlobIntegrationTest.java       |  3 +-
 2 files changed, 37 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/d8f48c48/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/blobstore/RegionScopedSwiftBlobStore.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/blobstore/RegionScopedSwiftBlobStore.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/blobstore/RegionScopedSwiftBlobStore.java
index 4516264..4192a96 100644
--- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/blobstore/RegionScopedSwiftBlobStore.java
+++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/blobstore/RegionScopedSwiftBlobStore.java
@@ -53,6 +53,7 @@ import org.jclouds.blobstore.options.PutOptions;
 import org.jclouds.blobstore.strategy.ClearListStrategy;
 import org.jclouds.collect.Memoized;
 import org.jclouds.domain.Location;
+import org.jclouds.io.ContentMetadata;
 import org.jclouds.io.Payload;
 import org.jclouds.io.payloads.ByteSourcePayload;
 import org.jclouds.openstack.swift.v1.SwiftApi;
@@ -78,7 +79,9 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import com.google.common.io.ByteSource;
+import com.google.common.net.HttpHeaders;
 import com.google.inject.AbstractModule;
 import com.google.inject.Injector;
 import com.google.inject.assistedinject.Assisted;
@@ -234,25 +237,45 @@ public class RegionScopedSwiftBlobStore implements BlobStore {
    public String copyBlob(String fromContainer, String fromName, String toContainer, String toName,
          CopyOptions options) {
       ObjectApi objectApi = api.getObjectApi(regionId, toContainer);
+      SwiftObject metadata = api.getObjectApi(regionId, fromContainer).getWithoutBody(fromName);
 
-      boolean copied = objectApi.copy(toName, fromContainer, fromName);
-      if (!copied) {
-         throw new RuntimeException("could not copy blob");
+      Map<String, String> userMetadata;
+      if (options.getUserMetadata().isPresent()) {
+         userMetadata = options.getUserMetadata().get();
+      } else {
+         userMetadata = metadata.getMetadata();
       }
 
-      // TODO: content disposition
-      // TODO: content encoding
-      // TODO: content language
-      // TODO: content type
+      // copy existing system metadata
+      Map<String, String> systemMetadata = Maps.newHashMap();
+      ContentMetadata contentMetadata = metadata.getPayload().getContentMetadata();
+      String contentDisposition = contentMetadata.getContentDisposition();
+      if (contentDisposition != null) {
+         systemMetadata.put(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
+      }
+      String contentEncoding = contentMetadata.getContentEncoding();
+      if (contentEncoding != null) {
+         systemMetadata.put(HttpHeaders.CONTENT_ENCODING, contentEncoding);
+      }
+      String contentLanguage = contentMetadata.getContentLanguage();
+      if (contentLanguage != null) {
+         systemMetadata.put(HttpHeaders.CONTENT_LANGUAGE, contentLanguage);
+      }
+      String contentType = contentMetadata.getContentType();
+      if (contentType != null) {
+         systemMetadata.put(HttpHeaders.CONTENT_TYPE, contentType);
+      }
 
-      Optional<Map<String, String>> userMetadata = options.getUserMetadata();
-      if (userMetadata.isPresent()) {
-         boolean updated = objectApi.updateMetadata(toName, userMetadata.get());
-         if (!updated) {
-            throw new RuntimeException("could not copy blob");
-         }
+      boolean copied = objectApi.copy(toName, fromContainer, fromName, userMetadata, systemMetadata);
+      if (!copied) {
+         throw new RuntimeException("could not copy blob");
       }
 
+      // TODO: override content disposition
+      // TODO: override content encoding
+      // TODO: override content language
+      // TODO: override content type
+
       return objectApi.getWithoutBody(toName).getETag();
    }
 

http://git-wip-us.apache.org/repos/asf/jclouds/blob/d8f48c48/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java
----------------------------------------------------------------------
diff --git a/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java b/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java
index 0d57fc5..1be2597 100644
--- a/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java
+++ b/blobstore/src/test/java/org/jclouds/blobstore/integration/internal/BaseBlobIntegrationTest.java
@@ -759,8 +759,7 @@ public class BaseBlobIntegrationTest extends BaseBlobStoreIntegrationTest {
          } finally {
             Closeables2.closeQuietly(is);
          }
-         // TODO: Swift does not preserve system metadata
-         //checkContentMetadata(toBlob);
+         checkContentMetadata(toBlob);
          assertThat(toBlob.getMetadata().getUserMetadata()).isEqualTo(userMetadata);
       } finally {
          returnContainer(toContainer);


[2/3] jclouds git commit: Adds live test.

Posted by ga...@apache.org.
Adds live test.


Project: http://git-wip-us.apache.org/repos/asf/jclouds/repo
Commit: http://git-wip-us.apache.org/repos/asf/jclouds/commit/576005a3
Tree: http://git-wip-us.apache.org/repos/asf/jclouds/tree/576005a3
Diff: http://git-wip-us.apache.org/repos/asf/jclouds/diff/576005a3

Branch: refs/heads/master
Commit: 576005a33553d3b682bdd7088c10c5e7da1c004e
Parents: a1cbec1
Author: Zack Shoylev <za...@rackspace.com>
Authored: Thu Apr 2 15:55:01 2015 -0500
Committer: Andrew Gaul <ga...@apache.org>
Committed: Mon Apr 6 15:35:59 2015 -0700

----------------------------------------------------------------------
 .../swift/v1/binders/BindMetadataToHeaders.java |  4 +-
 .../openstack/swift/v1/features/ObjectApi.java  | 24 +++++-
 .../swift/v1/features/ObjectApiLiveTest.java    | 81 ++++++++++++++++++++
 3 files changed, 105 insertions(+), 4 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/jclouds/blob/576005a3/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
index d27e19d..59ff29a 100644
--- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
+++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/binders/BindMetadataToHeaders.java
@@ -85,8 +85,8 @@ public class BindMetadataToHeaders implements Binder {
       }
    }
 
-   public static class BindHeaderMetadataToHeaders extends BindMetadataToHeaders {
-      BindHeaderMetadataToHeaders() {
+   public static class BindRawMetadataToHeaders extends BindMetadataToHeaders {
+      BindRawMetadataToHeaders() {
          super("");
       }
    }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/576005a3/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
index 7d89ba3..841fd9d 100644
--- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
+++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/features/ObjectApi.java
@@ -41,8 +41,8 @@ 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.BindHeaderMetadataToHeaders;
 import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindObjectMetadataToHeaders;
+import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindRawMetadataToHeaders;
 import org.jclouds.openstack.swift.v1.binders.BindMetadataToHeaders.BindRemoveObjectMetadataToHeaders;
 import org.jclouds.openstack.swift.v1.binders.SetPayload;
 import org.jclouds.openstack.swift.v1.domain.ObjectList;
@@ -208,6 +208,26 @@ public interface ObjectApi {
          @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> metadata);
 
    /**
+    * Creates or updates the metadata for a {@link SwiftObject} without escaping the key.
+    * This will also update metadata such as content-disposition.
+    *
+    * @param objectName
+    *           corresponds to {@link SwiftObject#getName()}.
+    * @param metadata
+    *           the metadata to create or update.
+    *
+    * @return {@code true} if the metadata was successfully created or updated,
+    *         {@code false} if not.
+    */
+   @Named("object:updateMetadata")
+   @POST
+   @Path("/{objectName}")
+   @Produces("")
+   @Fallback(FalseOnNotFoundOr404.class)
+   boolean updateRawMetadata(@PathParam("objectName") String objectName,
+         @BinderParam(BindRawMetadataToHeaders.class) Map<String, String> metadata);
+
+   /**
     * Deletes the metadata from a {@link SwiftObject}.
     *
     * @param objectName
@@ -293,6 +313,6 @@ public interface ObjectApi {
          @PathParam("sourceContainer") String sourceContainer,
          @PathParam("sourceObject") String sourceObject,
          @BinderParam(BindObjectMetadataToHeaders.class) Map<String, String> userMetadata,
-         @BinderParam(BindHeaderMetadataToHeaders.class) Map<String, String> objectMetadata);
+         @BinderParam(BindRawMetadataToHeaders.class) Map<String, String> objectMetadata);
 
 }

http://git-wip-us.apache.org/repos/asf/jclouds/blob/576005a3/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
----------------------------------------------------------------------
diff --git a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
index 5a472ff..12d769d 100644
--- a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
+++ b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/features/ObjectApiLiveTest.java
@@ -45,6 +45,7 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimap;
 import com.google.common.io.ByteSource;
 
 /**
@@ -96,6 +97,7 @@ public class ObjectApiLiveTest extends BaseSwiftApiLiveTest<SwiftApi> {
          api.getObjectApi(regionId, containerName).delete(objectName);
       }
    }
+
    public void testCopyObject() throws Exception {
       for (String regionId : regions) {
          // source
@@ -153,6 +155,85 @@ public class ObjectApiLiveTest extends BaseSwiftApiLiveTest<SwiftApi> {
       }
    }
 
+   public void testCopyObjectWithMetadata() throws Exception {
+      for (String regionId : regions) {
+         // source
+         String sourceContainer = "src" + containerName;
+         String sourceObjectName = "original.txt";
+         String badSource = "badSource";
+
+         // destination
+         String destinationContainer = "dest" + containerName;
+         String destinationObject = "copy.txt";
+         String destinationPath = "/" + destinationContainer + "/" + destinationObject;
+
+         ContainerApi containerApi = api.getContainerApi(regionId);
+
+         // create source and destination dirs
+         containerApi.create(sourceContainer);
+         containerApi.create(destinationContainer);
+
+         // get the api for this region and container
+         ObjectApi srcApi = api.getObjectApi(regionId, sourceContainer);
+         ObjectApi destApi = api.getObjectApi(regionId, destinationContainer);
+
+         // Create source object
+         assertNotNull(srcApi.put(sourceObjectName, PAYLOAD));
+         SwiftObject sourceObject = srcApi.get(sourceObjectName);
+         checkObject(sourceObject);
+
+         srcApi.updateMetadata(sourceObjectName,
+               ImmutableMap.of("userProvidedMetadataKey", "userProvidedMetadataValue"));
+
+         // Create the destination object
+         assertNotNull(destApi.put(destinationObject, PAYLOAD));
+         SwiftObject object = destApi.get(destinationObject);
+         checkObject(object);
+
+         // check the copy operation
+         assertTrue(destApi.copy(destinationObject, sourceContainer, sourceObjectName,
+               ImmutableMap.<String, String>of("additionalUserMetakey", "additionalUserMetavalue"),
+               ImmutableMap.of("Content-Disposition", "attachment; filename=\"updatedname.txt\"")));
+         assertNotNull(destApi.get(destinationObject));
+
+         // now get a real SwiftObject
+         SwiftObject destSwiftObject = destApi.get(destinationObject);
+         assertEquals(toStringAndClose(destSwiftObject.getPayload().openStream()), "swifty");
+
+         /**
+          * Make sure all src metadata is in dest
+          * Make sure the new content disposition is in dest
+          */
+         Multimap<String, String> srcHeaders = null;
+         Multimap<String, String> destHeaders = null;
+         srcHeaders = srcApi.get(sourceObjectName).getHeaders();
+         destHeaders = destApi.get(destinationObject).getHeaders();
+         for (Entry<String, String> header : srcHeaders.entries()) {
+            if (header.getKey().equals("Date"))continue;
+            if (header.getKey().equals("Last-Modified"))continue;
+            if (header.getKey().equals("X-Trans-Id"))continue;
+            if (header.getKey().equals("X-Timestamp"))continue;
+            assertTrue(destHeaders.containsEntry(header.getKey(), header.getValue()), "Could not find: " + header);
+         }
+         assertEquals(destApi.get(destinationObject).getPayload().getContentMetadata().getContentDisposition(), "attachment; filename=\"updatedname.txt\"");
+
+         // test exception thrown on bad source name
+         try {
+            destApi.copy(destinationObject, badSource, sourceObjectName);
+            fail("Expected CopyObjectException");
+         } catch (CopyObjectException e) {
+            assertEquals(e.getSourcePath(), "/" + badSource + "/" + sourceObjectName);
+            assertEquals(e.getDestinationPath(), destinationPath);
+         }
+
+         deleteAllObjectsInContainer(regionId, sourceContainer);
+         containerApi.deleteIfEmpty(sourceContainer);
+
+         deleteAllObjectsInContainer(regionId, destinationContainer);
+         containerApi.deleteIfEmpty(destinationContainer);
+      }
+   }
+
    public void testList() throws Exception {
       for (String regionId : regions) {
          ObjectApi objectApi = api.getObjectApi(regionId, containerName);