JCLOUDS-912: JCLOUDS-1547: GCS InputStream single-part upload

Previously this provider worked around a RestAnnotationProcessor quirk
by using multi-part uploads for InputStream payloads.  Instead work
around the quirk another way which allows a single-part upload.  This
allows inclusion of the Content-MD5 header during object creation.
Backfill tests with both ByteSource and InputStream inputs.
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 ef5bd7f..a814c00 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
@@ -267,7 +267,7 @@
       }
    }
 
-   private void putBlobWithMd5(byte[] payload, HashCode contentMD5) throws InterruptedException, IOException {
+   private void putBlobWithMd5(Payload payload, long contentLength, HashCode contentMD5) throws InterruptedException, IOException {
       String container = getContainerName();
       BlobStore blobStore = view.getBlobStore();
       try {
@@ -275,6 +275,7 @@
          Blob blob = blobStore
             .blobBuilder(blobName)
             .payload(payload)
+            .contentLength(contentLength)
             .contentMD5(contentMD5)
             .build();
          blobStore.putBlob(container, blob);
@@ -288,18 +289,39 @@
    }
 
    @Test(groups = { "integration", "live" })
-   public void testPutCorrectContentMD5() throws InterruptedException, IOException {
-      byte[] payload = createTestInput(1024).read();
-      HashCode contentMD5 = md5().hashBytes(payload);
-      putBlobWithMd5(payload, contentMD5);
+   public void testPutCorrectContentMD5ByteSource() throws InterruptedException, IOException {
+      ByteSource payload = createTestInput(1024);
+      HashCode contentMD5 = md5().hashBytes(payload.read());
+      putBlobWithMd5(new ByteSourcePayload(payload), payload.size(), contentMD5);
    }
 
    @Test(groups = { "integration", "live" })
-   public void testPutIncorrectContentMD5() throws InterruptedException, IOException {
-      byte[] payload = createTestInput(1024).read();
+   public void testPutIncorrectContentMD5ByteSource() throws InterruptedException, IOException {
+      ByteSource payload = createTestInput(1024);
       HashCode contentMD5 = md5().hashBytes(new byte[0]);
       try {
-         putBlobWithMd5(payload, contentMD5);
+         putBlobWithMd5(new ByteSourcePayload(payload), payload.size(), contentMD5);
+         fail();
+      } catch (HttpResponseException hre) {
+         if (hre.getResponse().getStatusCode() != getIncorrectContentMD5StatusCode()) {
+            throw hre;
+         }
+      }
+   }
+
+   @Test(groups = { "integration", "live" })
+   public void testPutCorrectContentMD5InputStream() throws InterruptedException, IOException {
+      ByteSource payload = createTestInput(1024);
+      HashCode contentMD5 = md5().hashBytes(payload.read());
+      putBlobWithMd5(new InputStreamPayload(payload.openStream()), payload.size(), contentMD5);
+   }
+
+   @Test(groups = { "integration", "live" })
+   public void testPutIncorrectContentMD5InputStream() throws InterruptedException, IOException {
+      ByteSource payload = createTestInput(1024);
+      HashCode contentMD5 = md5().hashBytes(new byte[0]);
+      try {
+         putBlobWithMd5(new InputStreamPayload(payload.openStream()), payload.size(), contentMD5);
          fail();
       } catch (HttpResponseException hre) {
          if (hre.getResponse().getStatusCode() != getIncorrectContentMD5StatusCode()) {
diff --git a/core/src/main/java/org/jclouds/http/internal/PayloadEnclosingImpl.java b/core/src/main/java/org/jclouds/http/internal/PayloadEnclosingImpl.java
index a8b5196..bff39b6 100644
--- a/core/src/main/java/org/jclouds/http/internal/PayloadEnclosingImpl.java
+++ b/core/src/main/java/org/jclouds/http/internal/PayloadEnclosingImpl.java
@@ -98,6 +98,14 @@
    }
 
    @Override
+   public void resetPayload(boolean release) {
+      if (release && payload != null) {
+         payload.release();
+      }
+      payload = null;
+   }
+
+   @Override
    public int hashCode() {
       final int prime = 31;
       int result = 1;
diff --git a/core/src/main/java/org/jclouds/io/PayloadEnclosing.java b/core/src/main/java/org/jclouds/io/PayloadEnclosing.java
index 3ed2983..dd3f28b 100644
--- a/core/src/main/java/org/jclouds/io/PayloadEnclosing.java
+++ b/core/src/main/java/org/jclouds/io/PayloadEnclosing.java
@@ -48,4 +48,5 @@
    @Nullable
    Payload getPayload();
 
+   void resetPayload(boolean release);
 }
diff --git a/providers/b2/src/test/java/org/jclouds/b2/blobstore/integration/B2BlobIntegrationLiveTest.java b/providers/b2/src/test/java/org/jclouds/b2/blobstore/integration/B2BlobIntegrationLiveTest.java
index 0ecb6bd..3f45077 100644
--- a/providers/b2/src/test/java/org/jclouds/b2/blobstore/integration/B2BlobIntegrationLiveTest.java
+++ b/providers/b2/src/test/java/org/jclouds/b2/blobstore/integration/B2BlobIntegrationLiveTest.java
@@ -80,9 +80,19 @@
    }
 
    @Override
-   public void testPutIncorrectContentMD5() throws InterruptedException, IOException {
+   public void testPutIncorrectContentMD5ByteSource() throws InterruptedException, IOException {
       try {
-         super.testPutIncorrectContentMD5();
+         super.testPutIncorrectContentMD5ByteSource();
+         failBecauseExceptionWasNotThrown(AssertionError.class);
+      } catch (AssertionError ae) {
+         throw new SkipException("B2 does not enforce Content-MD5", ae);
+      }
+   }
+
+   @Override
+   public void testPutIncorrectContentMD5InputStream() throws InterruptedException, IOException {
+      try {
+         super.testPutIncorrectContentMD5InputStream();
          failBecauseExceptionWasNotThrown(AssertionError.class);
       } catch (AssertionError ae) {
          throw new SkipException("B2 does not enforce Content-MD5", ae);
diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/binders/MultipartUploadBinder.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/binders/MultipartUploadBinder.java
index ca6422b..848b8c6 100644
--- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/binders/MultipartUploadBinder.java
+++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/binders/MultipartUploadBinder.java
@@ -57,6 +57,7 @@
       Part jsonPart = Part.create("Metadata", jsonPayload, new Part.PartOptions().contentType(APPLICATION_JSON));
       Part mediaPart = Part.create(template.name(), payload, new Part.PartOptions().contentType(contentType));
 
+      request.resetPayload(/*release=*/ false);
       request.setPayload(new MultipartForm(BOUNDARY_HEADER, jsonPart, mediaPart));
       // HeaderPart
       request.toBuilder().replaceHeader(CONTENT_TYPE, "Multipart/related; boundary= " + BOUNDARY_HEADER).build();
diff --git a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java
index 029ca03..8777643 100644
--- a/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java
+++ b/providers/google-cloud-storage/src/main/java/org/jclouds/googlecloudstorage/blobstore/GoogleCloudStorageBlobStore.java
@@ -211,7 +211,7 @@
    public String putBlob(String container, Blob blob, PutOptions options) {
       long length = checkNotNull(blob.getPayload().getContentMetadata().getContentLength());
 
-      if (length != 0 && (options.isMultipart() || !blob.getPayload().isRepeatable())) {
+      if (length != 0 && options.isMultipart()) {
          // JCLOUDS-912 prevents using single-part uploads with InputStream payloads.
          // Work around this with multi-part upload which buffers parts in-memory.
          return putMultipartBlob(container, blob, options);