Repository: googleapis/java-storage (java)

Issue:
Expose the methods of Notifications
Expose the following methods of Storage Notifications.

- [x] [createNotification](https://cloud.google.com/storage/docs/json_api/v1/notifications/insert)

- [x] [getNotification](https://cloud.google.com/storage/docs/json_api/v1/notifications/get)

- [x] [listNotification](https://cloud.google.com/storage/docs/json_api/v1/notifications/list)

- [x] [deleteNotification](https://cloud.google.com/storage/docs/json_api/v1/notifications/delete)

/cc: @frankyn @JesseLovelace 

Pull request description:
feat: expose all the methods of notification
Fixes #138 

This PR exposing all  the methods of [notifications](https://cloud.google.com/storage/docs/json_api/v1/notifications), also contain the unit test as well as system test of each method.

Patch:
diff --git a/README.md b/README.md
index 7afa65fb..a9bac3a9 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>libraries-bom</artifactId>
-      <version>6.0.0</version>
+      <version>7.0.0</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -207,6 +207,16 @@ display on your webpage.
 
 
 
+## Samples
+
+Samples are in the [`samples/`](https://github.com/googleapis/java-storage/tree/master/samples) directory. The samples' `README.md`
+has instructions for running the samples.
+
+| Sample                      | Source Code                       | Try it |
+| --------------------------- | --------------------------------- | ------ |
+| Quickstart Sample | [source code](https://github.com/googleapis/java-storage/blob/master/samples/snippets/src/main/java/com/example/storage/QuickstartSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-storage&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/storage/QuickstartSample.java) |
+
+
 
 ## Troubleshooting
 
diff --git a/google-cloud-storage/clirr-ignored-differences.xml b/google-cloud-storage/clirr-ignored-differences.xml
index ace7a6ef..2aababb9 100644
--- a/google-cloud-storage/clirr-ignored-differences.xml
+++ b/google-cloud-storage/clirr-ignored-differences.xml
@@ -26,4 +26,29 @@
         <method>com.google.cloud.storage.BucketInfo$Builder deleteLifecycleRules()</method>
         <differenceType>7013</differenceType>
     </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>com.google.api.services.storage.model.Notification createNotification(java.lang.String, com.google.cloud.storage.NotificationInfo)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>com.google.api.services.storage.model.Notification getNotification(java.lang.String, java.lang.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>java.util.List listNotifications(java.lang.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/Storage</className>
+        <method>boolean deleteNotification(java.lang.String, java.lang.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
+    <difference>
+        <className>com/google/cloud/storage/spi/v1/StorageRpc</className>
+        <method>com.google.api.services.storage.model.Notification getNotification(java.lang.String, java.lang.String)</method>
+        <differenceType>7012</differenceType>
+    </difference>
 </differences>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index d651252c..1be67d6b 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>1.110.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>1.110.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,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>1.110.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>1.110.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
@@ -155,6 +155,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/NotificationInfo.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java
new file mode 100644
index 00000000..34c2d326
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/NotificationInfo.java
@@ -0,0 +1,309 @@
+/*
+ * 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.api.services.storage.model.Notification;
+import com.google.common.base.Function;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Google Storage Notification metadata;
+ *
+ * @see <a href="https://cloud.google.com/storage/docs/concepts-techniques#concepts">Concepts and
+ *     Terminology</a>
+ */
+public class NotificationInfo implements Serializable {
+
+  private static final long serialVersionUID = 5725883368559753810L;
+  private static final PathTemplate PATH_TEMPLATE =
+      PathTemplate.createWithoutUrlEncoding("projects/{project}/topics/{topic}");
+
+  public enum PayloadFormat {
+    JSON_API_V1,
+    NONE
+  }
+
+  static final Function<Notification, NotificationInfo> FROM_PB_FUNCTION =
+      new Function<Notification, NotificationInfo>() {
+        @Override
+        public NotificationInfo apply(Notification pb) {
+          return NotificationInfo.fromPb(pb);
+        }
+      };
+  static final Function<NotificationInfo, Notification> TO_PB_FUNCTION =
+      new Function<NotificationInfo, Notification>() {
+        @Override
+        public Notification apply(NotificationInfo NotificationInfo) {
+          return NotificationInfo.toPb();
+        }
+      };
+  private final String generatedId;
+  private final String topic;
+  private final List<String> eventTypes;
+  private final Map<String, String> customAttributes;
+  private final PayloadFormat payloadFormat;
+  private final String objectNamePrefix;
+  private final String etag;
+  private final String selfLink;
+
+  public static final class Builder {
+
+    private String generatedId;
+    private String topic;
+    private List<String> eventTypes;
+    private Map<String, String> customAttributes;
+    private PayloadFormat payloadFormat;
+    private String objectNamePrefix;
+    private String etag;
+    private String selfLink;
+
+    Builder(String topic) {
+      this.topic = topic;
+    }
+
+    Builder(NotificationInfo NotificationInfo) {
+      generatedId = NotificationInfo.generatedId;
+      etag = NotificationInfo.etag;
+      selfLink = NotificationInfo.selfLink;
+      topic = NotificationInfo.topic;
+      eventTypes = NotificationInfo.eventTypes;
+      customAttributes = NotificationInfo.customAttributes;
+      payloadFormat = NotificationInfo.payloadFormat;
+      objectNamePrefix = NotificationInfo.objectNamePrefix;
+    }
+
+    Builder setGeneratedId(String generatedId) {
+      this.generatedId = generatedId;
+      return this;
+    }
+
+    Builder setSelfLink(String selfLink) {
+      this.selfLink = selfLink;
+      return this;
+    }
+
+    /** The name of the topic. It must have the format "projects/{project}/topics/{topic}". */
+    public Builder setTopic(String topic) {
+      this.topic = topic;
+      return this;
+    }
+
+    public Builder setPayloadFormat(PayloadFormat payloadFormat) {
+      this.payloadFormat = payloadFormat;
+      return this;
+    }
+
+    public Builder setObjectNamePrefix(String objectNamePrefix) {
+      this.objectNamePrefix = objectNamePrefix;
+      return this;
+    }
+
+    public Builder setEventTypes(Iterable<String> eventTypes) {
+      this.eventTypes = eventTypes != null ? ImmutableList.copyOf(eventTypes) : null;
+      return this;
+    }
+
+    Builder setEtag(String etag) {
+      this.etag = etag;
+      return this;
+    }
+
+    public Builder setCustomAttributes(Map<String, String> customAttributes) {
+      this.customAttributes =
+          customAttributes != null ? ImmutableMap.copyOf(customAttributes) : null;
+      return this;
+    }
+
+    public NotificationInfo build() {
+      checkNotNull(topic);
+      return new NotificationInfo(this);
+    }
+  }
+
+  NotificationInfo(Builder builder) {
+    generatedId = builder.generatedId;
+    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 getGeneratedId() {
+    return generatedId;
+  }
+
+  /** Returns the topic to which this subscription publishes. */
+  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 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 list of event types that this notification will apply to. If empty, notifications
+   * will be sent on all event types.
+   *
+   * @see <a href="https://cloud.google.com/storage/docs/cross-origin">Cross-Origin Resource Sharing
+   *     (CORS)</a>
+   */
+  public List<String> getEventTypes() {
+    return eventTypes;
+  }
+
+  /**
+   * Returns the list of additional attributes to attach to each Cloud PubSub message published for
+   * this notification subscription.
+   *
+   * @see <a href="https://cloud.google.com/storage/docs/access-control#About-Access-Control-Lists">
+   *     About Access Control Lists</a>
+   */
+  public Map<String, String> getCustomAttributes() {
+    return customAttributes;
+  }
+
+  /** Returns a builder for the current notification. */
+  public Builder toBuilder() {
+    return new Builder(this);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(getTopic());
+  }
+
+  @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", getTopic()).toString();
+  }
+
+  Notification toPb() {
+    Notification notificationPb = new Notification();
+    notificationPb.setId(generatedId);
+    notificationPb.setEtag(etag);
+    if (customAttributes != null) {
+      notificationPb.setCustomAttributes(customAttributes);
+    }
+    if (eventTypes != null) {
+      notificationPb.setEventTypes(eventTypes);
+    }
+    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 name.
+   *
+   * @param topic The name of the topic. It must have the format
+   *     "projects/{project}/topics/{topic}".
+   */
+  public static NotificationInfo of(String topic) {
+    PATH_TEMPLATE.validatedMatch(topic, "topic name must be in valid format");
+    return newBuilder(topic).build();
+  }
+
+  /**
+   * Returns a {@code NotificationInfo} builder where the topic's name is set to the provided name.
+   *
+   * @param topic The name of the topic. It must have the format
+   *     "projects/{project}/topics/{topic}".
+   */
+  public static Builder newBuilder(String topic) {
+    PATH_TEMPLATE.validatedMatch(topic, "topic name must be in valid format");
+    return new Builder(topic);
+  }
+
+  static NotificationInfo fromPb(Notification notificationPb) {
+    Builder builder = newBuilder(notificationPb.getTopic());
+    if (notificationPb.getId() != null) {
+      builder.setGeneratedId(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.getTopic() != null) {
+      builder.setTopic(notificationPb.getTopic());
+    }
+    if (notificationPb.getEventTypes() != null) {
+      builder.setEventTypes(notificationPb.getEventTypes());
+    }
+    if (notificationPb.getPayloadFormat() != null) {
+      builder.setPayloadFormat(PayloadFormat.valueOf(notificationPb.getPayloadFormat()));
+    }
+    return builder.build();
+  }
+}
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java
index 988f87d3..fa196c90 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
@@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 
 import com.google.api.core.InternalExtensionOnly;
 import com.google.api.gax.paging.Page;
+import com.google.api.services.storage.model.Notification;
 import com.google.auth.ServiceAccountSigner;
 import com.google.auth.ServiceAccountSigner.SigningException;
 import com.google.cloud.FieldSelector;
@@ -3433,4 +3434,36 @@ public interface Storage extends Service<StorageOptions> {
    * @throws StorageException upon failure
    */
   ServiceAccount getServiceAccount(String projectId);
+
+  /**
+   * Creates a notification with the specified entity on the specified bucket.
+   *
+   * @return the notification that was created.
+   * @throws StorageException upon failure
+   */
+  Notification createNotification(String bucket, NotificationInfo notification);
+
+  /**
+   * Get the notification with the specified name on the specified object.
+   *
+   * @return the notification object that exist on the bucket.
+   * @throws StorageException upon failure
+   */
+  Notification getNotification(String bucket, String notification);
+
+  /**
+   * List the notifications for the provided bucket.
+   *
+   * @return a list of {@link Notification} objects that exist on the bucket.
+   * @throws StorageException upon failure
+   */
+  List<Notification> listNotifications(String bucket);
+
+  /**
+   * Deletes the notification with the specified name on the specified object.
+   *
+   * @return {@code true} if the notification was deleted, {@code false} if it was not found
+   * @throws StorageException upon failure
+   */
+  boolean deleteNotification(String bucket, String notification);
 }
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 0e24521e..ef46249d 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
@@ -36,6 +36,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.api.gax.paging.Page;
 import com.google.api.services.storage.model.BucketAccessControl;
+import com.google.api.services.storage.model.Notification;
 import com.google.api.services.storage.model.ObjectAccessControl;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.api.services.storage.model.TestIamPermissionsResponse;
@@ -1698,6 +1699,79 @@ final class StorageImpl extends BaseService<StorageOptions> implements Storage {
     }
   }
 
+  @Override
+  public Notification createNotification(final String bucket, final NotificationInfo notification) {
+    final com.google.api.services.storage.model.Notification notificationPb = notification.toPb();
+    try {
+      return 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 notification) {
+    try {
+      return runWithRetries(
+          new Callable<Notification>() {
+            @Override
+            public Notification call() {
+              return storageRpc.getNotification(bucket, notification);
+            }
+          },
+          getOptions().getRetrySettings(),
+          EXCEPTION_HANDLER,
+          getOptions().getClock());
+    } catch (RetryHelperException e) {
+      throw StorageException.translateAndThrow(e);
+    }
+  }
+
+  @Override
+  public List<Notification> listNotifications(final String bucket) {
+    try {
+      return runWithRetries(
+          new Callable<List<Notification>>() {
+            @Override
+            public List<Notification> call() {
+              return storageRpc.listNotifications(bucket);
+            }
+          },
+          getOptions().getRetrySettings(),
+          EXCEPTION_HANDLER,
+          getOptions().getClock());
+    } catch (RetryHelperException e) {
+      throw StorageException.translateAndThrow(e);
+    }
+  }
+
+  @Override
+  public boolean deleteNotification(final String bucket, final String notification) {
+    try {
+      return runWithRetries(
+          new Callable<Boolean>() {
+            @Override
+            public Boolean call() {
+              return storageRpc.deleteNotification(bucket, notification);
+            }
+          },
+          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 e211c027..97ceafa3 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
@@ -1489,6 +1489,21 @@ 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()));
+      throw translate(ex);
+    } 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 a5e4daf7..11a33d8d 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 36c7a5ff..0291d920 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
@@ -532,6 +532,14 @@ public interface StorageRpc extends ServiceRpc {
    */
   Notification createNotification(String bucket, Notification notification);
 
+  /**
+   * Get the notification with the specified name on the specified object.
+   *
+   * @return the notification object that exist on the bucket.
+   * @throws StorageException upon failure
+   */
+  Notification getNotification(String bucket, String notification);
+
   /**
    * Lock retention policy for the provided bucket.
    *
diff --git a/pom.xml b/pom.xml
index a5cb6e9a..aa0146a4 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>1.110.0</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>1.110.1-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -80,6 +80,12 @@
         <artifactId>google-api-services-storage</artifactId>
         <version>v1-rev20200430-1.30.9</version>
       </dependency>
+      <dependency>
+        <groupId>com.google.cloud</groupId>
+        <artifactId>google-cloud-pubsub</artifactId>
+        <version>1.106.0</version>
+        <scope>test</scope>
+      </dependency>
       <dependency>
         <groupId>org.easymock</groupId>
         <artifactId>easymock</artifactId>
diff --git a/synth.metadata b/synth.metadata
index 24a58389..79e7a4fe 100644
--- a/synth.metadata
+++ b/synth.metadata
@@ -4,7 +4,7 @@
       "git": {
         "name": ".",
         "remote": "https://github.com/googleapis/java-storage.git",
-        "sha": "5d33d641d02634491f5b3a33b94729b69790359b"
+        "sha": "37f00ebc84ff4069f14e1cfa0f0a5f9552f08282"
       }
     },
     {
diff --git a/versions.txt b/versions.txt
index 9081168b..9d49b870 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,4 +1,4 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:1.110.0:1.110.0
\ No newline at end of file
+google-cloud-storage:1.110.0:1.110.1-SNAPSHOT
\ No newline at end of file


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 6bd2d487..0f8e651a 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
@@ -280,6 +280,11 @@ public class StorageRpcTestBase implements StorageRpc {
     throw new UnsupportedOperationException("Not implemented yet");
   }
 
+  @Override
+  public Notification getNotification(String bucket, String notification) {
+    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..40b8a5cc
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/NotificationInfoTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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 com.google.cloud.storage.NotificationInfo.PayloadFormat;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class NotificationInfoTest {
+
+  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 List<String> EVENT_TYPES =
+      ImmutableList.of("OBJECT_FINALIZE", "OBJECT_METADATA_UPDATE");
+  private static final String OBJECT_NAME_PREFIX = "index.html";
+  private static final PayloadFormat PAYLOAD_FORMAT = 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 =
+      NotificationInfo.newBuilder(TOPIC)
+          .setEtag(ETAG)
+          .setCustomAttributes(CUSTOM_ATTRIBUTES)
+          .setSelfLink(SELF_LINK)
+          .setEventTypes(EVENT_TYPES)
+          .setObjectNamePrefix(OBJECT_NAME_PREFIX)
+          .setPayloadFormat(PAYLOAD_FORMAT)
+          .setGeneratedId(GENERATED_ID)
+          .build();
+
+  @Test
+  public void testToBuilder() {
+    compareBuckets(NOTIFICATION_INFO, NOTIFICATION_INFO.toBuilder().build());
+    NotificationInfo bucketInfo = NOTIFICATION_INFO.toBuilder().setGeneratedId("id").build();
+    assertEquals("id", bucketInfo.getGeneratedId());
+    bucketInfo = bucketInfo.toBuilder().setGeneratedId(GENERATED_ID).build();
+    compareBuckets(NOTIFICATION_INFO, bucketInfo);
+  }
+
+  @Test
+  public void testToBuilderIncomplete() {
+    NotificationInfo incompleteBucketInfo = NotificationInfo.newBuilder(TOPIC).build();
+    compareBuckets(incompleteBucketInfo, incompleteBucketInfo.toBuilder().build());
+  }
+
+  @Test
+  public void testOf() {
+    NotificationInfo bucketInfo = NotificationInfo.of(TOPIC);
+    assertEquals(TOPIC, bucketInfo.getTopic());
+  }
+
+  @Test
+  public void testBuilder() {
+    assertEquals(ETAG, NOTIFICATION_INFO.getEtag());
+    assertEquals(GENERATED_ID, NOTIFICATION_INFO.getGeneratedId());
+    assertEquals(SELF_LINK, NOTIFICATION_INFO.getSelfLink());
+    assertEquals(EVENT_TYPES, NOTIFICATION_INFO.getEventTypes());
+    assertEquals(OBJECT_NAME_PREFIX, NOTIFICATION_INFO.getObjectNamePrefix());
+    assertEquals(PAYLOAD_FORMAT, NOTIFICATION_INFO.getPayloadFormat());
+    assertEquals(TOPIC, NOTIFICATION_INFO.getTopic());
+    assertEquals(CUSTOM_ATTRIBUTES, NOTIFICATION_INFO.getCustomAttributes());
+  }
+
+  @Test
+  public void testToPbAndFromPb() {
+    compareBuckets(NOTIFICATION_INFO, NotificationInfo.fromPb(NOTIFICATION_INFO.toPb()));
+    NotificationInfo bucketInfo =
+        NotificationInfo.of(TOPIC).toBuilder().setPayloadFormat(PayloadFormat.NONE).build();
+    compareBuckets(bucketInfo, NotificationInfo.fromPb(bucketInfo.toPb()));
+  }
+
+  private void compareBuckets(NotificationInfo expected, NotificationInfo value) {
+    assertEquals(expected, value);
+    assertEquals(expected.getGeneratedId(), value.getGeneratedId());
+    assertEquals(expected.getCustomAttributes(), value.getCustomAttributes());
+    assertEquals(expected.getEtag(), value.getEtag());
+    assertEquals(expected.getSelfLink(), value.getSelfLink());
+    assertEquals(expected.getEventTypes(), value.getEventTypes());
+    assertEquals(expected.getObjectNamePrefix(), value.getObjectNamePrefix());
+    assertEquals(expected.getPayloadFormat(), value.getPayloadFormat());
+    assertEquals(expected.getTopic(), value.getTopic());
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
index 86bbdef7..ec06f264 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageImplTest.java
@@ -31,6 +31,7 @@ import static org.junit.Assert.assertTrue;
 import com.google.api.client.googleapis.json.GoogleJsonError;
 import com.google.api.core.ApiClock;
 import com.google.api.gax.paging.Page;
+import com.google.api.services.storage.model.Notification;
 import com.google.api.services.storage.model.Policy.Bindings;
 import com.google.api.services.storage.model.StorageObject;
 import com.google.api.services.storage.model.TestIamPermissionsResponse;
@@ -56,6 +57,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.io.BaseEncoding;
+import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -72,6 +74,7 @@ import java.security.spec.EncodedKeySpec;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -369,6 +372,38 @@ public class StorageImplTest {
           .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 List<String> EVENT_TYPES =
+      ImmutableList.of("OBJECT_FINALIZE", "OBJECT_METADATA_UPDATE");
+  private static final String OBJECT_NAME_PREFIX = "index.html";
+  private static final NotificationInfo.PayloadFormat PAYLOAD_FORMAT =
+      NotificationInfo.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)
+          .setGeneratedId(GENERATED_ID)
+          .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)
+          .setGeneratedId(GENERATED_ID)
+          .build();
+
   private static final String ACCOUNT = "account";
   private static PrivateKey privateKey;
   private static PublicKey publicKey;
@@ -382,7 +417,8 @@ public class StorageImplTest {
   private Bucket expectedBucket1, expectedBucket2, expectedBucket3;
 
   @BeforeClass
-  public static void beforeClass() throws NoSuchAlgorithmException, InvalidKeySpecException {
+  public static void beforeClass()
+      throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
     KeyFactory keyFactory = KeyFactory.getInstance("RSA");
     EncodedKeySpec privateKeySpec =
         new PKCS8EncodedKeySpec(BaseEncoding.base64().decode(PRIVATE_KEY_STRING));
@@ -2471,4 +2507,56 @@ public class StorageImplTest {
     assertEquals(outputFields.get("key"), "my-object");
     assertEquals("https://storage.googleapis.com/my-bucket/", policy.getUrl());
   }
+
+  @Test
+  public void testCreateNotification() {
+    Notification notification = NOTIFICATION_INFO_01.toPb();
+    EasyMock.expect(storageRpcMock.createNotification(BUCKET_NAME1, notification))
+        .andReturn(notification);
+    EasyMock.replay(storageRpcMock);
+    initializeService();
+    Notification remoteNotification =
+        storage.createNotification(BUCKET_NAME1, NOTIFICATION_INFO_01);
+    compareBucketsNotification(remoteNotification);
+  }
+
+  @Test
+  public void testGetNotification() {
+    EasyMock.expect(storageRpcMock.getNotification(BUCKET_NAME1, GENERATED_ID))
+        .andReturn(NOTIFICATION_INFO_01.toPb());
+    EasyMock.replay(storageRpcMock);
+    initializeService();
+    Notification notification = storage.getNotification(BUCKET_NAME1, GENERATED_ID);
+    compareBucketsNotification(notification);
+  }
+
+  @Test
+  public void testListNotification() {
+    EasyMock.expect(storageRpcMock.listNotifications(BUCKET_NAME1))
+        .andReturn(Arrays.asList(NOTIFICATION_INFO_01.toPb(), NOTIFICATION_INFO_02.toPb()));
+    EasyMock.replay(storageRpcMock);
+    initializeService();
+    List<Notification> notifications = storage.listNotifications(BUCKET_NAME1);
+    assertEquals(2, notifications.size());
+  }
+
+  @Test
+  public void testDeleteNotification() {
+    EasyMock.expect(storageRpcMock.deleteNotification(BUCKET_NAME1, GENERATED_ID)).andReturn(true);
+    EasyMock.replay(storageRpcMock);
+    initializeService();
+    Boolean isDeleted = storage.deleteNotification(BUCKET_NAME1, GENERATED_ID);
+    assertEquals(isDeleted, Boolean.TRUE);
+  }
+
+  private void compareBucketsNotification(Notification value) {
+    assertEquals(GENERATED_ID, value.getId());
+    assertEquals(CUSTOM_ATTRIBUTES, value.getCustomAttributes());
+    assertEquals(ETAG, value.getEtag());
+    assertEquals(SELF_LINK, value.getSelfLink());
+    assertEquals(EVENT_TYPES, value.getEventTypes());
+    assertEquals(OBJECT_NAME_PREFIX, value.getObjectNamePrefix());
+    assertEquals(PAYLOAD_FORMAT.name(), value.getPayloadFormat());
+    assertEquals(TOPIC, value.getTopic());
+  }
 }
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 7966c928..38fff667 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
@@ -33,6 +33,7 @@ import com.google.api.client.http.HttpTransport;
 import com.google.api.client.http.apache.ApacheHttpTransport;
 import com.google.api.client.util.DateTime;
 import com.google.api.gax.paging.Page;
+import com.google.api.services.storage.model.Notification;
 import com.google.auth.ServiceAccountSigner;
 import com.google.auth.http.HttpTransportFactory;
 import com.google.auth.oauth2.GoogleCredentials;
@@ -41,6 +42,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 +56,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;
@@ -68,6 +71,7 @@ import com.google.cloud.storage.BucketInfo.LifecycleRule.LifecycleCondition;
 import com.google.cloud.storage.CopyWriter;
 import com.google.cloud.storage.HmacKey;
 import com.google.cloud.storage.HttpMethod;
+import com.google.cloud.storage.NotificationInfo;
 import com.google.cloud.storage.PostPolicyV4;
 import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
 import com.google.cloud.storage.ServiceAccount;
@@ -120,6 +124,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.logging.Level;
@@ -142,6 +147,8 @@ public class ITStorageTest {
 
   private static RemoteStorageHelper remoteStorageHelper;
   private static Storage storage;
+  private static TopicAdminClient topicAdminClient;
+  private static Notification notification;
   private static String kmsKeyOneResourcePath;
   private static String kmsKeyTwoResourcePath;
   private static Metadata requestParamsHeader = new Metadata();
@@ -196,6 +203,15 @@ public class ITStorageTest {
   private static final ImmutableList<LifecycleRule> LIFECYCLE_RULES =
       ImmutableList.of(LIFECYCLE_RULE_1, LIFECYCLE_RULE_2);
 
+  private static final String PROJECT = ServiceOptions.getDefaultProjectId();
+  private static final String ID = UUID.randomUUID().toString().substring(0, 8);
+  private static final NotificationInfo.PayloadFormat PAYLOAD_FORMAT =
+      NotificationInfo.PayloadFormat.JSON_API_V1.JSON_API_V1;
+
+  private static final String TOPIC =
+      String.format("projects/%s/topics/test_topic_foo_%s", PROJECT, ID);
+  private static final Map<String, String> CUSTOM_ATTRIBUTES = ImmutableMap.of("label1", "value1");
+
   @BeforeClass
   public static void beforeClass() throws IOException {
     remoteStorageHelper = RemoteStorageHelper.create();
@@ -213,6 +229,22 @@ public class ITStorageTest {
 
     // Prepare KMS KeyRing for CMEK tests
     prepareKmsKeys();
+
+    // Create pubsub topic for notification.
+    topicAdminClient = TopicAdminClient.create();
+    topicAdminClient.createTopic(TOPIC);
+    com.google.iam.v1.Policy policy = topicAdminClient.getIamPolicy(TOPIC);
+    Binding binding =
+        Binding.newBuilder().setRole("roles/owner").addMembers("allAuthenticatedUsers").build();
+    topicAdminClient.setIamPolicy(TOPIC, policy.toBuilder().addBindings(binding).build());
+
+    // Create a notification on a bucket.
+    NotificationInfo notificationInfo =
+        NotificationInfo.newBuilder(TOPIC)
+            .setCustomAttributes(CUSTOM_ATTRIBUTES)
+            .setPayloadFormat(PAYLOAD_FORMAT)
+            .build();
+    notification = storage.createNotification(BUCKET, notificationInfo);
   }
 
   @Before
@@ -235,6 +267,13 @@ public class ITStorageTest {
   @AfterClass
   public static void afterClass() throws ExecutionException, InterruptedException {
     if (storage != null) {
+
+      /* Delete the specified notification on the specified bucket as well as delete the pubsub topic */
+      if (topicAdminClient != null) {
+        storage.deleteNotification(BUCKET, notification.getId());
+        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);
@@ -3322,4 +3361,24 @@ public class ITStorageTest {
       RemoteStorageHelper.forceDelete(storage, bucketName, 5, TimeUnit.SECONDS);
     }
   }
+
+  @Test
+  public void testGetNotification() {
+    Notification actualNotification = storage.getNotification(BUCKET, notification.getId());
+    assertEquals(CUSTOM_ATTRIBUTES, actualNotification.getCustomAttributes());
+    assertEquals(PAYLOAD_FORMAT.name(), actualNotification.getPayloadFormat());
+    assertTrue(actualNotification.getTopic().contains(TOPIC));
+  }
+
+  @Test
+  public void testListNotification() {
+    List<Notification> notifications = storage.listNotifications(BUCKET);
+    for (Notification actualNotification : notifications) {
+      if (actualNotification.getId().equals(notification.getId())) {
+        assertEquals(CUSTOM_ATTRIBUTES, actualNotification.getCustomAttributes());
+        assertEquals(PAYLOAD_FORMAT.name(), actualNotification.getPayloadFormat());
+        assertTrue(actualNotification.getTopic().contains(TOPIC));
+      }
+    }
+  }
 }
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 =