Repository: googleapis/java-storage
Pull request #1969 (2023-04-05 00:28:37)

chore(deps): update dependency com.google.cloud:libraries-bom to v26.12.0
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [com.google.cloud:libraries-bom](https://cloud.google.com/java/docs/bom) ([source](https://togithub.com/googleapis/java-cloud-bom)) | `26.11.0` -> `26.12.0` | [![age](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.12.0/age-slim)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.12.0/adoption-slim)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.12.0/compatibility-slim/26.11.0)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://badges.renovateapi.com/packages/maven/com.google.cloud:libraries-bom/26.12.0/confidence-slim/26.11.0)](https://docs.renovatebot.com/merge-confidence/) |

---

### Release Notes

<details>
<summary>googleapis/java-cloud-bom</summary>

### [`v26.12.0`](https://togithub.com/googleapis/java-cloud-bom/blob/HEAD/CHANGELOG.md#&#8203;26120-httpsgithubcomgoogleapisjava-cloud-bomcomparev26110v26120-2023-04-04)

[Compare Source](https://togithub.com/googleapis/java-cloud-bom/compare/v26.11.0...v26.12.0)

##### Dependencies

-   update dependency com.google.cloud:first-party-dependencies to v3.6.0 ([#&#8203;5893](https://togithub.com/googleapis/java-cloud-bom/issues/5893)) ([1b6e4d0](https://togithub.com/googleapis/java-cloud-bom/commit/1b6e4d08838827942d1dd3cccc6e6b715e7aef1e))
-   update dependency com.google.cloud:gapic-libraries-bom to v1.8.0 ([#&#8203;5902](https://togithub.com/googleapis/java-cloud-bom/issues/5902)) ([2a292e0](https://togithub.com/googleapis/java-cloud-bom/commit/2a292e0d17e773d6a32b51f0c791bf50198ae87a))
-   update dependency com.google.cloud:google-cloud-bigquery to v2.24.3 ([#&#8203;5890](https://togithub.com/googleapis/java-cloud-bom/issues/5890)) ([278f74a](https://togithub.com/googleapis/java-cloud-bom/commit/278f74aa5139e5304556c2dfdb81e96a75066938))
-   update dependency com.google.cloud:google-cloud-bigquery to v2.24.4 ([#&#8203;5901](https://togithub.com/googleapis/java-cloud-bom/issues/5901)) ([14687f6](https://togithub.com/googleapis/java-cloud-bom/commit/14687f6199509bdede2eaee41fea1942bb40b849))
-   update dependency com.google.cloud:google-cloud-bigquerystorage-bom to v2.34.2 ([#&#8203;5900](https://togithub.com/googleapis/java-cloud-bom/issues/5900)) ([34de054](https://togithub.com/googleapis/java-cloud-bom/commit/34de05450df961a00e302fd6276e2fdc8f94f835))
-   update dependency com.google.cloud:google-cloud-bigtable-bom to v2.20.2 ([#&#8203;5904](https://togithub.com/googleapis/java-cloud-bom/issues/5904)) ([fe66de5](https://togithub.com/googleapis/java-cloud-bom/commit/fe66de5dc2c50fb45710543fa1b417cc9a0cd48e))
-   update dependency com.google.cloud:google-cloud-bigtable-bom to v2.20.3 ([#&#8203;5905](https://togithub.com/googleapis/java-cloud-bom/issues/5905)) ([970fdea](https://togithub.com/googleapis/java-cloud-bom/commit/970fdea74455b004cf23dbeb2d8a072616ef9fd3))
-   update dependency com.google.cloud:google-cloud-datastore-bom to v2.14.2 ([#&#8203;5899](https://togithub.com/googleapis/java-cloud-bom/issues/5899)) ([7dd6048](https://togithub.com/googleapis/java-cloud-bom/commit/7dd6048b2f888ab2d39df54096f7848c5bd5976a))
-   update dependency com.google.cloud:google-cloud-firestore-bom to v3.9.3 ([#&#8203;5897](https://togithub.com/googleapis/java-cloud-bom/issues/5897)) ([046528f](https://togithub.com/googleapis/java-cloud-bom/commit/046528f66480fa84d2095098f8a4f0dee0f81e41))
-   update dependency com.google.cloud:google-cloud-logging-bom to v3.14.7 ([#&#8203;5894](https://togithub.com/googleapis/java-cloud-bom/issues/5894)) ([f6820a1](https://togithub.com/googleapis/java-cloud-bom/commit/f6820a186666416a8e004845884d3b168f9d6609))
-   update dependency com.google.cloud:google-cloud-logging-logback to v0.130.9-alpha ([#&#8203;5895](https://togithub.com/googleapis/java-cloud-bom/issues/5895)) ([1af16aa](https://togithub.com/googleapis/java-cloud-bom/commit/1af16aa657c6a5ecfd2fc118c6c806b8937dd27e))
-   update dependency com.google.cloud:google-cloud-nio to v0.126.11 ([#&#8203;5910](https://togithub.com/googleapis/java-cloud-bom/issues/5910)) ([85f14a3](https://togithub.com/googleapis/java-cloud-bom/commit/85f14a3ec6ff117413d5fd8e1a056efb73a42864))
-   update dependency com.google.cloud:google-cloud-pubsub-bom to v1.123.8 ([#&#8203;5909](https://togithub.com/googleapis/java-cloud-bom/issues/5909)) ([bb4a0c6](https://togithub.com/googleapis/java-cloud-bom/commit/bb4a0c62b68dd8063ece2ce8b06a0958f001d59e))
-   update dependency com.google.cloud:google-cloud-pubsublite-bom to v1.12.2 ([#&#8203;5911](https://togithub.com/googleapis/java-cloud-bom/issues/5911)) ([d168583](https://togithub.com/googleapis/java-cloud-bom/commit/d168583255a4978ae06ccc8e3ce0321b21ce54f1))
-   update dependency com.google.cloud:google-cloud-spanner-bom to v6.38.2 ([#&#8203;5896](https://togithub.com/googleapis/java-cloud-bom/issues/5896)) ([5b82b21](https://togithub.com/googleapis/java-cloud-bom/commit/5b82b218bf39181c33d65580e02b583fdbb52e93))
-   update dependency com.google.cloud:google-cloud-spanner-jdbc to v2.9.11 ([#&#8203;5898](https://togithub.com/googleapis/java-cloud-bom/issues/5898)) ([e6061a6](https://togithub.com/googleapis/java-cloud-bom/commit/e6061a6f4f13c2d5c2dec4266c3f73e6b72c037a))
-   update dependency com.google.cloud:google-cloud-storage-bom to v2.21.0 ([#&#8203;5908](https://togithub.com/googleapis/java-cloud-bom/issues/5908)) ([a3f8081](https://togithub.com/googleapis/java-cloud-bom/commit/a3f80819ed3fbd385129a9b184b8c79b936e1136))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://app.renovatebot.com/dashboard#github/googleapis/java-storage).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNS4zMS40IiwidXBkYXRlZEluVmVyIjoiMzUuMzEuNCJ9-->


forking-renovate[bot]: ### Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

 ⚠ **Warning**: custom changes will be lost.

Patch:
diff --git a/README.md b/README.md
index 8eb886d8..fd132b1e 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>libraries-bom</artifactId>
-      <version>26.11.0</version>
+      <version>26.12.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -42,7 +42,7 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.20.2</version>
+  <version>2.21.0</version>
 </dependency>
 
 ```
@@ -50,20 +50,20 @@ If you are using Maven without the BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies:
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:26.11.0')
+implementation platform('com.google.cloud:libraries-bom:26.12.0')
 
 implementation 'com.google.cloud:google-cloud-storage'
 ```
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.20.2'
+implementation 'com.google.cloud:google-cloud-storage:2.21.0'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.20.2"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.21.0"
 ```
 <!-- {x-version-update-end} -->
 
@@ -426,7 +426,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
 [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-storage/java11.html
 [stability-image]: https://img.shields.io/badge/stability-stable-green
 [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-storage.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.20.2
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.21.0
 [authentication]: https://github.com/googleapis/google-cloud-java#authentication
 [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
 [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 10b2de66..074c095a 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.105.7</pubsub-proto.version>
+    <pubsub-proto.version>1.105.8</pubsub-proto.version>
     <junit-platform.version>5.9.2</junit-platform.version>
   </properties>
   <dependencies>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
index 5b50a04e..2fd275e8 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
@@ -89,6 +89,11 @@ final class ApiaryConversions {
   // when converting from gRPC to apiary or vice-versa we want to preserve this property. Until
   // such a time as the apiary model has a project field, we manually apply it with this name.
   private static final String PROJECT_ID_FIELD_NAME = "x_project";
+  // gRPC has a NotificationConfig.name property which contains the bucket the config is associated
+  // with which that apiary doesn't have yet.
+  // when converting from gRPC to apiary or vice-versa we want to preserve this property. Until
+  // such a time as the apiary model has a bucket field, we manually apply it with this name.
+  private static final String NOTIFICATION_BUCKET_FIELD_NAME = "x_bucket";
 
   private final Codec<Entity, String> entityCodec =
       Codec.of(this::entityEncode, this::entityDecode);
@@ -774,6 +779,7 @@ final class ApiaryConversions {
     to.setEtag(from.getEtag());
     to.setSelfLink(from.getSelfLink());
     to.setTopic(from.getTopic());
+    ifNonNull(from.getBucket(), b -> to.set(NOTIFICATION_BUCKET_FIELD_NAME, b));
     ifNonNull(from.getNotificationId(), to::setId);
     ifNonNull(from.getCustomAttributes(), to::setCustomAttributes);
     ifNonNull(from.getObjectNamePrefix(), to::setObjectNamePrefix);
@@ -799,6 +805,7 @@ final class ApiaryConversions {
   private NotificationInfo notificationDecode(
       com.google.api.services.storage.model.Notification from) {
     NotificationInfo.Builder builder = new NotificationInfo.BuilderImpl(from.getTopic());
+    ifNonNull(from.get(NOTIFICATION_BUCKET_FIELD_NAME), String.class::cast, builder::setBucket);
     ifNonNull(from.getId(), builder::setNotificationId);
     ifNonNull(from.getEtag(), builder::setEtag);
     ifNonNull(from.getCustomAttributes(), builder::setCustomAttributes);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobId.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobId.java
index 22eb3ded..c1914d6a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobId.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/BlobId.java
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 import com.google.common.base.MoreObjects;
 import java.io.Serializable;
 import java.util.Objects;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 /**
@@ -31,6 +32,7 @@ import java.util.regex.Pattern;
 public final class BlobId implements Serializable {
 
   private static final long serialVersionUID = 8201580858265557469L;
+  private static final Pattern gsUtilUriPattern = Pattern.compile("^gs://(.+?)/(.+?)(?:#(\\d+))?$");
   private final String bucket;
   private final String name;
   private final Long generation;
@@ -58,7 +60,7 @@ public final class BlobId implements Serializable {
 
   /** Returns this blob's Storage url which can be used with gsutil */
   public String toGsUtilUri() {
-    return "gs://" + bucket + "/" + name;
+    return "gs://" + bucket + "/" + name + (generation == null ? "" : ("#" + generation));
   }
 
   @Override
@@ -117,14 +119,18 @@ public final class BlobId implements Serializable {
    * @param gsUtilUri the Storage url to create the blob from
    */
   public static BlobId fromGsUtilUri(String gsUtilUri) {
-    if (!Pattern.matches("gs://.*/.*", gsUtilUri)) {
+    Matcher m = gsUtilUriPattern.matcher(gsUtilUri);
+    if (!m.matches()) {
       throw new IllegalArgumentException(
-          gsUtilUri + " is not a valid gsutil URI (i.e. \"gs://bucket/blob\")");
+          gsUtilUri
+              + " is not a valid gsutil URI (i.e. \"gs://bucket/blob\" or \"gs://bucket/blob#generation\")");
     }
-    int blobNameStartIndex = gsUtilUri.indexOf('/', 5);
-    String bucketName = gsUtilUri.substring(5, blobNameStartIndex);
-    String blobName = gsUtilUri.substring(blobNameStartIndex + 1);
 
-    return BlobId.of(bucketName, blobName);
+    String bucket = m.group(1);
+    String name = m.group(2);
+    String generationGroup = m.group(3);
+    Long generation = generationGroup == null ? null : Long.parseLong(generationGroup);
+
+    return BlobId.of(bucket, name, generation);
   }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
index 69bd541d..38766b94 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
@@ -20,6 +20,7 @@ import static com.google.cloud.storage.Utils.bucketNameCodec;
 import static com.google.cloud.storage.Utils.ifNonNull;
 import static com.google.cloud.storage.Utils.lift;
 import static com.google.cloud.storage.Utils.projectNameCodec;
+import static com.google.cloud.storage.Utils.topicNameCodec;
 
 import com.google.api.pathtemplate.PathTemplate;
 import com.google.cloud.Binding;
@@ -35,6 +36,8 @@ import com.google.cloud.storage.BucketInfo.Logging;
 import com.google.cloud.storage.BucketInfo.PublicAccessPrevention;
 import com.google.cloud.storage.Conversions.Codec;
 import com.google.cloud.storage.HmacKey.HmacKeyState;
+import com.google.cloud.storage.NotificationInfo.EventType;
+import com.google.cloud.storage.NotificationInfo.PayloadFormat;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -48,6 +51,8 @@ import com.google.storage.v2.Bucket.Website;
 import com.google.storage.v2.BucketAccessControl;
 import com.google.storage.v2.CryptoKeyName;
 import com.google.storage.v2.HmacKeyMetadata;
+import com.google.storage.v2.NotificationConfig;
+import com.google.storage.v2.NotificationConfigName;
 import com.google.storage.v2.Object;
 import com.google.storage.v2.ObjectAccessControl;
 import com.google.storage.v2.ObjectChecksums;
@@ -918,12 +923,54 @@ final class GrpcConversions {
     return toBuilder.build();
   }
 
-  private com.google.storage.v2.NotificationConfig notificationEncode(NotificationInfo from) {
-    return todo();
+  private NotificationConfig notificationEncode(NotificationInfo from) {
+    NotificationConfig.Builder to = NotificationConfig.newBuilder();
+    String id = from.getNotificationId();
+    if (id != null) {
+      if (NotificationConfigName.isParsableFrom(id)) {
+        ifNonNull(id, to::setName);
+      } else {
+        NotificationConfigName name = NotificationConfigName.of("_", from.getBucket(), id);
+        to.setName(name.toString());
+      }
+    }
+    ifNonNull(from.getTopic(), topicNameCodec::encode, to::setTopic);
+    ifNonNull(from.getEtag(), to::setEtag);
+    ifNonNull(from.getEventTypes(), toImmutableListOf(EventType::name), to::addAllEventTypes);
+    ifNonNull(from.getCustomAttributes(), to::putAllCustomAttributes);
+    ifNonNull(from.getObjectNamePrefix(), to::setObjectNamePrefix);
+    ifNonNull(from.getPayloadFormat(), PayloadFormat::name, to::setPayloadFormat);
+    return to.build();
   }
 
-  private NotificationInfo notificationDecode(com.google.storage.v2.NotificationConfig from) {
-    return todo();
+  private NotificationInfo notificationDecode(NotificationConfig from) {
+    NotificationInfo.Builder to =
+        NotificationInfo.newBuilder(topicNameCodec.decode(from.getTopic()));
+    if (!from.getName().isEmpty()) {
+      NotificationConfigName parse = NotificationConfigName.parse(from.getName());
+      // the case where parse could return null is already guarded by the preceding isEmpty check
+      //noinspection DataFlowIssue
+      to.setNotificationId(parse.getNotificationConfig());
+      to.setBucket(parse.getBucket());
+    }
+    if (!from.getEtag().isEmpty()) {
+      to.setEtag(from.getEtag());
+    }
+    if (!from.getEventTypesList().isEmpty()) {
+      EventType[] eventTypes =
+          from.getEventTypesList().stream().map(EventType::valueOf).toArray(EventType[]::new);
+      to.setEventTypes(eventTypes);
+    }
+    if (!from.getCustomAttributesMap().isEmpty()) {
+      to.setCustomAttributes(from.getCustomAttributesMap());
+    }
+    if (!from.getObjectNamePrefix().isEmpty()) {
+      to.setObjectNamePrefix(from.getObjectNamePrefix());
+    }
+    if (!from.getPayloadFormat().isEmpty()) {
+      to.setPayloadFormat(PayloadFormat.valueOf(from.getPayloadFormat()));
+    }
+    return to.build();
   }
 
   private com.google.iam.v1.Policy policyEncode(Policy from) {
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 dc332b2f..ce5ebf04 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
@@ -30,7 +30,6 @@ import static java.util.Objects.requireNonNull;
 import com.google.api.core.ApiFuture;
 import com.google.api.core.BetaApi;
 import com.google.api.gax.grpc.GrpcCallContext;
-import com.google.api.gax.grpc.GrpcStatusCode;
 import com.google.api.gax.paging.AbstractPage;
 import com.google.api.gax.paging.Page;
 import com.google.api.gax.retrying.ResultRetryAlgorithm;
@@ -39,7 +38,6 @@ import com.google.api.gax.rpc.ApiExceptions;
 import com.google.api.gax.rpc.NotFoundException;
 import com.google.api.gax.rpc.StatusCode;
 import com.google.api.gax.rpc.UnaryCallable;
-import com.google.api.gax.rpc.UnimplementedException;
 import com.google.cloud.BaseService;
 import com.google.cloud.Policy;
 import com.google.cloud.WriteChannel;
@@ -84,18 +82,25 @@ import com.google.storage.v2.ComposeObjectRequest;
 import com.google.storage.v2.ComposeObjectRequest.SourceObject;
 import com.google.storage.v2.CreateBucketRequest;
 import com.google.storage.v2.CreateHmacKeyRequest;
+import com.google.storage.v2.CreateNotificationConfigRequest;
 import com.google.storage.v2.DeleteBucketRequest;
 import com.google.storage.v2.DeleteHmacKeyRequest;
+import com.google.storage.v2.DeleteNotificationConfigRequest;
 import com.google.storage.v2.DeleteObjectRequest;
 import com.google.storage.v2.GetBucketRequest;
 import com.google.storage.v2.GetHmacKeyRequest;
+import com.google.storage.v2.GetNotificationConfigRequest;
 import com.google.storage.v2.GetObjectRequest;
 import com.google.storage.v2.GetServiceAccountRequest;
 import com.google.storage.v2.ListBucketsRequest;
 import com.google.storage.v2.ListHmacKeysRequest;
+import com.google.storage.v2.ListNotificationConfigsRequest;
+import com.google.storage.v2.ListNotificationConfigsResponse;
 import com.google.storage.v2.ListObjectsRequest;
 import com.google.storage.v2.ListObjectsResponse;
 import com.google.storage.v2.LockBucketRetentionPolicyRequest;
+import com.google.storage.v2.NotificationConfig;
+import com.google.storage.v2.NotificationConfigName;
 import com.google.storage.v2.Object;
 import com.google.storage.v2.ObjectAccessControl;
 import com.google.storage.v2.ObjectChecksums;
@@ -103,13 +108,13 @@ import com.google.storage.v2.ReadObjectRequest;
 import com.google.storage.v2.RewriteObjectRequest;
 import com.google.storage.v2.RewriteResponse;
 import com.google.storage.v2.StorageClient;
+import com.google.storage.v2.StorageClient.ListNotificationConfigsPage;
 import com.google.storage.v2.UpdateBucketRequest;
 import com.google.storage.v2.UpdateHmacKeyRequest;
 import com.google.storage.v2.UpdateObjectRequest;
 import com.google.storage.v2.WriteObjectRequest;
 import com.google.storage.v2.WriteObjectResponse;
 import com.google.storage.v2.WriteObjectSpec;
-import io.grpc.Status.Code;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -1404,23 +1409,92 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public Notification createNotification(String bucket, NotificationInfo notificationInfo) {
-    return throwNotYetImplemented(
-        fmtMethodName("createNotification", String.class, NotificationInfo.class));
+    NotificationConfig encode = codecs.notificationInfo().encode(notificationInfo);
+    CreateNotificationConfigRequest req =
+        CreateNotificationConfigRequest.newBuilder()
+            .setParent(bucketNameCodec.encode(bucket))
+            .setNotificationConfig(encode)
+            .build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> storageClient.createNotificationConfigCallable().call(req),
+        syntaxDecoders.notificationConfig);
   }
 
   @Override
   public Notification getNotification(String bucket, String notificationId) {
-    return throwNotYetImplemented(fmtMethodName("getNotification", String.class, String.class));
+    String name;
+    if (NotificationConfigName.isParsableFrom(notificationId)) {
+      name = notificationId;
+    } else {
+      NotificationConfigName configName = NotificationConfigName.of("_", bucket, notificationId);
+      name = configName.toString();
+    }
+    GetNotificationConfigRequest req =
+        GetNotificationConfigRequest.newBuilder().setName(name).build();
+    return Retrying.run(
+        getOptions(),
+        retryAlgorithmManager.getFor(req),
+        () -> {
+          try {
+            return storageClient.getNotificationConfigCallable().call(req);
+          } catch (NotFoundException e) {
+            return null;
+          }
+        },
+        syntaxDecoders.notificationConfig);
   }
 
   @Override
   public List<Notification> listNotifications(String bucket) {
-    return throwNotYetImplemented(fmtMethodName("listNotifications", String.class));
+    ListNotificationConfigsRequest req =
+        ListNotificationConfigsRequest.newBuilder()
+            .setParent(bucketNameCodec.encode(bucket))
+            .build();
+    ResultRetryAlgorithm<?> algorithm = retryAlgorithmManager.getFor(req);
+    return Retrying.run(
+        getOptions(),
+        algorithm,
+        () -> storageClient.listNotificationConfigsPagedCallable().call(req),
+        resp -> {
+          TransformingPageDecorator<
+                  ListNotificationConfigsRequest,
+                  ListNotificationConfigsResponse,
+                  NotificationConfig,
+                  ListNotificationConfigsPage,
+                  Notification>
+              page =
+                  new TransformingPageDecorator<>(
+                      resp.getPage(), syntaxDecoders.notificationConfig, getOptions(), algorithm);
+          return ImmutableList.copyOf(page.iterateAll());
+        });
   }
 
   @Override
   public boolean deleteNotification(String bucket, String notificationId) {
-    return throwNotYetImplemented(fmtMethodName("deleteNotification", String.class, String.class));
+    String name;
+    if (NotificationConfigName.isParsableFrom(notificationId)) {
+      name = notificationId;
+    } else {
+      NotificationConfigName configName = NotificationConfigName.of("_", bucket, notificationId);
+      name = configName.toString();
+    }
+    DeleteNotificationConfigRequest req =
+        DeleteNotificationConfigRequest.newBuilder().setName(name).build();
+    return Boolean.TRUE.equals(
+        Retrying.run(
+            getOptions(),
+            retryAlgorithmManager.getFor(req),
+            () -> {
+              try {
+                storageClient.deleteNotificationConfigCallable().call(req);
+                return true;
+              } catch (NotFoundException e) {
+                return false;
+              }
+            },
+            Decoder.identity()));
   }
 
   @Override
@@ -1448,6 +1522,8 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         o -> codecs.blobInfo().decode(o).asBlob(GrpcStorageImpl.this);
     final Decoder<com.google.storage.v2.Bucket, Bucket> bucket =
         b -> codecs.bucketInfo().decode(b).asBucket(GrpcStorageImpl.this);
+    final Decoder<NotificationConfig, Notification> notificationConfig =
+        n -> codecs.notificationInfo().decode(n).asNotification(GrpcStorageImpl.this);
   }
 
   /**
@@ -1668,14 +1744,6 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     throw new UnsupportedOperationException(message);
   }
 
-  static <T> T throwNotYetImplemented(String methodName) {
-    String message =
-        String.format(
-            "%s#%s is not yet implemented for GRPC transport. Please use StorageOptions.http() to construct a compatible instance in the interim.",
-            Storage.class.getName(), methodName);
-    throw new UnimplementedException(message, null, GrpcStatusCode.of(Code.UNIMPLEMENTED), false);
-  }
-
   private static String fmtMethodName(String name, Class<?>... args) {
     return name
         + "("
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
index 0652620a..f3e2bb81 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
@@ -88,6 +88,12 @@ public class Notification extends NotificationInfo {
       return this;
     }
 
+    @Override
+    Builder setBucket(String bucket) {
+      infoBuilder.setBucket(bucket);
+      return this;
+    }
+
     @Override
     public Notification build() {
       return new Notification(storage, infoBuilder);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java
index 58621577..7bf14779 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java
@@ -18,6 +18,7 @@ package com.google.cloud.storage;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
+import com.google.api.core.InternalApi;
 import com.google.api.pathtemplate.PathTemplate;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableMap;
@@ -34,11 +35,13 @@ public class NotificationInfo implements Serializable {
   private static final PathTemplate PATH_TEMPLATE =
       PathTemplate.createWithoutUrlEncoding("projects/{project}/topics/{topic}");
 
+  // TODO: Change to StringEnum in next major version
   public enum PayloadFormat {
     JSON_API_V1,
     NONE
   }
 
+  // TODO: Change to StringEnum in next major version
   public enum EventType {
     OBJECT_FINALIZE,
     OBJECT_METADATA_UPDATE,
@@ -54,6 +57,7 @@ public class NotificationInfo implements Serializable {
   private final String objectNamePrefix;
   private final String etag;
   private final String selfLink;
+  private final String bucket;
 
   /** Builder for {@code NotificationInfo}. */
   public abstract static class Builder {
@@ -75,6 +79,8 @@ public class NotificationInfo implements Serializable {
 
     public abstract Builder setCustomAttributes(Map<String, String> customAttributes);
 
+    abstract Builder setBucket(String bucket);
+
     /** Creates a {@code NotificationInfo} object. */
     public abstract NotificationInfo build();
   }
@@ -90,6 +96,7 @@ public class NotificationInfo implements Serializable {
     private String objectNamePrefix;
     private String etag;
     private String selfLink;
+    private String bucket;
 
     BuilderImpl(String topic) {
       this.topic = topic;
@@ -104,6 +111,7 @@ public class NotificationInfo implements Serializable {
       customAttributes = notificationInfo.customAttributes;
       payloadFormat = notificationInfo.payloadFormat;
       objectNamePrefix = notificationInfo.objectNamePrefix;
+      bucket = notificationInfo.bucket;
     }
 
     @Override
@@ -156,6 +164,12 @@ public class NotificationInfo implements Serializable {
       return this;
     }
 
+    @Override
+    Builder setBucket(String bucket) {
+      this.bucket = bucket;
+      return this;
+    }
+
     public NotificationInfo build() {
       checkNotNull(topic);
       checkTopicFormat(topic);
@@ -172,6 +186,7 @@ public class NotificationInfo implements Serializable {
     customAttributes = builder.customAttributes;
     payloadFormat = builder.payloadFormat;
     objectNamePrefix = builder.objectNamePrefix;
+    bucket = builder.bucket;
   }
 
   /** Returns the service-generated id for the notification. */
@@ -225,6 +240,15 @@ public class NotificationInfo implements Serializable {
     return customAttributes;
   }
 
+  /**
+   * gRPC has the bucket name encoded in the notification name, use this internal property to track
+   * it.
+   */
+  @InternalApi
+  String getBucket() {
+    return bucket;
+  }
+
   @Override
   public int hashCode() {
     return Objects.hash(
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 0cd1ff07..d1838985 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
@@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.util.Objects.requireNonNull;
 
+import com.google.api.core.BetaApi;
 import com.google.api.core.InternalExtensionOnly;
 import com.google.api.gax.paging.Page;
 import com.google.auth.ServiceAccountSigner;
@@ -1300,6 +1301,18 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
       return new BlobListOption(UnifiedOpts.endOffset(endOffset));
     }
 
+    /**
+     * Returns an option to set a glob pattern to filter results to blobs that match the pattern.
+     *
+     * @see <a href="https://cloud.google.com/storage/docs/json_api/v1/objects/list">List
+     *     Objects</a>
+     */
+    @BetaApi
+    @TransportCompatibility({Transport.HTTP})
+    public static BlobListOption matchGlob(@NonNull String glob) {
+      return new BlobListOption(UnifiedOpts.matchGlob(glob));
+    }
+
     /**
      * Returns an option to define the billing user project. This option is required by buckets with
      * `requester_pays` flag enabled to assign operation costs.
@@ -4053,7 +4066,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @return the created notification
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Notification createNotification(String bucket, NotificationInfo notificationInfo);
 
   /**
@@ -4072,7 +4085,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @return the {@code Notification} object with the given id or {@code null} if not found
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   Notification getNotification(String bucket, String notificationId);
 
   /**
@@ -4089,7 +4102,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @return a list of {@link Notification} objects added to the bucket.
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   List<Notification> listNotifications(String bucket);
 
   /**
@@ -4113,7 +4126,7 @@ public interface Storage extends Service<StorageOptions>, AutoCloseable {
    * @return {@code true} if the notification has been deleted, {@code false} if not found
    * @throws StorageException upon failure
    */
-  @TransportCompatibility({Transport.HTTP})
+  @TransportCompatibility({Transport.HTTP, Transport.GRPC})
   boolean deleteNotification(String bucket, String notificationId);
 
   /**
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 f632af92..295455f4 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
@@ -413,6 +413,11 @@ final class UnifiedOpts {
     return new KmsKeyName(kmsKeyName);
   }
 
+  static MatchGlob matchGlob(@NonNull String glob) {
+    requireNonNull(glob, "glob must be non null");
+    return new MatchGlob(glob);
+  }
+
   static Md5Match md5Match(@NonNull String md5) {
     requireNonNull(md5, "md5 must be non null");
     return new Md5Match(md5);
@@ -1070,6 +1075,20 @@ final class UnifiedOpts {
     }
   }
 
+  static final class MatchGlob extends RpcOptVal<String> implements ObjectListOpt {
+    private static final long serialVersionUID = 8819855597395473178L;
+
+    private MatchGlob(String val) {
+      super(StorageRpc.Option.MATCH_GLOB, val);
+    }
+
+    @Override
+    public Mapper<ListObjectsRequest.Builder> listObjects() {
+      return GrpcStorageImpl.throwHttpJsonOnly(
+          com.google.cloud.storage.Storage.BlobListOption.class, "matchGlob(String)");
+    }
+  }
+
   @Deprecated
   static final class Md5Match implements ObjectTargetOpt {
     private static final long serialVersionUID = 5237207911268363887L;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
index 0e67c845..81d7fbb9 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
@@ -152,6 +152,26 @@ final class Utils {
             }
           });
 
+  private static final String PUBSUB_PREFIX = "//pubsub.googleapis.com/";
+  static final Codec<String, String> topicNameCodec =
+      Codec.of(
+          topic -> {
+            requireNonNull(topic, "topic must be non null");
+            if (topic.startsWith(PUBSUB_PREFIX)) {
+              return topic;
+            } else {
+              return PUBSUB_PREFIX + topic;
+            }
+          },
+          resourceName -> {
+            requireNonNull(resourceName, "resourceName must be non null");
+            if (resourceName.startsWith(PUBSUB_PREFIX)) {
+              return resourceName.substring(PUBSUB_PREFIX.length());
+            } else {
+              return resourceName;
+            }
+          });
+
   static final Codec<Integer, String> crc32cCodec =
       Codec.of(Utils::crc32cEncode, Utils::crc32cDecode);
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
index 27572417..707ba457 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpc.java
@@ -423,6 +423,7 @@ public class HttpStorageRpc implements StorageRpc {
               .setDelimiter(Option.DELIMITER.getString(options))
               .setStartOffset(Option.START_OFF_SET.getString(options))
               .setEndOffset(Option.END_OFF_SET.getString(options))
+              .setMatchGlob(Option.MATCH_GLOB.getString(options))
               .setPrefix(Option.PREFIX.getString(options))
               .setMaxResults(Option.MAX_RESULTS.getLong(options))
               .setPageToken(Option.PAGE_TOKEN.getString(options))
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
index 1e1e93e0..bbfab105 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/StorageRpc.java
@@ -61,6 +61,7 @@ public interface StorageRpc extends ServiceRpc {
     DELIMITER("delimiter"),
     START_OFF_SET("startOffset"),
     END_OFF_SET("endOffset"),
+    MATCH_GLOB("matchGlob"),
     VERSIONS("versions"),
     FIELDS("fields"),
     CUSTOMER_SUPPLIED_KEY("customerSuppliedKey"),
diff --git a/pom.xml b/pom.xml
index 9aac4a35..c4af20ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,7 +80,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-pubsub</artifactId>
-        <version>1.123.7</version>
+        <version>1.123.8</version>
         <scope>test</scope>
       </dependency>
       <dependency>
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index d26cf02a..b37019de 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -30,7 +30,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.20.2</version>
+      <version>2.21.0</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.123.7</version>
+      <version>1.123.8</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index e3d8e1ec..64314055 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -29,7 +29,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.11.0</version>
+        <version>26.12.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -61,7 +61,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.123.7</version>
+      <version>1.123.8</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 6086dbc5..0f42fc23 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.123.7</version>
+      <version>1.123.8</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snippets/pom.xml b/samples/snippets/pom.xml
index 91b1179c..eedc3834 100644
--- a/samples/snippets/pom.xml
+++ b/samples/snippets/pom.xml
@@ -31,7 +31,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>libraries-bom</artifactId>
-        <version>26.11.0</version>
+        <version>26.12.0</version>
         <type>pom</type>
         <scope>import</scope>
       </dependency>
@@ -72,7 +72,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-pubsub</artifactId>
-      <version>1.123.7</version>
+      <version>1.123.8</version>
       <scope>test</scope>
     </dependency>
   </dependencies>


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobIdTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobIdTest.java
index 0c13fa42..03bf2218 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobIdTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BlobIdTest.java
@@ -39,6 +39,15 @@ public class BlobIdTest {
     assertEquals("gs://bucket/path/to/blob", blobId.toGsUtilUri());
   }
 
+  @Test
+  public void testToFromGsUtilUriWithGeneration() {
+    BlobId blobId = BlobId.fromGsUtilUri("gs://bucket/path/to/blob#1360887697105000");
+    assertEquals("bucket", blobId.getBucket());
+    assertEquals("path/to/blob", blobId.getName());
+    assertEquals(Long.valueOf(1360887697105000L), blobId.getGeneration());
+    assertEquals("gs://bucket/path/to/blob#1360887697105000", blobId.toGsUtilUri());
+  }
+
   @Test
   public void testEquals() {
     compareBlobIds(BLOB, BlobId.of("b", "n"));
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoPropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoPropertyTest.java
new file mode 100644
index 00000000..5a20dfc6
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoPropertyTest.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2023 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.storage.v2.NotificationConfig;
+
+final class NotificationInfoPropertyTest
+    extends BaseConvertablePropertyTest<
+        NotificationInfo, NotificationConfig, com.google.api.services.storage.model.Notification> {}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java
index 1e162f98..ce750154 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplMockitoTest.java
@@ -1308,6 +1308,27 @@ public class StorageImplMockitoTest {
     assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
   }
 
+  @Test
+  public void testListBlobsMatchGlob() {
+    String cursor = "cursor";
+    String matchGlob = "foo*bar";
+    Map<StorageRpc.Option, ?> options = ImmutableMap.of(StorageRpc.Option.MATCH_GLOB, matchGlob);
+    ImmutableList<BlobInfo> blobInfoList = ImmutableList.of(BLOB_INFO1, BLOB_INFO2);
+    Tuple<String, Iterable<com.google.api.services.storage.model.StorageObject>> result =
+        Tuple.of(
+            cursor, Iterables.transform(blobInfoList, Conversions.apiary().blobInfo()::encode));
+    doReturn(result)
+        .doThrow(UNEXPECTED_CALL_EXCEPTION)
+        .when(storageRpcMock)
+        .list(BUCKET_NAME1, options);
+
+    initializeService();
+    ImmutableList<Blob> blobList = ImmutableList.of(expectedBlob1, expectedBlob2);
+    Page<Blob> page = storage.list(BUCKET_NAME1, Storage.BlobListOption.matchGlob(matchGlob));
+    assertEquals(cursor, page.getNextPageToken());
+    assertArrayEquals(blobList.toArray(), Iterables.toArray(page.getValues(), Blob.class));
+  }
+
   @Test
   public void testListBlobsWithException() {
     doThrow(STORAGE_FAILURE).when(storageRpcMock).list(BUCKET_NAME1, EMPTY_RPC_OPTIONS);
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java
index 9aa528c1..69a06658 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITNotificationTest.java
@@ -16,18 +16,21 @@
 
 package com.google.cloud.storage.it;
 
+import static com.google.cloud.storage.TestUtils.assertAll;
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.cloud.pubsub.v1.TopicAdminClient;
 import com.google.cloud.storage.BucketInfo;
 import com.google.cloud.storage.Notification;
 import com.google.cloud.storage.NotificationInfo;
+import com.google.cloud.storage.NotificationInfo.PayloadFormat;
 import com.google.cloud.storage.Storage;
 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.registry.Generator;
 import com.google.common.collect.ImmutableMap;
 import com.google.iam.v1.Binding;
 import com.google.iam.v1.GetIamPolicyRequest;
@@ -35,7 +38,6 @@ import com.google.iam.v1.SetIamPolicyRequest;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
-import java.util.UUID;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import org.junit.After;
@@ -45,44 +47,58 @@ import org.junit.runner.RunWith;
 
 @RunWith(StorageITRunner.class)
 @CrossRun(
-    transports = {Transport.HTTP},
-    backends = {Backend.PROD})
+    transports = {Transport.HTTP, Transport.GRPC},
+    backends = {Backend.PROD, Backend.TEST_BENCH})
 public class ITNotificationTest {
-  private static final Notification.PayloadFormat PAYLOAD_FORMAT =
-      Notification.PayloadFormat.JSON_API_V1.JSON_API_V1;
+  private static final Notification.PayloadFormat PAYLOAD_FORMAT = PayloadFormat.JSON_API_V1;
   private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
   private static final Logger log = Logger.getLogger(ITNotificationTest.class.getName());
+  private static final String DOES_NOT_EXIST_ID = "something-that-does-not-exist-probably";
 
+  @Inject public Backend backend;
+  @Inject public Transport transport;
   @Inject public Storage storage;
   @Inject public BucketInfo bucket;
+  @Inject public Generator generator;
 
   private TopicAdminClient topicAdminClient;
   private String topic;
+  private NotificationInfo notificationInfo;
 
   @Before
   public void setup() throws IOException {
-    // Configure topic admin client for notification.
-    topicAdminClient = TopicAdminClient.create();
     String projectId = storage.getOptions().getProjectId();
-    String id = UUID.randomUUID().toString().substring(0, 8);
-    topic = String.format("projects/%s/topics/test_topic_foo_%s", projectId, id).trim();
+    // square brackets are not acceptable characters for topic names, replace them with dash
+    // https://cloud.google.com/pubsub/docs/admin#resource_names
+    String name = generator.randomObjectName().replaceAll("[\\[\\]]", "-");
+    topic = String.format("projects/%s/topics/%s", projectId, name).trim();
+    notificationInfo =
+        NotificationInfo.newBuilder(topic)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
 
-    topicAdminClient.createTopic(this.topic);
+    if (backend == Backend.PROD && transport == Transport.HTTP) {
 
-    GetIamPolicyRequest getIamPolicyRequest =
-        GetIamPolicyRequest.newBuilder().setResource(this.topic).build();
+      // Configure topic admin client for notification.
+      topicAdminClient = TopicAdminClient.create();
+      topicAdminClient.createTopic(this.topic);
 
-    com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest);
+      GetIamPolicyRequest getIamPolicyRequest =
+          GetIamPolicyRequest.newBuilder().setResource(this.topic).build();
 
-    Binding binding =
-        Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build();
+      com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest);
 
-    SetIamPolicyRequest setIamPolicyRequest =
-        SetIamPolicyRequest.newBuilder()
-            .setResource(this.topic)
-            .setPolicy(policy.toBuilder().addBindings(binding).build())
-            .build();
-    topicAdminClient.setIamPolicy(setIamPolicyRequest);
+      Binding binding =
+          Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build();
+
+      SetIamPolicyRequest setIamPolicyRequest =
+          SetIamPolicyRequest.newBuilder()
+              .setResource(this.topic)
+              .setPolicy(policy.toBuilder().addBindings(binding).build())
+              .build();
+      topicAdminClient.setIamPolicy(setIamPolicyRequest);
+    }
   }
 
   @After
@@ -100,49 +116,80 @@ public class ITNotificationTest {
   }
 
   @Test
-  public void testNotification() {
-    NotificationInfo notificationInfo =
-        NotificationInfo.newBuilder(topic)
-            .setCustomAttributes(CUSTOM_ATTRIBUTES)
-            .setPayloadFormat(PAYLOAD_FORMAT)
-            .build();
-    try {
-      assertThat(storage.listNotifications(bucket.getName())).isEmpty();
-      Notification notification = storage.createNotification(bucket.getName(), notificationInfo);
-      assertThat(notification.getNotificationId()).isNotNull();
-      assertThat(CUSTOM_ATTRIBUTES).isEqualTo(notification.getCustomAttributes());
-      assertThat(PAYLOAD_FORMAT.name()).isEqualTo(notification.getPayloadFormat().name());
-      assertThat(notification.getTopic().contains(topic)).isTrue();
-
-      // Gets the notification with the specified id.
-      Notification actualNotification =
-          storage.getNotification(bucket.getName(), notification.getNotificationId());
-      assertThat(actualNotification.getNotificationId())
-          .isEqualTo(notification.getNotificationId());
-      assertThat(actualNotification.getTopic().trim()).isEqualTo(notification.getTopic().trim());
-      assertThat(actualNotification.getEtag()).isEqualTo(notification.getEtag());
-      assertThat(actualNotification.getEventTypes()).isEqualTo(notification.getEventTypes());
-      assertThat(actualNotification.getPayloadFormat()).isEqualTo(notification.getPayloadFormat());
-      assertThat(actualNotification.getSelfLink()).isEqualTo(notification.getSelfLink());
-      assertThat(actualNotification.getCustomAttributes())
-          .isEqualTo(notification.getCustomAttributes());
-
-      // Retrieves the list of notifications associated with the bucket.
-      List<Notification> notifications = storage.listNotifications(bucket.getName());
-      assertThat(notifications.size()).isEqualTo(1);
-      assertThat(notifications.get(0).getNotificationId())
-          .isEqualTo(actualNotification.getNotificationId());
-
-      // Deletes the notification with the specified id.
-      assertThat(storage.deleteNotification(bucket.getName(), notification.getNotificationId()))
-          .isTrue();
-      assertThat(storage.deleteNotification(bucket.getName(), notification.getNotificationId()))
-          .isFalse();
-      assertThat(storage.getNotification(bucket.getName(), notification.getNotificationId()))
-          .isNull();
-      assertThat(storage.listNotifications(bucket.getName())).isEmpty();
-    } finally {
-      // delete is taken care of by BucketFixture now
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void listNotification_doesNotExist() throws Exception {
+    // create a temporary bucket to ensure we're immune from ordering on other tests
+    try (TemporaryBucket tempB =
+        TemporaryBucket.newBuilder()
+            .setBucketInfo(BucketInfo.of(generator.randomBucketName()))
+            .setStorage(storage)
+            .build()) {
+      List<Notification> notifications = storage.listNotifications(tempB.getBucket().getName());
+      assertThat(notifications).isEmpty();
     }
   }
+
+  @Test
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void listNotification_exists() {
+    Notification notification = storage.createNotification(bucket.getName(), notificationInfo);
+    List<Notification> notifications = storage.listNotifications(bucket.getName());
+    assertThat(notifications).isNotEmpty();
+    assertThat(notifications).contains(notification);
+  }
+
+  @Test
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void createNotification_doesNotExist() throws Exception {
+    Notification notification = storage.createNotification(bucket.getName(), notificationInfo);
+    assertAll(
+        () -> assertThat(notification.getNotificationId()).isNotNull(),
+        () -> assertThat(notification.getCustomAttributes()).isEqualTo(CUSTOM_ATTRIBUTES),
+        () -> assertThat(notification.getPayloadFormat()).isEqualTo(PAYLOAD_FORMAT),
+        () -> assertThat(notification.getTopic()).contains(topic));
+  }
+
+  @Test
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void getNotification_exists() throws Exception {
+    Notification notification = storage.createNotification(bucket.getName(), notificationInfo);
+
+    Notification getResult =
+        storage.getNotification(bucket.getName(), notification.getNotificationId());
+
+    assertAll(
+        () -> assertThat(getResult.getNotificationId()).isEqualTo(notification.getNotificationId()),
+        () -> assertThat(getResult.getTopic()).isEqualTo(notification.getTopic()),
+        () -> assertThat(getResult.getEtag()).isEqualTo(notification.getEtag()),
+        () -> assertThat(getResult.getEventTypes()).isEqualTo(notification.getEventTypes()),
+        () -> assertThat(getResult.getPayloadFormat()).isEqualTo(notification.getPayloadFormat()),
+        () ->
+            assertThat(getResult.getCustomAttributes())
+                .isEqualTo(notification.getCustomAttributes()),
+        () -> assertThat(getResult).isEqualTo(notification));
+  }
+
+  @Test
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void getNotification_doesNotExists() {
+    Notification getResult = storage.getNotification(bucket.getName(), DOES_NOT_EXIST_ID);
+
+    assertThat(getResult).isNull();
+  }
+
+  @Test
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void deleteNotification_exists() {
+    Notification notification = storage.createNotification(bucket.getName(), notificationInfo);
+    boolean deleteResult =
+        storage.deleteNotification(bucket.getName(), notification.getNotificationId());
+    assertThat(deleteResult).isTrue();
+  }
+
+  @Test
+  @CrossRun.Ignore(transports = Transport.GRPC, backends = Backend.PROD)
+  public void deleteNotification_doesNotExists() {
+    boolean deleteResult = storage.deleteNotification(bucket.getName(), DOES_NOT_EXIST_ID);
+    assertThat(deleteResult).isFalse();
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
index f2afbb2d..1da2d768 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITObjectTest.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.storage.it;
 
+import static com.google.cloud.storage.TestUtils.assertAll;
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertArrayEquals;
@@ -65,6 +66,7 @@ import com.google.cloud.storage.it.runner.registry.Generator;
 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.collect.Iterators;
 import com.google.common.hash.Hashing;
 import com.google.common.io.BaseEncoding;
@@ -542,6 +544,60 @@ public class ITObjectTest {
     assertThat(actual).contains(PackagePrivateMethodWorkarounds.noAcl(obj2Gen1));
   }
 
+  @Test
+  // When gRPC support is added for matchGlob, enable this test for gRPC.
+  @Exclude(transports = Transport.GRPC)
+  public void testListBlobsWithMatchGlob() throws Exception {
+    BucketInfo bucketInfo = BucketInfo.newBuilder(generator.randomBucketName()).build();
+    try (TemporaryBucket tempBucket =
+        TemporaryBucket.newBuilder().setBucketInfo(bucketInfo).setStorage(storage).build()) {
+      BucketInfo bucket = tempBucket.getBucket();
+      assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/bar").build()));
+      assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/baz").build()));
+      assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foo/foobar").build()));
+      assertNotNull(storage.create(BlobInfo.newBuilder(bucket, "foobar").build()));
+
+      Page<Blob> page1 = storage.list(bucket.getName(), BlobListOption.matchGlob("foo*bar"));
+      Page<Blob> page2 = storage.list(bucket.getName(), BlobListOption.matchGlob("foo**bar"));
+      Page<Blob> page3 = storage.list(bucket.getName(), BlobListOption.matchGlob("**/foobar"));
+      Page<Blob> page4 = storage.list(bucket.getName(), BlobListOption.matchGlob("*/ba[rz]"));
+      Page<Blob> page5 = storage.list(bucket.getName(), BlobListOption.matchGlob("*/ba[!a-y]"));
+      Page<Blob> page6 =
+          storage.list(bucket.getName(), BlobListOption.matchGlob("**/{foobar,baz}"));
+      Page<Blob> page7 =
+          storage.list(bucket.getName(), BlobListOption.matchGlob("foo/{foo*,*baz}"));
+      assertAll(
+          () ->
+              assertThat(Iterables.transform(page1.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foobar")
+                  .inOrder(),
+          () ->
+              assertThat(Iterables.transform(page2.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foo/bar", "foo/foobar", "foobar")
+                  .inOrder(),
+          () ->
+              assertThat(Iterables.transform(page3.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foo/foobar", "foobar")
+                  .inOrder(),
+          () ->
+              assertThat(Iterables.transform(page4.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foo/bar", "foo/baz")
+                  .inOrder(),
+          () ->
+              assertThat(Iterables.transform(page5.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foo/baz")
+                  .inOrder(),
+          () ->
+              assertThat(Iterables.transform(page6.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foo/baz", "foo/foobar", "foobar")
+                  .inOrder(),
+          () ->
+              assertThat(Iterables.transform(page7.iterateAll(), blob -> blob.getName()))
+                  .containsExactly("foo/baz", "foo/foobar")
+                  .inOrder());
+    }
+  }
+
   @Test
   public void testListBlobsMultiplePages() {
     String basePath = generator.randomObjectName();
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/NotificationArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/NotificationArbitraryProvider.java
new file mode 100644
index 00000000..8e960791
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/NotificationArbitraryProvider.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2023 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.jqwik;
+
+import static com.google.cloud.storage.PackagePrivateMethodWorkarounds.ifNonNull;
+
+import com.google.pubsub.v1.TopicName;
+import com.google.storage.v2.NotificationConfig;
+import com.google.storage.v2.NotificationConfigName;
+import java.util.Collections;
+import java.util.Set;
+import net.jqwik.api.Arbitraries;
+import net.jqwik.api.Arbitrary;
+import net.jqwik.api.Combinators;
+import net.jqwik.api.providers.ArbitraryProvider;
+import net.jqwik.api.providers.TypeUsage;
+import org.checkerframework.checker.nullness.qual.NonNull;
+
+public final class NotificationArbitraryProvider implements ArbitraryProvider {
+
+  @Override
+  public boolean canProvideFor(TypeUsage targetType) {
+    return targetType.isOfType(NotificationConfig.class);
+  }
+
+  @NonNull
+  @Override
+  public Set<Arbitrary<?>> provideFor(
+      @NonNull TypeUsage targetType, @NonNull SubtypeProvider subtypeProvider) {
+    Arbitrary<NotificationConfig> as =
+        Combinators.combine(
+                notificationName(),
+                topic(),
+                StorageArbitraries.etag().injectNull(0.5),
+                eventTypes(),
+                StorageArbitraries.buckets().labels(),
+                StorageArbitraries.objects().name().injectNull(0.5),
+                Arbitraries.of("JSON_API_V1", "NONE"))
+            .as(
+                (name, topic, etag, types, customAttributes, prefix, payloadFormat) -> {
+                  NotificationConfig.Builder b =
+                      NotificationConfig.newBuilder()
+                          .setName(name.toString())
+                          .setTopic(topic)
+                          .setPayloadFormat(payloadFormat);
+                  ifNonNull(types, b::addAllEventTypes);
+                  ifNonNull(customAttributes, b::putAllCustomAttributes);
+                  ifNonNull(etag, b::setEtag);
+                  ifNonNull(prefix, b::setObjectNamePrefix);
+                  return b.build();
+                });
+    return Collections.singleton(as);
+  }
+
+  private static Arbitrary<Set<String>> eventTypes() {
+    return Arbitraries.of(
+            "OBJECT_FINALIZE", "OBJECT_METADATA_UPDATE", "OBJECT_DELETE", "OBJECT_ARCHIVE")
+        .set()
+        .ofMinSize(0)
+        .ofMaxSize(4);
+  }
+
+  @NonNull
+  private static Arbitrary<String> topic() {
+    return Combinators.combine(
+            StorageArbitraries.projectID(),
+            StorageArbitraries.alphaString().ofMinLength(1).ofMaxLength(10))
+        .as((p, t) -> TopicName.of(p.get(), t))
+        .map(tn -> "//pubsub.googleapis.com/" + tn.toString());
+  }
+
+  @NonNull
+  private static Arbitrary<NotificationConfigName> notificationName() {
+    return Combinators.combine(
+            StorageArbitraries.buckets().name(), StorageArbitraries.alphaString().ofMinLength(1))
+        .as(
+            (bucket, notification) ->
+                NotificationConfigName.of(bucket.getProject(), bucket.getBucket(), notification));
+  }
+}
diff --git a/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider b/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider
index 2014e4ac..2fcbe230 100644
--- a/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider
+++ b/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider
@@ -19,3 +19,4 @@ com.google.cloud.storage.jqwik.BucketArbitraryProvider
 com.google.cloud.storage.jqwik.HmacKeyMetadataArbitraryProvider
 com.google.cloud.storage.jqwik.ServiceAccountArbitraryProvider
 com.google.cloud.storage.jqwik.IamPolicyArbitraryProvider
+com.google.cloud.storage.jqwik.NotificationArbitraryProvider