Repository: googleapis/java-storage
Pull request #1965 (2023-04-04 02:20:46)

feat: Add matchGlob parameter to BlobListOption
This is a new feature (b/236167515) where List Objects allows filtering results based on a provided glob pattern.

Currently only the JSON API supports the matchGlob parameter, so using the option with gRPC will throw an exception. gRPC support is planned for later.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc9a959a..6c0e8376 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,29 @@
 # Changelog
 
+## [2.21.0](https://github.com/googleapis/java-storage/compare/v2.20.2...v2.21.0) (2023-03-31)
+
+
+### Features
+
+* Remove Bucket.retention_period, now that we've migrated to retention_duration ([#1955](https://github.com/googleapis/java-storage/issues/1955)) ([c0ffe7c](https://github.com/googleapis/java-storage/commit/c0ffe7c7cb50181cc375a945b9e79f97da9aa290))
+
+
+### Bug Fixes
+
+* Update bucket creation to set project as `bucket.project` ([#1912](https://github.com/googleapis/java-storage/issues/1912)) ([65993c0](https://github.com/googleapis/java-storage/commit/65993c0614987b96962ff28404de2aaea4ee679d))
+
+
+### Dependencies
+
+* Update dependency com.google.cloud:google-cloud-shared-dependencies to v3.6.0 ([#1957](https://github.com/googleapis/java-storage/issues/1957)) ([af90f16](https://github.com/googleapis/java-storage/commit/af90f162ac038479a0f09fb45289b1aa258d1aec))
+* Update dependency net.jqwik:jqwik to v1.7.3 ([#1944](https://github.com/googleapis/java-storage/issues/1944)) ([a8b96c9](https://github.com/googleapis/java-storage/commit/a8b96c9a2da7fbdf9de5e15e833058117570c24e))
+
+
+### Documentation
+
+* Revert ChecksummedData message definition not to specify ctype=CORD, ([#1960](https://github.com/googleapis/java-storage/issues/1960)) ([3a2a643](https://github.com/googleapis/java-storage/commit/3a2a643ff389741efaad244bbabf494dcd9a799b))
+* Updated ChecksummedData message definition to specify ctype=CORD, and removed incorrect earlier attempt that set that annotation in the ReadObjectResponse message definition ([#1959](https://github.com/googleapis/java-storage/issues/1959)) ([3d97bdd](https://github.com/googleapis/java-storage/commit/3d97bddc668deb2059261260125f94c6e32aac76))
+
 ## [2.20.2](https://github.com/googleapis/java-storage/compare/v2.20.1...v2.20.2) (2023-03-22)
 
 
diff --git a/README.md b/README.md
index 8eb886d8..f67cca0e 100644
--- a/README.md
+++ b/README.md
@@ -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>
 
 ```
@@ -57,13 +57,13 @@ 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/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index 0faeb0e4..bce70a1d 100644
--- a/gapic-google-cloud-storage-v2/pom.xml
+++ b/gapic-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>gapic-google-cloud-storage-v2</artifactId>
-  <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
   <name>gapic-google-cloud-storage-v2</name>
   <description>GRPC library for gapic-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml
index d9fb2e61..6eda687b 100644
--- a/google-cloud-storage-bom/pom.xml
+++ b/google-cloud-storage-bom/pom.xml
@@ -19,7 +19,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-bom</artifactId>
-  <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>pom</packaging>
   <parent>
     <groupId>com.google.cloud</groupId>
@@ -69,22 +69,22 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+        <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index 74b01062..074c095a 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Storage</name>
   <url>https://github.com/googleapis/java-storage</url>
@@ -12,11 +12,11 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </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/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index 255b7dab..9acc55f9 100644
--- a/grpc-google-cloud-storage-v2/pom.xml
+++ b/grpc-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-v2</artifactId>
-  <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
   <name>grpc-google-cloud-storage-v2</name>
   <description>GRPC library for grpc-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/pom.xml b/pom.xml
index a8230652..c4af20ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -70,7 +70,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
@@ -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>
@@ -111,17 +111,17 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index cf827417..6fdfdbee 100644
--- a/proto-google-cloud-storage-v2/pom.xml
+++ b/proto-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-v2</artifactId>
-  <version>2.20.3-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.21.1-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
   <name>proto-google-cloud-storage-v2</name>
   <description>PROTO library for proto-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <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..a43d9d63 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.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 49c03caf..0f42fc23 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,7 +28,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.20.3-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.21.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
 
     <dependency>
@@ -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..32fee9e5 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.123.7</version>
+      <version>1.123.8</version>
       <scope>test</scope>
     </dependency>
   </dependencies>
diff --git a/versions.txt b/versions.txt
index fae6fe44..1ec74d83 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,7 +1,7 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.20.2:2.20.3-SNAPSHOT
-gapic-google-cloud-storage-v2:2.20.2-alpha:2.20.3-alpha-SNAPSHOT
-grpc-google-cloud-storage-v2:2.20.2-alpha:2.20.3-alpha-SNAPSHOT
-proto-google-cloud-storage-v2:2.20.2-alpha:2.20.3-alpha-SNAPSHOT
+google-cloud-storage:2.21.0:2.21.1-SNAPSHOT
+gapic-google-cloud-storage-v2:2.21.0-alpha:2.21.1-alpha-SNAPSHOT
+grpc-google-cloud-storage-v2:2.21.0-alpha:2.21.1-alpha-SNAPSHOT
+proto-google-cloud-storage-v2:2.21.0-alpha:2.21.1-alpha-SNAPSHOT


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