Repository: googleapis/java-storage
Pull request #1753 (2022-11-01 20:11:44)

fix: add missing preconditions and update samples
Adds BlobTargetOption.[meta]generationmatch(long) and updates the samples to use it

conventional-commit-lint-gcf[bot]: <!-- probot comment [1356997]-->
🤖 I detect that the PR title and the commit message differ and there's only one commit. To use the PR title for the commit history, you can use Github's automerge feature with squashing, or use `automerge` label. Good luck human!

 -- conventional-commit-lint bot
https://conventionalcommits.org/

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/pom.xml b/google-cloud-storage/pom.xml
index 54541ac3..386a348e 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -16,7 +16,7 @@
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
-    <pubsub-proto.version>1.104.1</pubsub-proto.version>
+    <pubsub-proto.version>1.104.2</pubsub-proto.version>
     <kms.version>0.101.0</kms.version>
     <junit-platform.version>5.9.1</junit-platform.version>
   </properties>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
index c12962d3..15d3c489 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Blob.java
@@ -1063,7 +1063,7 @@ public class Blob extends BlobInfo {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   public Acl getAcl(Entity entity) {
     return storage.getAcl(getBlobId(), entity);
   }
@@ -1085,7 +1085,7 @@ public class Blob extends BlobInfo {
    * @return {@code true} if the ACL was deleted, {@code false} if it was not found
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   public boolean deleteAcl(Entity entity) {
     return storage.deleteAcl(getBlobId(), entity);
   }
@@ -1101,7 +1101,7 @@ public class Blob extends BlobInfo {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   public Acl createAcl(Acl acl) {
     return storage.createAcl(getBlobId(), acl);
   }
@@ -1117,7 +1117,7 @@ public class Blob extends BlobInfo {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   public Acl updateAcl(Acl acl) {
     return storage.updateAcl(getBlobId(), acl);
   }
@@ -1136,7 +1136,7 @@ public class Blob extends BlobInfo {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   public List<Acl> listAcls() {
     return storage.listAcls(getBlobId());
   }
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 bcf4f584..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(),
@@ -904,7 +897,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
       }
       long metageneration = resp.getMetageneration();
 
-      UpdateBucketRequest req = createUpdateAclRequest(bucket, newAcls, metageneration);
+      UpdateBucketRequest req = createUpdateBucketAclRequest(bucket, newAcls, metageneration);
 
       com.google.storage.v2.Bucket updateResult = updateBucket(req);
       // read the response to ensure there is no longer an acl for the specified entity
@@ -954,7 +947,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
               .collect(ImmutableList.toImmutableList());
 
       UpdateBucketRequest req =
-          createUpdateAclRequest(bucket, newDefaultAcls, resp.getMetageneration());
+          createUpdateBucketAclRequest(bucket, newDefaultAcls, resp.getMetageneration());
 
       com.google.storage.v2.Bucket updateResult = updateBucket(req);
 
@@ -1109,27 +1102,117 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public Acl getAcl(BlobId blob, Entity entity) {
-    return throwNotYetImplemented(fmtMethodName("getAcl", BlobId.class, Entity.class));
+    try {
+      Object req = codecs.blobId().encode(blob);
+      Object resp = getObjectWithAcls(req);
+
+      Predicate<ObjectAccessControl> entityPredicate =
+          objectAclEntityOrAltEq(codecs.entity().encode(entity));
+
+      Optional<ObjectAccessControl> first =
+          resp.getAclList().stream().filter(entityPredicate).findFirst();
+
+      // HttpStorageRpc defaults to null if Not Found
+      return first.map(codecs.objectAcl()::decode).orElse(null);
+    } catch (NotFoundException e) {
+      return null;
+    } catch (StorageException se) {
+      if (se.getCode() == 404) {
+        return null;
+      } else {
+        throw se;
+      }
+    }
   }
 
   @Override
   public boolean deleteAcl(BlobId blob, Entity entity) {
-    return throwNotYetImplemented(fmtMethodName("deleteAcl", BlobId.class, Entity.class));
+    try {
+      Object obj = codecs.blobId().encode(blob);
+      Object resp = getObjectWithAcls(obj);
+      String encode = codecs.entity().encode(entity);
+
+      Predicate<ObjectAccessControl> entityPredicate = objectAclEntityOrAltEq(encode);
+
+      List<ObjectAccessControl> currentDefaultAcls = resp.getAclList();
+      ImmutableList<ObjectAccessControl> newDefaultAcls =
+          currentDefaultAcls.stream()
+              .filter(entityPredicate.negate())
+              .collect(ImmutableList.toImmutableList());
+      if (newDefaultAcls.equals(currentDefaultAcls)) {
+        // we didn't actually filter anything out, no need to send an RPC, simply return false
+        return false;
+      }
+      long metageneration = resp.getMetageneration();
+
+      UpdateObjectRequest req = createUpdateObjectAclRequest(obj, newDefaultAcls, metageneration);
+
+      Object updateResult = updateObject(req);
+      // read the response to ensure there is no longer an acl for the specified entity
+      Optional<ObjectAccessControl> first =
+          updateResult.getAclList().stream().filter(entityPredicate).findFirst();
+      return !first.isPresent();
+    } catch (NotFoundException e) {
+      // HttpStorageRpc returns false if the bucket doesn't exist :(
+      return false;
+    } catch (StorageException se) {
+      if (se.getCode() == 404) {
+        return false;
+      } else {
+        throw se;
+      }
+    }
   }
 
   @Override
   public Acl createAcl(BlobId blob, Acl acl) {
-    return throwNotYetImplemented(fmtMethodName("createAcl", BlobId.class, Acl.class));
+    return updateAcl(blob, acl);
   }
 
   @Override
   public Acl updateAcl(BlobId blob, Acl acl) {
-    return throwNotYetImplemented(fmtMethodName("updateAcl", BlobId.class, Acl.class));
+    try {
+      Object obj = codecs.blobId().encode(blob);
+      Object resp = getObjectWithAcls(obj);
+      ObjectAccessControl encode = codecs.objectAcl().encode(acl);
+      String entity = encode.getEntity();
+
+      Predicate<ObjectAccessControl> entityPredicate = objectAclEntityOrAltEq(entity);
+
+      ImmutableList<ObjectAccessControl> newDefaultAcls =
+          Streams.concat(
+                  resp.getAclList().stream().filter(entityPredicate.negate()), Stream.of(encode))
+              .collect(ImmutableList.toImmutableList());
+
+      UpdateObjectRequest req =
+          createUpdateObjectAclRequest(obj, newDefaultAcls, resp.getMetageneration());
+
+      Object updateResult = updateObject(req);
+
+      Optional<Acl> first =
+          updateResult.getAclList().stream()
+              .filter(entityPredicate)
+              .findFirst()
+              .map(codecs.objectAcl()::decode);
+
+      return first.orElseThrow(
+          () -> new StorageException(0, "Acl update call success, but not in response"));
+    } catch (NotFoundException e) {
+      throw StorageException.coalesce(e);
+    }
   }
 
   @Override
   public List<Acl> listAcls(BlobId blob) {
-    return throwNotYetImplemented(fmtMethodName("listAcls", BlobId.class));
+    try {
+      Object req = codecs.blobId().encode(blob);
+      Object resp = getObjectWithAcls(req);
+      return resp.getAclList().stream()
+          .map(codecs.objectAcl()::decode)
+          .collect(ImmutableList.toImmutableList());
+    } catch (NotFoundException e) {
+      throw StorageException.coalesce(e);
+    }
   }
 
   @Override
@@ -1637,7 +1720,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         .build();
   }
 
-  private static UpdateBucketRequest createUpdateAclRequest(
+  private static UpdateBucketRequest createUpdateBucketAclRequest(
       String bucket, ImmutableList<BucketAccessControl> newDefaultAcls, long metageneration) {
     com.google.storage.v2.Bucket update =
         com.google.storage.v2.Bucket.newBuilder()
@@ -1653,4 +1736,63 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         .setBucket(update)
         .build();
   }
+
+  private Object getObjectWithAcls(Object obj) {
+    Fields fields =
+        UnifiedOpts.fields(ImmutableSet.of(BucketField.ACL, BucketField.METAGENERATION));
+    GrpcCallContext grpcCallContext = GrpcCallContext.createDefault();
+    GetObjectRequest req =
+        fields
+            .getObject()
+            .apply(GetObjectRequest.newBuilder())
+            .setBucket(obj.getBucket())
+            .setObject(obj.getName())
+            .build();
+
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> storageClient.getObjectCallable().call(req, grpcCallContext),
+        Decoder.identity());
+  }
+
+  private static UpdateObjectRequest createUpdateObjectAclRequest(
+      Object obj, ImmutableList<ObjectAccessControl> newAcls, long metageneration) {
+    Object update =
+        Object.newBuilder()
+            .setBucket(obj.getBucket())
+            .setName(obj.getName())
+            .addAllAcl(newAcls)
+            .build();
+    Opts<BucketTargetOpt> opts =
+        Opts.from(
+            UnifiedOpts.fields(ImmutableSet.of(BlobField.ACL)),
+            UnifiedOpts.metagenerationMatch(metageneration));
+    return opts.updateObjectsRequest()
+        .apply(UpdateObjectRequest.newBuilder())
+        .setObject(update)
+        .build();
+  }
+
+  private Object updateObject(UpdateObjectRequest req) {
+    GrpcCallContext grpcCallContext = GrpcCallContext.createDefault();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> 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 451c454f..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());
     }
@@ -3599,7 +3635,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Acl getAcl(BlobId blob, Entity entity);
 
   /**
@@ -3623,7 +3659,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @return {@code true} if the ACL was deleted, {@code false} if it was not found
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   boolean deleteAcl(BlobId blob, Entity entity);
 
   /**
@@ -3651,7 +3687,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Acl createAcl(BlobId blob, Acl acl);
 
   /**
@@ -3669,7 +3705,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Acl updateAcl(BlobId blob, Acl acl);
 
   /**
@@ -3690,7 +3726,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    *
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   List<Acl> listAcls(BlobId blob);
 
   /**
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 dc5d10bf..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
@@ -713,7 +713,12 @@ final class UnifiedOpts {
   }
 
   static final class Fields extends RpcOptVal<ImmutableSet<NamedField>>
-      implements ObjectSourceOpt, ObjectListOpt, BucketSourceOpt, BucketTargetOpt, BucketListOpt {
+      implements ObjectSourceOpt,
+          ObjectListOpt,
+          ObjectTargetOpt,
+          BucketSourceOpt,
+          BucketTargetOpt,
+          BucketListOpt {
 
     /**
      * Apiary and gRPC have differing handling of where the field selector is evaluated relative to
@@ -772,6 +777,16 @@ final class UnifiedOpts {
       return b -> b.setReadMask(FieldMask.newBuilder().addAllPaths(getPaths()).build());
     }
 
+    @Override
+    public Mapper<UpdateObjectRequest.Builder> updateObject() {
+      return b -> b.setUpdateMask(FieldMask.newBuilder().addAllPaths(getPaths()).build());
+    }
+
+    @Override
+    public Mapper<RewriteObjectRequest.Builder> rewriteObject() {
+      return Mapper.identity();
+    }
+
     /**
      * Define a decoder which can clear out any fields which may have not been selected.
      *
@@ -1081,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/pom.xml b/pom.xml
index 855b78ed..8fd9c3a7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,7 +80,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
-        <version>1.122.1</version>
+        <version>1.122.2</version>
         <scope>test</scope>
       </dependency>
       <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index 3f93d189..bc075099 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.122.1</version>
+      <version>1.122.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index 02d708ac..0890fae9 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.122.1</version>
+      <version>1.122.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index e8cf7044..b558a451 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -52,7 +52,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.122.1</version>
+      <version>1.122.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 358b0eb9..19216412 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -72,7 +72,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.122.1</version>
+      <version>1.122.2</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
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 b9dd61c7..e1619887 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
@@ -1125,10 +1125,14 @@ public class ITAccessTest {
 
   static ImmutableList<Acl> dropEtags(List<Acl> defaultAcls) {
     return defaultAcls.stream()
-        .map(acl -> Acl.of(acl.getEntity(), acl.getRole()))
+        .map(ITAccessTest::dropEtag)
         .collect(ImmutableList.toImmutableList());
   }
 
+  static Acl dropEtag(Acl acl) {
+    return Acl.of(acl.getEntity(), acl.getRole());
+  }
+
   static Predicate<Acl> hasRole(Acl.Role expected) {
     return acl -> acl.getRole().equals(expected);
   }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectAclTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectAclTest.java
new file mode 100644
index 00000000..8d2256f5
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectAclTest.java
@@ -0,0 +1,336 @@
+/*
+ * 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;
+
+import static com.google.cloud.storage.TestUtils.retry429s;
+import static com.google.cloud.storage.it.ITAccessTest.dropEtag;
+import static com.google.cloud.storage.it.ITAccessTest.dropEtags;
+import static com.google.cloud.storage.it.ITAccessTest.hasProjectRole;
+import static com.google.cloud.storage.it.ITAccessTest.hasRole;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.cloud.storage.Acl;
+import com.google.cloud.storage.Acl.Entity;
+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.BucketInfo;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.Storage.BlobField;
+import com.google.cloud.storage.Storage.BlobGetOption;
+import com.google.cloud.storage.Storage.BlobTargetOption;
+import com.google.cloud.storage.StorageException;
+import com.google.cloud.storage.TransportCompatibility.Transport;
+import com.google.cloud.storage.it.runner.StorageITRunner;
+import com.google.cloud.storage.it.runner.annotations.Backend;
+import com.google.cloud.storage.it.runner.annotations.CrossRun;
+import com.google.cloud.storage.it.runner.annotations.Inject;
+import com.google.cloud.storage.it.runner.annotations.ParallelFriendly;
+import com.google.cloud.storage.it.runner.registry.ObjectsFixture;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.junit.runner.RunWith;
+
+@RunWith(StorageITRunner.class)
+@CrossRun(
+    transports = {Transport.HTTP, Transport.GRPC},
+    backends = {Backend.PROD})
+@ParallelFriendly
+public final class ITObjectAclTest {
+
+  @Rule public final TestName testName = new TestName();
+
+  @Inject public Storage storage;
+
+  @Inject public BucketInfo bucketInfo;
+
+  @Inject public ObjectsFixture objectsFixture;
+
+  private BlobId idWith404Bucket;
+
+  private static final Acl readAll = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
+
+  @Before
+  public void setUp() throws Exception {
+    String bucketName = bucketInfo.getName() + "x";
+    idWith404Bucket = BlobId.of(bucketName, "does-not-matter");
+  }
+
+  /** When a bucket doesn't exist, return null for the acl value */
+  @Test
+  public void object_acl_404_bucket_get() {
+    Acl acl = retry429s(() -> storage.getAcl(idWith404Bucket, User.ofAllUsers()), storage);
+
+    assertThat(acl).isNull();
+  }
+
+  @Test
+  public void object_acl_404_bucket_list() {
+    StorageException storageException =
+        assertThrows(
+            StorageException.class,
+            () -> retry429s(() -> storage.listAcls(idWith404Bucket), storage));
+
+    assertThat(storageException.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void object_acl_404_bucket_create() {
+    StorageException storageException =
+        assertThrows(
+            StorageException.class,
+            () -> retry429s(() -> storage.createAcl(idWith404Bucket, readAll), storage));
+
+    assertThat(storageException.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void object_acl_404_bucket_update() {
+    StorageException storageException =
+        assertThrows(
+            StorageException.class,
+            () -> retry429s(() -> storage.updateAcl(idWith404Bucket, readAll), storage));
+
+    assertThat(storageException.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void object_acl_404_bucket_delete() {
+    boolean actual =
+        retry429s(() -> storage.deleteAcl(idWith404Bucket, User.ofAllUsers()), storage);
+
+    assertThat(actual).isFalse();
+  }
+
+  /** When a blob doesn't exist, return null for the acl value */
+  @Test
+  public void object_acl_404_object_get() {
+    BlobId id = tmpId();
+    Acl acl = retry429s(() -> storage.getAcl(id, User.ofAllUsers()), storage);
+
+    assertThat(acl).isNull();
+  }
+
+  @Test
+  public void object_acl_404_object_list() {
+    BlobId id = tmpId();
+    StorageException storageException =
+        assertThrows(StorageException.class, () -> retry429s(() -> storage.listAcls(id), storage));
+
+    assertThat(storageException.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void object_acl_404_object_create() {
+    BlobId id = tmpId();
+    StorageException storageException =
+        assertThrows(
+            StorageException.class, () -> retry429s(() -> storage.createAcl(id, readAll), storage));
+
+    assertThat(storageException.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void object_acl_404_object_update() {
+    BlobId id = tmpId();
+    StorageException storageException =
+        assertThrows(
+            StorageException.class, () -> retry429s(() -> storage.updateAcl(id, readAll), storage));
+
+    assertThat(storageException.getCode()).isEqualTo(404);
+  }
+
+  @Test
+  public void object_acl_404_object_delete() {
+    BlobId id = tmpId();
+    // todo: json non-idempotent?
+    boolean actual = retry429s(() -> storage.deleteAcl(id, User.ofAllUsers()), storage);
+
+    assertThat(actual).isFalse();
+  }
+
+  /** When an object does exist, but an acl for the specified entity is not defined return null */
+  @Test
+  public void object_acl_404_acl_get() {
+    BlobId id = tmpId();
+    Acl acl = retry429s(() -> storage.getAcl(id, User.ofAllUsers()), storage);
+
+    assertThat(acl).isNull();
+  }
+
+  /** Update of an acl that doesn't exist should create it */
+  @Test
+  public void object_acl_404_acl_update() {
+    Blob mgen1 = tmpObject();
+
+    Acl actual =
+        // todo: json non-idempotent?
+        retry429s(() -> storage.updateAcl(mgen1.getBlobId(), readAll), storage);
+
+    assertThat(dropEtag(actual)).isEqualTo(readAll);
+  }
+
+  @Test
+  public void object_acl_404_acl_delete() {
+    Blob mgen1 = tmpObject();
+
+    boolean actual =
+        // todo: json non-idempotent?
+        retry429s(() -> storage.deleteAcl(mgen1.getBlobId(), User.ofAllUsers()), storage);
+
+    assertThat(actual).isFalse();
+  }
+
+  @Test
+  public void object_acl_200_get() {
+    // lookup an entity from the blob which is known to exist
+    BlobId id = objectsFixture.getInfo1().getBlobId();
+    Blob withAcls = storage.get(id, BlobGetOption.fields(BlobField.ACL));
+
+    Acl expected = withAcls.getAcl().iterator().next();
+
+    Acl acl = retry429s(() -> storage.getAcl(id, expected.getEntity()), storage);
+
+    assertThat(acl).isEqualTo(expected);
+  }
+
+  @Test
+  public void object_acl_200_list() {
+    BlobId id = objectsFixture.getInfo1().getBlobId();
+    // lookup an entity from the blob which is known to exist
+    Blob withAcls = storage.get(id, BlobGetOption.fields(BlobField.ACL));
+
+    Acl expected = withAcls.getAcl().iterator().next();
+
+    List<Acl> acls = retry429s(() -> storage.listAcls(id), storage);
+
+    assertThat(acls).contains(expected);
+  }
+
+  @Test
+  public void object_acl_200_create() {
+    Blob mgen1 = tmpObject();
+
+    Acl readAll = Acl.of(User.ofAllAuthenticatedUsers(), Role.READER);
+    Acl actual = retry429s(() -> storage.createAcl(mgen1.getBlobId(), readAll), storage);
+
+    assertThat(actual.getEntity()).isEqualTo(readAll.getEntity());
+    assertThat(actual.getRole()).isEqualTo(readAll.getRole());
+    assertThat(actual.getEtag()).isNotEmpty();
+
+    Blob updated = storage.get(mgen1.getBlobId(), BlobGetOption.fields(BlobField.values()));
+    assertThat(updated.getMetageneration()).isNotEqualTo(mgen1.getMetageneration());
+
+    // etags change when updates happen, drop before our comparison
+    List<Acl> expectedAcls = dropEtags(mgen1.getAcl());
+    List<Acl> actualAcls = dropEtags(updated.getAcl());
+    assertThat(actualAcls).containsAtLeastElementsIn(expectedAcls);
+    assertThat(actualAcls).contains(readAll);
+  }
+
+  @Test
+  public void object_acl_200_update() {
+    Blob mgen1 = tmpObject();
+
+    List<Acl> acls = mgen1.getAcl();
+    assertThat(acls).isNotEmpty();
+
+    Predicate<Acl> isProjectEditor = hasProjectRole(ProjectRole.EDITORS);
+
+    //noinspection OptionalGetWithoutIsPresent
+    Acl projectEditorAsOwner =
+        acls.stream().filter(hasRole(Role.OWNER).and(isProjectEditor)).findFirst().get();
+
+    // lower the privileges of project editors to writer from owner
+    Entity entity = projectEditorAsOwner.getEntity();
+    Acl projectEditorAsReader = Acl.of(entity, Role.READER);
+
+    Acl actual =
+        retry429s(() -> storage.updateAcl(mgen1.getBlobId(), projectEditorAsReader), storage);
+
+    assertThat(actual.getEntity()).isEqualTo(projectEditorAsReader.getEntity());
+    assertThat(actual.getRole()).isEqualTo(projectEditorAsReader.getRole());
+    assertThat(actual.getEtag()).isNotEmpty();
+
+    Blob updated = storage.get(mgen1.getBlobId(), BlobGetOption.fields(BlobField.values()));
+    assertThat(updated.getMetageneration()).isNotEqualTo(mgen1.getMetageneration());
+
+    // etags change when updates happen, drop before our comparison
+    List<Acl> expectedAcls =
+        dropEtags(
+            mgen1.getAcl().stream().filter(isProjectEditor.negate()).collect(Collectors.toList()));
+    List<Acl> actualAcls = dropEtags(updated.getAcl());
+    assertThat(actualAcls).containsAtLeastElementsIn(expectedAcls);
+    assertThat(actualAcls).doesNotContain(projectEditorAsOwner);
+    assertThat(actualAcls).contains(projectEditorAsReader);
+  }
+
+  @Test
+  public void object_acl_200_delete() {
+    Blob mgen1 = tmpObject();
+
+    List<Acl> acls = mgen1.getAcl();
+    assertThat(acls).isNotEmpty();
+
+    Predicate<Acl> isProjectEditor = hasProjectRole(ProjectRole.EDITORS);
+
+    //noinspection OptionalGetWithoutIsPresent
+    Acl projectEditorAsOwner =
+        acls.stream().filter(hasRole(Role.OWNER).and(isProjectEditor)).findFirst().get();
+
+    // lower the privileges of project editors to writer from owner
+    Entity entity = projectEditorAsOwner.getEntity();
+
+    boolean actual = retry429s(() -> storage.deleteAcl(mgen1.getBlobId(), entity), storage);
+
+    assertThat(actual).isTrue();
+
+    Blob updated = storage.get(mgen1.getBlobId(), BlobGetOption.fields(BlobField.values()));
+    assertThat(updated.getMetageneration()).isNotEqualTo(mgen1.getMetageneration());
+
+    // etags change when updates happen, drop before our comparison
+    List<Acl> expectedAcls =
+        dropEtags(
+            mgen1.getAcl().stream().filter(isProjectEditor.negate()).collect(Collectors.toList()));
+    List<Acl> actualAcls = dropEtags(updated.getAcl());
+    assertThat(actualAcls).containsAtLeastElementsIn(expectedAcls);
+    assertThat(actualAcls).doesNotContain(projectEditorAsOwner);
+    Optional<Entity> search =
+        actualAcls.stream().map(Acl::getEntity).filter(e -> e.equals(entity)).findAny();
+    assertThat(search.isPresent()).isFalse();
+  }
+
+  private BlobId tmpId() {
+    return BlobId.of(bucketInfo.getName(), testName.getMethodName());
+  }
+
+  private Blob tmpObject() {
+    BlobId id = tmpId();
+    BlobInfo info = BlobInfo.newBuilder(id).build();
+    return storage.create(info, BlobTargetOption.doesNotExist());
+  }
+}
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());
   }
 }