Repository: googleapis/java-storage
Pull request #1498 (2022-07-08 22:51:54)

feat: implement GrpcStorageImpl#compose(ComposeRequest): Blob
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-storage/issues/new/choose) before writing your code!  That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> ☕️

If you write sample code, please follow the [samples format](
https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).



Patch:
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 d4463770..bb20ec77 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
@@ -44,12 +44,17 @@ import com.google.cloud.storage.spi.v1.StorageRpc;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hashing;
 import com.google.common.io.ByteStreams;
 import com.google.protobuf.ByteString;
+import com.google.protobuf.FieldMask;
+import com.google.protobuf.Message;
 import com.google.storage.v2.BucketName;
 import com.google.storage.v2.CommonObjectRequestParams;
+import com.google.storage.v2.ComposeObjectRequest;
+import com.google.storage.v2.ComposeObjectRequest.SourceObject;
 import com.google.storage.v2.CreateBucketRequest;
 import com.google.storage.v2.DeleteBucketRequest;
 import com.google.storage.v2.DeleteHmacKeyRequest;
@@ -68,6 +73,8 @@ import com.google.storage.v2.StorageClient.ListHmacKeysPage;
 import com.google.storage.v2.StorageClient.ListHmacKeysPagedResponse;
 import com.google.storage.v2.StorageClient.ListObjectsPage;
 import com.google.storage.v2.StorageClient.ListObjectsPagedResponse;
+import com.google.storage.v2.UpdateBucketRequest;
+import com.google.storage.v2.UpdateObjectRequest;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
 import com.google.storage.v2.WriteObjectSpec;
@@ -133,20 +140,13 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    com.google.storage.v2.Bucket bucket =
-        com.google.storage.v2.Bucket.newBuilder()
-            .setName(BucketName.format(this.getOptions().getProjectId(), bucketInfo.getName()))
-            .setProject(this.getOptions().getProjectId())
-            .build();
-    CreateBucketRequest.Builder bucketRequestBuilder =
-        CreateBucketRequest.newBuilder().setBucket(bucket);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL),
-        bucketRequestBuilder::setPredefinedAcl);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_DEFAULT_OBJECT_ACL),
-        bucketRequestBuilder::setPredefinedDefaultObjectAcl);
-    CreateBucketRequest req = bucketRequestBuilder.build();
+    CreateBucketRequest.Builder builder =
+        CreateBucketRequest.newBuilder().setBucket(codecs.bucketInfo().encode(bucketInfo));
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
+        ZOpt.PREDEFINED_DEFAULT_OBJECT_ACL.consumeVia(builder::setPredefinedDefaultObjectAcl));
+    CreateBucketRequest req = builder.build();
     // TODO(frankyn): Do we care about projection because Apiary uses FULL for projection? Missing
     // projection=full
     return Retrying.run(
@@ -320,14 +320,12 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Bucket get(String bucket, BucketGetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    GetBucketRequest.Builder bucketRequestBuilder = GetBucketRequest.newBuilder().setName(bucket);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        bucketRequestBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    GetBucketRequest req = bucketRequestBuilder.build();
+    GetBucketRequest.Builder builder = GetBucketRequest.newBuilder().setName(bucket);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
+    GetBucketRequest req = builder.build();
     // TODO(frankyn): Do we care about projection because Apiary uses FULL for projection? Missing
     // projection=full
     return Retrying.run(
@@ -351,16 +349,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Blob get(BlobId blob, BlobGetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    GetObjectRequest.Builder getObjectRequestBuilder =
+    GetObjectRequest.Builder builder =
         GetObjectRequest.newBuilder().setBucket(blob.getBucket()).setObject(blob.getName());
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        getObjectRequestBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        getObjectRequestBuilder::setIfMetagenerationNotMatch);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
     // TODO(sydmunro) StorageRpc.Option.Fields
-    GetObjectRequest req = getObjectRequestBuilder.build();
+    GetObjectRequest req = builder.build();
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
@@ -379,15 +375,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         grpcStorageStub.listBucketsPagedCallable();
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    String projectId = (String) optionsMap.get(StorageRpc.Option.PROJECT_ID);
-    if (projectId == null) {
-      projectId = this.getOptions().getProjectId();
-    }
+    String projectId =
+        firstNonNull(ZOpt.PROJECT_ID.get(optionsMap), this.getOptions().getProjectId());
     ListBucketsRequest.Builder builder = ListBucketsRequest.newBuilder().setParent(projectId);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.MAX_RESULTS), Long::intValue, builder::setPageSize);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PAGE_TOKEN), builder::setPageToken);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PREFIX), builder::setPrefix);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),
+        ZOpt.PAGE_TOKEN.consumeVia(builder::setPageToken),
+        ZOpt.PREFIX.consumeVia(builder::setPrefix));
     // TODO(sydmunro): StorageRpc.Option.Fields
     ListBucketsPagedResponse call = listBucketsCallable.call(builder.build(), grpcCallContext);
     ListBucketsPage page = call.getPage();
@@ -401,14 +396,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
     ListObjectsRequest.Builder builder = ListObjectsRequest.newBuilder().setParent(bucket);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.MAX_RESULTS), Long::intValue, builder::setPageSize);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PAGE_TOKEN), builder::setPageToken);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PREFIX), builder::setPrefix);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.DELIMITER), builder::setDelimiter);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.START_OFF_SET), builder::setLexicographicStart);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.END_OFF_SET), builder::setLexicographicEnd);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),
+        ZOpt.PAGE_TOKEN.consumeVia(builder::setPageToken),
+        ZOpt.PREFIX.consumeVia(builder::setPrefix),
+        ZOpt.DELIMITER.consumeVia(builder::setDelimiter),
+        ZOpt.START_OFF_SET.consumeVia(builder::setLexicographicStart),
+        ZOpt.END_OFF_SET.consumeVia(builder::setLexicographicEnd));
     // TODO(sydmunro) StorageRpc.Option.Fields
     ListObjectsPagedResponse call = listObjectsCallable.call(builder.build(), grpcCallContext);
     ListObjectsPage page = call.getPage();
@@ -417,33 +412,64 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public Bucket update(BucketInfo bucketInfo, BucketTargetOption... options) {
-    return todo();
+    final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
+    GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
+    com.google.storage.v2.Bucket bucket = codecs.bucketInfo().encode(bucketInfo);
+    UpdateBucketRequest.Builder builder = UpdateBucketRequest.newBuilder().setBucket(bucket);
+    builder.setUpdateMask(fieldMaskGenerator(bucket));
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
+    UpdateBucketRequest req = builder.build();
+
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> grpcStorageStub.updateBucketCallable().call(req, grpcCallContext),
+        syntaxDecoders.bucket);
   }
 
   @Override
   public Blob update(BlobInfo blobInfo, BlobTargetOption... options) {
-    return todo();
+    final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
+    GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
+    Object object = codecs.blobInfo().encode(blobInfo);
+    UpdateObjectRequest.Builder builder = UpdateObjectRequest.newBuilder().setObject(object);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(builder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(builder::setIfGenerationNotMatch));
+    builder.setUpdateMask(fieldMaskGenerator(object));
+    UpdateObjectRequest req = builder.build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> grpcStorageStub.updateObjectCallable().call(req, grpcCallContext),
+        syntaxDecoders.blob);
   }
 
   @Override
   public Blob update(BlobInfo blobInfo) {
-    return todo();
+    return update(blobInfo, new BlobTargetOption[0]);
   }
 
   @Override
   public boolean delete(String bucket, BucketSourceOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    DeleteBucketRequest.Builder bucketRequestBuilder =
+    DeleteBucketRequest.Builder builder =
         DeleteBucketRequest.newBuilder()
             .setName(BucketName.format(this.getOptions().getProjectId(), bucket));
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    DeleteBucketRequest req = bucketRequestBuilder.build();
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
+    DeleteBucketRequest req = builder.build();
     try {
       Retrying.run(
           getOptions(),
@@ -466,22 +492,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public boolean delete(BlobId blob, BlobSourceOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    DeleteObjectRequest.Builder objectRequestBuilder =
+    DeleteObjectRequest.Builder builder =
         DeleteObjectRequest.newBuilder().setBucket(blob.getBucket()).setObject(blob.getName());
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        objectRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        objectRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        objectRequestBuilder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        objectRequestBuilder::setIfGenerationNotMatch);
-    ifNonNull(blob.getGeneration(), objectRequestBuilder::setGeneration);
-    DeleteObjectRequest req = objectRequestBuilder.build();
+    ifNonNull(blob.getGeneration(), builder::setGeneration);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(builder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(builder::setIfGenerationNotMatch));
+    DeleteObjectRequest req = builder.build();
     try {
       Retrying.run(
           getOptions(),
@@ -502,7 +522,42 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public Blob compose(ComposeRequest composeRequest) {
-    return todo();
+    final Map<StorageRpc.Option, ?> optionsMap =
+        StorageImpl.optionMap(Iterables.toArray(composeRequest.getTargetOptions(), Option.class));
+    GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
+    ComposeObjectRequest.Builder composeObjectReqBuilder = ComposeObjectRequest.newBuilder();
+    composeRequest.getSourceBlobs().stream()
+        .map(
+            src ->
+                SourceObject.newBuilder()
+                    .setName(src.getName())
+                    .setGeneration(src.getGeneration())
+                    .build())
+        .forEach(composeObjectReqBuilder::addSourceObjects);
+    final Object target = codecs.blobInfo().encode(composeRequest.getTarget());
+    composeObjectReqBuilder.setDestination(target);
+    ifNonNull(
+        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
+        composeObjectReqBuilder::setIfGenerationMatch);
+    ifNonNull(
+        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
+        composeObjectReqBuilder::setIfMetagenerationMatch);
+    ifNonNull(
+        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL),
+        composeObjectReqBuilder::setDestinationPredefinedAcl);
+    ifNonNull(
+        (String) optionsMap.get(StorageRpc.Option.KMS_KEY_NAME),
+        composeObjectReqBuilder::setKmsKey);
+    String encryptionKey = (String) optionsMap.get(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
+    if (encryptionKey != null) {
+      composeObjectReqBuilder.setCommonObjectRequestParams(commonRequestParams(encryptionKey));
+    }
+    ComposeObjectRequest req = composeObjectReqBuilder.build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> grpcStorageStub.composeObjectCallable().call(req, grpcCallContext),
+        syntaxDecoders.blob);
   }
 
   @Override
@@ -840,19 +895,15 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         grpcStorageStub.listHmacKeysPagedCallable();
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    String projectId = (String) optionsMap.get(StorageRpc.Option.PROJECT_ID);
-    if (projectId == null) {
-      projectId = this.getOptions().getProjectId();
-    }
+    String projectId =
+        firstNonNull(ZOpt.PROJECT_ID.get(optionsMap), this.getOptions().getProjectId());
     ListHmacKeysRequest.Builder builder = ListHmacKeysRequest.newBuilder().setProject(projectId);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.SERVICE_ACCOUNT_EMAIL),
-        builder::setServiceAccountEmail);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.MAX_RESULTS), Long::intValue, builder::setPageSize);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PAGE_TOKEN), builder::setPageToken);
-    ifNonNull(
-        (Boolean) optionsMap.get(StorageRpc.Option.SHOW_DELETED_KEYS), builder::setShowDeletedKeys);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.SERVICE_ACCOUNT_EMAIL.consumeVia(builder::setServiceAccountEmail),
+        ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),
+        ZOpt.PAGE_TOKEN.consumeVia(builder::setPageToken),
+        ZOpt.SHOW_DELETED_KEYS.consumeVia(builder::setShowDeletedKeys));
     ListHmacKeysPagedResponse call = listHmacKeysCallable.call(builder.build(), grpcCallContext);
     ListHmacKeysPage page = call.getPage();
     return new TransformingPageDecorator<>(page, codecs.hmacKeyMetadata());
@@ -1057,22 +1108,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     if (generation > 0) {
       builder.setGeneration(generation);
     }
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        builder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        builder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        builder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        builder::setIfGenerationNotMatch);
-    String key = (String) optionsMap.get(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
-    if (key != null) {
-      builder.setCommonObjectRequestParams(commonRequestParams(key));
-    }
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(builder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(builder::setIfGenerationNotMatch),
+        ZOpt.CUSTOMER_SUPPLIED_KEY.mapThenConsumeVia(
+            this::commonRequestParams, builder::setCommonObjectRequestParams));
     return builder.build();
   }
 
@@ -1082,7 +1125,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
       // GrpcCallContext is immutable, any modification we perform needs to be assigned back to
       // our variable otherwise it will effectively be lost.
       GrpcCallContext ctx = GrpcCallContext.createDefault();
-      String userProject = (String) optionsMap.get(StorageRpc.Option.USER_PROJECT);
+      String userProject = ZOpt.USER_PROJECT.get(optionsMap);
       if (userProject != null) {
         ctx =
             ctx.withExtraHeaders(
@@ -1113,33 +1156,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         WriteObjectRequest.newBuilder().setWriteObjectSpec(specBuilder);
 
     // TODO: Projection: Do we care?
-
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL), specBuilder::setPredefinedAcl);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        specBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        specBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        specBuilder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        specBuilder::setIfGenerationNotMatch);
-    String userProject = (String) optionsMap.get(StorageRpc.Option.USER_PROJECT);
-    if (userProject != null) {
-      // TODO: user project
-    }
-    String encryptionKey = (String) optionsMap.get(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
-    if (encryptionKey != null) {
-      requestBuilder.setCommonObjectRequestParams(commonRequestParams(encryptionKey));
-    }
-    String key = (String) optionsMap.get(StorageRpc.Option.KMS_KEY_NAME);
-    if (key != null) {
-      objectBuilder.setKmsKey(key);
-    }
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(specBuilder::setPredefinedAcl),
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(specBuilder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(specBuilder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(specBuilder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(specBuilder::setIfGenerationNotMatch),
+        ZOpt.CUSTOMER_SUPPLIED_KEY.mapThenConsumeVia(
+            this::commonRequestParams, requestBuilder::setCommonObjectRequestParams),
+        ZOpt.KMS_KEY_NAME.consumeVia(objectBuilder::setKmsKey));
 
     return requestBuilder;
   }
@@ -1154,4 +1180,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         .setEncryptionKeySha256Bytes(ByteString.copyFrom(keySha256.asBytes()))
         .build();
   }
+
+  private FieldMask fieldMaskGenerator(Message message) {
+    return FieldMask.newBuilder()
+        .addAllPaths(
+            message.getAllFields().entrySet().stream()
+                .filter(x -> x.getValue() != null)
+                .map(e -> e.getKey().getName())
+                .collect(Collectors.toList()))
+        .build();
+  }
 }
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 369b95b1..5bbf6bce 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
@@ -2331,7 +2331,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @see <a
    *     href="https://cloud.google.com/storage/docs/json_api/v1/objects/update">https://cloud.google.com/storage/docs/json_api/v1/objects/update</a>
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Blob update(BlobInfo blobInfo, BlobTargetOption... options);
 
   /**
@@ -2364,7 +2364,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @see <a
    *     href="https://cloud.google.com/storage/docs/json_api/v1/objects/update">https://cloud.google.com/storage/docs/json_api/v1/objects/update</a>
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Blob update(BlobInfo blobInfo);
 
   /**
@@ -2496,7 +2496,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @return the composed blob
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Blob compose(ComposeRequest composeRequest);
 
   /**
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ZOpt.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ZOpt.java
new file mode 100644
index 00000000..6c77c2dc
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ZOpt.java
@@ -0,0 +1,181 @@
+/*
+ * 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;
+
+import com.google.cloud.storage.spi.v1.StorageRpc;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A new helper abstraction which makes working with {@link StorageRpc.Option}s more type safe, and
+ * null safe. When working with a {@code Map<StorageRpc.Option, ?>} we loose the independent type
+ * information for each key. The type conversion methods on StorageRpc.Option are package-private,
+ * and we DO NOT want to make them public. Which leads to this new class, which essentially creates
+ * a mapping between a java type and a specific {@code StorageRpc.Option}.
+ *
+ * <p>We are defining each Option as a constant rather than an enum, because enum's can't have
+ * generic class parameters. This shouldn't pose an issue:
+ *
+ * <ol>
+ *   <li>This class is package-private
+ *   <li>This class is sealed, and can only be constructed from within it's definition
+ *   <li>This class is strictly used for mapping, never a property in a model that could be
+ *       serialized
+ * </ol>
+ */
+final class ZOpt<T> {
+
+  static final ZOpt<String> PREDEFINED_ACL = new ZOpt<>(StorageRpc.Option.PREDEFINED_ACL);
+  static final ZOpt<String> PREDEFINED_DEFAULT_OBJECT_ACL =
+      new ZOpt<>(StorageRpc.Option.PREDEFINED_DEFAULT_OBJECT_ACL);
+  static final ZOpt<Long> IF_METAGENERATION_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_METAGENERATION_MATCH);
+  static final ZOpt<Long> IF_METAGENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH);
+  static final ZOpt<Long> IF_GENERATION_MATCH = new ZOpt<>(StorageRpc.Option.IF_GENERATION_MATCH);
+  static final ZOpt<Long> IF_GENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_GENERATION_NOT_MATCH);
+  static final ZOpt<Long> IF_SOURCE_METAGENERATION_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_METAGENERATION_MATCH);
+  static final ZOpt<Long> IF_SOURCE_METAGENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_METAGENERATION_NOT_MATCH);
+  static final ZOpt<Long> IF_SOURCE_GENERATION_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_GENERATION_MATCH);
+  static final ZOpt<Long> IF_SOURCE_GENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_GENERATION_NOT_MATCH);
+  static final ZOpt<Boolean> IF_DISABLE_GZIP_CONTENT =
+      new ZOpt<>(StorageRpc.Option.IF_DISABLE_GZIP_CONTENT);
+  static final ZOpt<String> PREFIX = new ZOpt<>(StorageRpc.Option.PREFIX);
+  static final ZOpt<String> PROJECT_ID = new ZOpt<>(StorageRpc.Option.PROJECT_ID);
+  static final ZOpt<String> PROJECTION = new ZOpt<>(StorageRpc.Option.PROJECTION);
+  static final ZOpt<Long> MAX_RESULTS = new ZOpt<>(StorageRpc.Option.MAX_RESULTS);
+  static final ZOpt<String> PAGE_TOKEN = new ZOpt<>(StorageRpc.Option.PAGE_TOKEN);
+  static final ZOpt<String> DELIMITER = new ZOpt<>(StorageRpc.Option.DELIMITER);
+  static final ZOpt<String> START_OFF_SET = new ZOpt<>(StorageRpc.Option.START_OFF_SET);
+  static final ZOpt<String> END_OFF_SET = new ZOpt<>(StorageRpc.Option.END_OFF_SET);
+  static final ZOpt<Boolean> VERSIONS = new ZOpt<>(StorageRpc.Option.VERSIONS);
+  static final ZOpt<String> FIELDS = new ZOpt<>(StorageRpc.Option.FIELDS);
+  static final ZOpt<String> CUSTOMER_SUPPLIED_KEY =
+      new ZOpt<>(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
+  static final ZOpt<String> USER_PROJECT = new ZOpt<>(StorageRpc.Option.USER_PROJECT);
+  static final ZOpt<String> KMS_KEY_NAME = new ZOpt<>(StorageRpc.Option.KMS_KEY_NAME);
+  static final ZOpt<String> SERVICE_ACCOUNT_EMAIL =
+      new ZOpt<>(StorageRpc.Option.SERVICE_ACCOUNT_EMAIL);
+  static final ZOpt<Boolean> SHOW_DELETED_KEYS = new ZOpt<>(StorageRpc.Option.SHOW_DELETED_KEYS);
+  static final ZOpt<Long> REQUESTED_POLICY_VERSION =
+      new ZOpt<>(StorageRpc.Option.REQUESTED_POLICY_VERSION);
+  static final ZOpt<String> DETECT_CONTENT_TYPE = new ZOpt<>(StorageRpc.Option.DETECT_CONTENT_TYPE);
+  static final ZOpt<Boolean> RETURN_RAW_INPUT_STREAM =
+      new ZOpt<>(StorageRpc.Option.RETURN_RAW_INPUT_STREAM);
+
+  private final StorageRpc.Option key;
+
+  private ZOpt(StorageRpc.Option key) {
+    this.key = key;
+  }
+
+  StorageRpc.Option getKey() {
+    return key;
+  }
+
+  /**
+   * This method is only ever called from package private scope, as such the Map of option values
+   * has prior type checked values which are later erased. Here we suppress the unchecked warning
+   * since it's guarded ealier in the conversion.
+   */
+  @SuppressWarnings("unchecked")
+  T cast(Object o) {
+    return (T) o;
+  }
+
+  /**
+   * Attempt to get the typed value from {@code optionValues}. If the key is not present in the
+   * provided {@code optionValues} null will be returned.
+   */
+  @Nullable
+  T get(@NonNull Map<StorageRpc.Option, ?> optionValues) {
+    Object o = optionValues.get(key);
+    return o == null ? null : cast(o);
+  }
+
+  /**
+   * Create a new {@link ZOptConsumer} bound to the provided {@code consumer}
+   *
+   * @see ZOptConsumer#apply(Map)
+   */
+  ZOptConsumer<T, T> consumeVia(Consumer<@NonNull T> consumer) {
+    return new ZOptConsumer<>(this, Function.identity(), consumer);
+  }
+
+  /**
+   * Create a new {@link ZOptConsumer} which will apply {@code mapper} before consumption.
+   *
+   * @see ZOptConsumer#apply(Map)
+   */
+  <X> ZOptConsumer<T, X> mapThenConsumeVia(Function<T, X> mapper, Consumer<@NonNull X> consumer) {
+    return new ZOptConsumer<>(this, mapper, consumer);
+  }
+
+  /**
+   * For each consumer in {@code consumers} invoke {@link ZOptConsumer#apply(Map)} with {@code
+   * optionValues}
+   */
+  static void applyAll(Map<StorageRpc.Option, ?> optionValues, ZOptConsumer<?, ?>... consumers) {
+    for (ZOptConsumer<?, ?> consumer : consumers) {
+      consumer.apply(optionValues);
+    }
+  }
+
+  static class ZOptConsumer<T, X> {
+    private final ZOpt<T> opt;
+    private final Function<@NonNull T, @Nullable X> mapper;
+    private final Consumer<@Nullable X> consumer;
+
+    private ZOptConsumer(
+        ZOpt<T> opt, Function<@NonNull T, @Nullable X> mapper, Consumer<@NonNull X> consumer) {
+      this.opt = opt;
+      this.mapper = mapper;
+      this.consumer = consumer;
+    }
+
+    /**
+     * Apply this consumer to the provided {@code optionValues}.
+     *
+     * <p>A null value will never be passed to the {@code consumer}.
+     *
+     * <ul>
+     *   <li>If the provided {@code optionValues} does not contain a value related to {@code
+     *       this.opt.key}, the method will terminate.
+     *   <li>If the provided {@code optionValues} does contains a value, it will be sent through
+     *       {@code this.mapper} if the result is non-null it will be passed to {@code
+     *       this.consumer} otherwise the method will terminate.
+     * </ul>
+     */
+    void apply(Map<StorageRpc.Option, ?> optionValues) {
+      T t = opt.get(optionValues);
+      if (t != null) {
+        X x = mapper.apply(t);
+        if (x != null) {
+          consumer.accept(x);
+        }
+      }
+    }
+  }
+}


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ZOptTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ZOptTest.java
new file mode 100644
index 00000000..7508ad38
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ZOptTest.java
@@ -0,0 +1,137 @@
+/*
+ * 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;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.storage.spi.v1.StorageRpc;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+
+public final class ZOptTest {
+
+  @Test
+  public void happyPath() {
+    AtomicReference<String> ref1 = new AtomicReference<>();
+    AtomicLong ref2 = new AtomicLong(Long.MAX_VALUE);
+
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(StorageRpc.Option.PREDEFINED_ACL, "acl");
+    ZOpt.PREDEFINED_ACL.consumeVia(ref1::set).apply(map);
+    ZOpt.IF_GENERATION_MATCH.consumeVia(ref2::set).apply(map);
+
+    assertThat(ref1.get()).isEqualTo("acl");
+    assertThat(ref2.get()).isEqualTo(Long.MAX_VALUE);
+  }
+
+  @Test
+  public void applyAll() {
+    AtomicReference<String> ref1 = new AtomicReference<>();
+    AtomicLong ref2 = new AtomicLong(Long.MAX_VALUE);
+
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(StorageRpc.Option.PREDEFINED_ACL, "acl");
+    ZOpt.applyAll(
+        map,
+        ZOpt.PREDEFINED_ACL.consumeVia(ref1::set),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(ref2::set));
+
+    assertThat(ref1.get()).isEqualTo("acl");
+    assertThat(ref2.get()).isEqualTo(Long.MAX_VALUE);
+  }
+
+  @Test
+  public void aNullValueShouldNotBeProvidedToTheConsumer() {
+    ZOpt.PREDEFINED_ACL
+        .consumeVia((v) -> fail("null should not be provided"))
+        .apply(Collections.emptyMap());
+  }
+
+  @Test
+  public void ifMapperProducesNullItShouldNotBeProvidedToTheConsumer() {
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(StorageRpc.Option.PREDEFINED_ACL, "acl");
+    ZOpt.PREDEFINED_ACL
+        .mapThenConsumeVia((t) -> null, (v) -> fail("null should not be provided"))
+        .apply(map);
+  }
+
+  @Test
+  public void ensureEachTypeWorks() {
+    AtomicReference<String> ref1 = new AtomicReference<>();
+    AtomicLong ref2 = new AtomicLong(Long.MAX_VALUE);
+    AtomicBoolean ref3 = new AtomicBoolean(false);
+
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(
+            StorageRpc.Option.PREDEFINED_ACL,
+            "acl",
+            StorageRpc.Option.IF_GENERATION_MATCH,
+            0L,
+            StorageRpc.Option.IF_DISABLE_GZIP_CONTENT,
+            true);
+    ZOpt.applyAll(
+        map,
+        ZOpt.PREDEFINED_ACL.consumeVia(ref1::set),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(ref2::set),
+        ZOpt.IF_DISABLE_GZIP_CONTENT.consumeVia(ref3::set));
+
+    assertThat(ref1.get()).isEqualTo("acl");
+    assertThat(ref2.get()).isEqualTo(0L);
+    assertThat(ref3.get()).isTrue();
+  }
+
+  @Test
+  public void validateCoverageOfAllStorageRpcOptions() {
+    Comparator<StorageRpc.Option> comp = Comparator.comparingInt(Enum::ordinal);
+
+    ImmutableList<StorageRpc.Option> expected =
+        Arrays.stream(StorageRpc.Option.values())
+            .sorted(comp)
+            .collect(ImmutableList.toImmutableList());
+
+    ImmutableList<StorageRpc.Option> actual =
+        Arrays.stream(ZOpt.class.getDeclaredFields())
+            .filter(f -> Modifier.isStatic(f.getModifiers()))
+            .filter(f -> !Modifier.isPrivate(f.getModifiers()))
+            .filter(f -> f.getType().equals(ZOpt.class))
+            .map(ZOptTest::unsafeGet)
+            .map(ZOpt::getKey)
+            .sorted(comp)
+            .collect(ImmutableList.toImmutableList());
+
+    assertThat(actual).containsExactlyElementsIn(expected);
+  }
+
+  private static ZOpt<?> unsafeGet(Field f) {
+    try {
+      return (ZOpt<?>) f.get(null);
+    } catch (IllegalAccessException e) {
+      throw new RuntimeException("error attempting to access field: " + f, e);
+    }
+  }
+}