/*
 * 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.s3;

import static com.google.common.hash.Hashing.md5;
import static org.assertj.core.api.Assertions.assertThat;
import static org.jclouds.io.Payloads.newByteArrayPayload;
import static org.jclouds.s3.options.CopyObjectOptions.Builder.ifSourceETagDoesntMatch;
import static org.jclouds.s3.options.CopyObjectOptions.Builder.ifSourceETagMatches;
import static org.jclouds.s3.options.CopyObjectOptions.Builder.ifSourceModifiedSince;
import static org.jclouds.s3.options.CopyObjectOptions.Builder.ifSourceUnmodifiedSince;
import static org.jclouds.s3.options.CopyObjectOptions.Builder.overrideAcl;
import static org.jclouds.s3.options.CopyObjectOptions.Builder.overrideMetadataWith;
import static org.jclouds.s3.options.PutObjectOptions.Builder.withAcl;
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 static org.testng.Assert.fail;

import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;

import org.jclouds.ContextBuilder;
import org.jclouds.aws.domain.SessionCredentials;
import org.jclouds.blobstore.KeyNotFoundException;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.blobstore.domain.PageSet;
import org.jclouds.blobstore.domain.StorageMetadata;
import org.jclouds.blobstore.integration.internal.BaseBlobStoreIntegrationTest;
import org.jclouds.blobstore.options.ListContainerOptions;
import org.jclouds.domain.Credentials;
import org.jclouds.http.HttpResponseException;
import org.jclouds.io.ByteStreams2;
import org.jclouds.io.Payload;
import org.jclouds.s3.domain.AccessControlList;
import org.jclouds.s3.domain.AccessControlList.CanonicalUserGrantee;
import org.jclouds.s3.domain.AccessControlList.EmailAddressGrantee;
import org.jclouds.s3.domain.AccessControlList.GroupGranteeURI;
import org.jclouds.s3.domain.AccessControlList.Permission;
import org.jclouds.s3.domain.CannedAccessPolicy;
import org.jclouds.s3.domain.DeleteResult;
import org.jclouds.s3.domain.ListMultipartUploadResponse;
import org.jclouds.s3.domain.ListMultipartUploadsResponse;
import org.jclouds.s3.domain.ObjectMetadata;
import org.jclouds.s3.domain.ObjectMetadataBuilder;
import org.jclouds.s3.domain.S3Object;
import org.jclouds.s3.options.PutObjectOptions;
import org.jclouds.util.Strings2;
import org.jclouds.utils.TestUtils;
import org.testng.annotations.Test;

import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.hash.HashCode;
import com.google.common.io.ByteSource;

@Test(groups = { "integration", "live" })
public class S3ClientLiveTest extends BaseBlobStoreIntegrationTest {
   public static final String TEST_ACL_ID = "1a405254c932b52e5b5caaa88186bc431a1bacb9ece631f835daddaf0c47677c";
   public static final String TEST_ACL_EMAIL = "james@misterm.org";
   public static final String DEFAULT_OWNER_ID = "abc123";
   private static final ByteSource oneHundredOneConstitutions = TestUtils.randomByteSource().slice(0, 5 * 1024 * 1024 + 1);

   protected String sessionToken;

   public S3ClientLiveTest() {
      this.provider = "s3";
   }

   @Override
   protected Properties setupProperties() {
      Properties overrides = super.setupProperties();
      sessionToken = setIfTestSystemPropertyPresent(overrides, provider + ".sessionToken");
      return overrides;
   }

   @Override
   protected ContextBuilder newBuilder() {
      ContextBuilder builder = super.newBuilder();
      if (sessionToken != null) {
         builder.credentialsSupplier(new Supplier<Credentials>() {
            @Override
            public Credentials get() {
               return SessionCredentials.builder().identity(identity).credential(credential).sessionToken(sessionToken).build();
            }
         });
      }
      return builder;
   }

   public S3Client getApi() {
      return view.unwrapApi(S3Client.class);
   }

   /**
    * this method overrides containerName to ensure it isn't found
    */
   @Test(groups = { "integration", "live" })
   public void deleteContainerIfEmptyNotFound() throws Exception {
      assert getApi().deleteBucketIfEmpty("dbienf");
   }

   @Test(groups = { "integration", "live" })
   public void deleteContainerIfEmptyButHasContents() throws Exception {
      String containerName = getContainerName();
      try {
         addBlobToContainer(containerName, "test");
         assert !getApi().deleteBucketIfEmpty(containerName);
      } finally {
         returnContainer(containerName);
      }
   }

   protected URL getObjectURL(String containerName, String key) throws Exception {
      URL url = new URL(String.format("http://%s.%s/%s", containerName, URI.create(endpoint).getHost(), key));
      return url;
   }

   @Test(groups = {"fails-on-s3proxy"})
   public void testPutCannedAccessPolicyPublic() throws Exception {
      String containerName = getContainerName();
      try {
         String key = "hello";
         S3Object object = getApi().newS3Object();
         object.getMetadata().setKey(key);
         object.setPayload(TEST_STRING);
         getApi().putObject(containerName, object,

         withAcl(CannedAccessPolicy.PUBLIC_READ));

         URL url = this.getObjectURL(containerName, key);
         Strings2.toStringAndClose(url.openStream());
      } finally {
         returnContainer(containerName);
      }

   }

   @Test(groups = {"fails-on-s3proxy"})
   public void testCopyCannedAccessPolicyPublic() throws Exception {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();
      try {
         addBlobToContainer(containerName, sourceKey);
         validateContent(containerName, sourceKey);

         getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey,
                  overrideAcl(CannedAccessPolicy.PUBLIC_READ));

         validateContent(destinationContainer, destinationKey);

         URL url = getObjectURL(destinationContainer, destinationKey);
         Strings2.toStringAndClose(url.openStream());

      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);
      }
   }

   String sourceKey = "apples";
   String destinationKey = "pears";

   @Test(groups = {"fails-on-s3proxy"})
   public void testPublicWriteOnObject() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      final String publicReadWriteObjectKey = "public-read-write-acl";
      final String containerName = getContainerName();
      try {
         S3Object object = getApi().newS3Object();
         object.getMetadata().setKey(publicReadWriteObjectKey);
         object.setPayload("");
         // Public Read-Write object
         getApi()
                  .putObject(containerName, object,
                           new PutObjectOptions().withAcl(CannedAccessPolicy.PUBLIC_READ_WRITE));

         assertConsistencyAware(new Runnable() {
            public void run() {
               try {
                  AccessControlList acl = getApi().getObjectACL(containerName, publicReadWriteObjectKey);
                  assertEquals(acl.getGrants().size(), 3);
                  assertEquals(acl.getPermissions(GroupGranteeURI.ALL_USERS).size(), 2);
                  assertNotNull(acl.getOwner());
                  String ownerId = acl.getOwner().getId();
                  assertTrue(acl.hasPermission(ownerId, Permission.FULL_CONTROL));
                  assertTrue(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ));
                  assertTrue(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.WRITE));
                  assertFalse(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ_ACP));
                  assertFalse(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.WRITE_ACP));
                  assertFalse(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.FULL_CONTROL));
               } catch (Exception e) {
                  Throwables.propagateIfPossible(e);
               }
            }
         });
      } finally {
         returnContainer(containerName);
      }

   }

   @Test(groups = {"fails-on-s3proxy"})
   public void testUpdateObjectACL() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String containerName = getContainerName();
      try {
         String objectKey = "private-acl";

         // Private object
         addBlobToContainer(containerName, objectKey);
         AccessControlList acl = getApi().getObjectACL(containerName, objectKey);
         String ownerId = acl.getOwner().getId();

         assertEquals(acl.getGrants().size(), 1);
         assertTrue(acl.hasPermission(ownerId, Permission.FULL_CONTROL));

         addGrantsToACL(acl);
         assertEquals(acl.getGrants().size(), 4);
         assertTrue(getApi().putObjectACL(containerName, objectKey, acl));

         // Confirm that the updated ACL has stuck.
         acl = getApi().getObjectACL(containerName, objectKey);
         checkGrants(acl);

         /*
          * Revoke all of owner's permissions!
          */
         acl.revokeAllPermissions(new CanonicalUserGrantee(ownerId));
         if (!ownerId.equals(TEST_ACL_ID))
            acl.revokeAllPermissions(new CanonicalUserGrantee(TEST_ACL_ID));
         assertEquals(acl.getGrants().size(), 1);
         // Only public read permission should remain...
         assertTrue(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ));

         // Update the object's ACL settings
         assertTrue(getApi().putObjectACL(containerName, objectKey, acl));

         // Confirm that the updated ACL has stuck
         acl = getApi().getObjectACL(containerName, objectKey);
         assertEquals(acl.getGrants().size(), 1);
         assertEquals(acl.getPermissions(ownerId).size(), 0);
         assertTrue(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ), acl.toString());
      } finally {
         returnContainer(containerName);
      }

   }

   public void testPrivateAclIsDefaultForObject() throws InterruptedException, ExecutionException, TimeoutException,
            IOException {
      String privateObjectKey = "private-acl";
      String containerName = getContainerName();
      try {
         // Private object
         addBlobToContainer(containerName, privateObjectKey);
         AccessControlList acl = getApi().getObjectACL(containerName, privateObjectKey);

         assertEquals(acl.getGrants().size(), 1);
         assertNotNull(acl.getOwner());
         String ownerId = acl.getOwner().getId();
         assertTrue(acl.hasPermission(ownerId, Permission.FULL_CONTROL));
      } finally {
         returnContainer(containerName);
      }

   }

   public void testPublicReadOnObject() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      final String publicReadObjectKey = "public-read-acl";
      final String containerName = getContainerName();
      try {
         S3Object object = getApi().newS3Object();
         object.getMetadata().setKey(publicReadObjectKey);
         object.setPayload("");
         getApi().putObject(containerName, object, new PutObjectOptions().withAcl(CannedAccessPolicy.PUBLIC_READ));

         assertConsistencyAware(new Runnable() {
            public void run() {
               try {
                  AccessControlList acl = getApi().getObjectACL(containerName, publicReadObjectKey);

                  assertEquals(acl.getGrants().size(), 2);
                  assertEquals(acl.getPermissions(GroupGranteeURI.ALL_USERS).size(), 1);
                  assertNotNull(acl.getOwner());
                  String ownerId = acl.getOwner().getId();
                  assertTrue(acl.hasPermission(ownerId, Permission.FULL_CONTROL));
                  assertTrue(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ));
               } catch (Exception e) {
                  Throwables.propagateIfPossible(e);
               }
            }
         });

      } finally {
         returnContainer(containerName);
      }

   }

   protected String addBlobToContainer(String sourceContainer, String key) {
      S3Object sourceObject = getApi().newS3Object();
      sourceObject.getMetadata().setKey(key);
      sourceObject.getMetadata().getContentMetadata().setContentType("text/xml");
      sourceObject.setPayload(TEST_STRING);
      return getApi().putObject(sourceContainer, sourceObject);
   }

   protected S3Object validateObject(String sourceContainer, String key) throws InterruptedException,
            ExecutionException, TimeoutException, IOException {
      assertConsistencyAwareContainerSize(sourceContainer, 1);
      S3Object newObject = getApi().getObject(sourceContainer, key);
      assert newObject != null;
      assertEquals(Strings2.toStringAndClose(newObject.getPayload().openStream()), TEST_STRING);
      return newObject;
   }

   public void testMetadataWithCacheControlAndContentDisposition() throws Exception {
      String key = "hello";

      S3Object object = getApi().newS3Object();
      object.getMetadata().setKey(key);
      object.setPayload(TEST_STRING);
      object.getMetadata().setCacheControl("no-cache");
      object.getMetadata().getContentMetadata().setContentDisposition("attachment; filename=hello.txt");
      String containerName = getContainerName();
      try {
         getApi().putObject(containerName, object);

         S3Object newObject = validateObject(containerName, key);
         assertCacheControl(newObject, "no-cache");
         assertEquals(newObject.getMetadata().getContentMetadata().getContentDisposition(),
                  "attachment; filename=hello.txt");
      } finally {
         returnContainer(containerName);
      }
   }

   protected void assertCacheControl(S3Object newObject, String string) {
      assert newObject.getMetadata().getCacheControl().indexOf(string) != -1 : newObject.getMetadata()
               .getCacheControl();
   }

   protected void assertContentEncoding(S3Object newObject, String string) {
      assert newObject.getPayload().getContentMetadata().getContentEncoding().indexOf(string) != -1 : newObject
               .getPayload().getContentMetadata().getContentEncoding();
      assert newObject.getMetadata().getContentMetadata().getContentEncoding().indexOf(string) != -1 : newObject
               .getMetadata().getContentMetadata().getContentEncoding();
   }

   @Test(groups = { "integration", "live" })
   public void testMetadataContentEncoding() throws Exception {
      String key = "hello";

      S3Object object = getApi().newS3Object();
      object.getMetadata().setKey(key);
      object.setPayload(TEST_STRING);
      object.getMetadata().getContentMetadata().setContentEncoding("x-compress");
      String containerName = getContainerName();
      try {
         getApi().putObject(containerName, object);
         S3Object newObject = validateObject(containerName, key);
         assertContentEncoding(newObject, "x-compress");
      } finally {
         returnContainer(containerName);
      }
   }

   public void testCopyObject() throws Exception {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();

      try {
         addToContainerAndValidate(containerName, sourceKey);

         getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey);

         validateContent(destinationContainer, destinationKey);
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);

      }
   }

   public void testCopyObjectWithSourceKeyRequiringEncoding() throws Exception {
      String containerName = getContainerName();
      String sourceKeyRequiringEncoding = "apples#?:$&'\"<>čॐ";
      String destinationContainer = getContainerName();
      try {
         addToContainerAndValidate(containerName, sourceKeyRequiringEncoding);
         getApi().copyObject(containerName, sourceKeyRequiringEncoding, destinationContainer, destinationKey);
         validateContent(destinationContainer, destinationKey);
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);
      }
   }

   protected String addToContainerAndValidate(String containerName, String sourceKey) throws InterruptedException,
            ExecutionException, TimeoutException, IOException {
      String etag = addBlobToContainer(containerName, sourceKey);
      validateContent(containerName, sourceKey);
      return etag;
   }

   public void testCopyIfModifiedSince() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();
      try {
         Date before = new Date(System.currentTimeMillis() - 10 * 1000);
         addToContainerAndValidate(containerName, sourceKey + "mod");
         Date after = new Date(System.currentTimeMillis() + 10 * 1000);

         getApi().copyObject(containerName, sourceKey + "mod", destinationContainer, destinationKey,
                  ifSourceModifiedSince(before));
         validateContent(destinationContainer, destinationKey);

         // Sleep since Amazon returns 200 if the date is in the future:
         // https://forums.aws.amazon.com/message.jspa?messageID=325930
         TimeUnit.SECONDS.sleep(20);
         try {
            getApi().copyObject(containerName, sourceKey + "mod", destinationContainer, destinationKey,
                     ifSourceModifiedSince(after));
            fail("should have thrown HttpResponseException");
         } catch (HttpResponseException ex) {
            assertEquals(ex.getResponse().getStatusCode(), 412);
         }
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);

      }
   }

   public void testCopyIfUnmodifiedSince() throws InterruptedException, ExecutionException, TimeoutException,
            IOException {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();
      try {
         Date before = new Date(System.currentTimeMillis() - 10 * 1000);
         addToContainerAndValidate(containerName, sourceKey + "un");
         Date after = new Date(System.currentTimeMillis() + 10 * 1000);

         getApi().copyObject(containerName, sourceKey + "un", destinationContainer, destinationKey,
                  ifSourceUnmodifiedSince(after));
         validateContent(destinationContainer, destinationKey);

         try {
            getApi().copyObject(containerName, sourceKey + "un", destinationContainer, destinationKey,
                     ifSourceUnmodifiedSince(before));
            fail("should have thrown HttpResponseException");
         } catch (HttpResponseException ex) {
            assertEquals(ex.getResponse().getStatusCode(), 412);
         }
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);
      }
   }

   public void testCopyIfMatch() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();
      try {
         String goodETag = addToContainerAndValidate(containerName, sourceKey);

         getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey,
                  ifSourceETagMatches(goodETag));
         validateContent(destinationContainer, destinationKey);

         try {
            getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey,
                     ifSourceETagMatches("setsds"));
         } catch (HttpResponseException ex) {
            assertEquals(ex.getResponse().getStatusCode(), 412);
         }
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);
      }
   }

   public void testCopyIfNoneMatch() throws IOException, InterruptedException, ExecutionException, TimeoutException {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();
      try {
         String goodETag = addToContainerAndValidate(containerName, sourceKey);

         getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey,
                  ifSourceETagDoesntMatch("asfasdf"));
         validateContent(destinationContainer, destinationKey);

         try {
            getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey,
                     ifSourceETagDoesntMatch(goodETag));
         } catch (HttpResponseException ex) {
            assertEquals(ex.getResponse().getStatusCode(), 412);
         }
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);
      }
   }

   public void testCopyWithMetadata() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String containerName = getContainerName();
      String destinationContainer = getContainerName();
      try {
         addToContainerAndValidate(containerName, sourceKey);

         Map<String, String> metadata = Maps.newHashMap();
         metadata.put("adrian", "cole");

         getApi().copyObject(containerName, sourceKey, destinationContainer, destinationKey,
                  overrideMetadataWith(metadata));

         validateContent(destinationContainer, destinationKey);

         ObjectMetadata objectMeta = getApi().headObject(destinationContainer, destinationKey);

         assertEquals(objectMeta.getUserMetadata(), metadata);
      } finally {
         returnContainer(containerName);
         returnContainer(destinationContainer);

      }
   }

   // JCLOUDS-1401
   public void testUnusualKeyCharacters() throws InterruptedException, ExecutionException, TimeoutException, IOException {
      String containerName = getContainerName();
      try {
         String dirName = "a%2Fb&xxx#?:$'\\\"<>čॐ";
         String fileName = "foo%3Abar.xml";
         addToContainerAndValidate(containerName, dirName + '/' + fileName);
         PageSet<? extends StorageMetadata> list = view.getBlobStore().list(containerName,
                  ListContainerOptions.Builder.prefix(dirName + "/"));
         assertEquals(list.size(), 1);
         StorageMetadata md = list.iterator().next();
         assertEquals(md.getName(), dirName + '/' + fileName);
      } finally {
         returnContainer(containerName);
      }
   }

   public void testMultipartSynchronously() throws InterruptedException, IOException {
      HashCode oneHundredOneConstitutionsMD5 = oneHundredOneConstitutions.hash(md5());
      String containerName = getContainerName();
      S3Object object = null;
      try {
         String key = "constitution.txt";
         String uploadId = getApi().initiateMultipartUpload(containerName,
                  ObjectMetadataBuilder.create().key(key).contentMD5(oneHundredOneConstitutionsMD5.asBytes()).build());
         assertThat(getApi().listMultipartPartsFull(containerName, key, uploadId)).isEmpty();

         byte[] buffer = oneHundredOneConstitutions.read();
         assertEquals(oneHundredOneConstitutions.size(), (long) buffer.length);

         Payload part1 = newByteArrayPayload(buffer);
         part1.getContentMetadata().setContentLength((long) buffer.length);
         part1.getContentMetadata().setContentMD5(oneHundredOneConstitutionsMD5);

         String eTagOf1 = null;
         try {
            eTagOf1 = getApi().uploadPart(containerName, key, 1, uploadId, part1);
         } catch (KeyNotFoundException e) {
            // note that because of eventual consistency, the upload id may not be present yet
            // we may wish to add this condition to the retry handler

            // we may also choose to implement ListParts and wait for the uploadId to become
            // available there.
            eTagOf1 = getApi().uploadPart(containerName, key, 1, uploadId, part1);
         }
         Map<Integer, ListMultipartUploadResponse> map = getApi().listMultipartPartsFull(containerName, key, uploadId);
         assertThat(map).containsOnlyKeys(1);
         assertThat(map.get(1).eTag()).isEqualTo(eTagOf1);

         getApi().completeMultipartUpload(containerName, key, uploadId, ImmutableMap.of(1, eTagOf1));

         object = getApi().getObject(containerName, key);
         assertEquals(ByteStreams2.toByteArrayAndClose(object.getPayload().openStream()), buffer);
      } finally {
         if (object != null)
            object.getPayload().close();
         returnContainer(containerName);
      }
   }

   public void testMultipartCopy() throws Exception {
      String containerName = getContainerName();
      try {
         String fromObject = "fromObject";
         S3Object object = getApi().newS3Object();
         object.getMetadata().setKey(fromObject);
         object.setPayload(oneHundredOneConstitutions);
         object.getMetadata().getContentMetadata().setContentLength(oneHundredOneConstitutions.size());
         getApi().putObject(containerName, object);

         String toObject = "toObject";
         String uploadId = getApi().initiateMultipartUpload(containerName, ObjectMetadataBuilder.create().key(toObject).build());

         String eTagOf1 = getApi().uploadPartCopy(containerName, toObject, 1, uploadId, containerName, fromObject, 1, oneHundredOneConstitutions.size() - 1);

         getApi().completeMultipartUpload(containerName, toObject, uploadId, ImmutableMap.of(1, eTagOf1));

         object = getApi().getObject(containerName, toObject);
         assertEquals(ByteStreams2.toByteArrayAndClose(object.getPayload().openStream()), oneHundredOneConstitutions.slice(1, oneHundredOneConstitutions.size() - 1).read());
      } finally {
         returnContainer(containerName);
      }
   }

   public void testListMultipartUploads() throws Exception {
      String containerName = getContainerName();
      String key = "testListMultipartUploads";
      String uploadId = null;
      try {
         ListMultipartUploadsResponse response = getApi().listMultipartUploads(containerName, null, null, null, null, null);
         assertThat(response.bucket()).isEqualTo(containerName);
         assertThat(response.isTruncated()).isFalse();
         assertThat(response.uploads()).isEmpty();

         uploadId = getApi().initiateMultipartUpload(containerName, ObjectMetadataBuilder.create().key(key).build());

         response = getApi().listMultipartUploads(containerName, null, null, null, null, null);
         assertThat(response.bucket()).isEqualTo(containerName);
         assertThat(response.isTruncated()).isFalse();
         assertThat(response.uploads()).hasSize(1);

         ListMultipartUploadsResponse.Upload upload = response.uploads().get(0);
         assertThat(upload.key()).isEqualTo(key);
         assertThat(upload.uploadId()).isEqualTo(uploadId);
         assertThat(upload.storageClass()).isEqualTo(ObjectMetadata.StorageClass.STANDARD);
      } finally {
         if (uploadId != null) {
            getApi().abortMultipartUpload(containerName, key, uploadId);
         }
         returnContainer(containerName);
      }
   }

   public void testDeleteMultipleObjects() throws InterruptedException {
      String container = getContainerName();
      try {
         ImmutableSet.Builder<String> builder = ImmutableSet.builder();
         for (int i = 0; i < 5; i++) {
            String key = UUID.randomUUID().toString();

            Blob blob = view.getBlobStore().blobBuilder(key).payload("").build();
            view.getBlobStore().putBlob(container, blob);

            builder.add(key);
         }

         Set<String> keys = builder.build();
         DeleteResult result = getApi().deleteObjects(container, keys);

         assertTrue(result.getDeleted().containsAll(keys));
         assertEquals(result.getErrors().size(), 0);

         for (String key : keys) {
            assertConsistencyAwareBlobDoesntExist(container, key);
         }

      } finally {
         returnContainer(container);
      }
   }

   private void checkGrants(AccessControlList acl) {
      String ownerId = acl.getOwner().getId();

      assertEquals(acl.getGrants().size(), 4, acl.toString());

      assertTrue(acl.hasPermission(ownerId, Permission.FULL_CONTROL), acl.toString());
      assertTrue(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ), acl.toString());
      assertTrue(acl.hasPermission(ownerId, Permission.WRITE_ACP), acl.toString());
      // EmailAddressGrantee is replaced by a CanonicalUserGrantee, so we cannot test by email addr
      assertTrue(acl.hasPermission(TEST_ACL_ID, Permission.READ_ACP), acl.toString());
   }

   private void addGrantsToACL(AccessControlList acl) {
      String ownerId = acl.getOwner().getId();
      acl.addPermission(GroupGranteeURI.ALL_USERS, Permission.READ);
      acl.addPermission(new EmailAddressGrantee(TEST_ACL_EMAIL), Permission.READ_ACP);
      acl.addPermission(new CanonicalUserGrantee(ownerId), Permission.WRITE_ACP);
   }

   public void testUpdateBucketCannedACL() throws Exception {
      String containerName = getContainerName();
      try {
         getApi().updateBucketCannedACL(containerName, CannedAccessPolicy.PUBLIC_READ);
         AccessControlList acl = getApi().getBucketACL(containerName);
         assertThat(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ)).isTrue();

         getApi().updateBucketCannedACL(containerName, CannedAccessPolicy.PRIVATE);
         acl = getApi().getBucketACL(containerName);
         assertThat(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ)).isFalse();
      } finally {
         recycleContainerAndAddToPool(containerName);
      }
   }

   public void testUpdateObjectCannedACL() throws Exception {
      String containerName = getContainerName();
      try {
         String key = "testUpdateObjectCannedACL";
         S3Object object = getApi().newS3Object();
         object.getMetadata().setKey(key);
         object.setPayload(TEST_STRING);
         getApi().putObject(containerName, object);

         getApi().updateObjectCannedACL(containerName, key, CannedAccessPolicy.PUBLIC_READ);
         AccessControlList acl = getApi().getObjectACL(containerName, key);
         assertThat(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ)).isTrue();

         getApi().updateObjectCannedACL(containerName, key, CannedAccessPolicy.PRIVATE);
         acl = getApi().getObjectACL(containerName, key);
         assertThat(acl.hasPermission(GroupGranteeURI.ALL_USERS, Permission.READ)).isFalse();

         object = getApi().getObject(containerName, key);
         assertThat(Strings2.toStringAndClose(object.getPayload().openStream())).isEqualTo(TEST_STRING);
      } finally {
         returnContainer(containerName);
      }
   }
}
