Repository: googleapis/java-storage
Pull request #1303 (2022-03-18 18:18:59)

docs: adjust retry settings for hmac samples
Adjust retry settings for HMAC samples to make sure that these requests are getting retried. 

This was also needed in Go (See https://github.com/GoogleCloudPlatform/golang-samples/blob/main/storage/service_account/hmac/hmac_test.go#L195)

Should fix https://github.com/googleapis/java-storage/issues/1302


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 25079b25..525b0704 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,37 @@
 # Changelog
 
+### [2.5.1](https://github.com/googleapis/java-storage/compare/v2.5.0...v2.5.1) (2022-03-28)
+
+
+### Dependencies
+
+* update dependency com.google.cloud:google-cloud-pubsub to v1.116.2 ([#1310](https://github.com/googleapis/java-storage/issues/1310)) ([fb64493](https://github.com/googleapis/java-storage/commit/fb644932d4350b4e33481abda8cc8f498f9da85e))
+
+## [2.5.0](https://github.com/googleapis/java-storage/compare/v2.4.5...v2.5.0) (2022-03-25)
+
+
+### Features
+
+* allow limiting ReadChannel ([#1180](https://github.com/googleapis/java-storage/issues/1180)) ([2898ee8](https://github.com/googleapis/java-storage/commit/2898ee88545a93916d55c969fd0838e4fc703912))
+* expose the methods of Notifications ([#399](https://github.com/googleapis/java-storage/issues/399)) ([0bd17b1](https://github.com/googleapis/java-storage/commit/0bd17b1f70e47081941a44f018e3098b37ba2c47))
+
+
+### Documentation
+
+* Adding Samples for printing all Acls for a file and for a specific user ([#1288](https://github.com/googleapis/java-storage/issues/1288)) ([32fe388](https://github.com/googleapis/java-storage/commit/32fe388c8733cb237fc2a5b4676e36df76ef0dff))
+* Copy all storage samples from java-docs-samples ([#1258](https://github.com/googleapis/java-storage/issues/1258)) ([48b99be](https://github.com/googleapis/java-storage/commit/48b99beb692f529bea7e3de7ea5a36606876d96a))
+* **sample:** Add Native Image sample for Storage ([#1283](https://github.com/googleapis/java-storage/issues/1283)) ([375874d](https://github.com/googleapis/java-storage/commit/375874d38fc46bfec2df4c58e7d661c4f1f6c486))
+
+
+### Dependencies
+
+* allow snapshot to update properly ([#1311](https://github.com/googleapis/java-storage/issues/1311)) ([a5d32f2](https://github.com/googleapis/java-storage/commit/a5d32f2945247f76a21b030300a6e037084231b5))
+* update dependency com.google.cloud:native-image-support to v0.12.10 ([#1295](https://github.com/googleapis/java-storage/issues/1295)) ([3b3ecce](https://github.com/googleapis/java-storage/commit/3b3ecce262a3c7c95fbf0ddf3a5830a116022053))
+* update dependency org.graalvm.buildtools:junit-platform-native to v0.9.10 ([#1296](https://github.com/googleapis/java-storage/issues/1296)) ([6f1b142](https://github.com/googleapis/java-storage/commit/6f1b1423d1de6aef9aedbf6b89ce42bbc72196e9))
+* update dependency org.graalvm.buildtools:junit-platform-native to v0.9.11 ([#1306](https://github.com/googleapis/java-storage/issues/1306)) ([1527ba0](https://github.com/googleapis/java-storage/commit/1527ba0abad38acd55542ce92214d5c66a9c62ee))
+* update dependency org.graalvm.buildtools:native-maven-plugin to v0.9.10 ([#1297](https://github.com/googleapis/java-storage/issues/1297)) ([3f64f11](https://github.com/googleapis/java-storage/commit/3f64f117be7b7150a7a89c5240f09350d1add578))
+* update dependency org.graalvm.buildtools:native-maven-plugin to v0.9.11 ([#1307](https://github.com/googleapis/java-storage/issues/1307)) ([e45ae07](https://github.com/googleapis/java-storage/commit/e45ae0783bd9e0bea7e8accee1437dee4e974333))
+
 ### [2.4.5](https://github.com/googleapis/java-storage/compare/v2.4.4...v2.4.5) (2022-03-04)
 
 
diff --git a/README.md b/README.md
index 79f28764..5f38caab 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@ If you are using Maven without BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.4.5</version>
+  <version>2.5.1</version>
 </dependency>
 
 ```
@@ -56,13 +56,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.4.5'
+implementation 'com.google.cloud:google-cloud-storage:2.5.1'
 ```
 
 If you are using SBT, add this to your dependencies
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.4.5"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.5.1"
 ```
 
 ## Authentication
diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index 39cc8785..de9b294c 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.4.6-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.5.2-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.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index c45b439c..8374be3f 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -11,4 +11,29 @@
         <method>BucketInfo$LifecycleRule$LifecycleAction()</method>
         <differenceType>7004</differenceType>
     </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>*.Notification createNotification(*.String, *.NotificationInfo)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>*.Notification getNotification(*.String, *.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>java.util.List listNotifications(*.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>boolean deleteNotification(*.String, *.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/spi/v1/StorageRpc</className>
+        <method>*.model.Notification getNotification(*.String, *.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
 </differences>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index bddc0860..1b37b997 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.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.5.2-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,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
@@ -147,6 +147,11 @@
       <artifactId>truth</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.cloud</groupId>
+      <artifactId>google-cloud-pubsub</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.easymock</groupId>
       <artifactId>easymock</artifactId>
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
new file mode 100644
index 00000000..7f9107a5
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Notification.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.cloud.storage;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * The class representing Pub/Sub notifications for the Storage. See <a
+ * href="https://cloud.google.com/storage/docs/pubsub-notifications">pubsub-notifications</a> for
+ * details.
+ */
+public class Notification extends NotificationInfo {
+
+  private final StorageOptions options;
+  private transient Storage storage;
+
+  /** Builder for {@code Notification}. */
+  public static class Builder extends NotificationInfo.Builder {
+    private final Storage storage;
+    private final NotificationInfo.BuilderImpl infoBuilder;
+
+    Builder(Notification notification) {
+      this.storage = notification.storage;
+      this.infoBuilder = new NotificationInfo.BuilderImpl(notification);
+    }
+
+    @Override
+    Builder setNotificationId(String notificationId) {
+      infoBuilder.setNotificationId(notificationId);
+      return this;
+    }
+
+    @Override
+    public Builder setSelfLink(String selfLink) {
+      infoBuilder.setSelfLink(selfLink);
+      return this;
+    }
+
+    @Override
+    public Builder setTopic(String topic) {
+      infoBuilder.setTopic(topic);
+      return this;
+    }
+
+    @Override
+    public Builder setPayloadFormat(PayloadFormat payloadFormat) {
+      infoBuilder.setPayloadFormat(payloadFormat);
+      return this;
+    }
+
+    @Override
+    public Builder setObjectNamePrefix(String objectNamePrefix) {
+      infoBuilder.setObjectNamePrefix(objectNamePrefix);
+      return this;
+    }
+
+    @Override
+    public Builder setEventTypes(EventType... eventTypes) {
+      infoBuilder.setEventTypes(eventTypes);
+      return this;
+    }
+
+    @Override
+    public Builder setEtag(String etag) {
+      infoBuilder.setEtag(etag);
+      return this;
+    }
+
+    @Override
+    public Builder setCustomAttributes(Map<String, String> customAttributes) {
+      infoBuilder.setCustomAttributes(customAttributes);
+      return this;
+    }
+
+    @Override
+    public Notification build() {
+      return new Notification(storage, infoBuilder);
+    }
+  }
+
+  Notification(Storage storage, NotificationInfo.BuilderImpl infoBuilder) {
+    super(infoBuilder);
+    this.storage = checkNotNull(storage);
+    this.options = storage.getOptions();
+  }
+
+  /** Returns the notification's {@code Storage} object used to issue requests. */
+  public Storage getStorage() {
+    return storage;
+  }
+
+  @Override
+  public Builder toBuilder() {
+    return new Notification.Builder(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    Notification notification = (Notification) o;
+    return Objects.equals(toPb(), notification.toPb())
+        && Objects.equals(options, notification.options);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), options, storage);
+  }
+
+  static Notification fromPb(
+      Storage storage, com.google.api.services.storage.model.Notification notificationPb) {
+    return new Notification(
+        storage, new NotificationInfo.BuilderImpl(NotificationInfo.fromPb(notificationPb)));
+  }
+}
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
new file mode 100644
index 00000000..df986ce2
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.api.pathtemplate.PathTemplate;
+import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** The class representing Pub/Sub Notification metadata for the Storage. */
+public class NotificationInfo implements Serializable {
+
+  private static final long serialVersionUID = 5725883368559753810L;
+  private static final PathTemplate PATH_TEMPLATE =
+      PathTemplate.createWithoutUrlEncoding("projects/{project}/topics/{topic}");
+  static final Function<com.google.api.services.storage.model.Notification, NotificationInfo>
+      FROM_PB_FUNCTION =
+          new Function<com.google.api.services.storage.model.Notification, NotificationInfo>() {
+            @Override
+            public NotificationInfo apply(com.google.api.services.storage.model.Notification pb) {
+              return NotificationInfo.fromPb(pb);
+            }
+          };
+  static final Function<NotificationInfo, com.google.api.services.storage.model.Notification>
+      TO_PB_FUNCTION =
+          new Function<NotificationInfo, com.google.api.services.storage.model.Notification>() {
+            @Override
+            public com.google.api.services.storage.model.Notification apply(
+                NotificationInfo NotificationInfo) {
+              return NotificationInfo.toPb();
+            }
+          };
+
+  public enum PayloadFormat {
+    JSON_API_V1,
+    NONE
+  }
+
+  public enum EventType {
+    OBJECT_FINALIZE,
+    OBJECT_METADATA_UPDATE,
+    OBJECT_DELETE,
+    OBJECT_ARCHIVE
+  }
+
+  private final String notificationId;
+  private final String topic;
+  private final List<EventType> eventTypes;
+  private final Map<String, String> customAttributes;
+  private final PayloadFormat payloadFormat;
+  private final String objectNamePrefix;
+  private final String etag;
+  private final String selfLink;
+
+  /** Builder for {@code NotificationInfo}. */
+  public abstract static class Builder {
+    Builder() {}
+
+    abstract Builder setNotificationId(String notificationId);
+
+    public abstract Builder setSelfLink(String selfLink);
+
+    public abstract Builder setTopic(String topic);
+
+    public abstract Builder setPayloadFormat(PayloadFormat payloadFormat);
+
+    public abstract Builder setObjectNamePrefix(String objectNamePrefix);
+
+    public abstract Builder setEventTypes(EventType... eventTypes);
+
+    public abstract Builder setEtag(String etag);
+
+    public abstract Builder setCustomAttributes(Map<String, String> customAttributes);
+
+    /** Creates a {@code NotificationInfo} object. */
+    public abstract NotificationInfo build();
+  }
+
+  /** Builder for {@code NotificationInfo}. */
+  public static class BuilderImpl extends Builder {
+
+    private String notificationId;
+    private String topic;
+    private List<EventType> eventTypes;
+    private Map<String, String> customAttributes;
+    private PayloadFormat payloadFormat;
+    private String objectNamePrefix;
+    private String etag;
+    private String selfLink;
+
+    BuilderImpl(String topic) {
+      this.topic = topic;
+    }
+
+    BuilderImpl(NotificationInfo notificationInfo) {
+      notificationId = notificationInfo.notificationId;
+      etag = notificationInfo.etag;
+      selfLink = notificationInfo.selfLink;
+      topic = notificationInfo.topic;
+      eventTypes = notificationInfo.eventTypes;
+      customAttributes = notificationInfo.customAttributes;
+      payloadFormat = notificationInfo.payloadFormat;
+      objectNamePrefix = notificationInfo.objectNamePrefix;
+    }
+
+    @Override
+    Builder setNotificationId(String notificationId) {
+      this.notificationId = notificationId;
+      return this;
+    }
+
+    @Override
+    public Builder setSelfLink(String selfLink) {
+      this.selfLink = selfLink;
+      return this;
+    }
+
+    /** Sets a topic in the format of "projects/{project}/topics/{topic}". */
+    @Override
+    public Builder setTopic(String topic) {
+      this.topic = topic;
+      return this;
+    }
+
+    @Override
+    public Builder setPayloadFormat(PayloadFormat payloadFormat) {
+      this.payloadFormat = payloadFormat;
+      return this;
+    }
+
+    @Override
+    public Builder setObjectNamePrefix(String objectNamePrefix) {
+      this.objectNamePrefix = objectNamePrefix;
+      return this;
+    }
+
+    @Override
+    public Builder setEventTypes(EventType... eventTypes) {
+      this.eventTypes = eventTypes != null ? Arrays.asList(eventTypes) : null;
+      return this;
+    }
+
+    @Override
+    public Builder setEtag(String etag) {
+      this.etag = etag;
+      return this;
+    }
+
+    @Override
+    public Builder setCustomAttributes(Map<String, String> customAttributes) {
+      this.customAttributes =
+          customAttributes != null ? ImmutableMap.copyOf(customAttributes) : null;
+      return this;
+    }
+
+    public NotificationInfo build() {
+      checkNotNull(topic);
+      checkTopicFormat(topic);
+      return new NotificationInfo(this);
+    }
+  }
+
+  NotificationInfo(BuilderImpl builder) {
+    notificationId = builder.notificationId;
+    etag = builder.etag;
+    selfLink = builder.selfLink;
+    topic = builder.topic;
+    eventTypes = builder.eventTypes;
+    customAttributes = builder.customAttributes;
+    payloadFormat = builder.payloadFormat;
+    objectNamePrefix = builder.objectNamePrefix;
+  }
+
+  /** Returns the service-generated id for the notification. */
+  public String getNotificationId() {
+    return notificationId;
+  }
+
+  /** Returns the topic in Pub/Sub that receives notifications. */
+  public String getTopic() {
+    return topic;
+  }
+
+  /** Returns the canonical URI of this topic as a string. */
+  public String getSelfLink() {
+    return selfLink;
+  }
+
+  /** Returns the desired content of the Payload. */
+  public NotificationInfo.PayloadFormat getPayloadFormat() {
+    return payloadFormat;
+  }
+
+  /** Returns the object name prefix for which this notification configuration applies. */
+  public String getObjectNamePrefix() {
+    return objectNamePrefix;
+  }
+
+  /**
+   * Returns HTTP 1.1 Entity tag for the notification. See <a
+   * href="http://tools.ietf.org/html/rfc2616#section-3.11">Entity Tags</a>
+   */
+  public String getEtag() {
+    return etag;
+  }
+
+  /**
+   * Returns the events that trigger a notification to be sent. If empty, notifications are
+   * triggered by any event. See <a
+   * href="https://cloud.google.com/storage/docs/pubsub-notifications#events">Event types</a> to get
+   * list of available events.
+   */
+  public List<NotificationInfo.EventType> getEventTypes() {
+    return eventTypes;
+  }
+
+  /**
+   * Returns the list of additional attributes to attach to each Cloud PubSub message published for
+   * this notification subscription.
+   */
+  public Map<String, String> getCustomAttributes() {
+    return customAttributes;
+  }
+
+  @Override
+  public int hashCode() {
+    return toPb().hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    return obj == this
+        || obj != null
+            && obj.getClass().equals(NotificationInfo.class)
+            && Objects.equals(toPb(), ((NotificationInfo) obj).toPb());
+  }
+
+  @Override
+  public String toString() {
+    return MoreObjects.toStringHelper(this).add("topic", topic).toString();
+  }
+
+  com.google.api.services.storage.model.Notification toPb() {
+    com.google.api.services.storage.model.Notification notificationPb =
+        new com.google.api.services.storage.model.Notification();
+    if (notificationId != null) {
+      notificationPb.setId(notificationId);
+    }
+    notificationPb.setEtag(etag);
+    if (customAttributes != null) {
+      notificationPb.setCustomAttributes(customAttributes);
+    }
+    if (eventTypes != null && eventTypes.size() > 0) {
+      List<String> eventTypesPb = new ArrayList<>();
+      for (EventType eventType : eventTypes) {
+        eventTypesPb.add(eventType.toString());
+      }
+      notificationPb.setEventTypes(eventTypesPb);
+    }
+    if (objectNamePrefix != null) {
+      notificationPb.setObjectNamePrefix(objectNamePrefix);
+    }
+    if (payloadFormat != null) {
+      notificationPb.setPayloadFormat(payloadFormat.toString());
+    } else {
+      notificationPb.setPayloadFormat(PayloadFormat.NONE.toString());
+    }
+    notificationPb.setSelfLink(selfLink);
+    notificationPb.setTopic(topic);
+
+    return notificationPb;
+  }
+
+  /**
+   * Creates a {@code NotificationInfo} object for the provided topic.
+   *
+   * <p>Example of creating the NotificationInfo object:
+   *
+   * <pre>{@code
+   * String topic = "projects/myProject/topics/myTopic"
+   * NotificationInfo notificationInfo = NotificationInfo.of(topic)
+   * }</pre>
+   *
+   * @param topic a string in the format "projects/{project}/topics/{topic}"
+   */
+  public static NotificationInfo of(String topic) {
+    checkTopicFormat(topic);
+    return newBuilder(topic).build();
+  }
+  /**
+   * Creates a {@code NotificationInfo} object for the provided topic.
+   *
+   * @param topic a string in the format "projects/{project}/topics/{topic}"
+   */
+  public static Builder newBuilder(String topic) {
+    checkTopicFormat(topic);
+    return new BuilderImpl(topic);
+  }
+
+  /** Returns a builder for the current notification. */
+  public Builder toBuilder() {
+    return new BuilderImpl(this);
+  }
+
+  static NotificationInfo fromPb(
+      com.google.api.services.storage.model.Notification notificationPb) {
+    NotificationInfo.Builder builder = new NotificationInfo.BuilderImpl(notificationPb.getTopic());
+    if (notificationPb.getId() != null) {
+      builder.setNotificationId(notificationPb.getId());
+    }
+    if (notificationPb.getEtag() != null) {
+      builder.setEtag(notificationPb.getEtag());
+    }
+    if (notificationPb.getCustomAttributes() != null) {
+      builder.setCustomAttributes(notificationPb.getCustomAttributes());
+    }
+    if (notificationPb.getSelfLink() != null) {
+      builder.setSelfLink(notificationPb.getSelfLink());
+    }
+    if (notificationPb.getObjectNamePrefix() != null) {
+      builder.setObjectNamePrefix(notificationPb.getObjectNamePrefix());
+    }
+    if (notificationPb.getEventTypes() != null) {
+      List<String> eventTypesPb = notificationPb.getEventTypes();
+      EventType[] eventTypes = new EventType[eventTypesPb.size()];
+      for (int index = 0; index < eventTypesPb.size(); index++) {
+        eventTypes[index] = EventType.valueOf(eventTypesPb.get(index));
+      }
+      builder.setEventTypes(eventTypes);
+    }
+    if (notificationPb.getPayloadFormat() != null) {
+      builder.setPayloadFormat(PayloadFormat.valueOf(notificationPb.getPayloadFormat()));
+    }
+    return builder.build();
+  }
+
+  private static void checkTopicFormat(String topic) {
+    PATH_TEMPLATE.validatedMatch(topic, "topic name must be in valid format");
+  }
+}
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 21185994..634155f2 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
@@ -3625,4 +3625,84 @@ public interface Storage extends Service<StorageOptions> {
    * @throws StorageException upon failure
    */
   ServiceAccount getServiceAccount(String projectId);
+
+  /**
+   * Creates the notification for a given bucket.
+   *
+   * <p>Example of creating a notification:
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String topic = "projects/myProject/topics/myTopic"
+   * NotificationInfo notificationInfo = NotificationInfo.newBuilder(topic)
+   *  .setCustomAttributes(ImmutableMap.of("label1", "value1"))
+   *  .setEventTypes(NotificationInfo.EventType.OBJECT_FINALIZE)
+   *  .setPayloadFormat(NotificationInfo.PayloadFormat.JSON_API_V1)
+   *  .build();
+   * Notification notification = storage.createNotification(bucketName, notificationInfo);
+   * }</pre>
+   *
+   * @param bucket name of the bucket
+   * @param notificationInfo notification to create
+   * @return the created notification
+   * @throws StorageException upon failure
+   */
+  Notification createNotification(String bucket, NotificationInfo notificationInfo);
+
+  /**
+   * Gets the notification with the specified id.
+   *
+   * <p>Example of getting the notification:
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String notificationId = "my-unique-notification-id";
+   * Notification notification = storage.getNotification(bucketName, notificationId);
+   * }</pre>
+   *
+   * @param bucket name of the bucket
+   * @param notificationId notification ID
+   * @return the {@code Notification} object with the given id or {@code null} if not found
+   * @throws StorageException upon failure
+   */
+  Notification getNotification(String bucket, String notificationId);
+
+  /**
+   * Retrieves the list of notifications associated with the bucket.
+   *
+   * <p>Example of listing the bucket notifications:
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * List<Notification> notifications = storage.listNotifications(bucketName);
+   * }</pre>
+   *
+   * @param bucket name of the bucket
+   * @return a list of {@link Notification} objects added to the bucket.
+   * @throws StorageException upon failure
+   */
+  List<Notification> listNotifications(String bucket);
+
+  /**
+   * Deletes the notification with the specified id.
+   *
+   * <p>Example of deleting the notification:
+   *
+   * <pre>{@code
+   * String bucketName = "my-unique-bucket";
+   * String notificationId = "my-unique-notification-id";
+   * boolean deleted = storage.deleteNotification(bucketName, notificationId);
+   * if (deleted) {
+   *   // the notification was deleted
+   * } else {
+   *   // the notification was not found
+   * }
+   * }</pre>
+   *
+   * @param bucket name of the bucket
+   * @param notificationId ID of the notification to delete
+   * @return {@code true} if the notification has been deleted, {@code false} if not found
+   * @throws StorageException upon failure
+   */
+  boolean deleteNotification(String bucket, String notificationId);
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
index e8263767..874d997e 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
@@ -16,6 +16,7 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.RetryHelper.runWithRetries;
 import static com.google.cloud.storage.PolicyHelper.convertToApiPolicy;
 import static com.google.cloud.storage.SignedUrlEncodingHelper.Rfc3986UriEncode;
 import static com.google.cloud.storage.spi.v1.StorageRpc.Option.DELIMITER;
@@ -44,6 +45,7 @@ import com.google.cloud.PageImpl;
 import com.google.cloud.PageImpl.NextPageFetcher;
 import com.google.cloud.Policy;
 import com.google.cloud.ReadChannel;
+import com.google.cloud.RetryHelper.RetryHelperException;
 import com.google.cloud.Tuple;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.storage.Acl.Entity;
@@ -1374,6 +1376,98 @@ final class StorageImpl extends BaseService<StorageOptions> implements Storage {
     return Retrying.run(getOptions(), algorithm, c, f);
   }
 
+  @Override
+  public Notification createNotification(
+      final String bucket, final NotificationInfo notificationInfo) {
+    final com.google.api.services.storage.model.Notification notificationPb =
+        notificationInfo.toPb();
+    try {
+      return Notification.fromPb(
+          this,
+          runWithRetries(
+              new Callable<com.google.api.services.storage.model.Notification>() {
+                @Override
+                public com.google.api.services.storage.model.Notification call() {
+                  return storageRpc.createNotification(bucket, notificationPb);
+                }
+              },
+              getOptions().getRetrySettings(),
+              EXCEPTION_HANDLER,
+              getOptions().getClock()));
+    } catch (RetryHelperException e) {
+      throw StorageException.translateAndThrow(e);
+    }
+  }
+
+  @Override
+  public Notification getNotification(final String bucket, final String notificationId) {
+    try {
+      com.google.api.services.storage.model.Notification answer =
+          runWithRetries(
+              new Callable<com.google.api.services.storage.model.Notification>() {
+                @Override
+                public com.google.api.services.storage.model.Notification call() {
+                  return storageRpc.getNotification(bucket, notificationId);
+                }
+              },
+              getOptions().getRetrySettings(),
+              EXCEPTION_HANDLER,
+              getOptions().getClock());
+      return answer == null ? null : Notification.fromPb(this, answer);
+    } catch (RetryHelperException e) {
+      throw StorageException.translateAndThrow(e);
+    }
+  }
+
+  @Override
+  public List<Notification> listNotifications(final String bucket) {
+    try {
+      List<com.google.api.services.storage.model.Notification> answer =
+          runWithRetries(
+              new Callable<List<com.google.api.services.storage.model.Notification>>() {
+                @Override
+                public List<com.google.api.services.storage.model.Notification> call() {
+                  return storageRpc.listNotifications(bucket);
+                }
+              },
+              getOptions().getRetrySettings(),
+              EXCEPTION_HANDLER,
+              getOptions().getClock());
+      return answer == null
+          ? ImmutableList.<Notification>of()
+          : Lists.transform(
+              answer,
+              new com.google.common.base.Function<
+                  com.google.api.services.storage.model.Notification, Notification>() {
+                @Override
+                public Notification apply(
+                    com.google.api.services.storage.model.Notification notificationPb) {
+                  return Notification.fromPb(getOptions().getService(), notificationPb);
+                }
+              });
+    } catch (RetryHelperException e) {
+      throw StorageException.translateAndThrow(e);
+    }
+  }
+
+  @Override
+  public boolean deleteNotification(final String bucket, final String notificationId) {
+    try {
+      return runWithRetries(
+          new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+              return storageRpc.deleteNotification(bucket, notificationId);
+            }
+          },
+          getOptions().getRetrySettings(),
+          EXCEPTION_HANDLER,
+          getOptions().getClock());
+    } catch (RetryHelperException e) {
+      throw StorageException.translateAndThrow(e);
+    }
+  }
+
   private static <T> void addToOptionMap(
       StorageRpc.Option option, T defaultValue, Map<StorageRpc.Option, Object> map) {
     addToOptionMap(option, option, defaultValue, map);
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 71b952c4..d0c1ff8a 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
@@ -1602,6 +1602,25 @@ public class HttpStorageRpc implements StorageRpc {
     }
   }
 
+  @Override
+  public Notification getNotification(String bucket, String notification) {
+    Span span = startSpan(HttpStorageRpcSpans.SPAN_NAME_GET_NOTIFICATION);
+    Scope scope = tracer.withSpan(span);
+    try {
+      return storage.notifications().get(bucket, notification).execute();
+    } catch (IOException ex) {
+      span.setStatus(Status.UNKNOWN.withDescription(ex.getMessage()));
+      StorageException serviceException = translate(ex);
+      if (serviceException.getCode() == HTTP_NOT_FOUND) {
+        return null;
+      }
+      throw serviceException;
+    } finally {
+      scope.close();
+      span.end();
+    }
+  }
+
   @Override
   public Bucket lockRetentionPolicy(Bucket bucket, Map<Option, ?> options) {
     Span span = startSpan(HttpStorageRpcSpans.SPAN_LOCK_RETENTION_POLICY);
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java
index 4e169d47..3f3d27d9 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/spi/v1/HttpStorageRpcSpans.java
@@ -89,6 +89,8 @@ class HttpStorageRpcSpans {
   static final String SPAN_NAME_LIST_NOTIFICATIONS = getTraceSpanName("listNotifications(String)");
   static final String SPAN_NAME_CREATE_NOTIFICATION =
       getTraceSpanName("createNotification(String,Notification)");
+  static final String SPAN_NAME_GET_NOTIFICATION =
+      getTraceSpanName("getNotification(String,String)");
   static final String SPAN_LOCK_RETENTION_POLICY =
       getTraceSpanName("lockRetentionPolicy(String,Long)");
   static final String SPAN_NAME_GET_SERVICE_ACCOUNT = getTraceSpanName("getServiceAccount(String)");
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 201078ef..c600723b 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
@@ -561,15 +561,15 @@ public interface StorageRpc extends ServiceRpc {
       String bucket, List<String> permissions, Map<Option, ?> options);
 
   /**
-   * Deletes the notification with the specified name on the specified object.
+   * Deletes the notification with the specified id on the bucket.
    *
-   * @return {@code true} if the notification was deleted, {@code false} if it was not found
+   * @return {@code true} if the notification has been deleted, {@code false} if not found
    * @throws StorageException upon failure
    */
-  boolean deleteNotification(String bucket, String notification);
+  boolean deleteNotification(String bucket, String id);
 
   /**
-   * List the notifications for the provided bucket.
+   * Retrieves the list of notifications associated with the bucket.
    *
    * @return a list of {@link Notification} objects that exist on the bucket.
    * @throws StorageException upon failure
@@ -577,13 +577,21 @@ public interface StorageRpc extends ServiceRpc {
   List<Notification> listNotifications(String bucket);
 
   /**
-   * Creates a notification with the specified entity on the specified bucket.
+   * Creates the notification for a given bucket.
    *
-   * @return the notification that was created.
+   * @return the created notification.
    * @throws StorageException upon failure
    */
   Notification createNotification(String bucket, Notification notification);
 
+  /**
+   * Gets the notification with the specified id.
+   *
+   * @return the {@code Notification} object with the given id or {@code null} if not found
+   * @throws StorageException upon failure
+   */
+  Notification getNotification(String bucket, String id);
+
   /**
    * Lock retention policy for the provided bucket.
    *
diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index 56e5f2ac..a800e6e6 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.4.6-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.5.2-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.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/pom.xml b/pom.xml
index 321877ad..516998e3 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.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -14,7 +14,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-shared-config</artifactId>
-    <version>1.3.0</version>
+    <version>1.3.2</version>
   </parent>
 
   <developers>
@@ -54,7 +54,7 @@
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <github.global.server>github</github.global.server>
     <site.installationModule>google-cloud-storage-parent</site.installationModule>
-    <google.cloud.shared-dependencies.version>2.8.0</google.cloud.shared-dependencies.version>
+    <google.cloud.shared-dependencies.version>2.9.0</google.cloud.shared-dependencies.version>
   </properties>
 
   <dependencyManagement>
@@ -70,13 +70,19 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
         <artifactId>google-api-services-storage</artifactId>
         <version>v1-rev20220210-1.32.1</version>
       </dependency>
+      <dependency>
+        <groupId>com.google.cloud</groupId>
+        <artifactId>google-cloud-pubsub</artifactId>
+        <version>1.116.2</version>
+        <scope>test</scope>
+      </dependency>
       <dependency>
         <groupId>org.easymock</groupId>
         <artifactId>easymock</artifactId>
@@ -105,22 +111,22 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.4.6-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.5.2-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.4.6-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.5.2-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.4.6-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.5.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-conformance-tests</artifactId>
-        <version>0.2.8</version>
+        <version>0.2.9</version>
         <scope>test</scope>
         <exclusions>
           <exclusion>
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index cc4144c0..8c194935 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.4.6-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.5.2-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.4.6-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.5.2-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 1c8498fd..16815493 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -29,7 +29,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.4.5</version>
+      <version>2.5.1</version>
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index 47585024..8d98f37e 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -118,7 +118,7 @@
         <dependency>
           <groupId>org.graalvm.buildtools</groupId>
           <artifactId>junit-platform-native</artifactId>
-          <version>0.9.10</version>
+          <version>0.9.11</version>
           <scope>test</scope>
         </dependency>
       </dependencies>
@@ -139,7 +139,7 @@
           <plugin>
             <groupId>org.graalvm.buildtools</groupId>
             <artifactId>native-maven-plugin</artifactId>
-            <version>0.9.10</version>
+            <version>0.9.11</version>
             <extensions>true</extensions>
             <configuration>
               <mainClass>com.example.storage.NativeImageStorageSample</mainClass>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 36ad6b43..51f5b8ae 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -23,12 +23,11 @@
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   </properties>
 
-  <!-- {x-version-update-start::current} -->
   <dependencies>
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.4.6-SNAPSHOT</version>
+      <version>2.5.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
 
     <dependency>
@@ -50,7 +49,6 @@
       <scope>test</scope>
     </dependency>
   </dependencies>
-  <!-- {x-version-update-end} -->
 
   <!-- compile and run all snippet tests -->
   <build>
diff --git a/versions.txt b/versions.txt
index 76a9cfc0..035cfeef 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,7 +1,7 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.4.5:2.4.6-SNAPSHOT
-gapic-google-cloud-storage-v2:2.4.5-alpha:2.4.6-alpha-SNAPSHOT
-grpc-google-cloud-storage-v2:2.4.5-alpha:2.4.6-alpha-SNAPSHOT
-proto-google-cloud-storage-v2:2.4.5-alpha:2.4.6-alpha-SNAPSHOT
+google-cloud-storage:2.5.1:2.5.2-SNAPSHOT
+gapic-google-cloud-storage-v2:2.5.1-alpha:2.5.2-alpha-SNAPSHOT
+grpc-google-cloud-storage-v2:2.5.1-alpha:2.5.2-alpha-SNAPSHOT
+proto-google-cloud-storage-v2:2.5.1-alpha:2.5.2-alpha-SNAPSHOT


Test patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java
index 43e29d01..81e7eee5 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/testing/StorageRpcTestBase.java
@@ -287,7 +287,7 @@ public class StorageRpcTestBase implements StorageRpc {
   }
 
   @Override
-  public boolean deleteNotification(String bucket, String notification) {
+  public boolean deleteNotification(String bucket, String id) {
     throw new UnsupportedOperationException("Not implemented yet");
   }
 
@@ -301,6 +301,11 @@ public class StorageRpcTestBase implements StorageRpc {
     throw new UnsupportedOperationException("Not implemented yet");
   }
 
+  @Override
+  public Notification getNotification(String bucket, String id) {
+    throw new UnsupportedOperationException("Not implemented yet");
+  }
+
   @Override
   public Bucket lockRetentionPolicy(Bucket bucket, Map<Option, ?> options) {
     throw new UnsupportedOperationException("Not implemented yet");
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoTest.java
new file mode 100644
index 00000000..fd50120a
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.google.cloud.storage.NotificationInfo.EventType;
+import com.google.cloud.storage.NotificationInfo.PayloadFormat;
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Map;
+import org.junit.Test;
+
+public class NotificationInfoTest {
+  private static final String ETAG = "0xFF00";
+  private static final String SELF_LINK = "http://storage/b/n";
+  private static final String OBJECT_NAME_PREFIX = "index.html";
+  private static final String TOPIC = "projects/myProject/topics/topic1";
+  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
+  private static final PayloadFormat PAYLOAD_FORMAT = PayloadFormat.JSON_API_V1.JSON_API_V1;
+  private static final EventType[] EVENT_TYPES = {
+    EventType.OBJECT_FINALIZE, EventType.OBJECT_METADATA_UPDATE
+  };
+  private static final NotificationInfo NOTIFICATION_INFO =
+      NotificationInfo.newBuilder(TOPIC)
+          .setEtag(ETAG)
+          .setCustomAttributes(CUSTOM_ATTRIBUTES)
+          .setSelfLink(SELF_LINK)
+          .setEventTypes(EVENT_TYPES)
+          .setObjectNamePrefix(OBJECT_NAME_PREFIX)
+          .setPayloadFormat(PAYLOAD_FORMAT)
+          .build();
+
+  @Test
+  public void testToBuilder() {
+    compareBucketsNotification(NOTIFICATION_INFO, NOTIFICATION_INFO.toBuilder().build());
+    NotificationInfo notificationInfo = NOTIFICATION_INFO.toBuilder().setTopic(TOPIC).build();
+    assertEquals(TOPIC, notificationInfo.getTopic());
+    notificationInfo = notificationInfo.toBuilder().setTopic(TOPIC).build();
+    compareBucketsNotification(NOTIFICATION_INFO, notificationInfo);
+  }
+
+  @Test
+  public void testToBuilderIncomplete() {
+    NotificationInfo incompleteNotificationInfo = Notification.newBuilder(TOPIC).build();
+    compareBucketsNotification(
+        incompleteNotificationInfo, incompleteNotificationInfo.toBuilder().build());
+  }
+
+  @Test
+  public void testOf() {
+    NotificationInfo notificationInfo = NotificationInfo.of(TOPIC);
+    assertEquals(TOPIC, notificationInfo.getTopic());
+    assertNull(notificationInfo.getNotificationId());
+    assertNull(notificationInfo.getCustomAttributes());
+    assertNull(notificationInfo.getEtag());
+    assertNull(notificationInfo.getSelfLink());
+    assertNull(notificationInfo.getEventTypes());
+    assertNull(notificationInfo.getObjectNamePrefix());
+    assertNull(notificationInfo.getPayloadFormat());
+  }
+
+  @Test
+  public void testBuilder() {
+    assertEquals(ETAG, NOTIFICATION_INFO.getEtag());
+    assertNull(NOTIFICATION_INFO.getNotificationId());
+    assertEquals(SELF_LINK, NOTIFICATION_INFO.getSelfLink());
+    assertEquals(OBJECT_NAME_PREFIX, NOTIFICATION_INFO.getObjectNamePrefix());
+    assertEquals(PAYLOAD_FORMAT, NOTIFICATION_INFO.getPayloadFormat());
+    assertEquals(TOPIC, NOTIFICATION_INFO.getTopic());
+    assertEquals(CUSTOM_ATTRIBUTES, NOTIFICATION_INFO.getCustomAttributes());
+    assertEquals(Arrays.asList(EVENT_TYPES), NOTIFICATION_INFO.getEventTypes());
+  }
+
+  @Test
+  public void testToPbAndFromPb() {
+    compareBucketsNotification(
+        NOTIFICATION_INFO, NotificationInfo.fromPb(NOTIFICATION_INFO.toPb()));
+    NotificationInfo notificationInfo =
+        NotificationInfo.of(TOPIC).toBuilder().setPayloadFormat(PayloadFormat.NONE).build();
+    compareBucketsNotification(notificationInfo, Notification.fromPb(notificationInfo.toPb()));
+  }
+
+  private void compareBucketsNotification(NotificationInfo expected, NotificationInfo actual) {
+    assertEquals(expected, actual);
+    assertEquals(expected.getNotificationId(), actual.getNotificationId());
+    assertEquals(expected.getCustomAttributes(), actual.getCustomAttributes());
+    assertEquals(expected.getEtag(), actual.getEtag());
+    assertEquals(expected.getSelfLink(), actual.getSelfLink());
+    assertEquals(expected.getEventTypes(), actual.getEventTypes());
+    assertEquals(expected.getObjectNamePrefix(), actual.getObjectNamePrefix());
+    assertEquals(expected.getPayloadFormat(), actual.getPayloadFormat());
+    assertEquals(expected.getTopic(), actual.getTopic());
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationTest.java
new file mode 100644
index 00000000..2f1f7eb3
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2020 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+import com.google.cloud.storage.NotificationInfo.EventType;
+import com.google.cloud.storage.NotificationInfo.PayloadFormat;
+import com.google.common.collect.ImmutableMap;
+import java.util.Arrays;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class NotificationTest {
+
+  private static final String ETAG = "0xFF00";
+  private static final String SELF_LINK = "http://storage/b/n";
+  private static final String OBJECT_NAME_PREFIX = "index.html";
+  private static final String TOPIC = "projects/myProject/topics/topic1";
+  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
+  private static final PayloadFormat PAYLOAD_FORMAT = PayloadFormat.JSON_API_V1.JSON_API_V1;
+  private static final EventType[] EVENT_TYPES = {
+    EventType.OBJECT_FINALIZE, EventType.OBJECT_METADATA_UPDATE
+  };
+  private static final NotificationInfo NOTIFICATION_INFO =
+      NotificationInfo.newBuilder(TOPIC)
+          .setEtag(ETAG)
+          .setCustomAttributes(CUSTOM_ATTRIBUTES)
+          .setSelfLink(SELF_LINK)
+          .setEventTypes(EVENT_TYPES)
+          .setObjectNamePrefix(OBJECT_NAME_PREFIX)
+          .setPayloadFormat(PAYLOAD_FORMAT)
+          .build();
+
+  private Storage storage;
+  private StorageOptions mockOptions = createMock(StorageOptions.class);
+
+  @Before
+  public void setUp() {
+    storage = createStrictMock(Storage.class);
+  }
+
+  @After
+  public void tearDown() {
+    verify(storage);
+  }
+
+  @Test
+  public void testBuilder() {
+    expect(storage.getOptions()).andReturn(mockOptions).times(2);
+    replay(storage);
+    Notification.Builder builder =
+        new Notification.Builder(
+            new Notification(storage, new NotificationInfo.BuilderImpl(NOTIFICATION_INFO)));
+    Notification notification =
+        builder
+            .setEtag(ETAG)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setSelfLink(SELF_LINK)
+            .setEventTypes(EVENT_TYPES)
+            .setObjectNamePrefix(OBJECT_NAME_PREFIX)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
+    assertEquals(ETAG, notification.getEtag());
+    assertEquals(SELF_LINK, notification.getSelfLink());
+    assertEquals(OBJECT_NAME_PREFIX, notification.getObjectNamePrefix());
+    assertEquals(PAYLOAD_FORMAT, notification.getPayloadFormat());
+    assertEquals(TOPIC, notification.getTopic());
+    assertEquals(CUSTOM_ATTRIBUTES, notification.getCustomAttributes());
+    assertEquals(Arrays.asList(EVENT_TYPES), notification.getEventTypes());
+  }
+
+  @Test
+  public void testToBuilder() {
+    expect(storage.getOptions()).andReturn(mockOptions).times(2);
+    replay(storage);
+    Notification notification =
+        new Notification(storage, new NotificationInfo.BuilderImpl(NOTIFICATION_INFO));
+    compareBucketNotification(notification, notification.toBuilder().build());
+  }
+
+  @Test
+  public void testFromPb() {
+    expect(storage.getOptions()).andReturn(mockOptions).times(1);
+    replay(storage);
+    compareBucketNotification(
+        NOTIFICATION_INFO, Notification.fromPb(storage, NOTIFICATION_INFO.toPb()));
+  }
+
+  private void compareBucketNotification(NotificationInfo expected, NotificationInfo actual) {
+    assertEquals(expected.getNotificationId(), actual.getNotificationId());
+    assertEquals(expected.getCustomAttributes(), actual.getCustomAttributes());
+    assertEquals(expected.getEtag(), actual.getEtag());
+    assertEquals(expected.getSelfLink(), actual.getSelfLink());
+    assertEquals(expected.getEventTypes(), actual.getEventTypes());
+    assertEquals(expected.getObjectNamePrefix(), actual.getObjectNamePrefix());
+    assertEquals(expected.getPayloadFormat(), actual.getPayloadFormat());
+    assertEquals(expected.getTopic().trim(), actual.getTopic().trim());
+  }
+}
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 52c312b4..e69e3fe0 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
@@ -60,6 +60,7 @@ import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Map;
 import javax.crypto.spec.SecretKeySpec;
 import org.junit.Before;
@@ -366,6 +367,37 @@ public class StorageImplMockitoTest {
           .put('~', "~")
           .build();
 
+  // Notification
+  private static final String ETAG = "0xFF00";
+  private static final String GENERATED_ID = "B/N:1";
+  private static final String SELF_LINK = "http://storage/b/n";
+  private static final Notification.EventType[] EVENT_TYPES = {
+    Notification.EventType.OBJECT_FINALIZE, Notification.EventType.OBJECT_METADATA_UPDATE
+  };
+  private static final String OBJECT_NAME_PREFIX = "index.html";
+  private static final Notification.PayloadFormat PAYLOAD_FORMAT =
+      Notification.PayloadFormat.JSON_API_V1.JSON_API_V1;
+  private static final String TOPIC = "projects/myProject/topics/topic1";
+  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
+  private static final NotificationInfo NOTIFICATION_INFO_01 =
+      NotificationInfo.newBuilder(TOPIC)
+          .setEtag(ETAG)
+          .setCustomAttributes(CUSTOM_ATTRIBUTES)
+          .setSelfLink(SELF_LINK)
+          .setEventTypes(EVENT_TYPES)
+          .setObjectNamePrefix(OBJECT_NAME_PREFIX)
+          .setPayloadFormat(PAYLOAD_FORMAT)
+          .build();
+  private static final NotificationInfo NOTIFICATION_INFO_02 =
+      NotificationInfo.newBuilder(TOPIC)
+          .setEtag(ETAG)
+          .setCustomAttributes(CUSTOM_ATTRIBUTES)
+          .setSelfLink(SELF_LINK)
+          .setEventTypes(EVENT_TYPES)
+          .setObjectNamePrefix(OBJECT_NAME_PREFIX)
+          .setPayloadFormat(PAYLOAD_FORMAT)
+          .build();
+
   private static final String ACCOUNT = "account";
   private static PrivateKey privateKey;
   private static PublicKey publicKey;
@@ -1643,4 +1675,55 @@ public class StorageImplMockitoTest {
       assertSame(STORAGE_FAILURE, e.getCause());
     }
   }
+
+  @Test
+  public void testCreateNotification() {
+    doReturn(NOTIFICATION_INFO_01.toPb())
+        .when(storageRpcMock)
+        .createNotification(BUCKET_NAME1, NOTIFICATION_INFO_01.toPb());
+    initializeService();
+    Notification notification = storage.createNotification(BUCKET_NAME1, NOTIFICATION_INFO_01);
+    verifyBucketNotification(notification);
+  }
+
+  @Test
+  public void testGetNotification() {
+    doReturn(NOTIFICATION_INFO_01.toPb())
+        .when(storageRpcMock)
+        .getNotification(BUCKET_NAME1, GENERATED_ID);
+    initializeService();
+    Notification notification = storage.getNotification(BUCKET_NAME1, GENERATED_ID);
+    verifyBucketNotification(notification);
+  }
+
+  @Test
+  public void testListNotification() {
+    doReturn(Arrays.asList(NOTIFICATION_INFO_01.toPb(), NOTIFICATION_INFO_02.toPb()))
+        .when(storageRpcMock)
+        .listNotifications(BUCKET_NAME1);
+    initializeService();
+    List<Notification> notifications = storage.listNotifications(BUCKET_NAME1);
+    assertEquals(2, notifications.size());
+    verifyBucketNotification(notifications.get(0));
+    verifyBucketNotification(notifications.get(1));
+  }
+
+  @Test
+  public void testDeleteNotification() {
+    doReturn(true).when(storageRpcMock).deleteNotification(BUCKET_NAME1, GENERATED_ID);
+    initializeService();
+    Boolean isDeleted = storage.deleteNotification(BUCKET_NAME1, GENERATED_ID);
+    assertEquals(isDeleted, Boolean.TRUE);
+  }
+
+  private void verifyBucketNotification(Notification value) {
+    assertNull(value.getNotificationId());
+    assertEquals(CUSTOM_ATTRIBUTES, value.getCustomAttributes());
+    assertEquals(ETAG, value.getEtag());
+    assertEquals(SELF_LINK, value.getSelfLink());
+    assertEquals(OBJECT_NAME_PREFIX, value.getObjectNamePrefix());
+    assertEquals(PAYLOAD_FORMAT.name(), value.getPayloadFormat().name());
+    assertEquals(TOPIC, value.getTopic());
+    assertEquals(Arrays.asList(EVENT_TYPES), value.getEventTypes());
+  }
 }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java
index cacc0d55..b713a222 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/ITRetryConformanceTest.java
@@ -149,7 +149,13 @@ public class ITRetryConformanceTest {
             .setMappings(new RpcMethodMappings())
             .setProjectId("conformance-tests")
             .setHost(TEST_BENCH.getBaseUri().replaceAll("https?://", ""))
-            .setTestAllowFilter(RetryTestCaseResolver.includeAll())
+            .setTestAllowFilter(
+                RetryTestCaseResolver.includeAll()
+                    .and(
+                        (m, trc) ->
+                            trc.getScenarioId()
+                                < 7) // Temporarily exclude resumable media scenarios
+                )
             .build();
 
     List<RetryTestCase> retryTestCases = resolver.getRetryTestCases();
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java
index 557cfe3c..fe723270 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestBench.java
@@ -337,7 +337,7 @@ public final class TestBench implements TestRule {
     private static final String DEFAULT_BASE_URI = "http://localhost:9000";
     private static final String DEFAULT_IMAGE_NAME =
         "gcr.io/cloud-devrel-public-resources/storage-testbench";
-    private static final String DEFAULT_IMAGE_TAG = "v0.11.0";
+    private static final String DEFAULT_IMAGE_TAG = "v0.15.0";
     private static final String DEFAULT_CONTAINER_NAME = "default";
 
     private boolean ignorePullError;
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java
index 027da537..b9b8456d 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/conformance/retry/TestRetryConformance.java
@@ -111,13 +111,21 @@ final class TestRetryConformance {
             .map(s -> s.replace("return-", ""))
             .collect(Collectors.joining("_"));
     this.bucketName =
-        String.format("%s_s%03d-%s-m%03d_bkt1", BASE_ID, scenarioId, instructionsString, mappingId);
+        String.format(
+            "%s_s%03d-%s-m%03d_bkt1",
+            BASE_ID, scenarioId, instructionsString.toLowerCase(), mappingId);
     this.bucketName2 =
-        String.format("%s_s%03d-%s-m%03d_bkt2", BASE_ID, scenarioId, instructionsString, mappingId);
+        String.format(
+            "%s_s%03d-%s-m%03d_bkt2",
+            BASE_ID, scenarioId, instructionsString.toLowerCase(), mappingId);
     this.userProject =
-        String.format("%s_s%03d-%s-m%03d_prj1", BASE_ID, scenarioId, instructionsString, mappingId);
+        String.format(
+            "%s_s%03d-%s-m%03d_prj1",
+            BASE_ID, scenarioId, instructionsString.toLowerCase(), mappingId);
     this.objectName =
-        String.format("%s_s%03d-%s-m%03d_obj1", BASE_ID, scenarioId, instructionsString, mappingId);
+        String.format(
+            "%s_s%03d-%s-m%03d_obj1",
+            BASE_ID, scenarioId, instructionsString.toLowerCase(), mappingId);
   }
 
   public String getProjectId() {
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
index e9fa10a1..80d1ebc3 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/it/ITStorageTest.java
@@ -41,6 +41,7 @@ import com.google.cloud.Identity;
 import com.google.cloud.Policy;
 import com.google.cloud.ReadChannel;
 import com.google.cloud.RestorableState;
+import com.google.cloud.ServiceOptions;
 import com.google.cloud.TransportOptions;
 import com.google.cloud.WriteChannel;
 import com.google.cloud.http.HttpTransportOptions;
@@ -54,6 +55,7 @@ import com.google.cloud.kms.v1.KeyManagementServiceGrpc;
 import com.google.cloud.kms.v1.KeyManagementServiceGrpc.KeyManagementServiceBlockingStub;
 import com.google.cloud.kms.v1.KeyRingName;
 import com.google.cloud.kms.v1.LocationName;
+import com.google.cloud.pubsub.v1.TopicAdminClient;
 import com.google.cloud.storage.Acl;
 import com.google.cloud.storage.Acl.Role;
 import com.google.cloud.storage.Acl.User;
@@ -71,6 +73,8 @@ import com.google.cloud.storage.DataGeneration;
 import com.google.cloud.storage.HmacKey;
 import com.google.cloud.storage.HmacKey.HmacKeyState;
 import com.google.cloud.storage.HttpMethod;
+import com.google.cloud.storage.Notification;
+import com.google.cloud.storage.NotificationInfo;
 import com.google.cloud.storage.PostPolicyV4;
 import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
 import com.google.cloud.storage.Rpo;
@@ -99,6 +103,7 @@ import com.google.common.io.ByteStreams;
 import com.google.common.reflect.AbstractInvocationHandler;
 import com.google.common.reflect.Reflection;
 import com.google.iam.v1.Binding;
+import com.google.iam.v1.GetIamPolicyRequest;
 import com.google.iam.v1.IAMPolicyGrpc;
 import com.google.iam.v1.SetIamPolicyRequest;
 import io.grpc.ManagedChannel;
@@ -132,6 +137,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Random;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -162,6 +168,7 @@ public class ITStorageTest {
 
   private static RemoteStorageHelper remoteStorageHelper;
   private static Storage storage;
+  private static TopicAdminClient topicAdminClient;
   private static String kmsKeyOneResourcePath;
   private static String kmsKeyTwoResourcePath;
   private static Metadata requestParamsHeader = new Metadata();
@@ -219,6 +226,13 @@ public class ITStorageTest {
 
   @Rule public final TestName testName = new TestName();
   @Rule public final DataGeneration dataGeneration = new DataGeneration(new Random(1234567890));
+  private static final String PROJECT = ServiceOptions.getDefaultProjectId();
+  private static final String ID = UUID.randomUUID().toString().substring(0, 8);
+  private static final String TOPIC =
+      String.format("projects/%s/topics/test_topic_foo_%s", PROJECT, ID).trim();
+  private static final Notification.PayloadFormat PAYLOAD_FORMAT =
+      Notification.PayloadFormat.JSON_API_V1.JSON_API_V1;
+  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
 
   @BeforeClass
   public static void beforeClass() throws IOException {
@@ -239,6 +253,9 @@ public class ITStorageTest {
 
     // Prepare KMS KeyRing for CMEK tests
     prepareKmsKeys();
+
+    // Configure topic admin client for notification.
+    topicAdminClient = configureTopicAdminClient();
   }
 
   private static void unsetRequesterPays() {
@@ -260,6 +277,12 @@ public class ITStorageTest {
   @AfterClass
   public static void afterClass() throws ExecutionException, InterruptedException {
     if (storage != null) {
+
+      /* Delete the Pub/Sub topic */
+      if (topicAdminClient != null) {
+        topicAdminClient.deleteTopic(TOPIC);
+        topicAdminClient.close();
+      }
       // In beforeClass, we make buckets auto-delete blobs older than a day old.
       // Here, delete all buckets older than 2 days. They should already be empty and easy.
       long cleanTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2);
@@ -415,6 +438,23 @@ public class ITStorageTest {
     return kmsKeyResourcePath;
   }
 
+  private static TopicAdminClient configureTopicAdminClient() throws IOException {
+    TopicAdminClient topicAdminClient = TopicAdminClient.create();
+    topicAdminClient.createTopic(TOPIC);
+    GetIamPolicyRequest getIamPolicyRequest =
+        GetIamPolicyRequest.newBuilder().setResource(TOPIC).build();
+    com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(getIamPolicyRequest);
+    Binding binding =
+        Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build();
+    SetIamPolicyRequest setIamPolicyRequest =
+        SetIamPolicyRequest.newBuilder()
+            .setResource(TOPIC)
+            .setPolicy(policy.toBuilder().addBindings(binding).build())
+            .build();
+    topicAdminClient.setIamPolicy(setIamPolicyRequest);
+    return topicAdminClient;
+  }
+
   @Test(timeout = 5000)
   public void testListBuckets() throws InterruptedException {
     Iterator<Bucket> bucketIterator =
@@ -3788,6 +3828,53 @@ public class ITStorageTest {
     }
   }
 
+  @Test
+  public void testNotification() throws InterruptedException, ExecutionException {
+    String bucketName = RemoteStorageHelper.generateBucketName();
+    storage.create(BucketInfo.newBuilder(bucketName).setLocation("us").build());
+    NotificationInfo notificationInfo =
+        NotificationInfo.newBuilder(TOPIC)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
+    try {
+      assertThat(storage.listNotifications(bucketName)).isEmpty();
+      Notification notification = storage.createNotification(bucketName, 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(bucketName, 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(bucketName);
+      assertThat(notifications.size()).isEqualTo(1);
+      assertThat(notifications.get(0).getNotificationId())
+          .isEqualTo(actualNotification.getNotificationId());
+
+      // Deletes the notification with the specified id.
+      assertThat(storage.deleteNotification(bucketName, notification.getNotificationId())).isTrue();
+      assertThat(storage.deleteNotification(bucketName, notification.getNotificationId()))
+          .isFalse();
+      assertThat(storage.getNotification(bucketName, notification.getNotificationId())).isNull();
+      assertThat(storage.listNotifications(bucketName)).isEmpty();
+    } finally {
+      RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
+    }
+  }
+
   @Test
   public void testBlobTimeStorageClassUpdated() {
     String blobName = "test-blob-with-storage-class";
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/testing/StorageRpcTestBaseTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/testing/StorageRpcTestBaseTest.java
index b798ad57..7feef9f1 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/testing/StorageRpcTestBaseTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/testing/StorageRpcTestBaseTest.java
@@ -577,6 +577,17 @@ public class StorageRpcTestBaseTest {
         };
   }
 
+  @Test
+  public void testGetNotification() {
+    rpc =
+        new Callable<Notification>() {
+          @Override
+          public Notification call() {
+            return STORAGE_RPC.getNotification("bucket", "notification");
+          }
+        };
+  }
+
   @Test
   public void testLockRetentionPolicy() {
     rpc =
diff --git a/samples/snippets/src/test/java/com/example/storage/ITHmacSnippets.java b/samples/snippets/src/test/java/com/example/storage/ITHmacSnippets.java
index 2e1e45a3..57cc6009 100644
--- a/samples/snippets/src/test/java/com/example/storage/ITHmacSnippets.java
+++ b/samples/snippets/src/test/java/com/example/storage/ITHmacSnippets.java
@@ -33,51 +33,25 @@ import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
 import com.google.cloud.storage.HmacKey.HmacKeyState;
 import com.google.cloud.storage.ServiceAccount;
 import com.google.cloud.storage.Storage;
-import com.google.cloud.storage.testing.RemoteStorageHelper;
-import java.io.ByteArrayOutputStream;
-import java.io.FileDescriptor;
-import java.io.FileOutputStream;
-import java.io.PrintStream;
+import com.google.cloud.testing.junit4.MultipleAttemptsRule;
 import org.junit.Assert;
 import org.junit.Before;
-import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
 
-public class ITHmacSnippets {
+public class ITHmacSnippets extends TestBase {
   private static final String PROJECT_ID = ServiceOptions.getDefaultProjectId();
   private static final String HMAC_KEY_TEST_SERVICE_ACCOUNT =
       PROJECT_ID + "@" + PROJECT_ID + ".iam.gserviceaccount.com";
-  private final PrintStream standardOut = new PrintStream(new FileOutputStream(FileDescriptor.out));
-
-  private static Storage storage;
-
-  @BeforeClass
-  public static void beforeClass() {
-    RemoteStorageHelper helper = RemoteStorageHelper.create();
-    storage =
-        helper
-            .getOptions()
-            .toBuilder()
-            .setRetrySettings(
-                helper
-                    .getOptions()
-                    .getRetrySettings()
-                    .toBuilder()
-                    .setRetryDelayMultiplier(3.0)
-                    .build())
-            .build()
-            .getService();
-  }
 
   @Before
   public void before() {
     cleanUpHmacKeys(ServiceAccount.of(HMAC_KEY_TEST_SERVICE_ACCOUNT));
-
-    // This is just in case any tests failed before they could reset the value
-    System.setOut(standardOut);
   }
 
-  private static void cleanUpHmacKeys(ServiceAccount serviceAccount) {
+  @Rule public MultipleAttemptsRule multipleAttemptsRule = new MultipleAttemptsRule(5);
+
+  private void cleanUpHmacKeys(ServiceAccount serviceAccount) {
     Page<HmacKey.HmacKeyMetadata> metadatas =
         storage.listHmacKeys(Storage.ListHmacKeysOption.serviceAccount(serviceAccount));
     for (HmacKey.HmacKeyMetadata hmacKeyMetadata : metadatas.iterateAll()) {
@@ -92,11 +66,9 @@ public class ITHmacSnippets {
 
   @Test
   public void testCreateHmacKey() throws Exception {
-    final ByteArrayOutputStream snippetOutputCapture = new ByteArrayOutputStream();
-    System.setOut(new PrintStream(snippetOutputCapture));
+    ;
     CreateHmacKey.createHmacKey(HMAC_KEY_TEST_SERVICE_ACCOUNT, PROJECT_ID);
-    String snippetOutput = snippetOutputCapture.toString();
-    System.setOut(standardOut);
+    String snippetOutput = stdOut.getCapturedOutputAsUtf8String();
     String accessId = snippetOutput.split("Access ID: ")[1].split("\n")[0];
     Thread.sleep(5000);
     assertNotNull(storage.getHmacKey(accessId));
@@ -105,13 +77,9 @@ public class ITHmacSnippets {
   @Test
   public void testGetHmacKey() throws Exception {
     HmacKey hmacKey = storage.createHmacKey(ServiceAccount.of(HMAC_KEY_TEST_SERVICE_ACCOUNT));
-
-    final ByteArrayOutputStream snippetOutputCapture = new ByteArrayOutputStream();
-    System.setOut(new PrintStream(snippetOutputCapture));
     Thread.sleep(5000);
     GetHmacKey.getHmacKey(hmacKey.getMetadata().getAccessId(), PROJECT_ID);
-    String snippetOutput = snippetOutputCapture.toString();
-    System.setOut(standardOut);
+    String snippetOutput = stdOut.getCapturedOutputAsUtf8String();
     Assert.assertTrue(snippetOutput.contains(HMAC_KEY_TEST_SERVICE_ACCOUNT));
   }
 
@@ -129,9 +97,8 @@ public class ITHmacSnippets {
   @Test
   public void testDeactivateHmacKey() throws Exception {
     HmacKey hmacKey = storage.createHmacKey(ServiceAccount.of(HMAC_KEY_TEST_SERVICE_ACCOUNT));
-
-    DeactivateHmacKey.deactivateHmacKey(hmacKey.getMetadata().getAccessId(), PROJECT_ID);
     Thread.sleep(5000);
+    DeactivateHmacKey.deactivateHmacKey(hmacKey.getMetadata().getAccessId(), PROJECT_ID);
     assertEquals(
         HmacKeyState.INACTIVE, storage.getHmacKey(hmacKey.getMetadata().getAccessId()).getState());
   }
@@ -158,12 +125,9 @@ public class ITHmacSnippets {
             ServiceAccount.of(HMAC_KEY_TEST_SERVICE_ACCOUNT),
             Storage.CreateHmacKeyOption.projectId(PROJECT_ID));
 
-    final ByteArrayOutputStream snippetOutputCapture = new ByteArrayOutputStream();
-    System.setOut(new PrintStream(snippetOutputCapture));
     ListHmacKeys.listHmacKeys(PROJECT_ID);
-    String snippetOutput = snippetOutputCapture.toString();
+    String snippetOutput = stdOut.getCapturedOutputAsUtf8String();
     assertTrue(snippetOutput.contains(one.getMetadata().getAccessId()));
     assertTrue(snippetOutput.contains(two.getMetadata().getAccessId()));
-    System.setOut(standardOut);
   }
 }