Repository: googleapis/java-storage
Pull request #1819 (2022-12-16 18:13:53)

chore(test): expand StorageInstance vetoing to apply to bucket acl operations
Simplify StorageInstance vetoing implementation. Rather than using a runtime proxy to do invocation method argument matching we are now defining a concrete class to override the methods it needs to provide enforcement for.

This is simpler in that it uses standard java language implementation approach which is helped by compiler and IDEs. It is at the expense of needing to define the new ~500 line AbstractStorageDecorator class.

Delete several obsolete tests, which have been replace by other tests or do not make sense to be written the way they are.

Cleanup of TODO from https://github.com/googleapis/java-storage/pull/1785


Patch:
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index e1667075..7182f2a9 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -2,4 +2,9 @@
 <!-- see https://www.mojohaus.org/clirr-maven-plugin/examples/ignored-differences.html -->
 <differences>
 
+  <difference>
+    <differenceType>8001</differenceType>
+    <className>com/google/cloud/storage/Hasher$ConstantConcatValueHasher</className>
+  </difference>
+
 </differences>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
index 988ab936..7b4c6a94 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GapicUploadSessionBuilder.java
@@ -25,6 +25,7 @@ import com.google.storage.v2.StartResumableWriteRequest;
 import com.google.storage.v2.StartResumableWriteResponse;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
+import java.util.function.Function;
 
 final class GapicUploadSessionBuilder {
 
@@ -49,8 +50,16 @@ final class GapicUploadSessionBuilder {
     if (writeObjectRequest.hasCommonObjectRequestParams()) {
       b.setCommonObjectRequestParams(writeObjectRequest.getCommonObjectRequestParams());
     }
+    if (writeObjectRequest.hasObjectChecksums()) {
+      b.setObjectChecksums(writeObjectRequest.getObjectChecksums());
+    }
     StartResumableWriteRequest req = b.build();
+    Function<String, WriteObjectRequest> f =
+        uploadId ->
+            writeObjectRequest.toBuilder().clearWriteObjectSpec().setUploadId(uploadId).build();
     return ApiFutures.transform(
-        x.futureCall(req), (resp) -> new ResumableWrite(req, resp), MoreExecutors.directExecutor());
+        x.futureCall(req),
+        (resp) -> new ResumableWrite(req, resp, f),
+        MoreExecutors.directExecutor());
   }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index bad2cccb..10f60230 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -95,6 +95,7 @@ import com.google.storage.v2.ListObjectsRequest;
 import com.google.storage.v2.LockBucketRetentionPolicyRequest;
 import com.google.storage.v2.Object;
 import com.google.storage.v2.ObjectAccessControl;
+import com.google.storage.v2.ObjectChecksums;
 import com.google.storage.v2.ProjectName;
 import com.google.storage.v2.ReadObjectRequest;
 import com.google.storage.v2.RewriteObjectRequest;
@@ -221,6 +222,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
     WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
+    Hasher hasher = getHasherForRequest(req, Hasher.enabled());
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
@@ -231,7 +233,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
                   .byteChannel(
                       storageClient.writeObjectCallable().withDefaultCallContext(grpcCallContext))
                   .setByteStringStrategy(ByteStringStrategy.noCopy())
-                  .setHasher(Hasher.enabled())
+                  .setHasher(hasher)
                   .direct()
                   .unbuffered()
                   .setRequest(req)
@@ -273,10 +275,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
     WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
 
-    Hasher hasher = Hasher.enabled();
-    if (req.hasObjectChecksums() && req.getObjectChecksums().hasCrc32C()) {
-      hasher = Hasher.constant(req.getObjectChecksums().getCrc32C());
-    }
+    Hasher hasher = getHasherForRequest(req, Hasher.enabled());
     GapicWritableByteChannelSessionBuilder channelSessionBuilder =
         ResumableMedia.gapic()
             .write()
@@ -346,10 +345,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
     ApiFuture<ResumableWrite> start = startResumableWrite(grpcCallContext, req);
 
-    Hasher hasher = Hasher.enabled();
-    if (req.hasObjectChecksums() && req.getObjectChecksums().hasCrc32C()) {
-      hasher = Hasher.constant(req.getObjectChecksums().getCrc32C());
-    }
+    Hasher hasher = getHasherForRequest(req, Hasher.enabled());
     BufferedWritableByteChannelSession<WriteObjectResponse> session =
         ResumableMedia.gapic()
             .write()
@@ -736,10 +732,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     GrpcCallContext grpcCallContext =
         opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
     WriteObjectRequest req = getWriteObjectRequest(blobInfo, opts);
-    Hasher hasher = Hasher.noop();
-    if (req.hasObjectChecksums() && req.getObjectChecksums().hasCrc32C()) {
-      hasher = Hasher.constant(req.getObjectChecksums().getCrc32C());
-    }
+    Hasher hasher = getHasherForRequest(req, Hasher.enabled());
     return new GrpcBlobWriteChannel(
         storageClient.writeObjectCallable(),
         getOptions(),
@@ -1789,4 +1782,17 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         () -> storageClient.updateObjectCallable().call(req, grpcCallContext),
         Decoder.identity());
   }
+
+  private static Hasher getHasherForRequest(WriteObjectRequest req, Hasher defaultHasher) {
+    if (!req.hasObjectChecksums()) {
+      return defaultHasher;
+    } else {
+      ObjectChecksums checksums = req.getObjectChecksums();
+      if (!checksums.hasCrc32C() && checksums.getMd5Hash().isEmpty()) {
+        return defaultHasher;
+      } else {
+        return Hasher.noop();
+      }
+    }
+  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
index 5c29ce26..06fca041 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Hasher.java
@@ -47,17 +47,6 @@ interface Hasher {
     return GuavaHasher.INSTANCE;
   }
 
-  /**
-   * Create a Hasher which will always yield the specified value when {@link
-   * #nullSafeConcat(Crc32cLengthKnown, Crc32cLengthKnown)} is invoked.
-   */
-  // Not perfect, and not a great approach for a public API. However, this is the most pragmatic way
-  // right now to wire an externally defined value all the way down to the last write message of a
-  // resumable upload session.
-  static Hasher constant(int crc32c) {
-    return new ConstantConcatValueHasher(Crc32cValue.of(crc32c, -1));
-  }
-
   @Immutable
   class NoOpHasher implements Hasher {
     private static final NoOpHasher INSTANCE = new NoOpHasher();
@@ -112,26 +101,4 @@ interface Hasher {
       }
     }
   }
-
-  @Immutable
-  class ConstantConcatValueHasher implements Hasher {
-    private final Crc32cLengthKnown value;
-
-    private ConstantConcatValueHasher(Crc32cLengthKnown value) {
-      this.value = value;
-    }
-
-    @Override
-    public @Nullable Crc32cLengthKnown hash(ByteBuffer b) {
-      return null;
-    }
-
-    @Override
-    public void validate(Crc32cValue<?> expected, Supplier<ByteBuffer> b) {}
-
-    @Override
-    public @Nullable Crc32cLengthKnown nullSafeConcat(Crc32cLengthKnown r1, Crc32cLengthKnown r2) {
-      return value;
-    }
-  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java
index 47996e7b..75921032 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ResumableWrite.java
@@ -33,14 +33,13 @@ final class ResumableWrite implements WriteObjectRequestBuilderFactory {
 
   private final WriteObjectRequest writeRequest;
 
-  public ResumableWrite(StartResumableWriteRequest req, StartResumableWriteResponse res) {
+  public ResumableWrite(
+      StartResumableWriteRequest req,
+      StartResumableWriteResponse res,
+      Function<String, WriteObjectRequest> f) {
     this.req = req;
     this.res = res;
-    WriteObjectRequest.Builder b = WriteObjectRequest.newBuilder().setUploadId(res.getUploadId());
-    if (req.hasCommonObjectRequestParams()) {
-      b.setCommonObjectRequestParams(req.getCommonObjectRequestParams());
-    }
-    this.writeRequest = b.build();
+    this.writeRequest = f.apply(res.getUploadId());
   }
 
   public StartResumableWriteRequest getReq() {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 67f11210..5b3c06da 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
@@ -626,6 +626,24 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobTargetOption(UnifiedOpts.generationMatchExtractor());
     }
 
+    /**
+     * Returns an option for blob's data generation match. If this option is used the request will
+     * fail if blob's generation does not match the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobTargetOption generationMatch(long generation) {
+      return new BlobTargetOption(UnifiedOpts.generationMatch(generation));
+    }
+
+    /**
+     * Returns an option for blob's data generation mismatch. If this option is used the request
+     * will fail if generation matches the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobTargetOption generationNotMatch(long generation) {
+      return new BlobTargetOption(UnifiedOpts.generationNotMatch(generation));
+    }
+
     /**
      * Returns an option for blob's data generation mismatch. If this option is used the request
      * will fail if generation matches.
@@ -644,6 +662,24 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobTargetOption(UnifiedOpts.metagenerationMatchExtractor());
     }
 
+    /**
+     * Returns an option for blob's metageneration match. If this option is used the request will
+     * fail if blob's metageneration does not match the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobTargetOption metagenerationMatch(long metageneration) {
+      return new BlobTargetOption(UnifiedOpts.metagenerationMatch(metageneration));
+    }
+
+    /**
+     * Returns an option for blob's metageneration mismatch. If this option is used the request will
+     * fail if metageneration matches the provided value.
+     */
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
+    public static BlobTargetOption metagenerationNotMatch(long metageneration) {
+      return new BlobTargetOption(UnifiedOpts.metagenerationNotMatch(metageneration));
+    }
+
     /**
      * Returns an option for blob's metageneration mismatch. If this option is used the request will
      * fail if metageneration matches.
@@ -774,7 +810,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
      * @deprecated Please compute and use a crc32c checksum instead. {@link #crc32cMatch()}
      */
     @Deprecated
-    @TransportCompatibility(Transport.HTTP)
+    @TransportCompatibility({Transport.HTTP, Transport.GRPC})
     public static BlobWriteOption md5Match() {
       return new BlobWriteOption(UnifiedOpts.md5MatchExtractor());
     }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
index 3df773d0..6eecc74f 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/UnifiedOpts.java
@@ -1096,6 +1096,15 @@ final class UnifiedOpts {
       return Objects.equals(val, md5Match.val);
     }
 
+    @Override
+    public Mapper<WriteObjectRequest.Builder> writeObject() {
+      return b -> {
+        b.getObjectChecksumsBuilder()
+            .setMd5Hash(ByteString.copyFrom(BaseEncoding.base64().decode(val)));
+        return b;
+      };
+    }
+
     @Override
     public int hashCode() {
       return Objects.hash(val);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
index 0007ec68..7deb0043 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/WriteFlushStrategy.java
@@ -89,6 +89,28 @@ final class WriteFlushStrategy {
     return ret;
   }
 
+  /**
+   * Several fields of a WriteObjectRequest are only allowed on the "first" message sent to gcs,
+   * this utility method centralizes the logic necessary to clear those fields for use by subsequent
+   * messages.
+   */
+  private static WriteObjectRequest possiblyPairDownRequest(
+      WriteObjectRequest message, boolean firstMessageOfStream) {
+    if (firstMessageOfStream && message.getWriteOffset() == 0) {
+      return message;
+    }
+
+    WriteObjectRequest.Builder b = message.toBuilder();
+    if (!firstMessageOfStream) {
+      b.clearUploadId();
+    }
+
+    if (message.getWriteOffset() > 0) {
+      b.clearWriteObjectSpec().clearObjectChecksums();
+    }
+    return b.build();
+  }
+
   @FunctionalInterface
   interface FlusherFactory {
     /**
@@ -144,9 +166,7 @@ final class WriteFlushStrategy {
 
             boolean first = true;
             for (WriteObjectRequest message : segments) {
-              if (!first) {
-                message = message.toBuilder().clearUploadId().clearWriteObjectSpec().build();
-              }
+              message = possiblyPairDownRequest(message, first);
 
               write.onNext(message);
               first = false;
@@ -188,9 +208,7 @@ final class WriteFlushStrategy {
     public void flush(@NonNull List<WriteObjectRequest> segments) {
       ensureOpen();
       for (WriteObjectRequest message : segments) {
-        if (!first) {
-          message = message.toBuilder().clearUploadId().clearWriteObjectSpec().build();
-        }
+        message = possiblyPairDownRequest(message, first);
 
         stream.onNext(message);
         first = false;
@@ -201,9 +219,7 @@ final class WriteFlushStrategy {
     public void close(@Nullable WriteObjectRequest message) {
       ensureOpen();
       if (message != null) {
-        if (!first) {
-          message = message.toBuilder().clearUploadId().clearWriteObjectSpec().build();
-        }
+        message = possiblyPairDownRequest(message, first);
         stream.onNext(message);
       }
       stream.onCompleted();
diff --git a/samples/snippets/src/main/java/com/example/storage/object/ComposeObject.java b/samples/snippets/src/main/java/com/example/storage/object/ComposeObject.java
index 6c77ad73..a1707ce2 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/ComposeObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/ComposeObject.java
@@ -49,11 +49,19 @@ public class ComposeObject {
     // Optional: set a generation-match precondition to avoid potential race
     // conditions and data corruptions. The request returns a 412 error if the
     // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
+    Storage.BlobTargetOption precondition;
+    if (storage.get(bucketName, targetObjectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(bucketName, targetObjectName).getGeneration());
+    }
 
     Storage.ComposeRequest composeRequest =
         Storage.ComposeRequest.newBuilder()
diff --git a/samples/snippets/src/main/java/com/example/storage/object/CopyObject.java b/samples/snippets/src/main/java/com/example/storage/object/CopyObject.java
index 1d228d35..e540608f 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/CopyObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/CopyObject.java
@@ -46,11 +46,20 @@ public class CopyObject {
     // Optional: set a generation-match precondition to avoid potential race
     // conditions and data corruptions. The request returns a 412 error if the
     // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
+    Storage.BlobTargetOption precondition;
+    if (storage.get(targetBucketName, objectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(targetBucketName, objectName).getGeneration());
+    }
 
     storage.copy(
         Storage.CopyRequest.newBuilder().setSource(source).setTarget(target, precondition).build());
diff --git a/samples/snippets/src/main/java/com/example/storage/object/CopyOldVersionOfObject.java b/samples/snippets/src/main/java/com/example/storage/object/CopyOldVersionOfObject.java
index f718fed3..0c227ad1 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/CopyOldVersionOfObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/CopyOldVersionOfObject.java
@@ -48,11 +48,19 @@ public class CopyOldVersionOfObject {
     // Optional: set a generation-match precondition to avoid potential race
     // conditions and data corruptions. The request returns a 412 error if the
     // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
+    Storage.BlobTargetOption precondition;
+    if (storage.get(bucketName, newObjectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(bucketName, newObjectName).getGeneration());
+    }
 
     Storage.CopyRequest copyRequest =
         Storage.CopyRequest.newBuilder()
diff --git a/samples/snippets/src/main/java/com/example/storage/object/MoveObject.java b/samples/snippets/src/main/java/com/example/storage/object/MoveObject.java
index 53234c6a..ad59fe17 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/MoveObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/MoveObject.java
@@ -52,11 +52,19 @@ public class MoveObject {
     // Optional: set a generation-match precondition to avoid potential race
     // conditions and data corruptions. The request returns a 412 error if the
     // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
+    Storage.BlobTargetOption precondition;
+    if (storage.get(targetBucketName, targetObjectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(targetBucketName, targetObjectName).getGeneration());
+    }
 
     // Copy source object to target object
     storage.copy(
diff --git a/samples/snippets/src/main/java/com/example/storage/object/UploadEncryptedObject.java b/samples/snippets/src/main/java/com/example/storage/object/UploadEncryptedObject.java
index 2c5ff7ec..e51067c2 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/UploadEncryptedObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/UploadEncryptedObject.java
@@ -52,11 +52,19 @@ public class UploadEncryptedObject {
     // Optional: set a generation-match precondition to avoid potential race
     // conditions and data corruptions. The request returns a 412 error if the
     // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
+    Storage.BlobTargetOption precondition;
+    if (storage.get(bucketName, objectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(bucketName, objectName).getGeneration());
+    }
 
     storage.create(
         blobInfo,
diff --git a/samples/snippets/src/main/java/com/example/storage/object/UploadKmsEncryptedObject.java b/samples/snippets/src/main/java/com/example/storage/object/UploadKmsEncryptedObject.java
index 53ba41fb..3876971c 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/UploadKmsEncryptedObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/UploadKmsEncryptedObject.java
@@ -49,11 +49,19 @@ public class UploadKmsEncryptedObject {
     // Optional: set a generation-match precondition to avoid potential race
     // conditions and data corruptions. The request returns a 412 error if the
     // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
+    Storage.BlobTargetOption precondition;
+    if (storage.get(bucketName, objectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(bucketName, objectName).getGeneration());
+    }
 
     storage.create(blobInfo, data, Storage.BlobTargetOption.kmsKeyName(kmsKeyName), precondition);
 
diff --git a/samples/snippets/src/main/java/com/example/storage/object/UploadObject.java b/samples/snippets/src/main/java/com/example/storage/object/UploadObject.java
index 8316c804..6472edeb 100644
--- a/samples/snippets/src/main/java/com/example/storage/object/UploadObject.java
+++ b/samples/snippets/src/main/java/com/example/storage/object/UploadObject.java
@@ -41,18 +41,26 @@ public class UploadObject {
     // The path to your file to upload
     // String filePath = "path/to/your/file"
 
-    // Optional: set a generation-match precondition to avoid potential race
-    // conditions and data corruptions. The request returns a 412 error if the
-    // preconditions are not met.
-    // For a target object that does not yet exist, set the DoesNotExist precondition.
-    Storage.BlobTargetOption precondition = Storage.BlobTargetOption.doesNotExist();
-    // If the destination already exists in your bucket, instead set a generation-match
-    // precondition:
-    // Storage.BlobTargetOption precondition = Storage.BlobTargetOption.generationMatch();
-
     Storage storage = StorageOptions.newBuilder().setProjectId(projectId).build().getService();
     BlobId blobId = BlobId.of(bucketName, objectName);
     BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
+
+    // Optional: set a generation-match precondition to avoid potential race
+    // conditions and data corruptions. The request returns a 412 error if the
+    // preconditions are not met.
+    Storage.BlobTargetOption precondition;
+    if (storage.get(bucketName, objectName) == null) {
+      // For a target object that does not yet exist, set the DoesNotExist precondition.
+      // This will cause the request to fail if the object is created before the request runs.
+      precondition = Storage.BlobTargetOption.doesNotExist();
+    } else {
+      // If the destination already exists in your bucket, instead set a generation-match
+      // precondition. This will cause the request to fail if the existing object's generation
+      // changes before the request runs.
+      precondition =
+          Storage.BlobTargetOption.generationMatch(
+              storage.get(bucketName, objectName).getGeneration());
+    }
     storage.create(blobInfo, Files.readAllBytes(Paths.get(filePath)), precondition);
 
     System.out.println(


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java
index 80331e23..306b867d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/GapicUnbufferedWritableByteChannelTest.java
@@ -105,7 +105,7 @@ public final class GapicUnbufferedWritableByteChannelTest {
       WriteObjectResponse.newBuilder().setResource(obj.toBuilder().setSize(40)).build();
 
   private static final WriteObjectRequestBuilderFactory reqFactory =
-      new ResumableWrite(startReq, startResp);
+      new ResumableWrite(startReq, startResp, TestUtils.onlyUploadId());
 
   @Test
   public void directUpload() throws IOException, InterruptedException, ExecutionException {
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
index 1810060c..7e5ac8c7 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/TestUtils.java
@@ -37,6 +37,7 @@ import com.google.protobuf.Any;
 import com.google.protobuf.ByteString;
 import com.google.rpc.DebugInfo;
 import com.google.storage.v2.ChecksummedData;
+import com.google.storage.v2.WriteObjectRequest;
 import io.grpc.Status.Code;
 import io.grpc.StatusRuntimeException;
 import java.io.ByteArrayOutputStream;
@@ -49,6 +50,7 @@ import java.util.function.Function;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
 import java.util.zip.GZIPOutputStream;
+import org.checkerframework.checker.nullness.qual.NonNull;
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 public final class TestUtils {
@@ -179,4 +181,13 @@ public final class TestUtils {
       }
     }
   }
+
+  /**
+   * Return a function which when provided an {@code uploadId} will create a {@link
+   * WriteObjectRequest} with that {@code uploadId}
+   */
+  @NonNull
+  public static Function<String, WriteObjectRequest> onlyUploadId() {
+    return uId -> WriteObjectRequest.newBuilder().setUploadId(uId).build();
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
index e1619887..99b29818 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITAccessTest.java
@@ -35,7 +35,6 @@ import com.google.cloud.storage.Acl.Project.ProjectRole;
 import com.google.cloud.storage.Acl.Role;
 import com.google.cloud.storage.Acl.User;
 import com.google.cloud.storage.Blob;
-import com.google.cloud.storage.BlobId;
 import com.google.cloud.storage.BlobInfo;
 import com.google.cloud.storage.Bucket;
 import com.google.cloud.storage.BucketInfo;
@@ -45,7 +44,6 @@ import com.google.cloud.storage.Storage;
 import com.google.cloud.storage.Storage.BlobTargetOption;
 import com.google.cloud.storage.Storage.BucketField;
 import com.google.cloud.storage.Storage.BucketGetOption;
-import com.google.cloud.storage.Storage.BucketSourceOption;
 import com.google.cloud.storage.Storage.BucketTargetOption;
 import com.google.cloud.storage.StorageException;
 import com.google.cloud.storage.StorageRoles;
@@ -63,7 +61,6 @@ import com.google.common.collect.ImmutableSet;
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -97,37 +94,6 @@ public class ITAccessTest {
 
   @Inject public Generator generator;
 
-  @Test
-  @CrossRun.Ignore(transports = Transport.GRPC)
-  public void bucketAcl_requesterPays_true() {
-    String projectId = storage.getOptions().getProjectId();
-    testBucketAclRequesterPays(requesterPaysBucket, BucketSourceOption.userProject(projectId));
-  }
-
-  @Test
-  @CrossRun.Ignore(transports = Transport.GRPC)
-  public void bucketAcl_requesterPays_false() {
-    testBucketAclRequesterPays(bucket);
-  }
-
-  private void testBucketAclRequesterPays(
-      BucketInfo bucket, Storage.BucketSourceOption... bucketOptions) {
-    // TODO: break into individual tests
-    assertNull(storage.getAcl(bucket.getName(), User.ofAllAuthenticatedUsers(), bucketOptions));
-    assertFalse(storage.deleteAcl(bucket.getName(), User.ofAllAuthenticatedUsers(), bucketOptions));
-    Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
-    assertNotNull(storage.createAcl(bucket.getName(), acl, bucketOptions));
-    Acl updatedAcl =
-        storage.updateAcl(
-            bucket.getName(), acl.toBuilder().setRole(Role.WRITER).build(), bucketOptions);
-    assertEquals(Role.WRITER, updatedAcl.getRole());
-    Set<Acl> acls = new HashSet<>();
-    acls.addAll(storage.listAcls(bucket.getName(), bucketOptions));
-    assertTrue(acls.contains(updatedAcl));
-    assertTrue(storage.deleteAcl(bucket.getName(), User.ofAllAuthenticatedUsers(), bucketOptions));
-    assertNull(storage.getAcl(bucket.getName(), User.ofAllAuthenticatedUsers(), bucketOptions));
-  }
-
   @Test
   public void bucket_defaultAcl_get() {
     String bucketName = bucket.getName();
@@ -1061,68 +1027,6 @@ public class ITAccessTest {
     }
   }
 
-  @Test
-  @CrossRun.Ignore(transports = Transport.GRPC)
-  public void testBlobAcl() {
-    // TODO: break this test up into each of the respective scenarios
-    //   1. get ACL for specific entity
-    //   2. Create an ACL for specific entity
-    //   3. Update ACL to change role of a specific entity
-    //   4. List ACLs for an object
-    //   5. Delete an ACL for a specific entity
-    //   6. Attempt to get an acl for an object that doesn't exist
-    //   7. Attempt to delete an acl for an object that doesn't exist
-    //   8. Attempt to create an acl for an object that doesn't exist
-    //   9. Attempt to update an acl for an object that doesn't exist
-    //   10. Attempt to list acls for an object that doesn't exist
-    BlobId blobId = BlobId.of(bucket.getName(), "test-blob-acl");
-    BlobInfo blob = BlobInfo.newBuilder(blobId).build();
-    storage.create(blob);
-    assertNull(storage.getAcl(blobId, User.ofAllAuthenticatedUsers()));
-    Acl acl = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
-    assertNotNull(storage.createAcl(blobId, acl));
-    Acl updatedAcl = storage.updateAcl(blobId, acl.toBuilder().setRole(Role.OWNER).build());
-    assertEquals(Role.OWNER, updatedAcl.getRole());
-    Set<Acl> acls = new HashSet<>(storage.listAcls(blobId));
-    assertTrue(acls.contains(updatedAcl));
-    assertTrue(storage.deleteAcl(blobId, User.ofAllAuthenticatedUsers()));
-    assertNull(storage.getAcl(blobId, User.ofAllAuthenticatedUsers()));
-    // test non-existing blob
-    BlobId otherBlobId = BlobId.of(bucket.getName(), "test-blob-acl", -1L);
-    try {
-      assertNull(storage.getAcl(otherBlobId, User.ofAllAuthenticatedUsers()));
-      fail("Expected an 'Invalid argument' exception");
-    } catch (StorageException e) {
-      assertThat(e.getMessage()).contains("Invalid argument");
-    }
-
-    try {
-      assertFalse(storage.deleteAcl(otherBlobId, User.ofAllAuthenticatedUsers()));
-      fail("Expected an 'Invalid argument' exception");
-    } catch (StorageException e) {
-      assertThat(e.getMessage()).contains("Invalid argument");
-    }
-
-    try {
-      storage.createAcl(otherBlobId, acl);
-      fail("Expected StorageException");
-    } catch (StorageException ex) {
-      // expected
-    }
-    try {
-      storage.updateAcl(otherBlobId, acl);
-      fail("Expected StorageException");
-    } catch (StorageException ex) {
-      // expected
-    }
-    try {
-      storage.listAcls(otherBlobId);
-      fail("Expected StorageException");
-    } catch (StorageException ex) {
-      // expected
-    }
-  }
-
   static ImmutableList<Acl> dropEtags(List<Acl> defaultAcls) {
     return defaultAcls.stream()
         .map(ITAccessTest::dropEtag)
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectChecksumSupportTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectChecksumSupportTest.java
index 129deebd..5538626d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectChecksumSupportTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectChecksumSupportTest.java
@@ -18,7 +18,6 @@ package com.google.cloud.storage.it;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
-import static org.junit.Assert.fail;
 
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Blob;
@@ -61,6 +60,8 @@ public final class ITObjectChecksumSupportTest {
   @Inject public Storage storage;
   @Inject public BucketInfo bucket;
 
+  @Inject public Transport transport;
+
   @Parameter public ChecksummedTestContent content;
 
   public static final class ChecksummedTestContentProvider implements ParametersProvider {
@@ -158,18 +159,76 @@ public final class ITObjectChecksumSupportTest {
   }
 
   @Test
-  // Error Handling for GRPC not complete b/247621346
-  @CrossRun.Exclude(transports = Transport.GRPC)
-  public void testCreateBlobMd5Fail() {
+  public void testMd5Validated_createFrom_expectFailure() {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucket.getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setMd5(content.getMd5Base64()).build();
+
+    byte[] bytes = content.concat('x');
+    StorageException expected =
+        assertThrows(
+            StorageException.class,
+            () ->
+                storage.createFrom(
+                    blobInfo,
+                    new ByteArrayInputStream(bytes),
+                    BlobWriteOption.doesNotExist(),
+                    BlobWriteOption.md5Match()));
+    assertThat(expected.getCode()).isEqualTo(400);
+  }
+
+  @Test
+  public void testMd5Validated_createFrom_expectSuccess() throws IOException {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucket.getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setMd5(content.getMd5Base64()).build();
+
+    byte[] bytes = content.getBytes();
+    Blob blob =
+        storage.createFrom(
+            blobInfo,
+            new ByteArrayInputStream(bytes),
+            BlobWriteOption.doesNotExist(),
+            BlobWriteOption.md5Match());
+    assertThat(blob.getMd5()).isEqualTo(content.getMd5Base64());
+  }
+
+  @Test
+  public void testMd5Validated_writer_expectFailure() {
+    String blobName = testName.getMethodName();
+    BlobId blobId = BlobId.of(bucket.getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setMd5(content.getMd5Base64()).build();
+
+    byte[] bytes = content.concat('x');
+    StorageException expected =
+        assertThrows(
+            StorageException.class,
+            () -> {
+              try (ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(bytes));
+                  WriteChannel dst =
+                      storage.writer(
+                          blobInfo, BlobWriteOption.doesNotExist(), BlobWriteOption.md5Match())) {
+                ByteStreams.copy(src, dst);
+              }
+            });
+    assertThat(expected.getCode()).isEqualTo(400);
+  }
+
+  @Test
+  public void testMd5Validated_writer_expectSuccess() throws IOException {
     String blobName = testName.getMethodName();
-    BlobInfo blob =
-        BlobInfo.newBuilder(bucket, blobName).setMd5("O1R4G1HJSDUISJjoIYmVhQ==").build();
-    ByteArrayInputStream stream = content.bytesAsInputStream();
-    try {
-      storage.create(blob, stream, Storage.BlobWriteOption.md5Match());
-      fail("StorageException was expected");
-    } catch (StorageException ex) {
-      // expected
+    BlobId blobId = BlobId.of(bucket.getName(), blobName);
+    BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setMd5(content.getMd5Base64()).build();
+
+    byte[] bytes = content.getBytes();
+
+    try (ReadableByteChannel src = Channels.newChannel(new ByteArrayInputStream(bytes));
+        WriteChannel dst =
+            storage.writer(blobInfo, BlobWriteOption.doesNotExist(), BlobWriteOption.md5Match())) {
+      ByteStreams.copy(src, dst);
     }
+
+    Blob blob = storage.get(blobId);
+    assertThat(blob.getMd5()).isEqualTo(content.getMd5Base64());
   }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
new file mode 100644
index 00000000..11440cb1
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java
@@ -0,0 +1,490 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed 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 com.google.cloud.storage.it.runner.registry;
+
+import com.google.api.gax.paging.Page;
+import com.google.cloud.Policy;
+import com.google.cloud.ReadChannel;
+import com.google.cloud.WriteChannel;
+import com.google.cloud.storage.Acl;
+import com.google.cloud.storage.Acl.Entity;
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.BlobId;
+import com.google.cloud.storage.BlobInfo;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.BucketInfo;
+import com.google.cloud.storage.CopyWriter;
+import com.google.cloud.storage.HmacKey;
+import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
+import com.google.cloud.storage.HmacKey.HmacKeyState;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
+import com.google.cloud.storage.PostPolicyV4;
+import com.google.cloud.storage.PostPolicyV4.PostConditionsV4;
+import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
+import com.google.cloud.storage.ServiceAccount;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageBatch;
+import com.google.cloud.storage.StorageOptions;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Define a simplistic class which implements {@link Storage} while also delegating all calls to an
+ * underlying instance of {@link Storage}. When this class is extended it can then override
+ * individual methods rather than the entire Storage interface.
+ */
+abstract class AbstractStorageProxy implements Storage {
+
+  protected final Storage delegate;
+
+  protected AbstractStorageProxy(Storage delegate) {
+    this.delegate = delegate;
+  }
+
+  @Override
+  public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) {
+    return delegate.create(bucketInfo, options);
+  }
+
+  @Override
+  public Blob create(BlobInfo blobInfo, BlobTargetOption... options) {
+    return delegate.create(blobInfo, options);
+  }
+
+  @Override
+  public Blob create(BlobInfo blobInfo, byte[] content, BlobTargetOption... options) {
+    return delegate.create(blobInfo, content, options);
+  }
+
+  @Override
+  public Blob create(
+      BlobInfo blobInfo, byte[] content, int offset, int length, BlobTargetOption... options) {
+    return delegate.create(blobInfo, content, offset, length, options);
+  }
+
+  @Override
+  @Deprecated
+  public Blob create(BlobInfo blobInfo, InputStream content, BlobWriteOption... options) {
+    return delegate.create(blobInfo, content, options);
+  }
+
+  @Override
+  public Blob createFrom(BlobInfo blobInfo, Path path, BlobWriteOption... options)
+      throws IOException {
+    return delegate.createFrom(blobInfo, path, options);
+  }
+
+  @Override
+  public Blob createFrom(BlobInfo blobInfo, Path path, int bufferSize, BlobWriteOption... options)
+      throws IOException {
+    return delegate.createFrom(blobInfo, path, bufferSize, options);
+  }
+
+  @Override
+  public Blob createFrom(BlobInfo blobInfo, InputStream content, BlobWriteOption... options)
+      throws IOException {
+    return delegate.createFrom(blobInfo, content, options);
+  }
+
+  @Override
+  public Blob createFrom(
+      BlobInfo blobInfo, InputStream content, int bufferSize, BlobWriteOption... options)
+      throws IOException {
+    return delegate.createFrom(blobInfo, content, bufferSize, options);
+  }
+
+  @Override
+  public Bucket get(String bucket, BucketGetOption... options) {
+    return delegate.get(bucket, options);
+  }
+
+  @Override
+  public Bucket lockRetentionPolicy(BucketInfo bucket, BucketTargetOption... options) {
+    return delegate.lockRetentionPolicy(bucket, options);
+  }
+
+  @Override
+  public Blob get(String bucket, String blob, BlobGetOption... options) {
+    return delegate.get(bucket, blob, options);
+  }
+
+  @Override
+  public Blob get(BlobId blob, BlobGetOption... options) {
+    return delegate.get(blob, options);
+  }
+
+  @Override
+  public Blob get(BlobId blob) {
+    return delegate.get(blob);
+  }
+
+  @Override
+  public Page<Bucket> list(BucketListOption... options) {
+    return delegate.list(options);
+  }
+
+  @Override
+  public Page<Blob> list(String bucket, BlobListOption... options) {
+    return delegate.list(bucket, options);
+  }
+
+  @Override
+  public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) {
+    return delegate.update(bucketInfo, options);
+  }
+
+  @Override
+  public Blob update(BlobInfo blobInfo, BlobTargetOption... options) {
+    return delegate.update(blobInfo, options);
+  }
+
+  @Override
+  public Blob update(BlobInfo blobInfo) {
+    return delegate.update(blobInfo);
+  }
+
+  @Override
+  public boolean delete(String bucket, BucketSourceOption... options) {
+    return delegate.delete(bucket, options);
+  }
+
+  @Override
+  public boolean delete(String bucket, String blob, BlobSourceOption... options) {
+    return delegate.delete(bucket, blob, options);
+  }
+
+  @Override
+  public boolean delete(BlobId blob, BlobSourceOption... options) {
+    return delegate.delete(blob, options);
+  }
+
+  @Override
+  public boolean delete(BlobId blob) {
+    return delegate.delete(blob);
+  }
+
+  @Override
+  public Blob compose(ComposeRequest composeRequest) {
+    return delegate.compose(composeRequest);
+  }
+
+  @Override
+  public CopyWriter copy(CopyRequest copyRequest) {
+    return delegate.copy(copyRequest);
+  }
+
+  @Override
+  public byte[] readAllBytes(String bucket, String blob, BlobSourceOption... options) {
+    return delegate.readAllBytes(bucket, blob, options);
+  }
+
+  @Override
+  public byte[] readAllBytes(BlobId blob, BlobSourceOption... options) {
+    return delegate.readAllBytes(blob, options);
+  }
+
+  @Override
+  public StorageBatch batch() {
+    return delegate.batch();
+  }
+
+  @Override
+  public ReadChannel reader(String bucket, String blob, BlobSourceOption... options) {
+    return delegate.reader(bucket, blob, options);
+  }
+
+  @Override
+  public ReadChannel reader(BlobId blob, BlobSourceOption... options) {
+    return delegate.reader(blob, options);
+  }
+
+  @Override
+  public void downloadTo(BlobId blob, Path path, BlobSourceOption... options) {
+    delegate.downloadTo(blob, path, options);
+  }
+
+  @Override
+  public void downloadTo(BlobId blob, OutputStream outputStream, BlobSourceOption... options) {
+    delegate.downloadTo(blob, outputStream, options);
+  }
+
+  @Override
+  public WriteChannel writer(BlobInfo blobInfo, BlobWriteOption... options) {
+    return delegate.writer(blobInfo, options);
+  }
+
+  @Override
+  public WriteChannel writer(URL signedURL) {
+    return delegate.writer(signedURL);
+  }
+
+  @Override
+  public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options) {
+    return delegate.signUrl(blobInfo, duration, unit, options);
+  }
+
+  @Override
+  public PostPolicyV4 generateSignedPostPolicyV4(
+      BlobInfo blobInfo,
+      long duration,
+      TimeUnit unit,
+      PostFieldsV4 fields,
+      PostConditionsV4 conditions,
+      PostPolicyV4Option... options) {
+    return delegate.generateSignedPostPolicyV4(
+        blobInfo, duration, unit, fields, conditions, options);
+  }
+
+  @Override
+  public PostPolicyV4 generateSignedPostPolicyV4(
+      BlobInfo blobInfo,
+      long duration,
+      TimeUnit unit,
+      PostFieldsV4 fields,
+      PostPolicyV4Option... options) {
+    return delegate.generateSignedPostPolicyV4(blobInfo, duration, unit, fields, options);
+  }
+
+  @Override
+  public PostPolicyV4 generateSignedPostPolicyV4(
+      BlobInfo blobInfo,
+      long duration,
+      TimeUnit unit,
+      PostConditionsV4 conditions,
+      PostPolicyV4Option... options) {
+    return delegate.generateSignedPostPolicyV4(blobInfo, duration, unit, conditions, options);
+  }
+
+  @Override
+  public PostPolicyV4 generateSignedPostPolicyV4(
+      BlobInfo blobInfo, long duration, TimeUnit unit, PostPolicyV4Option... options) {
+    return delegate.generateSignedPostPolicyV4(blobInfo, duration, unit, options);
+  }
+
+  @Override
+  public List<Blob> get(BlobId... blobIds) {
+    return delegate.get(blobIds);
+  }
+
+  @Override
+  public List<Blob> get(Iterable<BlobId> blobIds) {
+    return delegate.get(blobIds);
+  }
+
+  @Override
+  public List<Blob> update(BlobInfo... blobInfos) {
+    return delegate.update(blobInfos);
+  }
+
+  @Override
+  public List<Blob> update(Iterable<BlobInfo> blobInfos) {
+    return delegate.update(blobInfos);
+  }
+
+  @Override
+  public List<Boolean> delete(BlobId... blobIds) {
+    return delegate.delete(blobIds);
+  }
+
+  @Override
+  public List<Boolean> delete(Iterable<BlobId> blobIds) {
+    return delegate.delete(blobIds);
+  }
+
+  @Override
+  public Acl getAcl(String bucket, Entity entity, BucketSourceOption... options) {
+    return delegate.getAcl(bucket, entity, options);
+  }
+
+  @Override
+  public Acl getAcl(String bucket, Entity entity) {
+    return delegate.getAcl(bucket, entity);
+  }
+
+  @Override
+  public boolean deleteAcl(String bucket, Entity entity, BucketSourceOption... options) {
+    return delegate.deleteAcl(bucket, entity, options);
+  }
+
+  @Override
+  public boolean deleteAcl(String bucket, Entity entity) {
+    return delegate.deleteAcl(bucket, entity);
+  }
+
+  @Override
+  public Acl createAcl(String bucket, Acl acl, BucketSourceOption... options) {
+    return delegate.createAcl(bucket, acl, options);
+  }
+
+  @Override
+  public Acl createAcl(String bucket, Acl acl) {
+    return delegate.createAcl(bucket, acl);
+  }
+
+  @Override
+  public Acl updateAcl(String bucket, Acl acl, BucketSourceOption... options) {
+    return delegate.updateAcl(bucket, acl, options);
+  }
+
+  @Override
+  public Acl updateAcl(String bucket, Acl acl) {
+    return delegate.updateAcl(bucket, acl);
+  }
+
+  @Override
+  public List<Acl> listAcls(String bucket, BucketSourceOption... options) {
+    return delegate.listAcls(bucket, options);
+  }
+
+  @Override
+  public List<Acl> listAcls(String bucket) {
+    return delegate.listAcls(bucket);
+  }
+
+  @Override
+  public Acl getDefaultAcl(String bucket, Entity entity) {
+    return delegate.getDefaultAcl(bucket, entity);
+  }
+
+  @Override
+  public boolean deleteDefaultAcl(String bucket, Entity entity) {
+    return delegate.deleteDefaultAcl(bucket, entity);
+  }
+
+  @Override
+  public Acl createDefaultAcl(String bucket, Acl acl) {
+    return delegate.createDefaultAcl(bucket, acl);
+  }
+
+  @Override
+  public Acl updateDefaultAcl(String bucket, Acl acl) {
+    return delegate.updateDefaultAcl(bucket, acl);
+  }
+
+  @Override
+  public List<Acl> listDefaultAcls(String bucket) {
+    return delegate.listDefaultAcls(bucket);
+  }
+
+  @Override
+  public Acl getAcl(BlobId blob, Entity entity) {
+    return delegate.getAcl(blob, entity);
+  }
+
+  @Override
+  public boolean deleteAcl(BlobId blob, Entity entity) {
+    return delegate.deleteAcl(blob, entity);
+  }
+
+  @Override
+  public Acl createAcl(BlobId blob, Acl acl) {
+    return delegate.createAcl(blob, acl);
+  }
+
+  @Override
+  public Acl updateAcl(BlobId blob, Acl acl) {
+    return delegate.updateAcl(blob, acl);
+  }
+
+  @Override
+  public List<Acl> listAcls(BlobId blob) {
+    return delegate.listAcls(blob);
+  }
+
+  @Override
+  public HmacKey createHmacKey(ServiceAccount serviceAccount, CreateHmacKeyOption... options) {
+    return delegate.createHmacKey(serviceAccount, options);
+  }
+
+  @Override
+  public Page<HmacKeyMetadata> listHmacKeys(ListHmacKeysOption... options) {
+    return delegate.listHmacKeys(options);
+  }
+
+  @Override
+  public HmacKeyMetadata getHmacKey(String accessId, GetHmacKeyOption... options) {
+    return delegate.getHmacKey(accessId, options);
+  }
+
+  @Override
+  public void deleteHmacKey(HmacKeyMetadata hmacKeyMetadata, DeleteHmacKeyOption... options) {
+    delegate.deleteHmacKey(hmacKeyMetadata, options);
+  }
+
+  @Override
+  public HmacKeyMetadata updateHmacKeyState(
+      HmacKeyMetadata hmacKeyMetadata, HmacKeyState state, UpdateHmacKeyOption... options) {
+    return delegate.updateHmacKeyState(hmacKeyMetadata, state, options);
+  }
+
+  @Override
+  public Policy getIamPolicy(String bucket, BucketSourceOption... options) {
+    return delegate.getIamPolicy(bucket, options);
+  }
+
+  @Override
+  public Policy setIamPolicy(String bucket, Policy policy, BucketSourceOption... options) {
+    return delegate.setIamPolicy(bucket, policy, options);
+  }
+
+  @Override
+  public List<Boolean> testIamPermissions(
+      String bucket, List<String> permissions, BucketSourceOption... options) {
+    return delegate.testIamPermissions(bucket, permissions, options);
+  }
+
+  @Override
+  public ServiceAccount getServiceAccount(String projectId) {
+    return delegate.getServiceAccount(projectId);
+  }
+
+  @Override
+  public Notification createNotification(String bucket, NotificationInfo notificationInfo) {
+    return delegate.createNotification(bucket, notificationInfo);
+  }
+
+  @Override
+  public Notification getNotification(String bucket, String notificationId) {
+    return delegate.getNotification(bucket, notificationId);
+  }
+
+  @Override
+  public List<Notification> listNotifications(String bucket) {
+    return delegate.listNotifications(bucket);
+  }
+
+  @Override
+  public boolean deleteNotification(String bucket, String notificationId) {
+    return delegate.deleteNotification(bucket, notificationId);
+  }
+
+  @Override
+  public void close() throws Exception {
+    delegate.close();
+  }
+
+  @Override
+  public StorageOptions getOptions() {
+    return delegate.getOptions();
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/StorageInstance.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/StorageInstance.java
index 59bab510..b653dea0 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/StorageInstance.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/StorageInstance.java
@@ -16,14 +16,13 @@
 
 package com.google.cloud.storage.it.runner.registry;
 
+import com.google.cloud.Policy;
+import com.google.cloud.storage.Acl;
+import com.google.cloud.storage.Acl.Entity;
+import com.google.cloud.storage.Bucket;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.Storage;
-import com.google.cloud.storage.Storage.BucketSourceOption;
-import com.google.cloud.storage.Storage.BucketTargetOption;
 import com.google.cloud.storage.StorageOptions;
-import com.google.common.reflect.Reflection;
-import java.lang.reflect.InvocationTargetException;
-import java.util.Arrays;
 
 final class StorageInstance implements ManagedLifecycle {
 
@@ -48,45 +47,10 @@ final class StorageInstance implements ManagedLifecycle {
     return proxy;
   }
 
-  private static final Object[] updateParameters = {BucketInfo.class, BucketTargetOption[].class};
-  private static final Object[] deleteParameters = {String.class, BucketSourceOption[].class};
-
   @Override
   public void start() {
     storage = options.getService();
-    // Define a proxy which can veto calls which attempt to mutate protected buckets
-    //   this helps guard against a test trying to mutate the global bucket rather than creating its
-    //   own bucket.
-    proxy =
-        Reflection.newProxy(
-            Storage.class,
-            ((proxy1, method, args) -> {
-              String methodName = method.getName();
-              Class<?>[] parameterTypes = method.getParameterTypes();
-              if (methodName.equals("update")
-                  && Arrays.deepEquals(parameterTypes, updateParameters)) {
-                BucketInfo bucketInfo = (BucketInfo) args[0];
-                if (protectedBucketNames.isProtected(bucketInfo.getName())) {
-                  throw err(bucketInfo.getName());
-                }
-              } else if (methodName.equals("delete")
-                  && Arrays.deepEquals(parameterTypes, deleteParameters)) {
-                String bucketName = (String) args[0];
-                if (protectedBucketNames.isProtected(bucketName)) {
-                  throw err(bucketName);
-                }
-              } else if (methodName.equals("setIamPolicy")) {
-                String bucketName = (String) args[0];
-                if (protectedBucketNames.isProtected(bucketName)) {
-                  throw err(bucketName);
-                }
-              }
-              try {
-                return method.invoke(storage, args);
-              } catch (InvocationTargetException e) {
-                throw e.getCause();
-              }
-            }));
+    proxy = new VetoingStorageProxy();
   }
 
   @Override
@@ -103,9 +67,119 @@ final class StorageInstance implements ManagedLifecycle {
     return new VetoedBucketUpdateException("Attempted to modify global bucket: " + bucketName);
   }
 
-  private static final class VetoedBucketUpdateException extends RuntimeException {
+  private static class VetoException extends RuntimeException {
+    private VetoException(String message) {
+      super(message);
+    }
+  }
+
+  private static final class VetoedBucketUpdateException extends VetoException {
     private VetoedBucketUpdateException(String message) {
       super(message);
     }
   }
+
+  /**
+   * Define a proxy which can veto calls attempting to mutate protected buckets. this helps guard
+   * against a test trying to mutate the global bucket rather than creating its own bucket.
+   */
+  private final class VetoingStorageProxy extends AbstractStorageProxy {
+
+    private VetoingStorageProxy() {
+      super(storage);
+    }
+
+    @Override
+    public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) {
+      checkBucketProtected(bucketInfo);
+      return super.update(bucketInfo, options);
+    }
+
+    @Override
+    public boolean delete(String bucket, BucketSourceOption... options) {
+      checkBucketProtected(bucket);
+      return super.delete(bucket, options);
+    }
+
+    @Override
+    public Policy setIamPolicy(String bucket, Policy policy, BucketSourceOption... options) {
+      checkBucketProtected(bucket);
+      return super.setIamPolicy(bucket, policy, options);
+    }
+
+    @Override
+    public boolean deleteDefaultAcl(String bucket, Entity entity) {
+      checkBucketProtected(bucket);
+      return super.deleteDefaultAcl(bucket, entity);
+    }
+
+    @Override
+    public Acl createDefaultAcl(String bucket, Acl acl) {
+      checkBucketProtected(bucket);
+      return super.createDefaultAcl(bucket, acl);
+    }
+
+    @Override
+    public Acl updateDefaultAcl(String bucket, Acl acl) {
+      checkBucketProtected(bucket);
+      return super.updateDefaultAcl(bucket, acl);
+    }
+
+    @Override
+    public boolean deleteAcl(String bucket, Entity entity, BucketSourceOption... options) {
+      checkBucketProtected(bucket);
+      return super.deleteAcl(bucket, entity, options);
+    }
+
+    @Override
+    public boolean deleteAcl(String bucket, Entity entity) {
+      checkBucketProtected(bucket);
+      return super.deleteAcl(bucket, entity);
+    }
+
+    @Override
+    public Acl createAcl(String bucket, Acl acl, BucketSourceOption... options) {
+      checkBucketProtected(bucket);
+      return super.createAcl(bucket, acl, options);
+    }
+
+    @Override
+    public Acl createAcl(String bucket, Acl acl) {
+      checkBucketProtected(bucket);
+      return super.createAcl(bucket, acl);
+    }
+
+    @Override
+    public Acl updateAcl(String bucket, Acl acl, BucketSourceOption... options) {
+      checkBucketProtected(bucket);
+      return super.updateAcl(bucket, acl, options);
+    }
+
+    @Override
+    public Acl updateAcl(String bucket, Acl acl) {
+      checkBucketProtected(bucket);
+      return super.updateAcl(bucket, acl);
+    }
+
+    @Override
+    public Bucket lockRetentionPolicy(BucketInfo bucket, BucketTargetOption... options) {
+      checkBucketProtected(bucket);
+      return super.lockRetentionPolicy(bucket, options);
+    }
+
+    @Override
+    public void close() throws Exception {
+      throw new VetoException("Called #close() on global Storage instance");
+    }
+
+    private void checkBucketProtected(BucketInfo bucket) {
+      checkBucketProtected(bucket.getName());
+    }
+
+    private void checkBucketProtected(String bucketName) {
+      if (protectedBucketNames.isProtected(bucketName)) {
+        throw err(bucketName);
+      }
+    }
+  }
 }